There was a thread the other day where people were talking about languages that were easily interoperable with the "C ABI" (although there is no such thing). Languages listed included C++, Rust, and maybe a few others--but no mention of Objective-C.
The ability to use a high-level, dynamic language (ObjC), C, and even inline assembly in a single source file is unique to Objective-C (at least among the "mainstream" languages), and something I think is often under-appreciated.
One comment on the code here: for message calls returning type 'id' (object type), you don't need to cast the dispatch function. It would make the code much more readable. For other return types, you need a cast, but I'd wrap it in a macro. You could even use a C11 generic macro to handle floating point return values (unfortunately necessary).
You should still cast the dispatch function even when your return type is id.
For one, C vararg type promotion makes it impossible to pass certain types in as parameters otherwise. For example, you can't pass a float.
It's also easy to make mistakes because of type mismatches. For example, if you pass `42` to a CGFloat parameter, this works fine when you cast msgSend to the right type, but will fail amusingly if you rely on varargs to make it through.
Perhaps most importantly, vararg calls aren't guaranteed to be compatible with a non-vararg target (as most/all methods you call will be), and you'll see this in action on ARM64.
Currently, you can #define OBJC_OLD_DISPATCH_PROTOTYPES 0 to have the compiler enforce this requirement. (This declares the functions to take (void) rather than (id, SEL, ...).) It's likely that Apple will eventually make this the default.
You're right and thanks for the correction. One point I'd add is that C generally encourages not using casts because casts themselves can be error-prone. So it's debatable whether it adds any real type-safety.
I don't see how it's debatable. The version without a cast is just plain incorrect, but happens to work anyway through luck and happenstance on many platforms. Even ignoring that, getting the types right without the cast requires looking at the type of every parameter, which could be variables declared far away, and understanding and applying C's vararg type promotion rules. Getting the types right for a cast just requires writing out those types correctly at the point of the cast.
I said you're right and I wasn't trying to disagree. I'm also a big fan of your blog and comments here (FWIW).
As for whether casts add much safety, I admitted it's debatable. C++ prefers casts (e.g. on malloc) whereas C doesn't. A downside of using explicit casts is that if the type changes elsewhere in the program, the casts (spread throughout the code) can mask errors. If you write code such that casts are unnecessary (which I'm agreeing is not possible in this case!), you will get a compile warning/error right away.
I think both perspectives are valid and there are risks either way. I try to avoid casts when writing plain C though.
I agree that unnecessary casts are undesirable, but that seems like a completely irrelevant point here, seeing as how the casts are not optional.
C++ doesn't prefer casts, it requires them in places where C doesn't. Casting the return value from malloc isn't optional in C++ (unless you're assigning to a void *, anyway).
I was referring to this part of your original post:
> It's also easy to make mistakes because of type mismatches. For example, if you pass `42` to a CGFloat parameter, this works fine when you cast msgSend to the right type, but will fail amusingly if you rely on varargs to make it through.
All of your other claims are completely valid. This one, I'd say, is partially valid.
No need to pick nits with my usage of the word "prefers."
The former will set a garbage alpha value, the latter works. More generally, the former requires much more care and attention to get the types right, whereas the latter just requires that the cast match the method declaration.
If the method signature changes (unlikely for a system header, but possible if you're calling into your own code) then you're hosed. Or you could just mess up and copy/paste the wrong signature, or forget to change it.
Basically, it's pretty error-prone no matter what you do. Using Objective-C directly is much safer (and luckily you usually can, since ObjC is pretty much a drop-in replacement for C).
I totally agree on all counts here. The cast saves you from this one specific thing but there's plenty more to go wrong. I think some people may overestimate the difficulty of adding some ObjC wrappers to a plain C project.
> The ability to use a high-level, dynamic language (ObjC), C, and even inline assembly in a single source file is unique to Objective-C (at least among the "mainstream" languages), and something I think is often under-appreciated.
You can even use C++, too, which is awesome. Putting Objective-C objects in C++ structs "just works" thanks to ARC.
One thing most people don't know is that Objective-C was originally implemented as a precompiler for C.
Objective-C was a modification of the GCC developed by NeXT. NeXT didn't publicly release their patches and the FSF/GNU threatened legal action. NeXT released their sources and the GCC now supports Obj-C.
> Objective-C was a modification of the GCC developed by NeXT.
No.
Objective-C was developed in the early '80s (as a C pre-processor, as the Grandparent post said) by Brad Cox, and commercialized by himself and Tom Love via a company they created together called Stepstone. Cox wrote a book about their product, Object Oriented Programming: An Evolutionary Approach, in 1986. I own a copy. It's pretty good!
Some time later, NeXT decided to build their user-space APIs around Objective-C, and bought the rights to Objective-C from Stepstone outright. It was at that point that they started modifying GCC to directly compile Objective-C rather than pre-process it into C.
I have the book too. It's interesting as a history lesson and initial motivations for Objective-C.
In particular, in the beginning Objective-C was a lot looser typed at compile time than it is now: no NSString* or NSWindow*, it was all just id. It was an attempt to make C look like Smalltalk.
The language eventually evolved more toward the static-typed end of the spectrum, culminating in support for lightweight generics and ostensibly birthing Swift along the way.
> Objective-C was a modification of the GCC developed by NeXT.
No. Objective-C started out as a set of C Macros, then an actual pre-processor was created, mostly to deal with uniquing selectors. Once the pre-processor was there it was used to create actual syntax.
Documented in "Object Oriented Programming: An Evolutionary Approach".[1] Still one of the best books on OO out there, because it treats OO as an architectural style with tradeoffs relative to other styles, rather than as a religion to be accepted (or nowadays as the devil to be cast into hell).
It also clearly describes the deliberate hybrid style that seems to be mostly forgotten now: object largely implemented in C, but connected via dynamic messaging. "Software-ICs".
I am somewhat surprised by seeing the 1986 release date, I guess I must have gotten it pretty soon after it was published. Used it as a template to implement an Objective-C pre-processor + runtime + basic classes on my Amiga (had just gotten a C compiler for it).
One of the reasons I got a NeXT was because NeXTStep was largely implemented in Objective-C, which to me meant they "got" it. And it also meant I could enjoy programming in Objective-C without having to maintain my own :-)
> No. Objective-C started out as a set of C Macros, then an actual pre-processor was created, mostly to deal with uniquing selectors. Once the pre-processor was there it was used to create actual syntax.
I'm curious. What did that early form of Obj-C look like?
> In order to circumvent the terms of the GPL, NeXT had originally intended to ship the Objective-C frontend separately, allowing the user to link it with GCC to produce the compiler executable. After being initially accepted by Richard M. Stallman, this plan was rejected after Stallman consulted with GNU's lawyers and NeXT agreed to make Objective-C part of GCC.
you mean objective-c++? c++ has no idea about obj-c...
another thing most people forget is that the entire programming community rejected obj-c as deficient in its very early days... it only persists thanks to the ego of former nextstep employees afaik. and now apple employees got lumped with it... which i'd suspect helped create the internal pressure for swift... so some good came at the end of it all. :)
Objective-C was the major inspiration for Java - you'll notice how little it looks like C++ - and therefore C# and co, so it's been very influential ever since the 90s.
It's not worth listening to anyone who dislikes Obj-C just because of the [] syntax, which doesn't matter after the first 48 hours.
Not true anymore; ObjC has generics with type erasure. (But not all the cases can be exported to Swift.)
> Instead, Objective-C has nil, which is like null except it won’t stop the program when you accidentally use it.
That's… not quite right… but I guess he has the visible effects down. Anyway, ObjC has nullability annotations now for this.
> For example, in Objective-C many objects have both a “core foundation” form and a “new style” “NeXTSTEP” form
This is not legacy baggage, it's an intentional C bridge.
> This is justified by conventions strongly favoring you not catching exceptions, and apparently the speed benefits are non-negligible, but it still blows my mind that Apple has their language default to incorrect behavior.
He shouldn't use ARC+exceptions, because Cocoa itself is not exception-safe. Just quit the app if you get one.
> It's not worth listening to anyone who dislikes Obj-C just because of the [] syntax, which doesn't matter after the first 48 hours.
That's pretty much my thoughts these days. If you have only the most superficial understanding of these languages, then that's what you'll get caught up on, but that's like saying Java and JavaScript are similar because they both use curly braces and dot notation for their methods. Yea, that is true, but it's a superficial similarity.
Objective-C is what makes developping applications on iOS so much more efficient than on Android; even if only indirectly, thru the frameworks provided: if Android cannot provide frameworks usable more efficiently than Apple, it's because they're using Java instead of Objective-C.
I think it actually was a precompiler for C up til a point. Still Objective-C objects have underlying C functions that are being called whenever you try to call a method or anything on an Objective-C object. Theoretically you should be able to call functions on Objective-C objects from C.
At least MSVC on x86 uses "thiscall" by default which puts the this pointer in ecx. You cannot call those functions from C without specific compiler support.
The claim was: The same goes for C++ or Swift. Getting the function pointer for a method can be involved, but all functions adhere to the C ABI.
This is clearly false since at least one C++ compiler generates functions that does not adhere to any of the common calling conventions for C on that platform (or "C ABIs" if you will).
Besides that, I said you could not call them from C. An asm stub is not C. Neither is inline assembly even though it's a common extension to C.
thats not true if you inspect the details of the specific ABI and cheat a little or use some inline assembler... but cl is easily the worst compiler for a C++ ABI.
they break it on tiny incremental updates sometimes... at least historically (in fairly recent history at least).
> thats not true if you inspect the details of the specific ABI and cheat a little or use some inline assembler...
Some x86 C compilers supports calling conventions that lets you put things in registers, e.g. Microsofts "fastcall". But I wouldn't consider that part of the standard "C ABI" for Windows given that it's practically never used for externally visible functions. As I said "without specfic compiler support".
Using inline assembler is just cheating, it's not C.
> but cl is easily the worst compiler for a C++ ABI.
they break it on tiny incremental updates sometimes... at least historically (in fairly recent history at least).
That is more about name mangling schemes and layout of standard library classes though. As far as I know the calling conventions has been quite fixed for a long time.
I don't believe C standardizes the ABI across all OS/hardware combinations. But some OS/hardware combinations are standardized; for example, there's the AMD64 ABI[1], which I think just about every operating system that runs on amd64 uses … except Windows.
I'm going to miss Objective C terribly. It is, hands down, still my favorite language (and ecosystem). It's interoperability with C got me into C. It was insanely powerful and fun.
I know not many people agree with me. I liked the square braces and crazy long function names. I know Swift is decent... It's not the same.
Oh well. Lamenting my path to software engineering doesn't mean much for anyone else. But I really am going to miss it.
Also, are there any particular resources that you would recommend for beginners? I'm looking forward to reading this article since it might expose the low-level details of how, say, message passing works under the hood. Most of the guides are a bit too "beginner-oriented"; I'd love to find something that pulls up the carpets to reveal all the infrastructure underneath.
If you are already familiar with high-level Objective-C, I would recommend looking at the headers for the language runtime. All of the inner workings are exposed as C functions you can play with. Also take a look at Mike Ash's blog, which dives into a lot of details: https://mikeash.com/pyblog/?tag=objectivec
Swift is the future Apple is pushing to developers. There is an insane amount of Objective C in the world for sure, so it's not going away completely, but I expect more new work will choose Swift, and eventually Objective C will be relegated to Apple devs working on OS X itself, plus legacy apps.
The C standard specifies absolutely no "C ABI" standard. The two "standard" calling conventions you cite, cdecl and stdcall, are Wintel conventions.
However, since C is the low-level implementation language for almost every dominant operating system, a given platform's Operating System ABI is almost always locally synonymous with the C ABI on that platform, and people tend to speak of "being compatible with the C ABI" when what they really mean is that they're compatible with the OS's ABI.
But if you move to, say, a Mainframe OS, the OS ABI is dramatically different than the "C ABI" you're imagining from Wintel.
To put it another way: C doesn't have an ABI; C has an API (or a wire protocol, if you prefer.) The abstraction layer of C doesn't exist between the machine-architecture and the C compiler; rather, the abstraction layer of C exists between the C compiler and the user, and takes the form of uncompiled C source code.
The design of C is predicated on an approach to portability that involves shipping source code, not binaries, around, with the destination machine having a compiler for its own architecture. (Given this, it's kind of shocking that things like Docker work at all. Goes to show how large a server-side monopoly those Intel ISAs currently hold.)
---
But wouldn't it be interesting if the "C compiler" were made into a low-level part of the operating system—basically taking the form of a JIT with a persistent cache—and then an executable format were specified, which really was just an archive of C source that was JIT-compiled by the OS when you first execv(3)ed it? Then you would have portable C binaries, because you'd be using the "correct" C ABI: C source-code.
Well, if you tack on a bit of pre-chewing to annotate the C source a bit (and maybe do some arch-neutral optimizations), and you have Apple's current LLVM-IR-based "Bitcode" approach to binaries.
That seems slightly pedantic. While it's true that there's no cross-platform C ABI, most platforms have a C ABI, and it's well-defined (and defined in terms of C data types) on that platform.
It's sort of like saying that there's no such thing as "rules of the road", because the road makers don't specify how you're supposed to drive on them, and the rules differ by jurisdiction and sometimes you're in a wilderness with no roads. The term is commonly understood to mean "rules for using roads, in the road-containing jurisdiction which you're in". I think the same thing is true of the phrase "C ABI", although if people are coming away thinking there's a cross-platform C ABI, then yes, we should be more precise.
> That seems slightly pedantic. While it's true that there's no cross-platform C ABI, most platforms have a C ABI, and it's well-defined (and defined in terms of C data types) on that platform.
Every platform has an ABI, obviously, and yes it's generally formed in view of C.
This doesn't change the fact that the phrase "The C ABI" is, in a vacuum, meaningless and that "The C ABI on Platform X" is just a convoluted, cart-before-the-horse way of saying "Platform X's ABI"
But what an ABI does is specify a binding between language-level concepts and binary representations. That's why mentioning the language matters. If "Platform X's ABI" says that structs are passed on the stack, or ints are a certain size, or pointers have a certain alignment, or whatever, it (usually) means that C structures are passed on the C stack, C ints are a certain size, C pointers have a certain alignment, etc. Another language can use its own data types and even its own concept of a stack. (For instance, Go uses a C-incompatible stack.) And the reason that we care about "the C ABI" is whether the implementation of a language on platform X has data types, stack usage, etc. that matches the implementation of C on platform X.
Put another way, I'm arguing that "C" is a more useful descriptor than "platform X's", because the sentence "Rust is compatible with the C ABI" is short for "forall X, Rust on platform X is compatible with the C ABI on platform X", and means something different from "Rust is compatible with the SysV ABI" (which implies that it follows the SysV ABI on Windows and OS X, too). And since the typical way of doing that is by interfacing with a C compiler or the C library (... and you could make this exact argument about the phrase "C library", I think), it's worth mentioning C. For instance, Vala and Nim, which both compile to C, are always compatible with the C ABI. If you port them to a mainframe they remain compatible with the C ABI, but not with the native ABI.
C is merely flexible, not in some position of authority where it dictates what platforms must do. C would be ABI-compatible on a platform that wasn't designed for it, too (which, historically, was the case on many platforms it was ported to).
Even on a platform that wasn't based on C, the C standard is flexible enough to match pretty much any native ABI. That's a small (and under-used) upside to all of that undefined behavior.
Most of the time, you will need a tiny bit of non-standard stuff in your compiler to do so.
For example, I don't think there is a portable way to let your compiler use Pascal calling conventions, or to pass information in specific registers or in processor flags.
As an extreme example, in classic Mac OS, you could register a function to be called for line breaking in text input boxes that had the ABI "Parameters are passed to the routine in registers A3, A4, and D0, and output is returned in the Z flag of the Status Register." (https://developer.apple.com/legacy/library/documentation/mac..., page 2-31 gives several other special-cased ABI's in Mac OS)
Browsing that, I also encountered "The routine follows the C calling conventions employed by the THINK C software development environment. Arguments are passed on the stack from right to left, and a result is returned in register D0."
(Aside: that compiler must have treated vararg functions differently)
So, apparently, one cannot even count on C compilers to push arguments from left to right. So, if your ABI says "push X first, then y", I don't think you can portably call that function from C.
So, apparently, one cannot even count on C compilers to push arguments from left to right.
Right-to-left is pretty much the most sensible way to implement variadic functions with a stack-based calling convention - the argument that the function uses to interpret the rest of them must be found at a known location relative to the stack "top", and due to how stacks work, that implies the varying portion has to be pushed first, thus right-to-left.
Maybe if C decided to design things a little differently:
int printf(..., const char *format);
...
printf(5, "There are %d lights.\n");
we would instead have right-to-left being the dominant C calling convention.
there is no standard in theory... but in practice one exists.
its a subtle difference some devs will pull out when they want to boost their egos by talking about things that sound smart, but they don't realise it requires that special kind of "smart-yet-dumb naivete" to consider worth caring or talking about.
> The ability to use a high-level, dynamic language (ObjC),
> C, and even inline assembly in a single source file
> is unique to Objective-C (at least among the
> "mainstream" languages)[...]
Of course if you do this in Perl every one of your calls will need to go through a foreign function interface where Perl's structures are translated back & forth between its idea of datastructures and the OS's idea, avoiding that is what people really mean when they talk about the "C ABI".
On OS X if OBJC_OLD_DISPATCH_PROTOTYPES is not defined, you will get this declaration with zero arguments: void objc_msgSend(void /* id self, SEL op, ... */ ), which is possible to call from C without cast by using implicit function declaration, but this call will fail if compiled as Objective-C code with error "too many arguments to function call", I've decided to write in a style that compiles as C and Objective-C code.
PS. Would be nice to be able to use functions from clang Objective-C runtime [1], but for some reason they are not exposed in the public headers.
SVR4 specified standard-conformant calling conventions (stack layout, register use etc). IIRC VMS went one "better" and tried to specify a standard calling convention that was supposed to apply to all programs (sadly, there's more to interoperability than calling convention).
You do actually need to cast the dispatch function, because there is no guarantee for all platforms that the ABI for calling a variadic function is the same as the ABI for calling a non-variadic function with the same arguments.
Unless MS has a custom C++ runtime that I'm not aware of, I wouldn't consider C++ a high level language like obj-c is, for better or worse. The benefits and drawbacks of runtime dispatch are highly debatable, but the amount of runtime metaprogramming you can do in ObjC is comparable to what you can do in a language like Ruby:
* Call any method (send message in objc parlance) based on the string name of the method
* Add methods to existing classes you don't own
* Swap existing method implementations of a class you do not own at runtime (method swizzling)
* Get and set variables by their string name for subclasses of NSObject
* Check if an object has a method with a given name (great for invoking methods conditionally)
The ability to use a high-level, dynamic language (ObjC), C, and even inline assembly in a single source file is unique to Objective-C (at least among the "mainstream" languages), and something I think is often under-appreciated.
One comment on the code here: for message calls returning type 'id' (object type), you don't need to cast the dispatch function. It would make the code much more readable. For other return types, you need a cast, but I'd wrap it in a macro. You could even use a C11 generic macro to handle floating point return values (unfortunately necessary).