Objective-C: Message Dispatch, Runtime Layers, and Windows Support
1. Message Send vs. Operator Overloading
[obj method] is fundamentally different from any C++ operator-overloading mechanism, including operator->.
operator-> (C++) | [obj method] (Objective-C) | |
|---|---|---|
| Resolved | At compile time, via static-type-driven overload resolution | At runtime, always — regardless of static type |
| What it resolves | Access syntax (how to get a pointer to dereference); the call after it is ordinary static/vtable call resolution | The call itself — dispatch and invocation are one operation |
| Can fail gracefully at runtime | No — fixed at compile time | Yes — falls through to message forwarding (forwardingTargetForSelector:, forwardInvocation:) if no method is found |
| Can be redirected after compilation | No | Yes — method swizzling can replace an implementation at runtime |
| Works with no static type info | No — must bottom out at a compile-time-known type | Yes — works on id (any object) via selector lookup alone |
One-line summary: operator-> is compiler sugar that lets a type imitate pointer syntax. [obj method] is the only mechanism by which any method is ever called in Objective-C — there is no "direct call" fallback the way there's a "built-in pointer" fallback for ->.
2. How objc_msgSend Actually Works
Not a vtable-style array index — it's a hash table lookup per class, keyed by selector:
- Load the object's
isapointer (its class). - Look up the class's method cache — a hash table mapping selector → implementation (
IMP). This cache is shared per-class, not per-call-site (unlike a classic JIT inline cache that patches the call site itself). - Cache hit: indirect jump to the cached
IMP. - Cache miss (first call, or hash collision): walk the class hierarchy checking each class's method list, then populate the cache.
Why the compiler can't optimize around it
Because any class's implementation can change at runtime (categories, method_setImplementation, swizzling, forwarding), the compiler can never prove what code a given send will execute — not even for a getter called twice in a row. This forecloses inlining, common-subexpression elimination, and any cross-call optimization. This is the structural reason dynamic dispatch here is categorically different from a C++ non-virtual call, even when the measured cost is similar (see §3).
__attribute__((objc_direct)) is Apple's explicit opt-out: a method marked this way promises not to be swizzled/overridden, letting the compiler skip objc_msgSend and call directly — recovering near-non-virtual performance.
3. Performance Comparison
Based on Mike Ash's widely-cited 2016 microbenchmark and related sources (see references).
| Mechanism | Relative cost | Notes |
|---|---|---|
| C++ non-virtual call, inlined | Free | Often zero instructions after inlining |
| C++ non-virtual call, not inlined | Baseline | One direct call/return |
| Lambda called by concrete/auto-deduced type | ≈ baseline | Compiler knows the exact closure type; often inlined |
| C++ virtual call | ~1 indirect call | Vtable slot load + jump |
objc_msgSend, cache hit | ~2.6 ns measured (≈9 cycles) — essentially tied with C++ virtual call | Full hash lookup + indirect jump, engineered to be this cheap |
std::function | Slower than a plain virtual call | Type erasure adds an extra indirection (heap/SBO-allocated wrapper) on top of its own internal indirect call |
objc_msgSend, cache miss | Noticeably slower | Walks class hierarchy, populates cache |
NSInvocation / performSelector: / dynamic forwarding | An order of magnitude+ slower | Constructs the call description at runtime |
Key takeaway: "runtime-resolved" does not automatically mean "slow." objc_msgSend's warm path is engineered to cost about the same as a C++ vtable indirect call — and is actually faster than a fully type-erased std::function. The real cost of Objective-C's dynamism shows up in the corners (first call, cache pressure, full dynamic forwarding), not in the steady-state common case.
4. The Runtime Layer vs. the Library Layer
Objective-C, like C++, splits into two distinct tiers — and these ship as genuinely separate libraries:
| Tier | C++ analogue | Objective-C |
|---|---|---|
| Runtime support — dispatch, exceptions, RTTI/reflection, thread-safe static-init guards | libc++abi / libsupc++ (__cxa_throw, RTTI comparisons, vtable machinery) | libobjc2 (GNUstep) or Apple's libobjc — objc_msgSend, class/metadata registration, method resolution, protocol tables, forwarding |
| Standard/foundation library — the actual data types/collections | libstdc++ / libc++ (std::string, std::vector) | Foundation / GNUstep Base (NSObject, NSString, NSArray) |
You can build and link libobjc2 without GNUstep Base at all — evidence that the split is real, not just conceptual.
Where does NSObject sit?
NSObject-the-class is a library convention, not a compiler requirement:
- You can write valid, ARC-compiled Objective-C with a custom root class (no superclass) that just implements the handful of selectors ARC's implicit retain/release calls need (
-retain,-release,-autorelease,-dealloc). The compiler doesn't hardcode a dependency on the classNSObject. - This puts
NSObject-the-class closer tostd::stringon the spectrum than toobjc_msgSend— arguably even less "part of the language," since it isn't standardized by any body (unlike ISO C++ standardizingstd::string).
There is one genuinely fuzzy middle case: the NSObject protocol (-class, -isEqual:, -retain, -release, -description, etc.), declared in <objc/NSObject.h> — a runtime header, not a Foundation header. ARC's synthesized code implicitly type-checks against this protocol. So there is a thin, compiler-aware seam here — but it's an interface, not an implementation, and any custom root class can conform to it. Comparable to C++'s operator""s hook into std::string: a narrow compiler-aware seam into what's otherwise ordinary library code.
What's unconditionally required, with no C++ equivalent you can opt out of: the dispatch mechanism itself. You can write large amounts of idiomatic C++ that never touches libc++abi (no virtual calls, no exceptions, no RTTI). You cannot write ordinary Objective-C that avoids message dispatch — [obj method] is the language.
5. Objective-C (and Objective-C++) on Windows
The parsing/AST/codegen layer was never the blocker. Clang's frontend support for Objective-C/Objective-C++ syntax is target-independent — it lowers to LLVM IR the same way regardless of target OS.
The actual blocker: Apple's own libobjc runtime is Darwin-only. No Objective-C runtime shipped for Windows, ever (unlike C++, where Microsoft has shipped a native runtime/ABI for decades — see the companion Clang doc).
The solution: GNUstep's libobjc2 — a from-scratch reimplementation of the Objective-C runtime, designed as a drop-in replacement for the GCC/Apple runtime APIs. Selected via -fobjc-runtime=gnustep-2.0 in Clang (the modern v2 ABI; older -fobjc-runtime=gnustep-1.9 and -fobjc-runtime=gcc ABIs also exist for compatibility).
Packaged for Windows specifically by gnustep/tools-windows-msvc:
- Builds a full toolchain (x64/arm64) using LLVM/Clang with the Visual Studio toolset — explicitly not MinGW.
- Supports Objective-C 2.0 features: blocks, ARC.
- Bundles: GNUstep Base (Foundation), GNUstep CoreBase (CoreFoundation),
libobjc2(gnustep-2.0 runtime), Apple'slibdispatch(same lineage used by Swift's corelibs),libffi,libiconv,libxml2/libxslt,libcurl. GNUstep GUI (AppKit) is optional/experimental. - Important caveat: MSVC itself cannot compile Objective-C source. Visual Studio projects must set "Platform Toolset" to "LLVM (clang-cl)".
- Required project settings: preprocessor defines (
GNUSTEP; GNUSTEP_WITH_DLL; GNUSTEP_RUNTIME=1; _NONFRAGILE_ABI=1; _NATIVE_OBJC_EXCEPTIONS), compiler flags (-fobjc-runtime=gnustep-2.0 -Xclang -fexceptions -Xclang -fobjc-exceptions -fblocks [-Xclang -fobjc-arc]), and linkinggnustep-base.lib; objc.lib; dispatch.lib. - Practical note: Clang 16+ is recommended — older versions had real Objective-C-on-Windows bugs (ARC + exception handling access violations, certain Objective-C++ constructs crashing Clang,
@finallyissues).
Structural parallel to Swift on Windows: in both cases, Clang/LLVM's ability to emit Windows PE code was never the obstacle — the obstacle was that no vendor-supplied runtime existed for the higher-level language, and the community/vendor had to build one from scratch (GNUstep's libobjc2 for Objective-C; The Browser Company/Apple's Swift runtime work for Swift).
References
- GNUstep
libobjc2(runtime): https://github.com/trunkmaster/libobjc2 / https://github.com/gnustep/libobjc2 - GNUstep Windows/MSVC toolchain: https://github.com/gnustep/tools-windows-msvc
- Mike Ash, "Performance Comparisons of Common Operations, 2016 Edition": https://www.mikeash.com/pyblog/friday-qa-2016-04-15-performance-comparisons-of-common-operations-2016-edition.html
- Saagar Jha, "Bypassing objc_msgSend": https://saagarjha.com/blog/2019/12/15/bypassing-objc-msgsend/
- "Objective-C Implementation and Performance Details for C and C++ Programmers": https://swolchok.github.io/objcperf/