From 52b2bf580b8806991f045c29224ed8f2eb4ad4fa Mon Sep 17 00:00:00 2001 From: FBarrca Date: Tue, 3 Nov 2026 13:53:32 +0100 Subject: [PATCH 1/2] Add macOS port for arm64 (Apple Silicon) and x86-64 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 Includes-AI-Code: true --- meson.build | 15 +- src/cimba.c | 8 + src/cmi_config.h | 15 +- src/meson.build | 3 +- src/port/aarch64/macos/cmb_random_hwseed.c | 19 +++ .../aarch64/macos/cmi_coroutine_context.S | 63 ++++++++ .../aarch64/macos/cmi_coroutine_context.c | 128 +++++++++++++++ src/port/aarch64/macos/cmi_cpu_cores.c | 15 ++ src/port/aarch64/macos/cmi_memutils.c | 46 ++++++ src/port/aarch64/macos/meson.build | 7 + src/port/meson.build | 11 +- src/port/x86-64/macos/cmb_random_hwseed.c | 19 +++ .../x86-64/macos/cmi_coroutine_context.asm | 59 +++++++ src/port/x86-64/macos/cmi_coroutine_context.c | 146 ++++++++++++++++++ src/port/x86-64/macos/cmi_cpu_cores.c | 15 ++ src/port/x86-64/macos/cmi_memutils.c | 46 ++++++ src/port/x86-64/macos/meson.build | 13 ++ src/port/x86-64/meson.build | 2 + 18 files changed, 620 insertions(+), 10 deletions(-) create mode 100644 src/port/aarch64/macos/cmb_random_hwseed.c create mode 100644 src/port/aarch64/macos/cmi_coroutine_context.S create mode 100644 src/port/aarch64/macos/cmi_coroutine_context.c create mode 100644 src/port/aarch64/macos/cmi_cpu_cores.c create mode 100644 src/port/aarch64/macos/cmi_memutils.c create mode 100644 src/port/aarch64/macos/meson.build create mode 100644 src/port/x86-64/macos/cmb_random_hwseed.c create mode 100644 src/port/x86-64/macos/cmi_coroutine_context.asm create mode 100644 src/port/x86-64/macos/cmi_coroutine_context.c create mode 100644 src/port/x86-64/macos/cmi_cpu_cores.c create mode 100644 src/port/x86-64/macos/cmi_memutils.c create mode 100644 src/port/x86-64/macos/meson.build diff --git a/meson.build b/meson.build index 7bd86e2f..ad8db781 100644 --- a/meson.build +++ b/meson.build @@ -11,8 +11,10 @@ project('cimba', ['c'], ] ) -# We also need the portable Netwide Assembler for assembly code -add_languages('nasm', required : true, native : true) +# We also need the portable Netwide Assembler for the x86-64 assembly code +if host_machine.cpu_family() == 'x86_64' + add_languages('nasm', required : true, native : true) +endif # Try to find CUDA, but don't fail if it's missing add_languages('cuda', required : false, native : true) @@ -33,11 +35,18 @@ cc = meson.get_compiler('c') # Flags common to all build types common_flags = [ '-Wno-pedantic', - '-D_POSIX_C_SOURCE=200809L', '-fno-semantic-interposition', '-ffp-contract=off', ] +if host_machine.system() == 'darwin' + # Keep Darwin extensions such as pthread_get_stackaddr_np() and + # arc4random_buf() visible + common_flags += '-D_DARWIN_C_SOURCE' +else + common_flags += '-D_POSIX_C_SOURCE=200809L' +endif + # Flags per build type # 'debug' : full asserts and logging, no optimization # 'release' : standard optimized build, release asserts only diff --git a/src/cimba.c b/src/cimba.c index ccc28e8d..6b860461 100644 --- a/src/cimba.c +++ b/src/cimba.c @@ -23,6 +23,7 @@ #include #include +#include #include #include "cimba.h" @@ -75,9 +76,16 @@ static uint64_t leaking_trials; static CMB_THREAD_LOCAL cimba_trial_cleanup_func *trial_cleanup_func = NULL; static CMB_THREAD_LOCAL void *trial_cleanup_arg = NULL; +#if CMI_ARCH == CMI_ARM64 +/* Clang does not implement __builtin_setjmp on AArch64, use the libc version */ +static CMB_THREAD_LOCAL jmp_buf recovery_buf; +#define recovery_set() setjmp(recovery_buf) +#define recovery_jump() longjmp(recovery_buf, 1) +#else static CMB_THREAD_LOCAL intptr_t recovery_buf[5]; #define recovery_set() __builtin_setjmp((void **)recovery_buf) #define recovery_jump() __builtin_longjmp((void **)recovery_buf, 1) +#endif extern void cmi_coroutine_recovery_prepare(void *stack_marker); extern void cmi_coroutine_recovery_finalize(void *stack_marker); diff --git a/src/cmi_config.h b/src/cmi_config.h index 0998cc6b..25c7933e 100644 --- a/src/cmi_config.h +++ b/src/cmi_config.h @@ -23,8 +23,8 @@ /* * Supported toolchains: - * - Architecture: x86-64 / AMD64 - * - Operating system: Linux, or Windows via MinGW-w64 + * - Architecture: x86-64 / AMD64, or arm64 (Apple Silicon) + * - Operating system: Linux, Windows via MinGW-w64, or macOS * - Compiler: GCC or Clang, C11 or later * (the library itself is built as C23) * @@ -32,25 +32,32 @@ */ /* - * Identify the processor architecture. So far, only AMD64/x86-64 is supported. + * Identify the processor architecture. AMD64/x86-64 everywhere, and arm64 on + * macOS (Apple Silicon). */ #define CMI_AMD64 1 +#define CMI_ARM64 2 #if (defined (__amd64__) || defined (__amd64) || defined (__x86_64__) || \ defined (__x86_64) || defined (_M_X64) || defined (_M_AMD64)) #define CMI_ARCH CMI_AMD64 +#elif defined (__aarch64__) || defined (__arm64__) + #define CMI_ARCH CMI_ARM64 #else #error "Platform architecture not yet supported." #endif /* - * Identify the operating system. So far, only Linux and Windows are supported. + * Identify the operating system. Linux, Windows, and macOS are supported. */ #define CMI_LINUX 1 #define CMI_WINDOWS 2 +#define CMI_MACOS 3 #if defined (__linux__) || defined (__linux) || defined (linux) #define CMI_OS CMI_LINUX #elif defined (_WIN64) || defined (_WIN32) || defined (__WIN32__) #define CMI_OS CMI_WINDOWS +#elif defined (__APPLE__) && defined (__MACH__) + #define CMI_OS CMI_MACOS #else #error "Platform operating system not yet supported." #endif diff --git a/src/meson.build b/src/meson.build index 15958bb9..daec7d7d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -66,7 +66,8 @@ if host_machine.system() == 'windows' elif lto_feature.enabled() or (lto_feature.auto() and get_option('buildtype').startswith('release')) cimba_lto = ['b_lto=true'] if get_option('default_library') != 'shared' - cimba_lto_args = ['-ffat-lto-objects'] + # Apple Clang does not accept -ffat-lto-objects + cimba_lto_args = cc.get_supported_arguments('-ffat-lto-objects') endif endif diff --git a/src/port/aarch64/macos/cmb_random_hwseed.c b/src/port/aarch64/macos/cmb_random_hwseed.c new file mode 100644 index 00000000..e2d4a9c3 --- /dev/null +++ b/src/port/aarch64/macos/cmb_random_hwseed.c @@ -0,0 +1,19 @@ +/* + * Random seed generation for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include + +#include "cmb_assert.h" +#include "cmb_random.h" + +uint64_t cmb_random_hwseed(void) +{ + uint64_t seed = 0u; + arc4random_buf(&seed, sizeof(seed)); + cmb_assert_debug(seed != 0u); + return seed; +} diff --git a/src/port/aarch64/macos/cmi_coroutine_context.S b/src/port/aarch64/macos/cmi_coroutine_context.S new file mode 100644 index 00000000..f7894333 --- /dev/null +++ b/src/port/aarch64/macos/cmi_coroutine_context.S @@ -0,0 +1,63 @@ +/* + * Context switch and coroutine trampoline for arm64 macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +.text +.p2align 2 + +.globl _cmi_coroutine_context_switch +_cmi_coroutine_context_switch: + sub sp, sp, #176 + stp x19, x20, [sp, #0] + stp x21, x22, [sp, #16] + stp x23, x24, [sp, #32] + stp x25, x26, [sp, #48] + stp x27, x28, [sp, #64] + stp x29, x30, [sp, #80] + stp d8, d9, [sp, #96] + stp d10, d11, [sp, #112] + stp d12, d13, [sp, #128] + stp d14, d15, [sp, #144] + mrs x9, fpcr + mrs x10, fpsr + stp x9, x10, [sp, #160] + + mov x9, sp + str x9, [x0] + ldr x9, [x1] + mov sp, x9 + + ldp x9, x10, [sp, #160] + msr fpcr, x9 + msr fpsr, x10 + ldp d14, d15, [sp, #144] + ldp d12, d13, [sp, #128] + ldp d10, d11, [sp, #112] + ldp d8, d9, [sp, #96] + ldp x29, x30, [sp, #80] + ldp x27, x28, [sp, #64] + ldp x25, x26, [sp, #48] + ldp x23, x24, [sp, #32] + ldp x21, x22, [sp, #16] + ldp x19, x20, [sp, #0] + add sp, sp, #176 + + mov x0, x2 + ret + +.p2align 2 +.globl _cmi_coroutine_trampoline +_cmi_coroutine_trampoline: + stp x29, x30, [sp, #-16]! + mov x29, sp + mov x0, x20 + mov x1, x21 + blr x19 + mov x16, x22 + ldp x29, x30, [sp], #16 + br x16 + +.subsections_via_symbols diff --git a/src/port/aarch64/macos/cmi_coroutine_context.c b/src/port/aarch64/macos/cmi_coroutine_context.c new file mode 100644 index 00000000..c95285ac --- /dev/null +++ b/src/port/aarch64/macos/cmi_coroutine_context.c @@ -0,0 +1,128 @@ +/* + * Coroutine stack initialization for arm64 macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include +#include +#include + +#include "cmb_assert.h" +#include "cmi_coroutine.h" +#include "cmi_memutils.h" +#include "cmi_sanitizer.h" + +#define CMI_ARM64_CONTEXT_SIZE 176u + +extern void cmi_coroutine_trampoline(void); +extern void *cmi_coroutine_launch(struct cmi_coroutine *cp, void *arg); + +bool cmi_coroutine_stack_valid(const struct cmi_coroutine *cp) +{ + cmb_assert_debug(cp != NULL); + cmb_assert_debug(cp->stack_base != NULL); + cmb_assert_debug(cp->stack_limit != NULL); + + const struct cmi_coroutine *cp_main = cmi_coroutine_main(); + if (cp == cp_main) { + cmb_assert_debug(cp->status == CMI_COROUTINE_RUNNING); + cmb_assert_debug(cp->stack == NULL); + } + else { + cmb_assert_debug(cp->stack != NULL); + cmb_assert_debug(cp->stack_pointer != NULL); + } + + if (cp->stack_pointer != NULL) { + cmb_assert_debug( + (uintptr_t)cp->stack_pointer > (uintptr_t)cp->stack_limit + ); + cmb_assert_debug( + (uintptr_t)cp->stack_pointer < (uintptr_t)cp->stack_base + ); + cmb_assert_debug(((uintptr_t)cp->stack_pointer % 16u) == 0u); + } + return true; +} + +void cmi_coroutine_context_init(struct cmi_coroutine *cp) +{ + cmb_assert_release(cp != NULL); + cmb_assert_debug(cp->stack != NULL); + + uintptr_t aligned_base = (uintptr_t)cp->stack_base & ~(uintptr_t)0x0fu; + unsigned char *stkptr = ( + (unsigned char *)aligned_base - CMI_ARM64_CONTEXT_SIZE + ); + (void)cmi_memset(stkptr, 0, CMI_ARM64_CONTEXT_SIZE); + + uintptr_t *registers = (uintptr_t *)stkptr; + registers[0] = (uintptr_t)cmi_coroutine_launch; /* X19 */ + registers[1] = (uintptr_t)cp; /* X20 */ + registers[2] = (uintptr_t)cp->context; /* X21 */ + registers[3] = (uintptr_t)( + cp->cr_exit == NULL ? cmi_coroutine_exit : cp->cr_exit + ); /* X22 */ + registers[11] = (uintptr_t)cmi_coroutine_trampoline; /* X30 */ + + cp->stack_pointer = stkptr; + cmb_assert_debug(cmi_coroutine_stack_valid(cp)); +} + +unsigned char *cmi_coroutine_stack_alloc(const size_t size, + unsigned char **base_p, + unsigned char **limit_p) +{ + const size_t pagesz = cmi_pagesize(); + unsigned char *raw = cmi_aligned_alloc(pagesz, size + pagesz); + cmb_assert_always(raw != NULL); + cmb_assert_always(mprotect(raw, pagesz, PROT_NONE) == 0); + + *base_p = raw + size + pagesz; + *limit_p = raw + pagesz; + return raw; +} + +void cmi_coroutine_stack_free(unsigned char *stack, size_t size) +{ + cmb_assert_release(stack != NULL); + const size_t pagesz = cmi_pagesize(); + cmi_asan_unpoison(stack + pagesz, size); + cmb_assert_always(mprotect(stack, pagesz, PROT_READ | PROT_WRITE) == 0); + cmi_aligned_free(stack); +} + +unsigned char *cmi_coroutine_stackbase(void) +{ + return pthread_get_stackaddr_np(pthread_self()); +} + +unsigned char *cmi_coroutine_stacklimit(void) +{ + unsigned char *base = pthread_get_stackaddr_np(pthread_self()); + return base - pthread_get_stacksize_np(pthread_self()); +} + +unsigned char *cmi_coroutine_stackraw(void) +{ + return NULL; +} + +/* macOS frees stacks immediately, so there is no recycled stack pool. */ +void cmi_coroutine_stack_cleanup(void) {} + +/* Darwin does not require updating OS stack bounds after longjmp. */ +void cmi_coroutine_os_adopt_stack(const struct cmi_coroutine *cp) +{ + cmb_unused(cp); +} + +/* Both Darwin ports preserve floating-point control state on every switch. */ +bool cmi_coroutine_registers_valid(const struct cmi_coroutine *cp) +{ + cmb_unused(cp); + return true; +} diff --git a/src/port/aarch64/macos/cmi_cpu_cores.c b/src/port/aarch64/macos/cmi_cpu_cores.c new file mode 100644 index 00000000..02054ab5 --- /dev/null +++ b/src/port/aarch64/macos/cmi_cpu_cores.c @@ -0,0 +1,15 @@ +/* + * CPU availability for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include + +uint32_t cmi_cpu_cores(void) +{ + const long count = sysconf(_SC_NPROCESSORS_ONLN); + return count > 0 ? (uint32_t)count : 1u; +} diff --git a/src/port/aarch64/macos/cmi_memutils.c b/src/port/aarch64/macos/cmi_memutils.c new file mode 100644 index 00000000..c36b081e --- /dev/null +++ b/src/port/aarch64/macos/cmi_memutils.c @@ -0,0 +1,46 @@ +/* + * Aligned allocation helpers for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include +#include +#include + +#include "cmb_assert.h" +#include "cmi_memutils.h" + +size_t cmi_pagesize(void) +{ + return (size_t)sysconf(_SC_PAGESIZE); +} + +void *cmi_aligned_alloc(const size_t align, const size_t sz) +{ + void *ptr = NULL; + const int result = posix_memalign(&ptr, align, sz); + cmb_assert_always(result == 0); + cmb_assert_always(ptr != NULL); + return ptr; +} + +void cmi_aligned_free(void *ptr) +{ + cmb_assert_release(ptr != NULL); + free(ptr); +} + +void *cmi_aligned_realloc(void *ptr, + const size_t align, + const size_t sz) +{ + cmb_assert_release(ptr != NULL); + void *replacement = cmi_aligned_alloc(align, sz); + const size_t old_size = malloc_size(ptr); + memcpy(replacement, ptr, old_size < sz ? old_size : sz); + free(ptr); + return replacement; +} diff --git a/src/port/aarch64/macos/meson.build b/src/port/aarch64/macos/meson.build new file mode 100644 index 00000000..e73b30f0 --- /dev/null +++ b/src/port/aarch64/macos/meson.build @@ -0,0 +1,7 @@ +# arm64/macOS/meson.build +sources += files('cmb_random_hwseed.c', + 'cmi_coroutine_context.c', + 'cmi_coroutine_context.S', + 'cmi_cpu_cores.c', + 'cmi_memutils.c' +) diff --git a/src/port/meson.build b/src/port/meson.build index 654071ab..1b650f6d 100644 --- a/src/port/meson.build +++ b/src/port/meson.build @@ -1,2 +1,9 @@ -# just passing through -subdir('x86-64') +# port/meson.build + +if host_machine.cpu_family() == 'x86_64' + subdir('x86-64') +elif host_machine.cpu_family() == 'aarch64' and host_machine.system() == 'darwin' + subdir('aarch64/macos') +else + error('Unsupported platform: ' + host_machine.system() + '/' + host_machine.cpu_family()) +endif diff --git a/src/port/x86-64/macos/cmb_random_hwseed.c b/src/port/x86-64/macos/cmb_random_hwseed.c new file mode 100644 index 00000000..e2d4a9c3 --- /dev/null +++ b/src/port/x86-64/macos/cmb_random_hwseed.c @@ -0,0 +1,19 @@ +/* + * Random seed generation for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include + +#include "cmb_assert.h" +#include "cmb_random.h" + +uint64_t cmb_random_hwseed(void) +{ + uint64_t seed = 0u; + arc4random_buf(&seed, sizeof(seed)); + cmb_assert_debug(seed != 0u); + return seed; +} diff --git a/src/port/x86-64/macos/cmi_coroutine_context.asm b/src/port/x86-64/macos/cmi_coroutine_context.asm new file mode 100644 index 00000000..18db6e70 --- /dev/null +++ b/src/port/x86-64/macos/cmi_coroutine_context.asm @@ -0,0 +1,59 @@ +; +; Context switch and coroutine trampoline for x86-64 macOS. +; Mach-O symbols use a leading underscore; the register convention is SysV. +; +; Copyright (c) FBarrca 2026. +; Licensed under the Apache License, Version 2.0. + +bits 64 +default rel + +section .text +global _cmi_coroutine_context_switch +global _cmi_coroutine_trampoline + +%macro save_context 0 + pushfq + %ifndef NMXCSR + sub rsp, 8 + stmxcsr [rsp + 4] + %endif + push rbp + push rbx + push r12 + push r13 + push r14 + push r15 +%endmacro + +%macro load_context 0 + pop r15 + pop r14 + pop r13 + pop r12 + pop rbx + pop rbp + %ifndef NMXCSR + ldmxcsr [rsp + 4] + add rsp, 8 + %endif + popfq +%endmacro + +_cmi_coroutine_context_switch: + save_context + mov [rdi], rsp + mov rsp, [rsi] + load_context + mov rax, rdx + pop r9 + jmp r9 + +_cmi_coroutine_trampoline: + mov rdi, r13 + mov rsi, r14 + xor rax, rax + call r12 + push rdi + mov rdi, rax + jmp r15 diff --git a/src/port/x86-64/macos/cmi_coroutine_context.c b/src/port/x86-64/macos/cmi_coroutine_context.c new file mode 100644 index 00000000..d70553b3 --- /dev/null +++ b/src/port/x86-64/macos/cmi_coroutine_context.c @@ -0,0 +1,146 @@ +/* + * Coroutine stack initialization for x86-64 macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include +#include +#include + +#include "cmb_assert.h" +#include "cmi_coroutine.h" +#include "cmi_memutils.h" +#include "cmi_sanitizer.h" + +extern void cmi_coroutine_trampoline(void); +extern void *cmi_coroutine_launch(struct cmi_coroutine *cp, void *arg); + +bool cmi_coroutine_stack_valid(const struct cmi_coroutine *cp) +{ + cmb_assert_debug(cp != NULL); + cmb_assert_debug(cp->stack_base != NULL); + cmb_assert_debug(cp->stack_limit != NULL); + + const struct cmi_coroutine *cp_main = cmi_coroutine_main(); + if (cp == cp_main) { + cmb_assert_debug(cp->status == CMI_COROUTINE_RUNNING); + cmb_assert_debug(cp->stack == NULL); + } + else { + cmb_assert_debug(cp->stack != NULL); + cmb_assert_debug(cp->stack_pointer != NULL); + } + + if (cp->stack_pointer != NULL) { + cmb_assert_debug( + (uintptr_t)cp->stack_pointer > (uintptr_t)cp->stack_limit + ); + cmb_assert_debug( + (uintptr_t)cp->stack_pointer < (uintptr_t)cp->stack_base + ); + #ifndef NMXCSR + cmb_assert_debug( + (((uintptr_t)cp->stack_pointer + 8u) % 16u) == 0u + ); + #else + cmb_assert_debug(((uintptr_t)cp->stack_pointer % 16u) == 0u); + #endif + } + return true; +} + +void cmi_coroutine_context_init(struct cmi_coroutine *cp) +{ + cmb_assert_release(cp != NULL); + cmb_assert_debug(cp->stack != NULL); + + uintptr_t aligned_base = (uintptr_t)cp->stack_base & ~(uintptr_t)0x0fu; + unsigned char *stkptr = (unsigned char *)aligned_base; + + stkptr -= 8u; + *(uintptr_t *)stkptr = (uintptr_t)cmi_coroutine_trampoline; + stkptr -= 8u; + *(uint64_t *)stkptr = 0x0202ull; + + #ifndef NMXCSR + stkptr -= 8u; + *(uint32_t *)(stkptr + 4u) = 0x1f80u; + *(uint32_t *)stkptr = 0u; + #endif + + stkptr -= 8u; + *(uint64_t *)stkptr = 0u; /* RBP */ + stkptr -= 8u; + *(uint64_t *)stkptr = 0u; /* RBX */ + stkptr -= 8u; + *(uintptr_t *)stkptr = (uintptr_t)cmi_coroutine_launch; /* R12 */ + stkptr -= 8u; + *(uintptr_t *)stkptr = (uintptr_t)cp; /* R13 */ + stkptr -= 8u; + *(uintptr_t *)stkptr = (uintptr_t)cp->context; /* R14 */ + stkptr -= 8u; + *(uintptr_t *)stkptr = (uintptr_t)( + cp->cr_exit == NULL ? cmi_coroutine_exit : cp->cr_exit + ); /* R15 */ + + cp->stack_pointer = stkptr; + cmb_assert_debug(cmi_coroutine_stack_valid(cp)); +} + +unsigned char *cmi_coroutine_stack_alloc(const size_t size, + unsigned char **base_p, + unsigned char **limit_p) +{ + const size_t pagesz = cmi_pagesize(); + unsigned char *raw = cmi_aligned_alloc(pagesz, size + pagesz); + cmb_assert_always(raw != NULL); + cmb_assert_always(mprotect(raw, pagesz, PROT_NONE) == 0); + + *base_p = raw + size + pagesz; + *limit_p = raw + pagesz; + return raw; +} + +void cmi_coroutine_stack_free(unsigned char *stack, size_t size) +{ + cmb_assert_release(stack != NULL); + const size_t pagesz = cmi_pagesize(); + cmi_asan_unpoison(stack + pagesz, size); + cmb_assert_always(mprotect(stack, pagesz, PROT_READ | PROT_WRITE) == 0); + cmi_aligned_free(stack); +} + +unsigned char *cmi_coroutine_stackbase(void) +{ + return pthread_get_stackaddr_np(pthread_self()); +} + +unsigned char *cmi_coroutine_stacklimit(void) +{ + unsigned char *base = pthread_get_stackaddr_np(pthread_self()); + return base - pthread_get_stacksize_np(pthread_self()); +} + +unsigned char *cmi_coroutine_stackraw(void) +{ + return NULL; +} + +/* macOS frees stacks immediately, so there is no recycled stack pool. */ +void cmi_coroutine_stack_cleanup(void) {} + +/* Darwin does not require updating OS stack bounds after longjmp. */ +void cmi_coroutine_os_adopt_stack(const struct cmi_coroutine *cp) +{ + cmb_unused(cp); +} + +/* Both Darwin ports preserve floating-point control state on every switch. */ +bool cmi_coroutine_registers_valid(const struct cmi_coroutine *cp) +{ + cmb_unused(cp); + return true; +} diff --git a/src/port/x86-64/macos/cmi_cpu_cores.c b/src/port/x86-64/macos/cmi_cpu_cores.c new file mode 100644 index 00000000..02054ab5 --- /dev/null +++ b/src/port/x86-64/macos/cmi_cpu_cores.c @@ -0,0 +1,15 @@ +/* + * CPU availability for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include + +uint32_t cmi_cpu_cores(void) +{ + const long count = sysconf(_SC_NPROCESSORS_ONLN); + return count > 0 ? (uint32_t)count : 1u; +} diff --git a/src/port/x86-64/macos/cmi_memutils.c b/src/port/x86-64/macos/cmi_memutils.c new file mode 100644 index 00000000..c36b081e --- /dev/null +++ b/src/port/x86-64/macos/cmi_memutils.c @@ -0,0 +1,46 @@ +/* + * Aligned allocation helpers for macOS. + * + * Copyright (c) FBarrca 2026. + * Licensed under the Apache License, Version 2.0. + */ + +#include +#include +#include +#include + +#include "cmb_assert.h" +#include "cmi_memutils.h" + +size_t cmi_pagesize(void) +{ + return (size_t)sysconf(_SC_PAGESIZE); +} + +void *cmi_aligned_alloc(const size_t align, const size_t sz) +{ + void *ptr = NULL; + const int result = posix_memalign(&ptr, align, sz); + cmb_assert_always(result == 0); + cmb_assert_always(ptr != NULL); + return ptr; +} + +void cmi_aligned_free(void *ptr) +{ + cmb_assert_release(ptr != NULL); + free(ptr); +} + +void *cmi_aligned_realloc(void *ptr, + const size_t align, + const size_t sz) +{ + cmb_assert_release(ptr != NULL); + void *replacement = cmi_aligned_alloc(align, sz); + const size_t old_size = malloc_size(ptr); + memcpy(replacement, ptr, old_size < sz ? old_size : sz); + free(ptr); + return replacement; +} diff --git a/src/port/x86-64/macos/meson.build b/src/port/x86-64/macos/meson.build new file mode 100644 index 00000000..db0a96e6 --- /dev/null +++ b/src/port/x86-64/macos/meson.build @@ -0,0 +1,13 @@ +# x86-64/macOS/meson.build +nasm_args += ['-f', 'macho64'] + +sources += files('cmb_random_hwseed.c', + 'cmi_coroutine_context.c', + 'cmi_cpu_cores.c', + 'cmi_memutils.c' +) + +sources += generator(find_program('nasm'), + output : '@BASENAME@.o', + arguments : nasm_args + ['@INPUT@', '-o', '@OUTPUT@'] +).process('cmi_coroutine_context.asm') diff --git a/src/port/x86-64/meson.build b/src/port/x86-64/meson.build index fc449e3d..70633424 100644 --- a/src/port/x86-64/meson.build +++ b/src/port/x86-64/meson.build @@ -4,6 +4,8 @@ if host_machine.system() == 'linux' subdir('linux') elif host_machine.system() == 'windows' subdir('windows') +elif host_machine.system() == 'darwin' + subdir('macos') else error('Unsupported platform: ' + host_machine.system()) endif From 0aa07d6e470aacbad6b2f5dfdb0e3396dc84b09a Mon Sep 17 00:00:00 2001 From: FBarrca Date: Tue, 3 Nov 2026 20:42:02 +0100 Subject: [PATCH 2/2] Recycle guarded coroutine stacks on macOS 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 Includes-AI-Code: true --- .../aarch64/macos/cmi_coroutine_context.c | 107 +++++++++++++++--- src/port/aarch64/macos/cmi_memutils.c | 10 +- src/port/x86-64/macos/cmi_coroutine_context.c | 107 +++++++++++++++--- src/port/x86-64/macos/cmi_memutils.c | 10 +- 4 files changed, 204 insertions(+), 30 deletions(-) diff --git a/src/port/aarch64/macos/cmi_coroutine_context.c b/src/port/aarch64/macos/cmi_coroutine_context.c index c95285ac..23fbb139 100644 --- a/src/port/aarch64/macos/cmi_coroutine_context.c +++ b/src/port/aarch64/macos/cmi_coroutine_context.c @@ -72,27 +72,87 @@ void cmi_coroutine_context_init(struct cmi_coroutine *cp) cmb_assert_debug(cmi_coroutine_stack_valid(cp)); } -unsigned char *cmi_coroutine_stack_alloc(const size_t size, +/* + * Intrusive singly linked list of recycled stacks, as in the Linux ports: + * mprotect is a system call on every guarded allocation and free, so each + * stack is protected once, recycled within the current thread, and freed + * at thread exit by cmi_coroutine_stack_cleanup(). + */ +struct stack_tag { + size_t size; /* Size of stacks in this list */ + unsigned char *head; /* The intrusive list */ + struct stack_tag *next; /* Next list, a different size stack */ +}; + +CMB_THREAD_LOCAL static struct stack_tag *stack_list = NULL; + +unsigned char *cmi_coroutine_stack_alloc(const size_t size_req, unsigned char **base_p, unsigned char **limit_p) { + cmb_assert_debug(size_req > 0u); + cmb_assert_debug((base_p != NULL) && (limit_p != NULL)); + const size_t pagesz = cmi_pagesize(); - unsigned char *raw = cmi_aligned_alloc(pagesz, size + pagesz); - cmb_assert_always(raw != NULL); - cmb_assert_always(mprotect(raw, pagesz, PROT_NONE) == 0); + cmb_assert_debug(size_req <= SIZE_MAX - pagesz); + const size_t size_rnd = (size_req + pagesz - 1u) & ~(pagesz - 1u); + + /* Do we have one lying around? */ + unsigned char *stack_raw = NULL; + struct stack_tag *st = stack_list; + while (st != NULL) { + if (st->size == size_rnd) { + if (st->head != NULL) { + stack_raw = st->head; + cmi_asan_unpoison(stack_raw + pagesz, size_rnd); + unsigned char **nextloc = (unsigned char **)(stack_raw + pagesz); + st->head = *nextloc; + } + break; + } + st = st->next; + } + + if (stack_raw == NULL) { + stack_raw = cmi_aligned_alloc(pagesz, size_rnd + pagesz); + cmb_assert_always(stack_raw != NULL); + cmb_assert_always(mprotect(stack_raw, pagesz, PROT_NONE) == 0); + } + + *base_p = stack_raw + pagesz + size_rnd; + *limit_p = stack_raw + pagesz; + cmb_assert_debug(!cmi_asan_region_poisoned(stack_raw + pagesz, size_rnd)); - *base_p = raw + size + pagesz; - *limit_p = raw + pagesz; - return raw; + return stack_raw; } -void cmi_coroutine_stack_free(unsigned char *stack, size_t size) +/* Push a stack back on the pool rather than freeing it */ +void cmi_coroutine_stack_free(unsigned char *stack_raw, size_t size_req) { - cmb_assert_release(stack != NULL); + cmb_assert_release(stack_raw != NULL); + const size_t pagesz = cmi_pagesize(); - cmi_asan_unpoison(stack + pagesz, size); - cmb_assert_always(mprotect(stack, pagesz, PROT_READ | PROT_WRITE) == 0); - cmi_aligned_free(stack); + cmb_assert_debug(size_req <= SIZE_MAX - pagesz); + const size_t size_rnd = (size_req + pagesz - 1u) & ~(pagesz - 1u); + cmi_asan_unpoison(stack_raw + pagesz, size_rnd); + + struct stack_tag *st = stack_list; + while (st != NULL) { + if (st->size == size_rnd) { + unsigned char **nextloc = (unsigned char **)(stack_raw + pagesz); + *nextloc = st->head; + st->head = stack_raw; + return; + } + st = st->next; + } + + st = cmi_malloc(sizeof(*st)); + *(unsigned char **)(stack_raw + pagesz) = NULL; + st->size = size_rnd; + st->head = stack_raw; + st->next = stack_list; + stack_list = st; } unsigned char *cmi_coroutine_stackbase(void) @@ -111,8 +171,27 @@ unsigned char *cmi_coroutine_stackraw(void) return NULL; } -/* macOS frees stacks immediately, so there is no recycled stack pool. */ -void cmi_coroutine_stack_cleanup(void) {} +/* Free the recycled stacks, called at thread exit */ +void cmi_coroutine_stack_cleanup(void) +{ + const size_t pagesz = cmi_pagesize(); + struct stack_tag *st = stack_list; + while (st != NULL) { + while (st->head != NULL) { + unsigned char *raw = st->head; + unsigned char *next = *(unsigned char **)(raw + pagesz); + cmb_assert_always(mprotect(raw, pagesz, PROT_READ | PROT_WRITE) == 0); + cmi_aligned_free(raw); + st->head = next; + } + + struct stack_tag *next = st->next; + cmi_free(st); + st = next; + } + + stack_list = NULL; +} /* Darwin does not require updating OS stack bounds after longjmp. */ void cmi_coroutine_os_adopt_stack(const struct cmi_coroutine *cp) diff --git a/src/port/aarch64/macos/cmi_memutils.c b/src/port/aarch64/macos/cmi_memutils.c index c36b081e..5b832949 100644 --- a/src/port/aarch64/macos/cmi_memutils.c +++ b/src/port/aarch64/macos/cmi_memutils.c @@ -8,14 +8,22 @@ #include #include #include +#include #include #include "cmb_assert.h" #include "cmi_memutils.h" +/* The page size is fixed for the life of the process, so ask the OS once */ size_t cmi_pagesize(void) { - return (size_t)sysconf(_SC_PAGESIZE); + static _Atomic size_t pagesz = 0u; + size_t value = atomic_load_explicit(&pagesz, memory_order_relaxed); + if (value == 0u) { + value = (size_t)sysconf(_SC_PAGESIZE); + atomic_store_explicit(&pagesz, value, memory_order_relaxed); + } + return value; } void *cmi_aligned_alloc(const size_t align, const size_t sz) diff --git a/src/port/x86-64/macos/cmi_coroutine_context.c b/src/port/x86-64/macos/cmi_coroutine_context.c index d70553b3..86fb4797 100644 --- a/src/port/x86-64/macos/cmi_coroutine_context.c +++ b/src/port/x86-64/macos/cmi_coroutine_context.c @@ -90,27 +90,87 @@ void cmi_coroutine_context_init(struct cmi_coroutine *cp) cmb_assert_debug(cmi_coroutine_stack_valid(cp)); } -unsigned char *cmi_coroutine_stack_alloc(const size_t size, +/* + * Intrusive singly linked list of recycled stacks, as in the Linux ports: + * mprotect is a system call on every guarded allocation and free, so each + * stack is protected once, recycled within the current thread, and freed + * at thread exit by cmi_coroutine_stack_cleanup(). + */ +struct stack_tag { + size_t size; /* Size of stacks in this list */ + unsigned char *head; /* The intrusive list */ + struct stack_tag *next; /* Next list, a different size stack */ +}; + +CMB_THREAD_LOCAL static struct stack_tag *stack_list = NULL; + +unsigned char *cmi_coroutine_stack_alloc(const size_t size_req, unsigned char **base_p, unsigned char **limit_p) { + cmb_assert_debug(size_req > 0u); + cmb_assert_debug((base_p != NULL) && (limit_p != NULL)); + const size_t pagesz = cmi_pagesize(); - unsigned char *raw = cmi_aligned_alloc(pagesz, size + pagesz); - cmb_assert_always(raw != NULL); - cmb_assert_always(mprotect(raw, pagesz, PROT_NONE) == 0); + cmb_assert_debug(size_req <= SIZE_MAX - pagesz); + const size_t size_rnd = (size_req + pagesz - 1u) & ~(pagesz - 1u); + + /* Do we have one lying around? */ + unsigned char *stack_raw = NULL; + struct stack_tag *st = stack_list; + while (st != NULL) { + if (st->size == size_rnd) { + if (st->head != NULL) { + stack_raw = st->head; + cmi_asan_unpoison(stack_raw + pagesz, size_rnd); + unsigned char **nextloc = (unsigned char **)(stack_raw + pagesz); + st->head = *nextloc; + } + break; + } + st = st->next; + } + + if (stack_raw == NULL) { + stack_raw = cmi_aligned_alloc(pagesz, size_rnd + pagesz); + cmb_assert_always(stack_raw != NULL); + cmb_assert_always(mprotect(stack_raw, pagesz, PROT_NONE) == 0); + } + + *base_p = stack_raw + pagesz + size_rnd; + *limit_p = stack_raw + pagesz; + cmb_assert_debug(!cmi_asan_region_poisoned(stack_raw + pagesz, size_rnd)); - *base_p = raw + size + pagesz; - *limit_p = raw + pagesz; - return raw; + return stack_raw; } -void cmi_coroutine_stack_free(unsigned char *stack, size_t size) +/* Push a stack back on the pool rather than freeing it */ +void cmi_coroutine_stack_free(unsigned char *stack_raw, size_t size_req) { - cmb_assert_release(stack != NULL); + cmb_assert_release(stack_raw != NULL); + const size_t pagesz = cmi_pagesize(); - cmi_asan_unpoison(stack + pagesz, size); - cmb_assert_always(mprotect(stack, pagesz, PROT_READ | PROT_WRITE) == 0); - cmi_aligned_free(stack); + cmb_assert_debug(size_req <= SIZE_MAX - pagesz); + const size_t size_rnd = (size_req + pagesz - 1u) & ~(pagesz - 1u); + cmi_asan_unpoison(stack_raw + pagesz, size_rnd); + + struct stack_tag *st = stack_list; + while (st != NULL) { + if (st->size == size_rnd) { + unsigned char **nextloc = (unsigned char **)(stack_raw + pagesz); + *nextloc = st->head; + st->head = stack_raw; + return; + } + st = st->next; + } + + st = cmi_malloc(sizeof(*st)); + *(unsigned char **)(stack_raw + pagesz) = NULL; + st->size = size_rnd; + st->head = stack_raw; + st->next = stack_list; + stack_list = st; } unsigned char *cmi_coroutine_stackbase(void) @@ -129,8 +189,27 @@ unsigned char *cmi_coroutine_stackraw(void) return NULL; } -/* macOS frees stacks immediately, so there is no recycled stack pool. */ -void cmi_coroutine_stack_cleanup(void) {} +/* Free the recycled stacks, called at thread exit */ +void cmi_coroutine_stack_cleanup(void) +{ + const size_t pagesz = cmi_pagesize(); + struct stack_tag *st = stack_list; + while (st != NULL) { + while (st->head != NULL) { + unsigned char *raw = st->head; + unsigned char *next = *(unsigned char **)(raw + pagesz); + cmb_assert_always(mprotect(raw, pagesz, PROT_READ | PROT_WRITE) == 0); + cmi_aligned_free(raw); + st->head = next; + } + + struct stack_tag *next = st->next; + cmi_free(st); + st = next; + } + + stack_list = NULL; +} /* Darwin does not require updating OS stack bounds after longjmp. */ void cmi_coroutine_os_adopt_stack(const struct cmi_coroutine *cp) diff --git a/src/port/x86-64/macos/cmi_memutils.c b/src/port/x86-64/macos/cmi_memutils.c index c36b081e..5b832949 100644 --- a/src/port/x86-64/macos/cmi_memutils.c +++ b/src/port/x86-64/macos/cmi_memutils.c @@ -8,14 +8,22 @@ #include #include #include +#include #include #include "cmb_assert.h" #include "cmi_memutils.h" +/* The page size is fixed for the life of the process, so ask the OS once */ size_t cmi_pagesize(void) { - return (size_t)sysconf(_SC_PAGESIZE); + static _Atomic size_t pagesz = 0u; + size_t value = atomic_load_explicit(&pagesz, memory_order_relaxed); + if (value == 0u) { + value = (size_t)sysconf(_SC_PAGESIZE); + atomic_store_explicit(&pagesz, value, memory_order_relaxed); + } + return value; } void *cmi_aligned_alloc(const size_t align, const size_t sz)