Skip to main content

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)
ResolvedAt compile time, via static-type-driven overload resolutionAt runtime, always — regardless of static type
What it resolvesAccess syntax (how to get a pointer to dereference); the call after it is ordinary static/vtable call resolutionThe call itself — dispatch and invocation are one operation
Can fail gracefully at runtimeNo — fixed at compile timeYes — falls through to message forwarding (forwardingTargetForSelector:, forwardInvocation:) if no method is found
Can be redirected after compilationNoYes — method swizzling can replace an implementation at runtime
Works with no static type infoNo — must bottom out at a compile-time-known typeYes — 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:

  1. Load the object's isa pointer (its class).
  2. 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).
  3. Cache hit: indirect jump to the cached IMP.
  4. 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).

MechanismRelative costNotes
C++ non-virtual call, inlinedFreeOften zero instructions after inlining
C++ non-virtual call, not inlinedBaselineOne direct call/return
Lambda called by concrete/auto-deduced type≈ baselineCompiler knows the exact closure type; often inlined
C++ virtual call~1 indirect callVtable slot load + jump
objc_msgSend, cache hit~2.6 ns measured (≈9 cycles) — essentially tied with C++ virtual callFull hash lookup + indirect jump, engineered to be this cheap
std::functionSlower than a plain virtual callType erasure adds an extra indirection (heap/SBO-allocated wrapper) on top of its own internal indirect call
objc_msgSend, cache missNoticeably slowerWalks class hierarchy, populates cache
NSInvocation / performSelector: / dynamic forwardingAn order of magnitude+ slowerConstructs 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:

TierC++ analogueObjective-C
Runtime support — dispatch, exceptions, RTTI/reflection, thread-safe static-init guardslibc++abi / libsupc++ (__cxa_throw, RTTI comparisons, vtable machinery)libobjc2 (GNUstep) or Apple's libobjcobjc_msgSend, class/metadata registration, method resolution, protocol tables, forwarding
Standard/foundation library — the actual data types/collectionslibstdc++ / 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 class NSObject.
  • This puts NSObject-the-class closer to std::string on the spectrum than to objc_msgSend — arguably even less "part of the language," since it isn't standardized by any body (unlike ISO C++ standardizing std::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's libdispatch (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 linking gnustep-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, @finally issues).

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