@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 +4 -4
- package/lib/esm/index.js +1 -1
- package/lib/types/index.d.ts +2 -2
- package/meson.build +26 -5
- package/package.json +20 -15
- package/src/vala/sab-helpers.c +203 -4
- package/src/vala/sab-helpers.h +7 -6
- package/src/vala/shared-buffer.vala +3 -2
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`
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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_*
|
|
10
|
-
#
|
|
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
|
-
|
|
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',
|
|
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.
|
|
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.
|
|
68
|
-
"@girs/glib-2.0": "5.
|
|
69
|
-
"@girs/gobject-2.0": "5.
|
|
70
|
-
"@girs/gio-2.0": "5.
|
|
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.
|
|
74
|
-
"@gjsify/unit": "^0.
|
|
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-
|
|
91
|
-
"@gjsify/sab-native-
|
|
92
|
-
"@gjsify/sab-native-linux-
|
|
93
|
-
"@gjsify/sab-native-linux-
|
|
94
|
-
"@gjsify/sab-native-linux-
|
|
95
|
-
"@gjsify/sab-native-linux-
|
|
96
|
-
"@gjsify/sab-native-linux-
|
|
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
|
}
|
package/src/vala/sab-helpers.c
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* sab-helpers.c — see sab-helpers.h for the contract.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
}
|
package/src/vala/sab-helpers.h
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Tiny C shim around
|
|
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)
|
|
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
|
|
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) —
|
|
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
|
|
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
|
|
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
|
|
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,
|