@gjsify/sab-native 0.51.1 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # @gjsify/sab-native
2
2
 
3
- Optional native Vala bridge providing cross-process shared memory and atomics for `@gjsify/worker_threads` on GJS. Implements `SharedBuffer` via Linux `memfd_create` + `mmap(MAP_SHARED)` with typed accessors and SEQ_CST atomics backed by `__atomic_*` GCC builtins and `SYS_futex` for wait/notify. Also exposes `FdChannel` for passing file descriptors between processes via SCM_RIGHTS over a Unix-domain socket.
3
+ Optional native Vala bridge providing cross-process shared memory and atomics for `@gjsify/worker_threads` on GJS. Implements `SharedBuffer` as an anonymous `mmap(MAP_SHARED)` region (Linux `memfd_create`, macOS `shm_open` + immediate `shm_unlink`) with typed accessors and SEQ_CST atomics backed by `__atomic_*` builtins, and an address-keyed cross-process wait/notify (Linux `SYS_futex`, macOS `os_sync_wait_on_address`). Also exposes `FdChannel` for passing file descriptors between processes via SCM_RIGHTS over a Unix-domain socket.
4
4
 
5
5
  Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
6
6
 
7
7
  ## Platform support
8
8
 
9
- **Cross-process `SharedBuffer` is Linux-only today.** This is a property of the package, not of your installation — see [ADR 0013](../../../docs/adr/0013-sab-native-platform-scope.md) for the full reasoning.
9
+ **Cross-process `SharedBuffer` works on Linux and macOS 14.4+; Windows is not supported.** This is a property of the package, not of your installation — see [ADR 0013](../../../docs/adr/0013-sab-native-platform-scope.md) for the full reasoning.
10
10
 
11
11
  | Platform | Cross-process `SharedBuffer` | Notes |
12
12
  |---|---|---|
13
13
  | Linux `x86_64`, `aarch64`, `ppc64`, `s390x`, `riscv64` | ✅ Supported | `memfd_create(2)` + `mmap(MAP_SHARED)`, `SYS_futex` (non-private flavour, so waits match across processes), `SCM_RIGHTS`. Prebuilds ship for all five architectures. |
14
- | macOS | ❌ Not yet | Planned, design fixed: `shm_open` + `ftruncate` + immediate `shm_unlink` for the region, unchanged `SCM_RIGHTS`, and `os_sync_wait_on_address` / `os_sync_wake_by_address_*` with the `…_SHARED` flag for wait/notify. Requires **macOS 14.4+**. Blocked on a macOS prebuild CI job. |
14
+ | macOS `arm64`, `x86_64` | ✅ Supported | `shm_open` + `ftruncate` + immediate `shm_unlink` for the region, `os_sync_wait_on_address` / `os_sync_wake_by_address_any` with the `…_SHARED` flag for wait/notify, `SCM_RIGHTS` over a `SOCK_STREAM` pair (Darwin has no Unix-domain `SOCK_SEQPACKET`). Requires **macOS 14.4+**. Prebuilds come from CI's macOS job. |
15
15
  | Windows | ❌ Not supported | Blocked twice over: GJS itself does not run on Windows, and Windows has no cross-process address-keyed wait — `WaitOnAddress`/`WakeByAddressSingle` are process-local by documentation, so `wait32`/`notify32` would need a different contract (named kernel objects). Revisit only via a new ADR. |
16
16
  | Node.js / browser | ❌ n/a | GJS-only package (`runtimes.node`/`.browser` = `none`). |
17
17
 
@@ -63,7 +63,7 @@ if (hasNativeSab()) {
63
63
  }
64
64
  ```
65
65
 
66
- Ships as a prebuilt `.so` + `.typelib` for `linux-{x64,arm64,ppc64,s390x,riscv64}`.
66
+ Ships as a prebuilt library + `.typelib` for `linux-{x64,arm64,ppc64,s390x,riscv64}` (`.so`) and `darwin-{arm64,x64}` (`.dylib`), each in its own `@gjsify/sab-native-<os>-<arch>` package.
67
67
 
68
68
  ## License
69
69
 
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";const e=globalThis;function resolveNativeSab(e){if(!e)return null;let t;try{t=e.GjsifySabNative}catch{return null}if(!t||typeof t!=`object`)return null;let n=t;return typeof n.SharedBuffer?.create!=`function`||typeof n.FdChannel?.make_pair!=`function`?null:n}const t=resolveNativeSab(e.imports?.gi),n=t;function hasNativeSab(){return t!==null}const r="@gjsify/sab-native: the native backend is not available on this platform. Cross-process SharedBuffer is currently Linux-only (memfd_create + futex + SCM_RIGHTS); prebuilds ship for linux-{x64,arm64,ppc64,s390x,riscv64} only — macOS support is planned and Windows is unsupported (see docs/adr/0013-sab-native-platform-scope.md). Guard with hasNativeSab() before using SharedBuffer. On Linux, a missing prebuild can be built locally with `gjsify workspace @gjsify/sab-native build:prebuilds`.";var i=class SharedBuffer{_native;constructor(e){this._native=e}static create(e){if(!t)throw Error(r);if(!Number.isInteger(e)||e<=0)throw TypeError(`SharedBuffer.create: size must be a positive integer`);let n=t.SharedBuffer.create(e);if(!n)throw Error(`memfd_create or mmap failed`);return new SharedBuffer(n)}static fromFd(e,n){if(!t)throw Error(r);if(!Number.isInteger(e)||e<0)throw TypeError(`SharedBuffer.fromFd: fd must be a non-negative integer`);if(!Number.isInteger(n)||n<=0)throw TypeError(`SharedBuffer.fromFd: size must be a positive integer`);let i=t.SharedBuffer.from_fd(e,n);if(!i)throw Error(`mmap failed`);return new SharedBuffer(i)}get byteLength(){return this._assertOpen().byte_length}get fd(){return this._assertOpen().fd}get closed(){return this._native===null}close(){this._native=null}getUint8(e){return this._assertOpen().get_u8(e)}setUint8(e,t){this._assertOpen().set_u8(e,t&255)}getUint32LE(e){return this._assertOpen().get_u32_le(e)}setUint32LE(e,t){this._assertOpen().set_u32_le(e,t>>>0)}getInt32LE(e){return this._assertOpen().get_i32_le(e)}setInt32LE(e,t){this._assertOpen().set_i32_le(e,t|0)}getBigUint64LE(e){return this._assertOpen().get_u64_le(e)}setBigUint64LE(e,t){this._assertOpen().set_u64_le(e,t)}readBytes(t,n){let r=this._assertOpen().read_bytes(t,n),i=e.imports?.byteArray;if(i&&typeof i.fromGBytes==`function`){let e=i.fromGBytes(r);return new Uint8Array(e)}return new Uint8Array(r.toArray())}viewBytes(e,t){return this.readBytes(e,t)}toBuffer(t=0,n){let r=n??this.byteLength-t,i=this.viewBytes(t,r),a=e.Buffer;if(typeof a?.from!=`function`)throw Error(`SharedBuffer.toBuffer: globalThis.Buffer is not registered. Import "@gjsify/buffer/register" or rely on --globals auto.`);return a.from(i.buffer,i.byteOffset,i.byteLength)}writeBytes(t,n){let r=e.imports?.byteArray,i;if(r&&typeof r.toGBytes==`function`)i=r.toGBytes(n);else{let t=e.imports?.gi?.GLib;i=t?.Bytes?new t.Bytes(n):n}this._assertOpen().write_bytes(t,i)}get _nativeHandle(){return this._assertOpen()}_assertOpen(){if(!this._native)throw Error(`SharedBuffer has been closed`);return this._native}};const a={add32(e,t,n){return e._nativeHandle.atomic_add_i32(t,n|0)},sub32(e,t,n){return e._nativeHandle.atomic_sub_i32(t,n|0)},load32(e,t){return e._nativeHandle.atomic_load_i32(t)},store32(e,t,n){e._nativeHandle.atomic_store_i32(t,n|0)},exchange32(e,t,n){return e._nativeHandle.atomic_xchg_i32(t,n|0)},compareExchange32(e,t,n,r){return e._nativeHandle.atomic_cmpxchg_i32(t,n|0,r|0)},wait32(e,t,n,r){let i=e._nativeHandle.futex_wait(t,n|0,r|0);if(i===0)return`ok`;if(i===-1)return`not-equal`;if(i===-2)return`timed-out`;if(i===-3)return`interrupted`;throw Error(`futex_wait returned errno ${-i}`)},notify32(e,t,n){return e._nativeHandle.futex_wake(t,n|0)}},o=t?{makePair(){let[e,n,r]=t.FdChannel.make_pair();if(!e)throw Error(`socketpair() failed`);return{parentFd:n,childFd:r}},sendFd(e,n,r){return t.FdChannel.send_fd(e,n,r>>>0)},recvFd(e){let[n,r]=t.FdChannel.recv_fd(e);if(n===0)return null;if(n<0)throw Error(`recvmsg failed`);return{fd:n,tag:r>>>0}},closeFd(e){t.FdChannel.close_fd(e)}}:null;export{r as NATIVE_SAB_UNAVAILABLE,i as SharedBuffer,a as atomics,o as fdChannel,hasNativeSab,n as nativeSab,resolveNativeSab};
1
+ import"./_virtual/_rolldown/runtime.js";import{openNativeLibrary as e}from"@gjsify/utils/core";const t=globalThis;function resolveNativeSab(t){if(!t)return null;let n;try{n=t.GjsifySabNative}catch{return null}if(!n||typeof n!=`object`)return null;let r=e(`GjsifySabNative`,t);if(r)return console.debug(r.message),null;let i=n;return typeof i.SharedBuffer?.create!=`function`||typeof i.FdChannel?.make_pair!=`function`?null:i}const n=resolveNativeSab(t.imports?.gi),r=n;function hasNativeSab(){return n!==null}const i="@gjsify/sab-native: the native backend is not available on this platform. Cross-process SharedBuffer is supported on Linux (memfd_create + futex + SCM_RIGHTS; prebuilds for linux-{x64,arm64,ppc64,s390x,riscv64}) and on macOS 14.4+ (shm_open + os_sync_wait_on_address; prebuilds for darwin-{arm64,x64}) — Windows is unsupported (see docs/adr/0013-sab-native-platform-scope.md). Guard with hasNativeSab() before using SharedBuffer. On Linux or macOS, a missing prebuild can be built locally with `gjsify workspace @gjsify/sab-native build:prebuilds`.";var a=class SharedBuffer{_native;constructor(e){this._native=e}static create(e){if(!n)throw Error(i);if(!Number.isInteger(e)||e<=0)throw TypeError(`SharedBuffer.create: size must be a positive integer`);let t=n.SharedBuffer.create(e);if(!t)throw Error(`memfd_create or mmap failed`);return new SharedBuffer(t)}static fromFd(e,t){if(!n)throw Error(i);if(!Number.isInteger(e)||e<0)throw TypeError(`SharedBuffer.fromFd: fd must be a non-negative integer`);if(!Number.isInteger(t)||t<=0)throw TypeError(`SharedBuffer.fromFd: size must be a positive integer`);let r=n.SharedBuffer.from_fd(e,t);if(!r)throw Error(`mmap failed`);return new SharedBuffer(r)}get byteLength(){return this._assertOpen().byte_length}get fd(){return this._assertOpen().fd}get closed(){return this._native===null}close(){this._native=null}getUint8(e){return this._assertOpen().get_u8(e)}setUint8(e,t){this._assertOpen().set_u8(e,t&255)}getUint32LE(e){return this._assertOpen().get_u32_le(e)}setUint32LE(e,t){this._assertOpen().set_u32_le(e,t>>>0)}getInt32LE(e){return this._assertOpen().get_i32_le(e)}setInt32LE(e,t){this._assertOpen().set_i32_le(e,t|0)}getBigUint64LE(e){return this._assertOpen().get_u64_le(e)}setBigUint64LE(e,t){this._assertOpen().set_u64_le(e,t)}readBytes(e,n){let r=this._assertOpen().read_bytes(e,n),i=t.imports?.byteArray;if(i&&typeof i.fromGBytes==`function`){let e=i.fromGBytes(r);return new Uint8Array(e)}return new Uint8Array(r.toArray())}viewBytes(e,t){return this.readBytes(e,t)}toBuffer(e=0,n){let r=n??this.byteLength-e,i=this.viewBytes(e,r),a=t.Buffer;if(typeof a?.from!=`function`)throw Error(`SharedBuffer.toBuffer: globalThis.Buffer is not registered. Import "@gjsify/buffer/register" or rely on --globals auto.`);return a.from(i.buffer,i.byteOffset,i.byteLength)}writeBytes(e,n){let r=t.imports?.byteArray,i;if(r&&typeof r.toGBytes==`function`)i=r.toGBytes(n);else{let e=t.imports?.gi?.GLib;i=e?.Bytes?new e.Bytes(n):n}this._assertOpen().write_bytes(e,i)}get _nativeHandle(){return this._assertOpen()}_assertOpen(){if(!this._native)throw Error(`SharedBuffer has been closed`);return this._native}};const o={add32(e,t,n){return e._nativeHandle.atomic_add_i32(t,n|0)},sub32(e,t,n){return e._nativeHandle.atomic_sub_i32(t,n|0)},load32(e,t){return e._nativeHandle.atomic_load_i32(t)},store32(e,t,n){e._nativeHandle.atomic_store_i32(t,n|0)},exchange32(e,t,n){return e._nativeHandle.atomic_xchg_i32(t,n|0)},compareExchange32(e,t,n,r){return e._nativeHandle.atomic_cmpxchg_i32(t,n|0,r|0)},wait32(e,t,n,r){let i=e._nativeHandle.futex_wait(t,n|0,r|0);if(i===0)return`ok`;if(i===-1)return`not-equal`;if(i===-2)return`timed-out`;if(i===-3)return`interrupted`;throw Error(`futex_wait returned errno ${-i}`)},notify32(e,t,n){return e._nativeHandle.futex_wake(t,n|0)}},s=n?{makePair(){let[e,t,r]=n.FdChannel.make_pair();if(!e)throw Error(`socketpair() failed`);return{parentFd:t,childFd:r}},sendFd(e,t,r){return n.FdChannel.send_fd(e,t,r>>>0)},recvFd(e){let[t,r]=n.FdChannel.recv_fd(e);if(t===0)return null;if(t<0)throw Error(`recvmsg failed`);return{fd:t,tag:r>>>0}},closeFd(e){n.FdChannel.close_fd(e)}}:null;export{i as NATIVE_SAB_UNAVAILABLE,a as SharedBuffer,o as atomics,s as fdChannel,hasNativeSab,r as nativeSab,resolveNativeSab};
@@ -37,7 +37,7 @@ export interface GjsifySabNativeModule {
37
37
  /**
38
38
  * Resolve the `GjsifySabNative` GI module, normalising EVERY unavailable case to
39
39
  * `null`: no `gi` at all (not GJS), a throwing property access (typelib not on
40
- * `GI_TYPELIB_PATH` — the macOS/Windows case), and a value that is not the module
40
+ * `GI_TYPELIB_PATH` — the Windows case, or a target with no prebuild), and a value that is not the module
41
41
  * we expect (stale, partial or shadowed typelib). The third needs the shape check:
42
42
  * a partial value otherwise leaves `hasNativeSab()` reporting `true` and fails with
43
43
  * an opaque `TypeError` at first use.
@@ -50,7 +50,7 @@ export declare function resolveNativeSab(gi: Record<string, unknown> | undefined
50
50
  export declare const nativeSab: GjsifySabNativeModule | null;
51
51
  /**
52
52
  * THE gate for cross-process `SharedBuffer`, and platform-conditional by design:
53
- * `true` only on Linux (ADR 0013), `false` on macOS, Windows, Node and the browser.
53
+ * `true` only on Linux and macOS (ADR 0013), `false` on Windows, Node and the browser.
54
54
  * Guard every use of `SharedBuffer` / `atomics` / `fdChannel` with it.
55
55
  */
56
56
  export declare function hasNativeSab(): boolean;
package/meson.build CHANGED
@@ -1,4 +1,10 @@
1
- project('GjsifySabNative', ['c', 'vala'], version: '1.0')
1
+ # `buildtype=plain` = NO `-g`. ld64 emits a DEBUG MAP into any image whose objects
2
+ # carry DWARF — one `N_OSO` stab per object, holding its MTIME — so with `-g` every
3
+ # rebuild of this commit differed in bytes (plus the `LC_UUID` and the ad-hoc signature
4
+ # computed over them) and `commit-prebuilds` re-committed all 16 darwin dylibs. That map
5
+ # names intermediate `.o` files no consumer can have. Why: docs/prebuilds.md.
6
+ project('GjsifySabNative', ['c', 'vala'], version: '1.0',
7
+ default_options: ['buildtype=plain'])
2
8
 
3
9
  root_dir = meson.current_source_dir()
4
10
 
@@ -6,8 +12,12 @@ root_dir = meson.current_source_dir()
6
12
  # dnf install vala meson ninja-build
7
13
  #
8
14
  # No external C libraries — sab-native uses libc-only primitives:
9
- # memfd_create(2), mmap(2), syscall(SYS_futex), __atomic_* GCC builtins,
10
- # AF_UNIX socketpair + SCM_RIGHTS. All available on every Linux glibc since 3.17.
15
+ # Linux: memfd_create(2), mmap(2), syscall(SYS_futex), __atomic_* builtins,
16
+ # AF_UNIX socketpair + SCM_RIGHTS. Every Linux glibc since 3.17.
17
+ # darwin: shm_open(2), mmap(2), os_sync_wait_on_address (macOS 14.4+),
18
+ # __atomic_* builtins, AF_UNIX socketpair + SCM_RIGHTS
19
+ # (Homebrew: brew install vala meson ninja gobject-introspection).
20
+ # See docs/adr/0013-sab-native-platform-scope.md for why the two differ.
11
21
  #
12
22
  # Vala can't express the futex syscall + SCM_RIGHTS ancillary-data dance
13
23
  # cleanly, so we wrap them in a small C shim (src/vala/sab-helpers.{h,c})
@@ -29,8 +39,19 @@ sources = files(
29
39
  # Compiler picks up sab-helpers.h via the include dir.
30
40
  inc = include_directories('src/vala')
31
41
 
42
+ host_system = host_machine.system()
43
+
32
44
  # -D_GNU_SOURCE: needed for memfd_create + MFD_CLOEXEC + SYS_futex visibility.
33
- c_args = ['-D_GNU_SOURCE']
45
+ # darwin exposes everything its backend uses by default.
46
+ c_args = host_system == 'darwin' ? [] : ['-D_GNU_SOURCE']
47
+
48
+ # The typelib records the library's leaf, and GI dlopens exactly that name —
49
+ # a `.so` leaf on darwin is found via GI_TYPELIB_PATH and then never loads.
50
+ if host_system == 'darwin'
51
+ shared_lib_name = 'libgjsifysabnative.dylib'
52
+ else
53
+ shared_lib_name = 'libgjsifysabnative.so'
54
+ endif
34
55
 
35
56
  libGjsifySabNative = library('gjsifysabnative', sources,
36
57
  dependencies: vala_deps,
@@ -46,7 +67,7 @@ g_ir_compiler = find_program('g-ir-compiler')
46
67
  custom_target(meson.project_name() + '-1.0.typelib',
47
68
  command: [
48
69
  g_ir_compiler,
49
- '--shared-library', 'libgjsifysabnative.so',
70
+ '--shared-library', shared_lib_name,
50
71
  '--output', '@OUTPUT@',
51
72
  meson.current_build_dir() / meson.project_name() + '-1.0.gir',
52
73
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/sab-native",
3
- "version": "0.51.1",
3
+ "version": "0.53.0",
4
4
  "description": "Optional Vala/GObject bridge providing cross-process shared memory + atomics for @gjsify/worker_threads. Wraps Linux memfd_create(2) + mmap(MAP_SHARED) to back JS-visible SharedBuffer regions whose fds can be passed to child workers via SCM_RIGHTS over a Unix-domain socket; exposes typed accessors (getInt32LE / setUint8 / etc.) plus atomics built on __atomic_* GCC builtins and SYS_futex for wait/notify. Workaround for the stock-GJS gap where SharedArrayBuffer is unavailable (Mozilla disables the SAB constructor without COOP/COEP opts) and SAB cannot share backing stores across Gio.Subprocess workers anyway.",
5
5
  "type": "module",
6
6
  "main": "lib/esm/index.js",
@@ -25,7 +25,9 @@
25
25
  "linux-arm64-musl",
26
26
  "linux-ppc64",
27
27
  "linux-s390x",
28
- "linux-riscv64"
28
+ "linux-riscv64",
29
+ "darwin-arm64",
30
+ "darwin-x64"
29
31
  ],
30
32
  "runtimes": {
31
33
  "gjs": "polyfill",
@@ -64,14 +66,15 @@
64
66
  "native"
65
67
  ],
66
68
  "dependencies": {
67
- "@girs/gjs": "5.0.0",
68
- "@girs/glib-2.0": "5.0.0",
69
- "@girs/gobject-2.0": "5.0.0",
70
- "@girs/gio-2.0": "5.0.0"
69
+ "@girs/gjs": "5.4.0",
70
+ "@girs/glib-2.0": "5.4.0",
71
+ "@girs/gobject-2.0": "5.4.0",
72
+ "@girs/gio-2.0": "5.4.0",
73
+ "@gjsify/utils": "^0.53.0"
71
74
  },
72
75
  "devDependencies": {
73
- "@gjsify/cli": "^0.51.1",
74
- "@gjsify/unit": "^0.51.1",
76
+ "@gjsify/cli": "^0.53.0",
77
+ "@gjsify/unit": "^0.53.0",
75
78
  "@ts-for-gir/cli": "^4.1.0",
76
79
  "@types/node": "^25.9.2",
77
80
  "typescript": "^6.0.3"
@@ -87,12 +90,14 @@
87
90
  },
88
91
  "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/node/sab-native#readme",
89
92
  "optionalDependencies": {
90
- "@gjsify/sab-native-linux-arm64": "0.51.1",
91
- "@gjsify/sab-native-linux-arm64-musl": "0.51.1",
92
- "@gjsify/sab-native-linux-ppc64": "0.51.1",
93
- "@gjsify/sab-native-linux-riscv64": "0.51.1",
94
- "@gjsify/sab-native-linux-s390x": "0.51.1",
95
- "@gjsify/sab-native-linux-x64": "0.51.1",
96
- "@gjsify/sab-native-linux-x64-musl": "0.51.1"
93
+ "@gjsify/sab-native-darwin-arm64": "0.53.0",
94
+ "@gjsify/sab-native-darwin-x64": "0.53.0",
95
+ "@gjsify/sab-native-linux-arm64": "0.53.0",
96
+ "@gjsify/sab-native-linux-arm64-musl": "0.53.0",
97
+ "@gjsify/sab-native-linux-ppc64": "0.53.0",
98
+ "@gjsify/sab-native-linux-riscv64": "0.53.0",
99
+ "@gjsify/sab-native-linux-s390x": "0.53.0",
100
+ "@gjsify/sab-native-linux-x64": "0.53.0",
101
+ "@gjsify/sab-native-linux-x64-musl": "0.53.0"
97
102
  }
98
103
  }
@@ -1,8 +1,16 @@
1
1
  /*
2
2
  * sab-helpers.c — see sab-helpers.h for the contract.
3
3
  *
4
- * Linux-only. Compile with -D_GNU_SOURCE so memfd_create, MFD_CLOEXEC,
5
- * and the FUTEX_*_PRIVATE constants are visible.
4
+ * Two backends behind one contract (ADR 0013 §1: the seam is this shim, not
5
+ * the primitive — every platform supplies an ADDRESS-KEYED compare-and-wait):
6
+ *
7
+ * Linux — memfd_create + non-private SYS_futex + SOCK_SEQPACKET. Compile
8
+ * with -D_GNU_SOURCE so memfd_create, MFD_CLOEXEC and the FUTEX_*
9
+ * constants are visible.
10
+ * darwin — shm_open + immediate shm_unlink, os_sync_wait_on_address with
11
+ * the _SHARED flag (macOS 14.4+), SOCK_STREAM with fixed 4-byte
12
+ * frames. Every darwin difference is marked `#if defined(__APPLE__)`
13
+ * at the point it applies, with the measured reason next to it.
6
14
  *
7
15
  * ONE ARTIFACT MUST SERVE BOTH LIBCS — no glibc-private symbols.
8
16
  *
@@ -37,19 +45,82 @@
37
45
 
38
46
  #include <errno.h>
39
47
  #include <fcntl.h>
40
- #include <linux/futex.h>
41
48
  #include <stdatomic.h>
42
49
  #include <stdint.h>
43
50
  #include <string.h>
44
51
  #include <sys/mman.h>
45
52
  #include <sys/socket.h>
46
- #include <sys/syscall.h>
47
53
  #include <sys/time.h>
48
54
  #include <sys/types.h>
49
55
  #include <sys/un.h>
50
56
  #include <time.h>
51
57
  #include <unistd.h>
52
58
 
59
+ #if defined(__APPLE__)
60
+ # include <os/clock.h>
61
+ # include <os/os_sync_wait_on_address.h>
62
+ # include <sys/stat.h>
63
+ #else
64
+ # include <linux/futex.h>
65
+ # include <sys/syscall.h>
66
+ #endif
67
+
68
+ #if defined(__APPLE__)
69
+
70
+ /* ── darwin: anonymous shared memory ────────────────────────────────────
71
+ *
72
+ * Darwin has neither memfd_create nor FreeBSD's SHM_ANON, so the closest
73
+ * equivalent of an anonymous memfd is a POSIX shm object that is unlinked
74
+ * the moment it exists: the fd stays valid, mmap/SCM_RIGHTS work on it as on
75
+ * any descriptor, and the object dies with its last reference (ADR 0013 §3).
76
+ *
77
+ * The name is visible in a global namespace only between shm_open and
78
+ * shm_unlink; a crash inside that window leaks one object until reboot —
79
+ * the documented difference from the Linux backend. PSHMNAMLEN caps names at
80
+ * 31 bytes, hence a short random name rather than a descriptive one, and
81
+ * O_EXCL + retry so two processes can never open each other's region. */
82
+ static int
83
+ gjsify_shm_anonymous (void)
84
+ {
85
+ for (int attempt = 0; attempt < 16; attempt++) {
86
+ char name[32];
87
+ g_snprintf (name, sizeof name, "/gsab.%x.%08x",
88
+ (unsigned) getpid (), (unsigned) g_random_int ());
89
+ /* POSIX sets FD_CLOEXEC on a shm_open descriptor. */
90
+ int fd = shm_open (name, O_CREAT | O_EXCL | O_RDWR, 0600);
91
+ if (fd >= 0) {
92
+ shm_unlink (name);
93
+ return fd;
94
+ }
95
+ if (errno != EEXIST) return -1;
96
+ }
97
+ errno = EEXIST;
98
+ return -1;
99
+ }
100
+
101
+ /* Plain fcntl: darwin has no LFS redirect (the musl concern below is a
102
+ * Linux one), and syscall(2) is deprecated there. */
103
+ static int
104
+ gjsify_dup_cloexec (int fd)
105
+ {
106
+ return fcntl (fd, F_DUPFD_CLOEXEC, 0);
107
+ }
108
+
109
+ /* Darwin's CMSG_* macros align to 4 bytes (__DARWIN_ALIGN32) and it defines
110
+ * no CMSG_ALIGN, so the size_t-aligned fallback the Linux path carries would
111
+ * compute the WRONG next header here. There is no second libc to stay
112
+ * compatible with on darwin, so the system macro is the correct one. */
113
+ #define gjsify_cmsg_nxthdr(msg, cmsg) CMSG_NXTHDR ((msg), (cmsg))
114
+
115
+ static gboolean
116
+ gjsify_set_cloexec (int fd)
117
+ {
118
+ int flags = fcntl (fd, F_GETFD);
119
+ return flags >= 0 && fcntl (fd, F_SETFD, flags | FD_CLOEXEC) == 0;
120
+ }
121
+
122
+ #else /* Linux */
123
+
53
124
  /* glibc < 2.27 doesn't expose memfd_create() as a libc wrapper. We use the
54
125
  * raw syscall to stay portable across the prebuild distro matrix. */
55
126
  #ifndef MFD_CLOEXEC
@@ -97,6 +168,8 @@ gjsify_cmsg_nxthdr (const struct msghdr *msg, struct cmsghdr *cmsg)
97
168
  return (struct cmsghdr *) next;
98
169
  }
99
170
 
171
+ #endif /* __APPLE__ */
172
+
100
173
  /* ────────────────────────────────────────────────────────────────────── *
101
174
  * GjsifySabRegion: opaque shared-memory region
102
175
  * ────────────────────────────────────────────────────────────────────── */
@@ -139,7 +212,11 @@ gjsify_sab_region_new_anonymous (gsize size)
139
212
  {
140
213
  if (size == 0) { errno = EINVAL; return NULL; }
141
214
 
215
+ #if defined(__APPLE__)
216
+ int fd = gjsify_shm_anonymous ();
217
+ #else
142
218
  int fd = gjsify_memfd_create ("gjsify-sab", MFD_CLOEXEC);
219
+ #endif
143
220
  if (fd < 0) return NULL;
144
221
 
145
222
  if (ftruncate (fd, (off_t) size) < 0) {
@@ -378,6 +455,39 @@ gjsify_sab_region_futex_wait (GjsifySabRegion *region,
378
455
  BOUNDS_CHECK (region, offset, 4);
379
456
  int32_t *addr = (int32_t *) ((guint8 *) region->ptr + offset);
380
457
 
458
+ #if defined(__APPLE__)
459
+ /* os_sync_wait_on_address reports "value differed" as SUCCESS, exactly
460
+ * like a wake, so the not-equal answer has to come from our own compare.
461
+ * The window between this load and the kernel's compare only ever turns
462
+ * a would-be 'not-equal' into 'ok' — indistinguishable from a notify that
463
+ * landed just after we slept, which Atomics.wait already permits. */
464
+ if (__atomic_load_n (addr, __ATOMIC_SEQ_CST) != expected) return -1;
465
+ /* A zero timeout is EINVAL to the timed variant, and a 0 ms wait on a
466
+ * matching value is by definition an immediate time-out. */
467
+ if (timeout_ms == 0) return -2;
468
+
469
+ /* _SHARED is the darwin spelling of Linux's non-private FUTEX_WAIT: the
470
+ * address lives in a region other processes map at other addresses, and
471
+ * the wake may come from one of them. */
472
+ int ret;
473
+ if (timeout_ms < 0) {
474
+ ret = os_sync_wait_on_address (addr, (uint64_t) (uint32_t) expected, 4,
475
+ OS_SYNC_WAIT_ON_ADDRESS_SHARED);
476
+ } else {
477
+ ret = os_sync_wait_on_address_with_timeout (addr, (uint64_t) (uint32_t) expected, 4,
478
+ OS_SYNC_WAIT_ON_ADDRESS_SHARED,
479
+ OS_CLOCK_MACH_ABSOLUTE_TIME,
480
+ (uint64_t) timeout_ms * 1000000ULL);
481
+ }
482
+ if (ret >= 0) return 0;
483
+ int e = errno;
484
+ if (e == ETIMEDOUT) return -2;
485
+ /* Apple documents EFAULT and ENOMEM as TRANSIENT early returns the caller
486
+ * should retry after re-reading the value (the address itself is already
487
+ * bounds-checked) — the same contract as EINTR. */
488
+ if (e == EINTR || e == EFAULT || e == ENOMEM) return -3;
489
+ return -e;
490
+ #else
381
491
  struct timespec ts;
382
492
  struct timespec *ts_ptr = NULL;
383
493
  if (timeout_ms >= 0) {
@@ -399,6 +509,7 @@ gjsify_sab_region_futex_wait (GjsifySabRegion *region,
399
509
  if (e == ETIMEDOUT) return -2;
400
510
  if (e == EINTR) return -3;
401
511
  return -e;
512
+ #endif
402
513
  }
403
514
 
404
515
  gint
@@ -408,9 +519,25 @@ gjsify_sab_region_futex_wake (GjsifySabRegion *region,
408
519
  {
409
520
  BOUNDS_CHECK (region, offset, 4);
410
521
  int32_t *addr = (int32_t *) ((guint8 *) region->ptr + offset);
522
+ #if defined(__APPLE__)
523
+ /* FUTEX_WAKE returns how many it woke; os_sync_wake_by_address_all does
524
+ * not, and notify32's return value is that count. So wake one at a time:
525
+ * each successful _any wakes exactly one waiter, and ENOENT means none is
526
+ * left — which also makes `count` a real upper bound, as on Linux. */
527
+ gint woken = 0;
528
+ while (woken < count) {
529
+ if (os_sync_wake_by_address_any (addr, 4, OS_SYNC_WAKE_BY_ADDRESS_SHARED) != 0) {
530
+ if (errno == ENOENT) break;
531
+ return woken > 0 ? woken : -errno;
532
+ }
533
+ woken++;
534
+ }
535
+ return woken;
536
+ #else
411
537
  long ret = syscall (SYS_futex, addr, FUTEX_WAKE, count,
412
538
  NULL, NULL, 0);
413
539
  return (gint) ret;
540
+ #endif
414
541
  }
415
542
 
416
543
  /* ────────────────────────────────────────────────────────────────────── *
@@ -421,9 +548,35 @@ gboolean
421
548
  gjsify_sab_socketpair (gint *parent_fd, gint *child_fd)
422
549
  {
423
550
  int sv[2];
551
+ #if defined(__APPLE__)
552
+ /* Measured on macOS 27: socketpair(AF_UNIX, SOCK_SEQPACKET) fails with
553
+ * EPROTONOSUPPORT, and SOCK_CLOEXEC does not exist. SOCK_DGRAM keeps
554
+ * message boundaries but reports a closed peer as ECONNRESET once and then
555
+ * blocks forever, losing recv_fd's "0 = orderly EOF". SOCK_STREAM keeps the
556
+ * EOF, and the boundaries come back by framing: every message is exactly
557
+ * one 4-byte tag carrying one SCM_RIGHTS fd, and recv reads exactly 4. */
558
+ if (socketpair (AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
559
+ return FALSE;
560
+ }
561
+ /* Not atomic with creation as SOCK_CLOEXEC is: a fork() on another thread
562
+ * in between would leak the pair into that child. Nothing forks
563
+ * concurrently with FdChannel.make_pair in a GJS process. */
564
+ for (int i = 0; i < 2; i++) {
565
+ int one = 1;
566
+ if (!gjsify_set_cloexec (sv[i])
567
+ || setsockopt (sv[i], SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof one) < 0) {
568
+ int e = errno;
569
+ close (sv[0]);
570
+ close (sv[1]);
571
+ errno = e;
572
+ return FALSE;
573
+ }
574
+ }
575
+ #else
424
576
  if (socketpair (AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sv) < 0) {
425
577
  return FALSE;
426
578
  }
579
+ #endif
427
580
  *parent_fd = sv[0];
428
581
  *child_fd = sv[1];
429
582
  return TRUE;
@@ -462,6 +615,17 @@ gjsify_sab_send_fd (gint socket_fd, gint fd_to_send, guint32 tag)
462
615
  do {
463
616
  n = sendmsg (socket_fd, &msg, MSG_NOSIGNAL);
464
617
  } while (n < 0 && errno == EINTR);
618
+ #if defined(__APPLE__)
619
+ /* SOCK_STREAM (see gjsify_sab_socketpair) may accept a frame short; the
620
+ * receiver reads exactly four bytes per frame, so a missing tail would shift
621
+ * every later tag. The fd already travelled with the first byte. */
622
+ while (n >= 0 && (size_t) n < sizeof tag_be) {
623
+ ssize_t m = send (socket_fd, (guint8 *) &tag_be + n, sizeof tag_be - (size_t) n, 0);
624
+ if (m < 0 && errno == EINTR) continue;
625
+ if (m < 0) return FALSE;
626
+ n += m;
627
+ }
628
+ #endif
465
629
  return n >= 0;
466
630
  }
467
631
 
@@ -488,13 +652,45 @@ gjsify_sab_recv_fd (gint socket_fd, guint32 *tag)
488
652
  msg.msg_controllen = sizeof cmsg_buf.buf;
489
653
 
490
654
  ssize_t n;
655
+ #if defined(__APPLE__)
656
+ /* No MSG_CMSG_CLOEXEC on darwin — the received fd gets FD_CLOEXEC below. */
657
+ do {
658
+ n = recvmsg (socket_fd, &msg, 0);
659
+ } while (n < 0 && errno == EINTR);
660
+ #else
491
661
  do {
492
662
  n = recvmsg (socket_fd, &msg, MSG_CMSG_CLOEXEC);
493
663
  } while (n < 0 && errno == EINTR);
664
+ #endif
494
665
 
495
666
  if (n == 0) return 0; /* orderly EOF */
496
667
  if (n < 0) return -1; /* error — errno preserved */
497
668
 
669
+ #if defined(__APPLE__)
670
+ /* SOCK_STREAM (see gjsify_sab_socketpair): a frame may in principle arrive
671
+ * short. The fd rides on the frame's first byte, so it is already in the
672
+ * control buffer; only the rest of the tag is still to read. */
673
+ while ((size_t) n < sizeof tag_be) {
674
+ ssize_t m = recv (socket_fd, (guint8 *) &tag_be + n, sizeof tag_be - (size_t) n, 0);
675
+ if (m < 0 && errno == EINTR) continue;
676
+ if (m <= 0) {
677
+ int e = m == 0 ? EBADMSG : errno;
678
+ /* The frame's fd was already installed in this process by the first
679
+ * recvmsg; the caller never sees it on this path, so close it here. */
680
+ for (struct cmsghdr *cm = CMSG_FIRSTHDR (&msg); cm != NULL; cm = gjsify_cmsg_nxthdr (&msg, cm)) {
681
+ if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) {
682
+ int fd;
683
+ memcpy (&fd, CMSG_DATA (cm), sizeof (int));
684
+ close (fd);
685
+ }
686
+ }
687
+ errno = e;
688
+ return -1;
689
+ }
690
+ n += m;
691
+ }
692
+ #endif
693
+
498
694
  *tag = GUINT32_FROM_BE (tag_be);
499
695
 
500
696
  /* Extract the fd from the control message. */
@@ -502,6 +698,9 @@ gjsify_sab_recv_fd (gint socket_fd, guint32 *tag)
502
698
  if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) {
503
699
  int fd;
504
700
  memcpy (&fd, CMSG_DATA (cm), sizeof (int));
701
+ #if defined(__APPLE__)
702
+ gjsify_set_cloexec (fd);
703
+ #endif
505
704
  return fd;
506
705
  }
507
706
  }
@@ -1,12 +1,13 @@
1
1
  /*
2
- * Tiny C shim around Linux shared-memory + atomics primitives that Vala
3
- * can't express cleanly:
2
+ * Tiny C shim around the shared-memory + atomics primitives that Vala
3
+ * can't express cleanly (Linux first, darwin in brackets — the per-platform
4
+ * reasons live in sab-helpers.c):
4
5
  *
5
- * - memfd_create(2) — anonymous shared-memory fd (no path needed)
6
+ * - memfd_create(2) [shm_open + shm_unlink] — anonymous shared-memory fd
6
7
  * - mmap(2) / munmap(2) — MAP_SHARED region creation
7
- * - SYS_futex (FUTEX_WAIT_PRIVATE / FUTEX_WAKE_PRIVATE) — wait/notify
8
+ * - SYS_futex, non-private [os_sync_wait_on_address, _SHARED] — wait/notify
8
9
  * - __atomic_* GCC builtins — load/store/add/cmpxchg with SEQ_CST
9
- * - socketpair(AF_UNIX, SOCK_SEQPACKET) — IPC side-channel for fd-passing
10
+ * - socketpair(AF_UNIX, SOCK_SEQPACKET [SOCK_STREAM]) — fd-passing channel
10
11
  * - sendmsg/recvmsg + SCM_RIGHTS — cross-process fd transfer
11
12
  *
12
13
  * Same design as @gjsify/http2-native: opaque struct (full definition in .c),
@@ -99,7 +100,7 @@ gint32 gjsify_sab_region_atomic_xchg_i32 (GjsifySabRegion *region, gsize of
99
100
  gboolean gjsify_sab_region_atomic_cmpxchg_i32 (GjsifySabRegion *region, gsize offset, gint32 *expected, gint32 desired);
100
101
 
101
102
  /* ────────────────────────────────────────────────────────────────────── *
102
- * Futex wait / wake (Linux SYS_futex, FUTEX_*_PRIVATE flavour)
103
+ * Futex wait / wake (Linux non-private SYS_futex; darwin os_sync _SHARED)
103
104
  * ────────────────────────────────────────────────────────────────────── *
104
105
  *
105
106
  * `futex_wait`:
@@ -23,7 +23,8 @@
23
23
  * ───────
24
24
  * `atomic_*_i32` family operates with SEQ_CST memory ordering via GCC
25
25
  * `__atomic_*` builtins. `futex_wait` / `futex_wake` give cross-process
26
- * wait/notify via Linux SYS_futex (FUTEX_*_PRIVATE flavour).
26
+ * wait/notify: Linux SYS_futex (the NON-private flavour) and, on macOS,
27
+ * os_sync_wait_on_address with the _SHARED flag — see sab-helpers.c.
27
28
  *
28
29
  * Buffer ownership: read_bytes returns a GLib.Bytes that borrows the
29
30
  * mmap'd memory (no copy) and ref-counts the region so the mapping
@@ -173,7 +174,7 @@ namespace GjsifySabNative {
173
174
  }
174
175
 
175
176
  /**
176
- * Linux futex_wait. Compares *((int32_t*)region+offset) to @expected;
177
+ * Address-keyed wait (futex on Linux, os_sync on macOS). Compares *((int32_t*)region+offset) to @expected;
177
178
  * if equal, blocks until woken or timeout. timeout_ms < 0 = infinite.
178
179
  *
179
180
  * Return: 0 woken, -1 not-equal (EAGAIN), -2 timed-out, -3 interrupted,