From 8f4c28a7e0de7de498d811d89597c32b6b79a970 Mon Sep 17 00:00:00 2001 From: Aoto Date: Mon, 21 Sep 2026 17:28:29 +0200 Subject: [PATCH 1/3] stack: growable stacks on Darwin --- docs/memory.md | 8 ++++++-- src/cfiber/stack/growable_stack.c | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/memory.md b/docs/memory.md index 4681375..7bd5681 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -83,7 +83,10 @@ and writable across its whole extent, and the kernel commits a physical page the first time the fiber's stack pointer reaches it. `MAP_NORESERVE` keeps the untouched remainder from counting against commit accounting under the default overcommit heuristic; with `vm.overcommit_memory=2` the whole extent is charged -at `mmap` time. An idle fiber costs +at `mmap` time. On Darwin there is no overcommit toggle, `MAP_NORESERVE` is +accepted and ignored, `MAP_STACK` does not exist (the flag compiles out), and +pages are 16 KiB, so the guard page and the growth granularity are four times +the common x86-64 size. An idle fiber costs the pages it has actually touched, not the pages it might touch, which is what makes tens of thousands of reactor fibers with a 64 KB ceiling practical. @@ -94,7 +97,8 @@ happened to sit below. `cfiber_growable_stack_allocator_t` pools these. It keeps up to `cache_capacity` stacks for reuse and can pre-allocate `initial_cached` of them. A stack returned to the -pool is recycled with `MADV_DONTNEED` over its grown pages, keeping the top page +pool is recycled with `MADV_DONTNEED` (`MADV_FREE_REUSABLE` on Darwin, where +`MADV_DONTNEED` keeps pages resident) over its grown pages, keeping the top page warm: that drops the physical memory while leaving the mapping and the guard page in place, so reuse costs no `mmap` and the next fiber starts from a small resident footprint. `cfiber_growable_stack_allocator_destroy()` returns `-1` if stacks diff --git a/src/cfiber/stack/growable_stack.c b/src/cfiber/stack/growable_stack.c index 6e6c662..2078168 100644 --- a/src/cfiber/stack/growable_stack.c +++ b/src/cfiber/stack/growable_stack.c @@ -11,6 +11,12 @@ #include #include +/* Darwin has no MAP_STACK; dropping it is harmless (MAP_NORESERVE exists + * there and is ignored). */ +#ifndef MAP_STACK +#define MAP_STACK 0 +#endif + /* 0 if the page size cannot be queried; callers fail with errno set. */ static size_t page_size(void) { const long ps = sysconf(_SC_PAGESIZE); @@ -80,5 +86,10 @@ void cfiber_growable_stack_recycle(cfiber_stack_t* const stack) { } const size_t length = cfiber_stack_usable_size(stack) - page; +#ifdef __APPLE__ + /* MADV_DONTNEED keeps pages resident on Darwin. */ + (void)madvise(stack->usable_base, length, MADV_FREE_REUSABLE); +#else (void)madvise(stack->usable_base, length, MADV_DONTNEED); +#endif } From 3451f3e0c603ee0a1fe4ad25ecf60c609dc95ed6 Mon Sep 17 00:00:00 2001 From: Aoto Date: Mon, 21 Sep 2026 17:36:02 +0200 Subject: [PATCH 2/3] macos: build script, docs for Apple Silicon --- CHANGELOG.md | 8 ++++++++ README.md | 10 +++++++--- docs/building.md | 7 ++++++- tests/CMakeLists.txt | 20 +++++++++++--------- utils/make.sh | 20 ++++++++++---------- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6f864..3e8f57c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ change the API. ## [Unreleased] +### Added + +- macOS support, Apple Silicon (arm64) only: growable stacks recycle with + `MADV_FREE_REUSABLE`, the assembly builds through the Mach-O branch of + `asm_defs.inc`, and a native `macos-15` CI job covers Release, Debug, ASan + and the shared `libcfiber.dylib`. The reactor, fuzzing and ThreadSanitizer + stay Linux-only; x86_64 macOS is not claimed. + ### Changed - The hosted assembly files take their object-format bracketing (symbol diff --git a/README.md b/README.md index 28ce1a6..c0f483e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![x86_64](https://github.com/aotodev/cfiber/actions/workflows/x86_64.yml/badge.svg?branch=master)](https://github.com/aotodev/cfiber/actions/workflows/x86_64.yml) [![aarch64](https://github.com/aotodev/cfiber/actions/workflows/aarch64.yml/badge.svg?branch=master)](https://github.com/aotodev/cfiber/actions/workflows/aarch64.yml) +[![macos](https://github.com/aotodev/cfiber/actions/workflows/macos.yml/badge.svg?branch=master)](https://github.com/aotodev/cfiber/actions/workflows/macos.yml) [![cortex-m0](https://github.com/aotodev/cfiber/actions/workflows/cortex-m0.yml/badge.svg?branch=master)](https://github.com/aotodev/cfiber/actions/workflows/cortex-m0.yml) [![cortex-m3](https://github.com/aotodev/cfiber/actions/workflows/cortex-m3.yml/badge.svg?branch=master)](https://github.com/aotodev/cfiber/actions/workflows/cortex-m3.yml) [![cortex-m4](https://github.com/aotodev/cfiber/actions/workflows/cortex-m4.yml/badge.svg?branch=master)](https://github.com/aotodev/cfiber/actions/workflows/cortex-m4.yml) @@ -39,11 +40,13 @@ cooperative scheduler and the allocators to back them. Two targets, one API: | ------------------- | -------------- | ------------------------------------ | | x86_64 Linux | System V AMD64 | Native | | AArch64 Linux | AAPCS64 | `qemu-aarch64` user-mode emulation | +| macOS arm64 | AAPCS64 (Apple)| Native (GitHub `macos-15` runner) | | ARM Cortex-M0 / M0+ | AAPCS Thumb-1 | `qemu-system-arm -M microbit` | | ARM Cortex-M3 | AAPCS Thumb-2 | `qemu-system-arm -M mps2-an385` | | ARM Cortex-M4 / M7 | AAPCS Thumb-2 | `qemu-system-arm -M mps2-an386/an500`| -No other host is supported. The reactor and the fuzz targets are Linux only. +No other host is supported (x86_64 macOS is not claimed). The reactor, the +fuzz targets and ThreadSanitizer are Linux only. ## Why fibers @@ -117,8 +120,9 @@ Cross-compilation, the full option table and integration notes are in Every push runs the CI matrix above: x86_64 under AddressSanitizer, UndefinedBehaviorSanitizer and the stack sanitizer, AArch64 under `qemu-user`, -Cortex-M0/M3/M4/M7 under `qemu-system-arm`, the reactor under ThreadSanitizer, -and libFuzzer harnesses for the allocators, the scheduler and the reactor. +macOS arm64 natively (Release, Debug, ASan, shared), Cortex-M0/M3/M4/M7 under +`qemu-system-arm`, the reactor under ThreadSanitizer, and libFuzzer harnesses +for the allocators, the scheduler and the reactor. Per-architecture tests check the callee-saved set across a context switch. This is a young library. Treat the above as what is exercised today, not as a diff --git a/docs/building.md b/docs/building.md index db37e0d..d83c018 100644 --- a/docs/building.md +++ b/docs/building.md @@ -8,6 +8,9 @@ used (`constexpr`, `nullptr`, `#elifdef`) and are unverified. - For ARM cross-builds: `arm-none-eabi-gcc` and `qemu-system-arm` - For AArch64 cross-builds: `aarch64-linux-gnu-gcc` and `qemu-user` +- On macOS (arm64 only): Apple Clang 17 or newer (Xcode 16.3+) for C23 + `constexpr`; on an older Xcode, `brew install llvm` and point `CC` at it. + Native builds need no coreutils; `timeout` is only used for QEMU runs. Select a compiler at configure time with `CC=clang cmake ...` or `CC=clang ./utils/make.sh ...`. @@ -80,7 +83,9 @@ library and everything linked against it must be built with the same The default is a static archive. Shared builds export only the documented API (everything declared with `CFIBER_EXPORT` in the public headers) and hide everything else via `-fvisibility=hidden`. Shared builds carry the major -version as `SOVERSION`. +version as `SOVERSION`; on macOS this yields `libcfiber.dylib` with the same +versioned names, and a consumer run against a staged install needs +`DYLD_LIBRARY_PATH` where Linux uses `LD_LIBRARY_PATH`. ## API and ABI diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f27c2aa..452a2a7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,7 +9,9 @@ include(arm_executable) add_library(cfiber_test STATIC test/test.c) target_include_directories(cfiber_test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) # cfiber: the framework follows the library's hosted/freestanding split -# (CFIBER_FREESTANDING) for its fork-based death helper. +# (CFIBER_FREESTANDING) for its fork-based death helper. Test targets get +# cfiber through this PUBLIC link; naming it again makes Apple's ld warn +# about a duplicate library. target_link_libraries(cfiber_test PUBLIC cfiber project_flags) # add_test() can only run the binary on the build host, so register CTest cases @@ -41,7 +43,7 @@ endif() target_include_directories(${_cfiber_test_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/src) -target_link_libraries(${_cfiber_test_name} PRIVATE cfiber cfiber_test project_flags m) # m: fenv +target_link_libraries(${_cfiber_test_name} PRIVATE cfiber_test project_flags m) # m: fenv if(_cfiber_register_ctest) add_test(NAME context_switch COMMAND ${_cfiber_test_name}) @@ -53,7 +55,7 @@ endif() if(_cfiber_arch STREQUAL "arm") add_arm_executable(test_cortex_syscalls cortex/test_syscalls.c) target_include_directories(test_cortex_syscalls PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(test_cortex_syscalls PRIVATE cfiber cfiber_test project_flags) + target_link_libraries(test_cortex_syscalls PRIVATE cfiber_test project_flags) endif() # ====================================================================================== @@ -62,7 +64,7 @@ endif() if(NOT CFIBER_FREESTANDING) add_executable(test_memory_allocators memory/test_multislab_alloc.c) target_include_directories(test_memory_allocators PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(test_memory_allocators PRIVATE cfiber cfiber_test project_flags) + target_link_libraries(test_memory_allocators PRIVATE cfiber_test project_flags) if(_cfiber_register_ctest) add_test(NAME memory_allocators COMMAND test_memory_allocators) @@ -75,7 +77,7 @@ endif() if(NOT CFIBER_FREESTANDING) add_executable(test_scheduler scheduler/test_scheduler.c) target_include_directories(test_scheduler PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(test_scheduler PRIVATE cfiber cfiber_test project_flags) + target_link_libraries(test_scheduler PRIVATE cfiber_test project_flags) if(_cfiber_register_ctest) add_test(NAME scheduler COMMAND test_scheduler) @@ -88,13 +90,13 @@ endif() if(NOT CFIBER_FREESTANDING) add_executable(test_fixed_size_stack_allocator stack/test_fixed_size_stack_allocator.c) target_include_directories(test_fixed_size_stack_allocator PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(test_fixed_size_stack_allocator PRIVATE cfiber cfiber_test project_flags) + target_link_libraries(test_fixed_size_stack_allocator PRIVATE cfiber_test project_flags) add_executable(test_growable_stack stack/test_growable_stack.c) target_include_directories(test_growable_stack PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) # sigaction, sigsetjmp: hidden under strict -std=c23 target_compile_definitions(test_growable_stack PRIVATE _POSIX_C_SOURCE=200809L) - target_link_libraries(test_growable_stack PRIVATE cfiber cfiber_test project_flags) + target_link_libraries(test_growable_stack PRIVATE cfiber_test project_flags) if(_cfiber_register_ctest) add_test(NAME fixed_size_stack_allocator COMMAND test_fixed_size_stack_allocator) @@ -144,7 +146,7 @@ if(NOT CFIBER_FREESTANDING) add_executable(test_death death/test_death.c) target_include_directories(test_death PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(test_death PRIVATE _GNU_SOURCE) - target_link_libraries(test_death PRIVATE cfiber cfiber_test project_flags) + target_link_libraries(test_death PRIVATE cfiber_test project_flags) if(CFIBER_REACTOR) target_compile_definitions(test_death PRIVATE CFIBER_TEST_HAVE_REACTOR=1) target_link_libraries(test_death PRIVATE cfiber_reactor) @@ -179,7 +181,7 @@ if(CFIBER_STACK_SANITIZER) add_executable(test_stack_sanitizer stack/test_stack_sanitizer.c) endif() target_include_directories(test_stack_sanitizer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(test_stack_sanitizer PRIVATE cfiber cfiber_test project_flags) + target_link_libraries(test_stack_sanitizer PRIVATE cfiber_test project_flags) if(_cfiber_register_ctest) add_test(NAME stack_sanitizer COMMAND test_stack_sanitizer) diff --git a/utils/make.sh b/utils/make.sh index a4e9241..4da5996 100755 --- a/utils/make.sh +++ b/utils/make.sh @@ -14,9 +14,9 @@ # -t, --tests Build and run unit tests # -v, --verbose Verbose build output # --sanitizer Enable the stack sanitizer (canary + watermark) -# --asan Enable AddressSanitizer (hosted x86_64 only) -# --ubsan Enable UndefinedBehaviorSanitizer (hosted x86_64 only) -# --tsan Enable ThreadSanitizer (hosted x86_64 only; implies --reactor) +# --asan Enable AddressSanitizer (hosted native builds only) +# --ubsan Enable UndefinedBehaviorSanitizer (hosted native builds only) +# --tsan Enable ThreadSanitizer (Linux x86_64 only; implies --reactor) # --shared Build cfiber as a shared library (default: static) # --pic Build the static library with -fPIC # --reactor Build + test the optional epoll reactor (Linux only) @@ -62,11 +62,11 @@ Usage: $(basename "$0") [options] -t, --tests Build and run unit tests -v, --verbose Verbose build output --sanitizer Enable the stack sanitizer (canary + watermark) - --asan Enable AddressSanitizer (hosted x86_64 only; mutually - exclusive with --sanitizer) - --ubsan Enable UndefinedBehaviorSanitizer (hosted x86_64 only; may - be combined with --asan) - --tsan Enable ThreadSanitizer (hosted x86_64 only; mutually + --asan Enable AddressSanitizer (hosted native builds only; + mutually exclusive with --sanitizer) + --ubsan Enable UndefinedBehaviorSanitizer (hosted native builds + only; may be combined with --asan) + --tsan Enable ThreadSanitizer (Linux x86_64 only; mutually exclusive with --asan; implies --reactor) --shared Build cfiber as a shared library (default: static) --pic Build the static library with -fPIC (ignored with --shared) @@ -90,12 +90,12 @@ project_root=$(cd "${script_dir}/.." && pwd) cd "${project_root}" # -------------------------------------------------------------------------------------- -# Host OS / job-count detection. Linux, or macOS unverified. Not Windows. +# Host OS / job-count detection. Linux or macOS. Not Windows. # -------------------------------------------------------------------------------------- host_os="$(uname -s)" case "${host_os}" in Linux|Darwin) ;; - *) die "unsupported host OS: '${host_os}' (cfiber builds on Linux; macOS is unverified)" ;; + *) die "unsupported host OS: '${host_os}' (cfiber builds on Linux and macOS)" ;; esac detect_jobs() { From 0def67e37c40e675d2c73f62ccabfd8d1153622d Mon Sep 17 00:00:00 2001 From: Aoto Date: Mon, 21 Sep 2026 17:36:35 +0200 Subject: [PATCH 3/3] macos: add ci job --- .github/workflows/macos.yml | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/macos.yml diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000..2105ce0 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2026 Aoto +# SPDX-License-Identifier: MIT + +# Native macOS arm64 build + tests: Release and Debug, ASan, shared. Apple +# clang on the runner; no container. The reactor, fuzzing and TSan stay +# Linux-only. +name: macos + +on: + push: + branches: [master] + paths: + - 'src/**' + - 'include/**' + - 'tests/**' + - 'examples/**' + - 'utils/**' + - 'cmake/**' + - '**/CMakeLists.txt' + - '.github/workflows/macos.yml' + pull_request: + +permissions: + contents: read + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + build-test: + runs-on: macos-15 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - name: release + args: "-t -e" + - name: debug + args: "-t -e -d" + - name: asan + args: "-t -e --asan" + - name: shared + args: "-t -e --shared" + name: "macos arm64 (${{ matrix.name }})" + steps: + - uses: actions/checkout@v4 + - name: Build and run tests + run: ./utils/make.sh ${{ matrix.args }}