Modes explains what native mode is and when to choose it. This page is the long version: how each part works, what it costs, and exactly what it cannot run yet.
Apple ends general-purpose Rosetta after macOS 27, and with it the dyld_shared_cache_x86_64 that every guest binds against by default. Native mode is the answer to that. The guest keeps its x86_64 Darwin personality, but its system libraries are synthesized x86 images whose exports lead into the host’s own arm64 code, so a call into libSystem runs the real native implementation instead of translated Intel code. It is selected with -native or OCERZ_MODE=native, and cache mode stays the default.
Today native mode runs command-line programs whose calls stay inside what is bridged, which is most of libSystem: 2766 of its functions cross to the host, among them the C string, memory, stdio, file, time, locale and user-database functions and their fortified _chk forms, the heap, POSIX threads with their mutexes and condition variables, libdispatch with its block and function-pointer entry points, queues, groups and semaphores, and atexit; thread-local variables and signal handlers with their masks and alternate stacks are answered by ocerz itself; 781 CoreFoundation functions cross, among them CoreFoundation’s strings, arrays, dictionaries, numbers, data and run loop, including guest callbacks and run-loop timers, observers and sources. Objective-C programs that use Foundation’s strings, numbers, collections, values and formatted output, and hand Foundation blocks, run against the native Objective-C runtime, and so do their own classes, categories and protocols, including NSView subclasses that AppKit draws. A program compiled normally, with optimization and clang’s default stack protector, binds and runs, and so does one linked for a macOS older than 12 with classic lazy binding. AppKit and CoreGraphics are synthesized from the SDK like the rest, and an application bundle starts through NSApplicationMain with its own Info.plist, resources and name. Swift programs run too, on the swift.org runtime installed as guest code, as long as they stay within the Swift standard library. Every kernel of xbench_dyn produces byte-identical output in native mode and cache mode, under both the JIT and the interpreter.
A guest that links /usr/lib/libSystem.B.dylib finds nothing behind it, because on a current macOS that library exists only inside the cache. So ocerz builds one, and one for each other library native mode covers. src/vdylib.c assembles a real x86_64 Mach-O in memory, with load commands, __TEXT, __DATA and an export trie, and the loader takes it as an ordinary image. Nothing in the loader reopens a file, so import resolution and the loader’s own symbol lookups work on it unchanged. No cache is mapped, the dyld API shim is not installed, and the host workqueue bridge stays off, because the host’s own libdispatch needs the process’s single workqueue slot.
What each synthesized library exports, and how each export crosses, is data rather than code: one text file per library under runtime/apis/macos/<sdk version>/, generated from the builder’s own SDK by make apis rather than committed, such as libSystem.B.dylib.api and CoreFoundation.api, with a record per export. A fn record names the host function and its signature, data a native variable the export resolves to, var a variable ocerz fills in itself because the host’s answer is not the one the guest may see, special a function ocerz answers itself, stub an export that binds but stops with a named message when called, with the reason, shape and struct records describe the structures of function pointers the bridge converts, and inplace converts a function pointer that lives inside a structure the guest owns, for the length of one call, which is how zlib and bzip2 hand over their allocators. ocerz reads the files for the guest’s minimum macOS version, beside its own executable or from OCERZ_APIDB, and refuses a malformed file whole, naming its line, rather than binding some imports to the wrong thing. Supporting another library is a data change, not a rebuild.
The files are generated. tools/sdkgen.sh <library> reads the library’s .tbd for the exact list of x86_64 exports and parses its headers with the Command Line Tools’ libclang once for x86_64 and once for arm64. tools/sdkgen/libraries lists the supported libraries, including CFNetwork, SystemConfiguration, Security and CoreServices. An export whose declaration maps onto the engine’s classes on both architectures crosses, blocks included, each written with its own declared signature; one that cannot cross correctly, because it is variadic, takes a va_list, a long double inside a structure or a callback, or a union or bitfield structure by value, returns a block without declaring that the reference is the caller’s, or points at a structure whose layout differs between the architectures, binds as a stub that names itself when called unless a dedicated veneer handles it. tools/sdkgen/overrides holds what no header can say: the functions ocerz answers itself, the shapes of callback structures, and the calls a generic crossing would get wrong inside an emulator, such as fork, exec, setjmp, dlopen and mmap. Each run writes its coverage to tools/sdkgen/baseline and fails if a crossing was lost, and tools/sdkgen/layout_check.sh checks the generator’s structure layouts against clang’s own, using each architecture’s type names where the SDK declares different records.
In native mode, OCERZ_GUEST_ROOT selects a directory mirroring absolute guest library paths. A supplied x86_64 image takes priority over its native API database, for startup imports and dlopen, while its system dependencies still use bridges. Without that variable, the loader checks runtime/guest beside ocerz; an empty value disables this lookup. An invalid supplied image is refused instead of silently replaced with native code. tests/run_guest_library_tests.sh checks guest execution, native fallback, runtime loading and architecture refusals under both the JIT and interpreter. Cache mode ignores this lookup.
The x86_64 executable is only a launcher; the browser’s application logic resides in the private Safari framework. Forwarding SafariMain to ARM code does not exercise that x86 logic, so that shortcut has been removed. Running Safari’s application logic in native mode still requires a loadable x86 Safari framework and bridges for its dependencies. The installed x86 framework is in the shared cache, not a standalone file; an analysis-only extraction is not sufficient. Safari’s existing cache-mode path remains available.
make guest-cxx builds LLVM 21.1.8’s libc++, libc++abi and libunwind for x86_64 into runtime/guest, using pinned source revision 2078da43e25a4623cab2d0d60decddf709aaea28. It needs Git, CMake and the Command Line Tools; the first build downloads LLVM source. These are guest libraries, not Apple’s cache and not ARM replacements: ocerz executes their x86 instructions through the JIT, while libc and pthread calls cross native bridges. Their vtables, RTTI and exception unwinder stay on the guest side. _dyld_find_unwind_sections supplies guest Mach-O unwind metadata, libSystem unwind imports resolve to the loaded guest unwinder, and C++ process and thread-local destructor callbacks cross back into guest code. Locale-aware snprintf_l has a format veneer for libc++’s numeric formatting. The generated libraries and build checkout are ignored by Git; run the build on a new checkout rather than expecting the binaries to be committed.
Programs built with GCC, or with Apple’s compilers before libc++, import GNU libstdc++ from /usr/lib/libstdc++.6.dylib. Rosetta’s x86_64 shared cache still has it; on Apple silicon only an arm64 build exists, whose objects, vtables and unwinder x86 code cannot share. bash tools/fetch_guest_gnu_cxx.sh fetches the x86_64 libstdc++ and libgcc_s of GCC 15.2 and GNU libiconv 1.19 from MacPorts, pinned by SHA-256, into runtime/guest-gnu, which the loader searches after runtime/guest (OCERZ_GUEST_GNU_ROOT names another directory, and an empty value disables it). GCC 15’s libstdc++ still exports the old std::string and iostream ABI those programs were built against. The directory is ignored by Git and no release carries it, as with the 32-bit runtimes. VLC 2.2’s matroska, DASH, live555, UPnP and taglib plug-ins load through it. Without it, a program importing libstdc++ stops with exit 71 and a line naming the script.
make native-cxx compares x86 JIT and interpreter execution with independently compiled ARM fixtures, including containers, strings, streams, regex, path manipulation, RTTI, nested exceptions, guest dylib unwinding, callbacks, static initialization, threads, condition variables, TLS destructors and futures. The gate requires positive JIT translation counts and native bridge crossings and checks that no x86 cache was mapped. OCERZ_TEST_CACHE=1 make native-cxx additionally checks the existing cache-mode exception fixture. The default cache mode is unchanged. Resolving the C++ imports is not full application compatibility: applications can still stop on unbridged APIs.
The macOS 27 databases add 97 callable CFNetwork exports, 166 SystemConfiguration exports, 632 Security exports and 1177 CoreServices exports. These are SDK-checked bridge signatures, not a claim that every API or application has been tested. Callback contexts for CFHost, CFNetService, proxy auto-configuration, dynamic-store sessions, network connections, reachability and preferences convert their retain, release and description functions into guest callbacks. Unknown context versions are refused. LaunchServices LSOpen* calls remain stubs because forwarding process launch unchanged could run application code outside ocerz.
make native-frameworks tests proxy dictionaries and local PAC scripts, asynchronous proxy callbacks, callback-context lifetimes, read-only system configuration, validation of a temporary self-signed certificate using only an in-memory trust anchor with network fetching disabled, and CoreServices file references. It compares JIT, interpreter and non-fastcall bridge output against a separately compiled ARM executable, checks positive translated-instruction and bridge counts, and requires native mode without the x86 cache. OCERZ_TEST_CACHE=1 make native-frameworks also compares cache mode. On 2026-09-20 the Steam bootstrapper’s previous 18 unresolved framework imports all bind; with the guest C++ runtime it executed translated code and stopped at _sscanf, not at framework loading, which was not a successful Steam launch. One day later it was one; see Compatibility.
va_list formattingThe vprintf, vfprintf, vsprintf, vsnprintf, vasprintf, vasprintf_l, vdprintf, vsnprintf_l, __vsprintf_chk and __vsnprintf_chk bridges, and the verr, vwarn and vsscanf_l families, NSLogv and CoreFoundation’s ...AndArguments calls, read x86 register-save and overflow areas and build ARM argument slots using the existing format parser. They do not pass an x86 va_list directly to native code. make native-formats checks mixed integer and floating-point arguments past both register limits, copied and partially consumed lists, truncation, allocation, locale formatting, file descriptors, errno and fortified entry points in both engines and the non-fastcall bridge. Positional formats, long double and %n remain explicit refusals. OCERZ_TEST_CACHE=1 make native-formats adds a cache-mode comparison.
Every export is twelve bytes of real x86: a move of the export’s id into r11, then a jump through a slot that holds one address for the whole process, inside the trap window the decoder and both engines already watch. src/bridge.c catches the trap, or, from translated code, is called directly from inside the block that jumps to it. src/abi.c reads the arguments out of the guest’s register state according to the function’s signature, and src/abicall.s loads them into the arm64 argument registers and calls the real function. The signature matters because the two ABIs count integer and floating-point arguments in separate sequences, so one double in the middle of a signature moves nothing on one side and everything on the other. It also keeps widths honest: arm64 makes the caller extend a narrow argument, and a 32-bit result can come back with the upper half of the register dirty.
A native function that takes a function pointer calls it, and when the guest supplied that pointer it names x86 code native code cannot jump to. So a callback argument carries its own signature, qsort being v(pLLc{i(pp)}), and the guest function is bound to one slot in a fixed, assembled bank of 65536 arm64 trampolines. Native code receives the slot’s address, and the same function always gets the same address. When native code calls the slot, the guest function runs on that thread with its arguments where System V expects them. A comparator can make bridged calls of its own, and nesting goes as deep as the guest’s stack allows.
A framework calls back on threads of its own: libdispatch runs work on its workers, and a run loop or an audio device has a thread of its own too. Such a thread has no x86 registers and no guest stack. The first time one calls a guest function it is given a guest personality of its own: a cpu, a guest stack and a guest thread block behind gs, registered like any other guest thread and reused on every later call. Because a second thread running guest code is a second observer of guest memory, that also retires plain memory mode, as starting a guest thread already does. The personality is torn down when the host thread exits. dispatch_async_f, dispatch_apply_f spreading work across several workers at once, and guest work that makes bridged calls and nested callbacks of its own all run this way.
pthread_create’s start routine is exactly such a callback, so guest threads need nothing further: the native pthread_create starts a host thread, that thread is given a guest personality on its way into the start routine, and the result comes back through pthread_join. Mutexes and condition variables are forwarded unchanged. That is sound because pthread_mutex_t, pthread_cond_t and the other pthread types have the same size, alignment and initializer values on x86_64 and arm64, so a mutex a guest initialized statically is already a valid native one.
thread_suspend, thread_resume and thread_get_state are bridged, because garbage collectors use them: JavaScriptCore’s allocator and Chromium’s samplers stop every other thread and read its registers. The kernel call is the real one, since a cooperative stop leaves the suspend count where the caller can see it, and what makes it safe is refusing to leave a thread stopped in a place that would deadlock the caller. A thread holding the JIT lock or the map lock is one, which a per-thread critical depth records; a thread anywhere inside the host’s own allocator is another, read off its stack, since the suspender’s next malloc would wait for a lock it holds. Such a target is resumed and tried again. thread_get_state answers the guest’s x86 registers, reconstructed from the arm64 registers the stopped thread was holding them in.
A __thread variable is reached through a descriptor whose first word is a thunk the compiler calls with the descriptor in RDI, expecting the variable’s address back in RAX. In cache mode that thunk is dyld’s own x86 code from the shared cache. In native mode the synthesized libSystem exports __tlv_bootstrap and ocerz answers it: each image’s descriptors are rewritten at load time with an ocerz key per image, and each thread keeps a table of its per-image blocks in its guest thread block, filled from the image’s template on first touch and freed when the thread goes away. The thunk’s convention preserves every register except RAX, and compilers rely on that, so its stub, unlike every other, saves r11 across the trap.
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation is the second synthesized image, and its exports lead into the host’s own CoreFoundation. A CFTypeRef crosses unchanged in both directions: a position-independent guest runs in the identity address map, so a native object is an address the guest can hold, compare and hand back, and the same object fetched twice is the same pointer. Its data symbols, kCFAllocatorDefault, kCFTypeArrayCallBacks, the CFSTR class reference and the rest, are exported as absolute addresses of the native variables themselves rather than as copies, so a CFSTR literal compiled into the guest is a valid native constant string and &kCFTypeArrayCallBacks is the address CoreFoundation knows. Boolean and UniChar travel as 8- and 16-bit classes, extended the way arm64 expects and x86 does not guarantee. A structure of function pointers passed by address, an array’s or dictionary’s callbacks or a timer’s, observer’s or source’s context, is copied and each guest function in it is given a trampoline, while a pointer that already names native code, as every field of a copy of kCFTypeArrayCallBacks does, passes through unchanged. Opening the native framework runs its initializers, and __CFInitialize calls setenv, so the guest’s environment is a copy taken before loading starts.
libobjc and Foundation are synthesized too, and an Intel program that sends messages to Foundation objects runs against the native runtime with no x86 runtime of its own. Every class it names, NSString or the NSConstantIntegerNumber behind an @42 literal, is exported as the native class itself. Before any guest code runs, each image’s selector references are rewritten to the native runtime’s selectors, so a guest compares selectors the way native code does. objc_msgSend and its super and _stret forms are answered by ocerz: it asks the native runtime for the receiver’s class and the method’s type encoding, turns the encoding into a signature, caches it per class and selector, and sends the message through the native objc_msgSend under that signature, with a structure result such as an NSRange or a CGRect placed wherever each ABI wants it. A method the class does not implement is signed through methodSignatureForSelector: and forwarded natively. A nil receiver answers zero. Foundation’s methods declared with an ellipsis, stringWithFormat:, arrayWithObjects: and the rest, are listed by name, since an encoding does not say a method is variadic, and their extra arguments are gathered from where System V left them and packed into eight-byte stack words as Apple’s arm64 expects. The printf family, NSLog and CFStringCreateWithFormat work the same way: the format string says what each argument is, and the arguments go to the native v form as a va_list. An uncaught Objective-C exception is reported with the bridged call it escaped from before the process ends as it would natively.
A program that defines Objective-C classes, subclassing NSObject or NSView, has them realized by the native runtime in place: the class structures the Intel compiler emitted are laid out the same on both architectures, so ocerz rewrites each method list to hold native trampolines into the guest’s x86 methods, points the class at it and hands the class to objc_readClassPair, superclasses first. The guest’s own references to its classes stay valid, native code calls a guest -drawRect:, -hash or -compare: like any other method, and the runtime slides a subclass’s instance variables past the native superclass’s arm64 size and writes the offsets the guest’s code reads. Categories are added to guest or native classes, protocols are matched to native ones by name or registered, and +load methods run before main, followed by every guest image’s C and C++ initializers, a library’s before the program’s. A method whose types cannot cross is installed as a stub that names itself if native code calls it, and the guest’s own sends of it go straight to its x86 code: VLC initializes its menu items with a union by value. A subclass that reads a native superclass’s instance variable through its _OBJC_IVAR_$ symbol, as VLC does NSWindow’s _styleMask, gets the native runtime’s offset for it, since the object is the native class’s. NSBundle’s bundleForClass: finds the bundle of the image ocerz mapped a guest class from, which Sequel Pro loads its nibs through. The trampoline bank holds 65,536 methods, sharing one parsed signature per distinct type string.
A block is an object whose third word is the function that runs it, called with the block itself as its first argument, and whose descriptor holds its size, the helpers that copy and dispose of what it captured, and its signature. The layout, the flags and the signature strings are the same on x86_64 and arm64, and in native mode the guest’s __NSConcreteStackBlock and its siblings are the native classes, so a block the guest made is a well-formed native block in every word but its functions, which are x86 code. So a block crosses as a pointer that is converted, class k in the notation with the block’s declared signature in braces: dispatch_async is v(pk{v()}), and an @? in a method’s type encoding is k{}, because the block carries its own signature and that is the one used. A guest block handed to native code becomes a native wrapper: a block whose invoke is a trampoline of the callback bank bound to the guest block’s own function under the guest block’s own signature, whose first argument the trampoline turns back into the guest block, and whose dispose helper releases the guest block. A global guest block gets one global wrapper for the life of the process, a heap block the same wrapper for as long as that wrapper lives, and a stack block is first copied to the heap guest-side, its copy helper running as guest code. A native block handed to guest code, as the completion handler a native API passes to a guest method, or as the block a function returns, becomes a guest view whose invoke is a twelve-byte x86 trampoline in a guest page, so guest code calls it the way it calls any block, and the trampoline calls the native block under its own signature. Either wrapper handed back to the side that made the block turns into the block again. Both are heap or global blocks with native helpers, so Block_copy, Block_release, objc_retainBlock and the rest are right on them from either side. The block runtime a guest imports is libclosure’s own except where libclosure would run a guest helper as arm64 code: _Block_copy, objc_retainBlock and _Block_object_assign copy a guest stack block, and a __block variable still on the stack, guest-side, and give the heap copy helpers that are callback trampolines, so the native _Block_release that finally frees it runs the guest’s dispose helper as guest code. An id argument that holds a guest block where the class’s own encoding says @ crosses as a block when the receiver’s method signature says @?, which is what an NSXPCConnection proxy’s methods look like, and a protocol of the guest’s own carries its extended method types into the native runtime, which NSXPCInterface needs to describe a reply block. Stickies, which stopped at its first dispatch_once, now runs: it opens a note window and stays up.
Native Foundation decides which bundle is the main one, what the process is called and what its arguments were by asking the process, and the process is ocerz. So ocerz answers for the guest before anything asks. Before a framework is loaded, the host’s own argv, argc and program name become the guest’s, and the first framework opened opens CoreFoundation with CFProcessPath naming the guest’s executable, which CoreFoundation honours in an unrestricted process and keeps once its initializer has read it. The variable is gone again as soon as that initializer returns, so neither the guest’s environment nor a child it starts inherits it. [NSBundle mainBundle] and CFBundleGetMainBundle are then the application’s bundle, with its Info.plist and resources; NSProcessInfo, the prefix NSLog writes and getprogname name the application; LaunchServices registers the process under the bundle’s identifier; _NSGetArgv, _NSGetArgc, _NSGetEnviron and _NSGetProgname answer the same variables as the guest’s NXArgv, NXArgc, environ and __progname; and _NSGetExecutablePath and _NSGetMachExecuteHeader, which host dyld would answer for ocerz, are answered by ocerz for the guest. NSApplicationMain crosses as an ordinary call. It reads the Info.plist, looks the principal class up by name, loads the main nib or storyboard out of the application’s bundle, instantiating the guest’s own classes by name, and runs the application on the main thread with every delegate method and action a call back into guest code. When the application terminates, the host’s exit runs the guest’s atexit handlers and flushes its output, and the process ends with the status it was given. The x86_64 slice of Image Capture runs this way: its principal class is an NSApplication subclass of its own, its window, split view and toolbar come out of its storyboard as classes of its own, and it opens that window and quits with status 0 when asked to.
Host dyld has never heard of an image ocerz loaded, and the host’s dlopen would load arm64 code, so dlopen, dlsym, dladdr, dlclose, dlerror, dlopen_preflight and the dyld calls a program walks its images with are answered by ocerz’s own loader. A guest x86_64 dylib or bundle is mapped and its whole closure bound first; only then is it listed, its thread-local variables registered, every _dyld_register_func_for_add_image callback called for it, its selectors rewritten and its classes, categories and protocols defined in the native runtime, and then, image by image with dependencies first, its +load methods and its initializers run on the calling thread, the order dyld uses. A load that cannot bind, because a symbol is missing or a dependency will not load, fails with dyld’s own wording, Symbol not found or Library not loaded, and leaves nothing mapped behind it. A path resolves as dyld resolves it, @executable_path, @loader_path and @rpath against the calling image, a bare name through the library search paths, and a library a database describes, reached by its install name, a symlink such as a framework’s top-level binary, or an alias the host’s shared cache knows such as /usr/lib/libc.dylib, answers with its synthesized image. An arm64 library with no database, on disk or in the host’s cache, is refused with native library without an API database. A system library no Mac with Apple silicon has at all, under /System/Library or /usr/lib and neither on disk, in the host’s cache, in a guest root nor in a database, is one Apple shipped for Intel Macs and Rosetta’s cache still holds: QTKit, VideoDecodeAcceleration, the 0.9.x libcrypto. Its imports, for the program or for a plug-in it opens, bind to stubs answering 0 or nil, each named on standard error as it binds, rather than refusing the whole program over it; Transmission and Sequel Pro link the old libcrypto through Sparkle, and VLC’s interface links QTKit for its capture panel alone. OCERZ_NO_STUB_REMOVED=1 refuses them again, and OCERZ_STUB_MISSING=1 stubs any import that does not bind. A handle is the image’s mach header, with the low bit set for RTLD_FIRST, and dlopen(NULL) answers RTLD_DEFAULT, as dyld does; dlsym searches with dyld’s order for a handle and its dependencies, RTLD_DEFAULT, RTLD_NEXT, RTLD_SELF and RTLD_MAIN_ONLY, RTLD_LOCAL hides an image from the global searches, and a plug-in bundle linked with -bundle_loader binds to the classes and functions of the program that loads it. dladdr names a guest function from its image’s symbol table and a synthesized library’s export from its export trie, and an address in native memory, such as a data export’s target, is answered by the host’s own dladdr. The image list is the program and then every image the loader holds, the synthesized libraries among them, since those are the libraries x86 code expects to find there; the program’s SDK, minimum OS and platform come from its own LC_BUILD_VERSION. dlerror is kept per thread in guest memory, and nothing is ever unloaded, so dlclose answers 0.
A signal handler is x86 code, so a native sigaction cannot be handed one. sigaction, signal, sigprocmask, pthread_sigmask, sigaltstack, raise, kill and pthread_kill are answered by ocerz itself, against the same handler table, masks and alternate stacks the syscalls change in cache mode, and a handler is entered through a small x86 trampoline ocerz writes into guest memory in place of the _sigtramp an x86 libc would supply. A handler runs on the state after the call that raised it, so raise returns with its handler already run, and a signal unblocked by sigprocmask is delivered before that call returns. A signal from outside, or from another thread, is delivered when the receiving thread’s next bridged call returns, the way cache mode delivers one at the next syscall; a thread spinning in its own code without calls does not see it in either mode.
Four more groups of libSystem calls cannot cross by signature, and ocerz answers them itself, in src/sysbridge.c. open, openat, fcntl, ioctl, sem_open, shm_open, semctl and ulimit are declared with an ellipsis that stands for one argument, or two, whose presence and type the named arguments decide: the handler reads it where x86-64 left it, only when the call uses it, and calls the host function through its real variadic prototype, so a mode reaches open only with O_CREAT and fcntl translates its argument only for the commands that take a pointer. Every structure those calls point at, struct flock, fstore_t, struct winsize, struct termios, struct semid_ds and the rest, has the same layout on both architectures, which was checked rather than assumed. mmap, munmap, mprotect, madvise and the Mach vm_allocate, vm_deallocate and vm_protect calls run the code the syscalls and Mach traps run in cache mode, so guest memory comes out of the guest arena, is accounted for in 4 KB pages, and code a guest writes into a mapping and then makes executable is translated afresh; a range ocerz never mapped, a page from the host’s malloc or a list a native Mach call returned, goes to the host kernel. setjmp, sigsetjmp, longjmp and their relatives save and restore the guest’s own registers, signal mask, alternate-stack state, MXCSR and x87 control word in Apple’s x86_64 jmp_buf layout, so a jump out of a signal handler works as it does natively. A jump from inside a callback a native function made, a qsort comparator say, to a setjmp taken before that call would leave the native function’s frames behind, and is refused by name rather than performed. fork, vfork, the seven exec functions, posix_spawn, posix_spawnp, system and popen go through the fork, exec and spawn that cache mode’s syscalls use, so every program a guest starts runs under ocerz, in native mode, with the argv the guest built, argv[0] included, and a script starts under its #! interpreter. What runs is decided as in cache mode: a universal binary runs its x86_64 slice under ocerz, and a program with an arm64 slice and no x86_64 slice runs natively, as it would under Rosetta. system and popen run their command under /bin/sh, as Apple’s x86 libc does, and on macOS that is bash, which links libncurses, so native mode synthesizes that library as well. A fork child is a working native-mode process, whose bridged calls, callbacks and new threads behave as the parent’s; only a fork inside a native callback that translated code called is refused, because the child cannot return into translations it does not inherit.
Not every import is a function. A stack-protected program reads ___stack_chk_guard in every function prologue, so the synthesized libSystem exports it as a slot of its own holding a random canary drawn when the image is built. Every other variable is exported as the native variable itself, an absolute address in the export trie: ___stdoutp and ___stderrp are the host’s FILEs, so the x86 getc and putc macros read the native buffers, whose layout is the same on both architectures; __DefaultRuneLocale is the host’s table the ctype macros read; _environ is the host’s environment, which is the guest’s. A variable that holds a function pointer the guest could overwrite, such as _vprintf_stderr_func, is left out, because native code would call whatever x86 address the guest stored there.
A bridged call is the one place a thread the guest drives runs native code. A fault during one is reported as such, naming the call and saying whether the faulting address was in guest space, meaning the guest passed a bad pointer, or outside it, meaning ocerz marshalled the call wrong. The process stops there, because a native frame can be neither resumed nor unwound:
ocerz: BRIDGE-FAULT[35366] SIGBUS inside a bridged call, not in guest code
ocerz: call=/usr/lib/libSystem.B.dylib:_strlen sig='L(p)' host_fn=0x18980eac0 depth=1
ocerz: cause: the fault address is in guest space, so the guest passed a bad pointer to _strlen
One fault inside native code is expected and not reported: the native Swift runtime calling a guest Swift class’s destroy function, which ocerz runs as guest code instead, as Swift describes. A guest fault inside a callback is the guest’s own and is handled the ordinary way. A guest writing into a page it has already executed still has its translation invalidated and carries on, even when the write came from a bridged memcpy.
objc_allocateClassPair, class_addIvar, class_addMethod and objc_registerClassPair work in native mode. Added guest methods receive native callback trampolines from their Objective-C type encodings, including floating-point and structure arguments and results. This also lets a guest’s +resolveInstanceMethod: install its implementation on demand. Adding an inherited-method override or allocating a new class invalidates cached message signatures, so an old signature is not reused after a method changes or a disposed class’s address is recycled. Unsupported method encodings stop with a named error before installing an unsafe implementation. The native_objc_runtime fixture compares native-mode JIT and interpreter results with arm64 and cache-mode execution.
A Swift program carries no runtime of its own. It links /usr/lib/swift/libswiftCore.dylib, which on a current macOS is arm64 code inside the shared cache, and Swift’s calling convention, metadata and generics give no signature a crossing could convert. So native mode runs the Swift runtime as guest code. make guest-swift downloads the swift.org 6.4.0 toolchain package, checks its SHA-256 and its Developer ID signature, and copies the x86_64 slices of the thirteen runtime libraries, libswiftCore, libswift_Concurrency, libswift_StringProcessing and the rest, into runtime/guest/usr/lib/swift, with the Swift project’s license beside them as LICENSE.swift.txt. The lookup described under Guest application libraries then loads them in place of the native databases. OCERZ_SWIFT_PKG names a package already downloaded, and OCERZ_SWIFT_BUILD the scratch directory, which holds only the package and is emptied after the copy. Every Swift call stays x86 code, and only the runtime’s own calls into libSystem and libobjc cross, as a C program’s do.
Most of those calls are ordinary crossings. Four kinds are not, and ocerz answers them itself:
_objc_realizeClassFromSwift; the classes it instantiates while running arrive through objc_readClassPair; and a specialization the compiler emitted ahead of time, which no class list names, is defined the first time it reaches the native runtime. The guest runtime’s own root class, Swift._SwiftObject, is defined like any other, which is why native mode now accepts root classes.objc_setHook_getClass, objc_setHook_getImageName and objc_setHook_lazyClassNamer, and asks through objc_addLoadImageFunc to see every image. Each hook runs behind a native wrapper that calls the guest’s function and falls back to the hook it displaced, and the image function is called for every guest image, including each one a dlopen loads later.pthread_key_create makes, and pthread_key_init_np records a destructor for one.AnyObject value with a plain objc_release, and every element of an array of class instances is stored as one; libobjc hands a Swift object on to swift_release. The native libobjc would hand it to the native Swift runtime, so the guest’s objc_release of a guest Swift object goes to the guest’s own swift_release instead. When native code drops the last reference itself, as an autorelease pool the native runtime drains does, the native swift_release calls the class’s destroy function, which is x86 code. That call faults, and ocerz recognizes it by the object Swift passes in its context register, runs the destroy function as guest code, and returns to the native caller. Freeing a million objects through arrays takes 130 ms, against 52 ms under Rosetta.Swift Concurrency runs on its own guest runtime, libswift_Concurrency, whose executor is libdispatch’s thread pool, and needs two things from native mode beyond the voucher and recursive-lock calls it makes. An asynchronous main ends in dispatch_main, which in libdispatch ends the main thread; native mode runs the native main run loop there for good instead, which drains the main queue that main-actor jobs are sent to. And the runtime makes its sleep deadlines out of nanoseconds and hands them to libdispatch as a dispatch_time_t, because on an Intel Mac a tick is a nanosecond. On Apple silicon it is 125/3 of one, which put every Task.sleep deadline about forty times the machine’s uptime in the future. So a dispatch_time_t crosses in Intel’s units: dispatch_time answers nanoseconds, and dispatch_after, dispatch_after_f, dispatch_source_set_timer and the wait functions convert what they are handed to host ticks, while wall-clock times, which are nanoseconds on both, pass as they are. mach_absolute_time and mach_timebase_info stay the host’s, so a host time a native framework hands back, a CoreAudio timestamp or a display link’s, still compares with them. Actors, task groups, async let, detached tasks, the main actor, cancellation and AsyncStream give the arm64 build’s output under both engines.
make native-swift runs tests/run_native_swift_tests.sh. It compiles each fixture for x86_64 and arm64 from the same source and module name, and the x86_64 build under -native must print exactly what the arm64 build prints, under the JIT and under the interpreter. native_swift covers classes, generics, protocols with associated types and existentials, enums, errors, closures, Mirror, key paths, Unicode strings, a Regex built at run time and a weak reference that must read nil once its object is gone. native_swift_release covers the release paths above, and requires the destroy crossing for three objects whose last release comes from a natively drained pool. native_swift_concurrency is the Concurrency program just described, and native_dispatch_time times one call of each libdispatch function that takes or returns a dispatch_time_t, from C, so a failure points at the boundary rather than at the Swift runtime. The suite skips without the guest runtime or without a swiftc.
What does not run yet is the Swift API of any system framework. import Foundation and import Dispatch bring Swift overlays whose exports are Swift functions, String._bridgeToObjectiveC or a DispatchQueue method, and those have no crossing: the program stops with exit 72 or 71 naming the first one, as Foundation’s _$sSS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF. SwiftUI, Combine and every other framework with a Swift interface are the same.
An export with no bridge behind it names itself and stops with exit status 72. An import that never bound at all stops the process with 71 before the guest runs, so the two failures stay distinguishable. An import is looked for only in the library it names: a CoreFoundation import CoreFoundation does not export is reported as missing rather than bound to a libSystem export of the same name. A static image is refused outright.
$ ./ocerz -native ./read_number
ocerz: bridge: /usr/lib/libSystem.B.dylib _scanf not implemented
A variadic function crosses only through a veneer that knows where its named arguments stop and what the rest are. Apple’s arm64 passes every variadic argument on the stack in eight-byte slots and uses no floating-point register, the opposite of its packing for an ordinary call, so a fixed signature would be wrong. The printf family, NSLog, CoreFoundation’s format functions and Foundation’s variadic methods have veneers, and open, fcntl, ioctl and the other calls whose optional argument is fixed have handlers of their own; scanf and the rest do not yet. A structure passed or returned by value is written with its members in braces, {LL} for NSRange and {{dd}{dd}} for CGRect, and crosses as bytes gathered from wherever one ABI put it and scattered to wherever the other wants it: System V classifies a small structure eightbyte by eightbyte and returns anything over sixteen bytes through a pointer in RDI, while Apple’s arm64 passes up to four floats or doubles in vector registers, any other structure over sixteen bytes as a pointer to a copy, and returns the largest through x8.
A getpid crossing costs about 17 ns on an Apple M5, measured as a guest loop making ten million calls and timing itself, against 0.3 ns per iteration for the same loop with no call. An Objective-C send, -[NSString length], costs about 28 ns. The eleven string and memory routines named under Performance do not cross at all: a short strlen costs 2 ns where its crossing cost 17, and a 48-byte memcpy 3 ns where it cost 21.
| guest loop, ns per iteration | trap path (OCERZ_NO_BRIDGE_FASTCALL=1) |
default | arm64 build |
|---|---|---|---|
getpid |
40.5 | 17.0 | 0.7 |
strlen of a 12-byte string |
42.0 | 2.0 | 0.9 |
memcpy of 48 bytes |
45.6 | 3.0 | 2.7 |
-[NSString length] |
51.3 | 28.3 | 3.6 |
qsort comparator, per call back into guest code |
426 | 430 | 3.1 |
The first three rows were measured on 2026-09-21; the last two are older measurements on the same machine. With OCERZ_NO_LEAF_INPLACE=1 the strlen row reads 17.3 and the memcpy row 20.8.
A fault inside one of those routines is the guest’s own. The routine runs with the guest’s registers still in host registers and never writes the link register, so the handler attributes the fault to the branch that reached it, recovers every guest register from the faulting thread, and delivers the signal at the stub’s instruction: a guest that hands strlen a bad pointer and has a SIGSEGV handler gets its handler, with rbx and r12 to r15 holding what the caller left there (sys_strings_fault in tests/run_native_tests.sh). A fault inside any other bridged call is still several native frames deep in code ocerz can neither resume nor unwind, and ends the process with a report naming the call.
Translated code does not take the trap. When the JIT translates a stub it calls the bridge from inside the block and then returns to the caller with a real ret, so the host return predictor stays in step with the guest’s calls, where the trap path left the block, reached the bridge through the run loop and came back through the dispatcher. Only the xmm registers an export’s signature reads or writes are spilled around the call, because System V leaves every xmm register volatile across a call; __tlv_bootstrap and ___chkstk_darwin, whose callers rely on every register surviving, spill all sixteen. A signature whose arguments all fit in registers crosses without building a stack block, and FPCR is written only when the guest’s rounding mode is not already the default. A crossing that delivers a signal, retires translated code or has to stop the thread goes back through the dispatcher.
A call back into guest code is the expensive direction: about 430 ns, most of it three system calls that save and clear the signal mask and the alternate-stack state for the callback’s fault recovery point, and a copy of the 6 KB cpu.
Against cache mode, the cost shows only in code that crosses in a tight loop. Each ratio is the paired delta over the kernel at its default size and at half of it, on an Apple M5:
| kernel | native vs cache | trap path vs cache | why |
|---|---|---|---|
str |
1.03x | 15.0x | strlen on short strings; answered in place by default, one crossing per call on the trap path |
memcpy |
0.99x | 2.33x | copies of up to 4 KB; answered in place up to 16 KB, by the host’s memmove above that |
hash |
0.99x | 0.99x | no bridged calls |
depchain |
1.01x | 1.00x | no bridged calls |
Guest code that never crosses pays nothing, which is why the last two rows are at parity. The table is from 2026-09-21; before the in-place routines the first two rows read 5.05x and 0.97x by default.
The mode is process-wide and fixed before the VM starts, because the JIT materializes its trap-window bounds once, and a spawned child inherits it through OCERZ_MODE, even one started with no environment at all. tests/run_native_tests.sh pins all of this end to end. tests/unit/test_apidb.c, test_vdylib.c, test_bridge.c, test_abi.c, test_callback.c, test_objcbridge.c and test_blocks.c pin the database format and its refusals, the synthesized image, the crossings built from the database and the calls ocerz answers itself, argument placement across the signature space, the trampoline bank, the Objective-C encodings, formats, sends and class realization, and the block wrappers, their lifetimes and the guest’s block runtime; test_syscall.c pins the argument vector and environment a spawned or exec’d child is started with.
Each of these stops a program with a named message, exit 71 when an import cannot bind or 72 when a bound export has no crossing, rather than running it wrongly:
runtime/apis/macos/27.0/: libSystem, libobjc, CoreFoundation, Foundation, AppKit, CoreGraphics, ApplicationServices, CoreServices, Carbon, Cocoa, Security, IOKit, Metal, OpenGL, QuartzCore, ImageIO, WebKit, CoreData, Contacts, CloudKit, Network, UniformTypeIdentifiers, AddressBook, AudioUnit, PDFKit, MetalKit, Kerberos, JavaScriptCore, ScreenCaptureKit, SceneKit, SpriteKit, GameKit, MapKit, NetworkExtension, ColorSync, Quartz, the audio and video frameworks, sqlite3, libxml2, libxslt, libcurl, libedit, libexpat, libsasl2, libpam and libcompression, and the smaller dylibs a program picks up on the way, libz, libbz2, libiconv, libsandbox, libcups and libresolv among them. libxo, libmd and libarchive have databases whose exports are all stubs, since no SDK header declares them. Other system libraries still need one, and unsupported exports within these databases remain stubs. Missing imports are named; dlopen of a native library without a database answers NULL with native library without an API database. The system frameworks’ Swift APIs, WebKit and GameKit remain among the gaps for larger applications.make guest-cxx), C++ exceptions unwind between guest frames and guest dylibs, and so do Objective-C ones: @throw, @try, @catch by class, subclass or id, @finally, @synchronized and rethrow work, the guest’s exceptions being built on its own libc++abi as Apple’s libobjc builds them on the C++ runtime. An exception a native method raises, [NSException raise:...] or Foundation’s range and argument checks, is caught where the send returns to the guest and thrown on into the guest’s frames. One that is never caught calls the uncaught-exception handler and aborts, as it does natively. A guest exception still cannot unwind out of a callback native code made into the guest, nor a native C++ exception into the guest; both end as uncaught. Without the guest runtime the Objective-C exception calls stop the program with a named message./usr, /bin, /sbin, /System or /Library/Apple that has an arm64 slice and is not a shell or another launcher (sh, env, xargs, perl, python3 and the like, which go on to start programs of their own), runs as itself: nothing about it needs translating, and its x86_64 slice would often need a library native mode only has stubs for, as wc does with libxo. OCERZ_NO_NATIVE_CHILDREN=1 runs those under ocerz too. Everything else a guest starts runs under ocerz in native mode and stops the way any program does when it needs what native mode lacks. A program with neither slice ocerz can use is refused, as it is in cache mode. forkpty, daemon and wordexp are stubs, and a fork inside a callback that translated code made is refused.scanf family except scanf, fscanf, sscanf, sscanf_l and their v forms, and asl_log, are stubs; the printf family including the _l and wide forms, syslog, warn, warnx, warnc, err, errx, errc, NSLog, CoreFoundation’s format functions, the four CFCalendar calls that take one argument per component letter, and Foundation’s variadic methods work, and so do open, fcntl, ioctl and the other calls whose optional argument is fixed.class_addMethod, class_replaceMethod, method_setImplementation, imp_implementationWithBlock and lazy method resolution work, and an IMP the native runtime hands the guest is a thunk it can call. The bulk method APIs remain stubs, and imp_implementationWithBlock refuses a block native code made.Beyond the list above:
va_list the bridge has no veneer for, or a union or bitfield structure by value, are stubs; a long double argument or result crosses as a double, so it loses the x87 format’s extra precision, as is a function taking a guest function pointer inside a structure the bridge has no shape for;pack(2) on x86, which moves a gss_OID_desc’s pointer; their exported descriptors and descriptor pointers are variables holding x86 copies, and calls that only pass buffers, contexts and names cross, but a call that passes or returns a descriptor is still a stub;fenv.h functions work on the guest’s own x87 and MXCSR state, the way x86 libm does, but an exception the guest unmasks does not trap, and stmxcsr does not show the flags arithmetic raised, though fetestexcept does;longjmp from inside a callback a native function made, to a setjmp taken before that call, is refused, since it would leave the native function’s frames behind;msync, mlock and minherit reach the host kernel with the guest’s address unchanged, as they do in cache mode;XDR_* macros do, would call arm64 code;stderr’s _write, runs when native code calls it, named once on standard error, with the first six integer arguments; one taking floating point or a structure by value gets garbage there (OCERZ_NO_NATIVE_BOUNCE=1 stops the process instead);@? in a type encoding, or an id argument the receiver’s method signature calls @?; one that crosses as a plain object, an id argument or result or an element of a collection, is passed as the pointer it is, so either side can retain, copy and release it but only the side that made it can call it, and a guest stack block handed over that way under manual reference counting would be copied by native code with its x86 helpers;Contents/XPCServices, is not reached, most likely because launchd looks such a service up for the executable that asks, which is ocerz: Stickies logs that it could not communicate with its migration helper and carries on without it;dispatch_once predicate with a generation count before the ~0 an x86 header’s inline dispatch_once tests for, so the calls just after the first still cross to the native dispatch_once, which returns at once;-no-jit they keep their values.