@gjsify/sab-native 0.4.12

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.
@@ -0,0 +1 @@
1
+ var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";let e=null;const t=globalThis.imports?.gi;if(t)try{e=t.GjsifySabNative}catch{}const n=e;function hasNativeSab(){return e!==null}const r="@gjsify/sab-native prebuild is not loaded — install the package on a Linux system with the prebuild available, or build locally via `meson compile` in packages/node/sab-native/.";var i=class SharedBuffer{_native;constructor(e){this._native=e}static create(t){if(!e)throw Error(r);if(!Number.isInteger(t)||t<=0)throw TypeError(`SharedBuffer.create: size must be a positive integer`);let n=e.SharedBuffer.create(t);if(!n)throw Error(`memfd_create or mmap failed`);return new SharedBuffer(n)}static fromFd(t,n){if(!e)throw Error(r);if(!Number.isInteger(t)||t<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=e.SharedBuffer.from_fd(t,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(e,t){let n=this._assertOpen().read_bytes(e,t),r=globalThis.imports?.byteArray;if(r&&typeof r.fromGBytes==`function`){let e=r.fromGBytes(n);return new Uint8Array(e)}return new Uint8Array(n.toArray())}writeBytes(e,t){let n=globalThis.imports?.byteArray,r;if(n&&typeof n.toGBytes==`function`)r=n.toGBytes(t);else{let e=globalThis.imports?.gi?.GLib;r=e?.Bytes?new e.Bytes(t):t}this._assertOpen().write_bytes(e,r)}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=e?{makePair(){let[t,n,r]=e.FdChannel.make_pair();if(!t)throw Error(`socketpair() failed`);return{parentFd:n,childFd:r}},sendFd(t,n,r){return e.FdChannel.send_fd(t,n,r>>>0)},recvFd(t){let[n,r]=e.FdChannel.recv_fd(t);if(n===0)return null;if(n<0)throw Error(`recvmsg failed`);return{fd:n,tag:r>>>0}},closeFd(t){e.FdChannel.close_fd(t)}}:null;export{i as SharedBuffer,a as atomics,o as fdChannel,hasNativeSab,n as nativeSab};
@@ -0,0 +1,186 @@
1
+ export interface NativeSharedBuffer {
2
+ readonly fd: number;
3
+ readonly byte_length: number;
4
+ get_u8(offset: number): number;
5
+ set_u8(offset: number, v: number): void;
6
+ get_u32_le(offset: number): number;
7
+ set_u32_le(offset: number, v: number): void;
8
+ get_i32_le(offset: number): number;
9
+ set_i32_le(offset: number, v: number): void;
10
+ get_u64_le(offset: number): bigint;
11
+ set_u64_le(offset: number, v: bigint): void;
12
+ read_bytes(offset: number, length: number): unknown;
13
+ write_bytes(offset: number, data: unknown): void;
14
+ atomic_add_i32(offset: number, v: number): number;
15
+ atomic_sub_i32(offset: number, v: number): number;
16
+ atomic_load_i32(offset: number): number;
17
+ atomic_store_i32(offset: number, v: number): void;
18
+ atomic_xchg_i32(offset: number, v: number): number;
19
+ atomic_cmpxchg_i32(offset: number, expected: number, desired: number): number;
20
+ futex_wait(offset: number, expected: number, timeout_ms: number): number;
21
+ futex_wake(offset: number, count: number): number;
22
+ }
23
+ export interface NativeSharedBufferClass {
24
+ create(size: number): NativeSharedBuffer | null;
25
+ from_fd(fd: number, size: number): NativeSharedBuffer | null;
26
+ }
27
+ export interface NativeFdChannelClass {
28
+ make_pair(): [boolean, number, number];
29
+ send_fd(socket_fd: number, fd_to_send: number, tag: number): boolean;
30
+ recv_fd(socket_fd: number): [number, number];
31
+ close_fd(fd: number): boolean;
32
+ }
33
+ export interface GjsifySabNativeModule {
34
+ SharedBuffer: NativeSharedBufferClass;
35
+ FdChannel: NativeFdChannelClass;
36
+ }
37
+ /** The native GjsifySabNative module, or null if not installed. */
38
+ export declare const nativeSab: GjsifySabNativeModule | null;
39
+ /** Returns true when the GjsifySabNative native library is available. */
40
+ export declare function hasNativeSab(): boolean;
41
+ /**
42
+ * A shared-memory region backed by an anonymous memfd (Linux) and
43
+ * mmap(MAP_SHARED). The `.fd` may be passed to a child process via
44
+ * SCM_RIGHTS over a Unix-domain socket; the child mmaps the same fd via
45
+ * `SharedBuffer.fromFd()` to share the backing store.
46
+ *
47
+ * Reads and writes are explicitly little-endian regardless of host
48
+ * byte-order — the C shim swaps where needed (s390x / ppc64).
49
+ *
50
+ * Atomic operations on `SharedBuffer` go through the `atomics` namespace
51
+ * exported from this module — they cannot use JS's built-in
52
+ * `Atomics` because `Atomics` rejects non-SharedArrayBuffer views.
53
+ *
54
+ * Lifecycle: the underlying memfd is closed and the mmap freed when the
55
+ * `SharedBuffer` instance is garbage-collected. Call `.close()` to
56
+ * release explicitly.
57
+ */
58
+ export declare class SharedBuffer {
59
+ /** @internal */
60
+ private _native;
61
+ private constructor();
62
+ /**
63
+ * Allocate a fresh anonymous shared-memory region.
64
+ * @param size byte length. SHOULD be a multiple of the system page size
65
+ * (4096 on x86_64) for efficient mmap; smaller values work
66
+ * but waste a whole page per region.
67
+ * @throws Error if the prebuild is not loaded, or if memfd_create/mmap fail.
68
+ */
69
+ static create(size: number): SharedBuffer;
70
+ /**
71
+ * Map an existing shared-memory fd into this process. The fd is dup'd
72
+ * — the caller retains ownership of their copy and may close it.
73
+ *
74
+ * @param fd file descriptor for a shared-memory object (typically
75
+ * received via SCM_RIGHTS from a parent process).
76
+ * @param size MUST match the sender's view of the region size; the
77
+ * kernel does not check this for memfd, so a mismatch
78
+ * results in silent partial maps.
79
+ */
80
+ static fromFd(fd: number, size: number): SharedBuffer;
81
+ /** Region size in bytes. */
82
+ get byteLength(): number;
83
+ /**
84
+ * File descriptor of the backing memfd. Pass this to a child process
85
+ * via Gio.SubprocessLauncher.take_fd() (pre-spawn) or SCM_RIGHTS
86
+ * (post-spawn) so the child can call `SharedBuffer.fromFd(fd, size)`.
87
+ */
88
+ get fd(): number;
89
+ /** True if this region has been released via close(). */
90
+ get closed(): boolean;
91
+ /**
92
+ * Release the underlying memfd + mmap explicitly. Idempotent. The
93
+ * backing store survives in any other process that still has the fd
94
+ * mapped.
95
+ */
96
+ close(): void;
97
+ getUint8(offset: number): number;
98
+ setUint8(offset: number, v: number): void;
99
+ getUint32LE(offset: number): number;
100
+ setUint32LE(offset: number, v: number): void;
101
+ getInt32LE(offset: number): number;
102
+ setInt32LE(offset: number, v: number): void;
103
+ getBigUint64LE(offset: number): bigint;
104
+ setBigUint64LE(offset: number, v: bigint): void;
105
+ /**
106
+ * Read a byte range out as a Uint8Array. ONE-TIME COPY — modifications
107
+ * to the returned array do NOT propagate back to the region. Use
108
+ * `writeBytes()` to commit changes back.
109
+ */
110
+ readBytes(offset: number, length: number): Uint8Array;
111
+ /**
112
+ * Write a byte range into the region. memcpy on the C side.
113
+ */
114
+ writeBytes(offset: number, data: Uint8Array): void;
115
+ /** @internal Internal escape-hatch for atomics + worker_threads transfer. */
116
+ get _nativeHandle(): NativeSharedBuffer;
117
+ private _assertOpen;
118
+ }
119
+ /**
120
+ * Atomic operations against a `SharedBuffer`. Memory order: SEQ_CST.
121
+ *
122
+ * **Cannot be used with `Atomics.*` built-ins** — those reject anything
123
+ * that isn't a typed-array view over a real `SharedArrayBuffer`. This
124
+ * namespace mirrors the most common Atomics surface against our
125
+ * memfd-backed regions instead.
126
+ */
127
+ export declare const atomics: {
128
+ /** `[*(int32_t*)(sb+offset)] += v`, returns previous value. */
129
+ add32(sb: SharedBuffer, offset: number, v: number): number;
130
+ sub32(sb: SharedBuffer, offset: number, v: number): number;
131
+ load32(sb: SharedBuffer, offset: number): number;
132
+ store32(sb: SharedBuffer, offset: number, v: number): void;
133
+ exchange32(sb: SharedBuffer, offset: number, v: number): number;
134
+ /**
135
+ * Strong compare-and-swap. Returns the previous value. CAS succeeded iff
136
+ * `returned === expected`.
137
+ */
138
+ compareExchange32(sb: SharedBuffer, offset: number, expected: number, desired: number): number;
139
+ /**
140
+ * Linux futex_wait. Compare `*(int32_t*)(sb+offset)` to `expected`; if
141
+ * equal, block until woken or timeout (0 ms = non-blocking probe;
142
+ * `-1` ms = infinite).
143
+ *
144
+ * Returns:
145
+ * - `'ok'` — woken by a matching `notify32()` call.
146
+ * - `'not-equal'` — value didn't match `expected`; no wait happened.
147
+ * - `'timed-out'` — timeout expired before any wake.
148
+ * - `'interrupted'` — interrupted by signal (EINTR); caller may retry.
149
+ */
150
+ wait32(sb: SharedBuffer, offset: number, expected: number, timeoutMs: number): "ok" | "not-equal" | "timed-out" | "interrupted";
151
+ /** Wake up to `count` waiters on `sb+offset`. Returns number actually woken. */
152
+ notify32(sb: SharedBuffer, offset: number, count: number): number;
153
+ };
154
+ /**
155
+ * Unix-domain socket pair + SCM_RIGHTS fd-transfer helper.
156
+ *
157
+ * **Internal API.** Exposed for `@gjsify/worker_threads` to wire up the
158
+ * cross-process SharedBuffer transfer at `Worker` spawn time. Direct
159
+ * consumers should use `Worker.postMessage(value, [sb])` instead.
160
+ */
161
+ export declare const fdChannel: {
162
+ makePair(): {
163
+ parentFd: number;
164
+ childFd: number;
165
+ };
166
+ /**
167
+ * Send one fd over an open SOCK_SEQPACKET pair via SCM_RIGHTS. Returns
168
+ * `true` on success, `false` on `sendmsg()` failure (errno preserved on
169
+ * the calling thread — caller surfaces the error in whatever shape
170
+ * makes sense for the situation).
171
+ */
172
+ sendFd(socketFd: number, fdToSend: number, tag: number): boolean;
173
+ /**
174
+ * Blocking recv of one fd. Returns the received fd + tag, or null on
175
+ * orderly EOF.
176
+ */
177
+ recvFd(socketFd: number): {
178
+ fd: number;
179
+ tag: number;
180
+ } | null;
181
+ /**
182
+ * close(2) on a fd previously created by `makePair()` (or any fd, really).
183
+ * Idempotent — closing an already-closed fd is fine.
184
+ */
185
+ closeFd(fd: number): void;
186
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/meson.build ADDED
@@ -0,0 +1,57 @@
1
+ project('GjsifySabNative', ['c', 'vala'], version: '1.0')
2
+
3
+ root_dir = meson.current_source_dir()
4
+
5
+ # Required system packages (Fedora 43+):
6
+ # dnf install vala meson ninja-build
7
+ #
8
+ # 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.
11
+ #
12
+ # Vala can't express the futex syscall + SCM_RIGHTS ancillary-data dance
13
+ # cleanly, so we wrap them in a small C shim (src/vala/sab-helpers.{h,c})
14
+ # and call into it from Vala via [CCode] extern declarations. Buffer ownership
15
+ # stays C-side, mirroring the @gjsify/http2-native nghttp2-helpers pattern.
16
+
17
+ vala_deps = [
18
+ dependency('glib-2.0'),
19
+ dependency('gobject-2.0'),
20
+ dependency('gio-2.0'),
21
+ ]
22
+
23
+ sources = files(
24
+ 'src/vala/sab-helpers.c',
25
+ 'src/vala/shared-buffer.vala',
26
+ 'src/vala/fd-passing.vala',
27
+ )
28
+
29
+ # Compiler picks up sab-helpers.h via the include dir.
30
+ inc = include_directories('src/vala')
31
+
32
+ # -D_GNU_SOURCE: needed for memfd_create + MFD_CLOEXEC + SYS_futex visibility.
33
+ c_args = ['-D_GNU_SOURCE']
34
+
35
+ libGjsifySabNative = library('gjsifysabnative', sources,
36
+ dependencies: vala_deps,
37
+ include_directories: inc,
38
+ c_args: c_args,
39
+ vala_gir: meson.project_name() + '-1.0.gir',
40
+ install: true,
41
+ install_dir: [true, true, true, true],
42
+ )
43
+
44
+ g_ir_compiler = find_program('g-ir-compiler')
45
+
46
+ custom_target(meson.project_name() + '-1.0.typelib',
47
+ command: [
48
+ g_ir_compiler,
49
+ '--shared-library', 'libgjsifysabnative.so',
50
+ '--output', '@OUTPUT@',
51
+ meson.current_build_dir() / meson.project_name() + '-1.0.gir',
52
+ ],
53
+ output: meson.project_name() + '-1.0.typelib',
54
+ depends: libGjsifySabNative,
55
+ install: true,
56
+ install_dir: get_option('libdir') / 'girepository-1.0',
57
+ )
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@gjsify/sab-native",
3
+ "version": "0.4.12",
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
+ "type": "module",
6
+ "main": "lib/esm/index.js",
7
+ "module": "lib/esm/index.js",
8
+ "types": "lib/types/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/types/index.d.ts",
12
+ "default": "./lib/esm/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "lib",
17
+ "prebuilds",
18
+ "meson.build",
19
+ "src/vala"
20
+ ],
21
+ "gjsify": {
22
+ "prebuilds": "prebuilds"
23
+ },
24
+ "scripts": {
25
+ "clear": "rm -rf lib build tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs || exit 0",
26
+ "check": "tsc --noEmit",
27
+ "init:meson": "meson setup build .",
28
+ "init:meson:wipe": "gjsify run init:meson --wipe",
29
+ "build": "gjsify run build:gjsify && gjsify run build:types",
30
+ "build:gjsify": "gjsify build --library 'src/ts/**/*.{ts,js}' --exclude 'src/ts/**/*.spec.{mts,ts}' 'src/ts/test.{mts,ts}'",
31
+ "build:meson": "gjsify run init:meson && meson compile -C build",
32
+ "build:types": "tsc",
33
+ "build:prebuilds": "gjsify run build:meson && mkdir -p prebuilds/linux-x86_64 && cp build/libgjsifysabnative.so build/GjsifySabNative-1.0.gir build/GjsifySabNative-1.0.typelib prebuilds/linux-x86_64/",
34
+ "build:gir-types": "ts-for-gir generate --externalDeps --allowMissingDeps --girDirectories=./prebuilds/linux-x86_64 --girDirectories=/usr/share/gir-1.0 --modules=GjsifySabNative-1.0 --outdir=src/ts --npmScope=@girs --package=false --ignoreVersionConflicts=true",
35
+ "build:test": "gjsify run build:test:gjs",
36
+ "build:test:gjs": "gjsify build src/ts/test.mts --app gjs --outfile test.gjs.mjs",
37
+ "test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
38
+ "test:gjs": "gjsify run test.gjs.mjs"
39
+ },
40
+ "keywords": [
41
+ "gjs",
42
+ "shared-memory",
43
+ "shared-array-buffer",
44
+ "atomics",
45
+ "memfd",
46
+ "mmap",
47
+ "futex",
48
+ "worker-threads",
49
+ "vala",
50
+ "native"
51
+ ],
52
+ "dependencies": {
53
+ "@girs/gjs": "4.0.0-rc.15",
54
+ "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
55
+ "@girs/gobject-2.0": "2.88.0-4.0.0-rc.15",
56
+ "@girs/gio-2.0": "2.88.0-4.0.0-rc.15"
57
+ },
58
+ "devDependencies": {
59
+ "@gjsify/cli": "^0.4.12",
60
+ "@gjsify/unit": "^0.4.12",
61
+ "@ts-for-gir/cli": "^4.0.0-rc.15",
62
+ "@types/node": "^25.6.2",
63
+ "typescript": "^6.0.3"
64
+ }
65
+ }