Skip to content

Add macOS port for arm64 (Apple Silicon) and x86-64 - #3

Open
FBarrca wants to merge 2 commits into
ambonvik:mainfrom
FBarrca:macos-port
Open

FBarrca wants to merge 2 commits into
ambonvik:mainfrom
FBarrca:macos-port

Conversation

@FBarrca

@FBarrca FBarrca commented Nov 3, 2026 •

Copy link
Copy Markdown

Adds macOS support on both Apple Silicon and Intel Macs. Nothing changes for Linux or Windows: every shared-code change applies only to the new platforms.

New ports

src/port/aarch64/macos and src/port/x86-64/macos, using the same port interface as x86-64/linux:

  • Context switch. On arm64, AArch64 assembly saves the callee-saved registers (X19–X30, D8–D15) and FPCR/FPSR. The trampoline uses the same register handoff as the x86-64 one. The x86-64 version is the existing NASM switch built as macho64, with Mach-O's leading-underscore symbols.
  • Stacks. Coroutine stacks are allocated with posix_memalign, with a PROT_NONE guard page set by mprotect. As in x86-64/linux, freed stacks go to a per-thread pool and are reused by later coroutines of the same size, so each stack is protected once rather than on every spawn and retire. cmi_coroutine_stack_cleanup() frees the pool at thread exit. The main thread's stack bounds come from pthread_get_stackaddr_np / pthread_get_stacksize_np.
  • Page size. cmi_pagesize() asks the OS once and caches the value.
  • Seeds and cores. Seeds come from arc4random_buf, the core count from sysconf(_SC_NPROCESSORS_ONLN).

Shared changes

  • cmi_config.h: adds CMI_ARM64 and CMI_MACOS detection.
  • cimba.c: Clang does not implement __builtin_setjmp/__builtin_longjmp on AArch64, so trial recovery uses setjmp/longjmp on arm64. x86-64 keeps the builtins.
  • meson.build: NASM is required only on x86-64. macOS builds use -D_DARWIN_C_SOURCE instead of -D_POSIX_C_SOURCE=200809L, because the POSIX define hides pthread_get_stackaddr_np and arc4random_buf.
  • src/meson.build: -ffat-lto-objects is passed only when the compiler accepts it. Apple Clang rejects it.
  • src/port/meson.build: picks the port from the CPU family and OS.

Testing

  • arm64 macOS (Apple Clang): meson setup build && meson test -C build passes all 27 tests in a release build with LTO, and in a debug build with AddressSanitizer.
  • Stack pool: a churn of short-lived processes (about 100 alive, 4M spawned) drops from 1.73 to 0.70 µs per spawn on an Apple M-series Mac. Before the pool, mprotect was about 55% of the run; it no longer shows in the profile.
  • x86-64 macOS: I don't have an x86-64 toolchain, so I compiled the library with clang -arch x86_64 and nasm -f macho64, linked the test programs against it and ran them under Rosetta. All test programs pass. The meson x86-64 build path itself was not run. The stack pool commit was compiled for x86-64 with clang -arch x86_64, but not run there.

Co-Authored-By: Claude noreply@anthropic.com
Includes-AI-Code: true

New ports under src/port/aarch64/macos and src/port/x86-64/macos:
- Context switch: AArch64 assembly saving X19-X30, D8-D15, FPCR/FPSR,
  and a Mach-O (macho64) variant of the x86-64 NASM switch.
- Guarded coroutine stacks via posix_memalign + mprotect, main stack
  bounds from pthread_get_stackaddr_np / pthread_get_stacksize_np.
- Seeds from arc4random_buf, core count from sysconf.

Shared changes, all limited to the new platforms:
- cmi_config.h recognises arm64 and macOS.
- Trial recovery uses setjmp/longjmp on arm64, since Clang does not
  implement __builtin_setjmp on AArch64.
- Build: dispatch to the new ports, only require NASM on x86-64, use
  _DARWIN_C_SOURCE instead of _POSIX_C_SOURCE on macOS, and only pass
  -ffat-lto-objects when the compiler accepts it (Apple Clang does not).

Co-Authored-By: Claude <noreply@anthropic.com>
Includes-AI-Code: true
@FBarrca FBarrca mentioned this pull request Nov 3, 2026
Both macOS ports called mprotect on every coroutine stack allocation and
free, so every spawned process cost two system calls. Keep a per-thread
pool of recycled guarded stacks, as the x86-64 Linux port does: a stack
is protected once when first allocated, reused by later coroutines of
the same size, and unprotected and freed by cmi_coroutine_stack_cleanup()
at thread exit. Stacks are rounded up to whole pages, as on Linux.

cmi_pagesize() now asks the OS once and caches the value.

On an Apple M-series Mac, a churn of short-lived processes (~100 alive,
4M spawned) drops from 1.73 to 0.70 us per spawn; mprotect was 55% of
the trial before and is gone from the profile.

Co-Authored-By: Claude <noreply@anthropic.com>
Includes-AI-Code: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant