@gjsify/sab-native 0.36.0 → 0.37.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.
Files changed (2) hide show
  1. package/lib/types/index.d.ts +69 -124
  2. package/package.json +8 -8
@@ -35,99 +35,77 @@ export interface GjsifySabNativeModule {
35
35
  FdChannel: NativeFdChannelClass;
36
36
  }
37
37
  /**
38
- * Resolve the `GjsifySabNative` GI module out of a GJS `imports.gi` object,
39
- * normalising **every** unavailable case to `null`.
38
+ * Resolve the `GjsifySabNative` GI module, normalising EVERY unavailable case to
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
41
+ * we expect (stale, partial or shadowed typelib). The third needs the shape check:
42
+ * a partial value otherwise leaves `hasNativeSab()` reporting `true` and fails with
43
+ * an opaque `TypeError` at first use.
40
44
  *
41
- * Exported so the degradation contract can be pinned by a spec without a
42
- * second process (see `shared-buffer.gjs.spec.ts`). Three ways this returns
43
- * `null`, all of which must behave identically to the caller:
44
- *
45
- * 1. `gi` itself is absent — we are not on GJS at all (Node, browser).
46
- * 2. The property access throws — GJS raises when the typelib is not on
47
- * `GI_TYPELIB_PATH`, i.e. no prebuild for this platform. This is the
48
- * macOS/Windows case.
49
- * 3. The access succeeds but yields something that is not the module we
50
- * expect — a stale, partial or shadowed typelib. Without the shape check
51
- * a `undefined`/partial value would leave `hasNativeSab()` reporting
52
- * `true` and then blow up with an opaque `TypeError` at first use; the
53
- * check converts that into the same clean "unavailable" answer.
54
- *
55
- * @internal
45
+ * @internal exported so a spec can pin the degradation contract in-process
46
+ * (`shared-buffer.gjs.spec.ts`).
56
47
  */
57
48
  export declare function resolveNativeSab(gi: Record<string, unknown> | undefined): GjsifySabNativeModule | null;
58
49
  /** The native GjsifySabNative module, or null if not installed. */
59
50
  export declare const nativeSab: GjsifySabNativeModule | null;
60
51
  /**
61
- * Returns true when the GjsifySabNative native library is available.
62
- *
63
- * This is THE gate for cross-process `SharedBuffer` support and it is
64
- * platform-conditional by design: it is `true` only on Linux (see ADR 0013),
65
- * and `false` on macOS, Windows, Node and the browser. Guard every use of
66
- * `SharedBuffer` / `atomics` / `fdChannel` with it.
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.
54
+ * Guard every use of `SharedBuffer` / `atomics` / `fdChannel` with it.
67
55
  */
68
56
  export declare function hasNativeSab(): boolean;
69
57
  /**
70
- * Message thrown by `SharedBuffer.create()` / `.fromFd()` when the native
71
- * backend is unavailable. Names the actual situation — a platform-scoped
72
- * capability — instead of implying a broken install.
58
+ * Thrown by `SharedBuffer.create()` / `.fromFd()` when the native backend is
59
+ * unavailable. Says "platform-scoped capability", not "broken install".
73
60
  *
74
61
  * @internal exported so specs and docs can assert the exact contract text.
75
62
  */
76
63
  export declare const NATIVE_SAB_UNAVAILABLE: string;
77
64
  /**
78
- * A shared-memory region backed by an anonymous memfd (Linux) and
79
- * mmap(MAP_SHARED). The `.fd` may be passed to a child process via
80
- * SCM_RIGHTS over a Unix-domain socket; the child mmaps the same fd via
81
- * `SharedBuffer.fromFd()` to share the backing store.
82
- *
83
- * Reads and writes are explicitly little-endian regardless of host
84
- * byte-order — the C shim swaps where needed (s390x / ppc64).
65
+ * A shared-memory region backed by an anonymous memfd and mmap(MAP_SHARED). The
66
+ * `.fd` may be passed to a child over a Unix-domain socket via SCM_RIGHTS; the
67
+ * child mmaps the same fd with `SharedBuffer.fromFd()` to share the backing store.
85
68
  *
86
- * Atomic operations on `SharedBuffer` go through the `atomics` namespace
87
- * exported from this module — they cannot use JS's built-in
88
- * `Atomics` because `Atomics` rejects non-SharedArrayBuffer views.
69
+ * Reads and writes are little-endian regardless of host byte-order — the C shim
70
+ * swaps where needed (s390x / ppc64).
89
71
  *
90
- * Lifecycle: the underlying memfd is closed and the mmap freed when the
91
- * `SharedBuffer` instance is garbage-collected. Call `.close()` to
92
- * release explicitly.
72
+ * Lifecycle: the memfd is closed and the mmap freed by the native destructor, i.e.
73
+ * when this instance is garbage-collected. `.close()` does NOT release the region;
74
+ * it only drops our reference so later access throws.
93
75
  */
94
76
  export declare class SharedBuffer {
95
77
  /** @internal */
96
78
  private _native;
97
79
  private constructor();
98
80
  /**
99
- * Allocate a fresh anonymous shared-memory region.
100
- * @param size byte length. SHOULD be a multiple of the system page size
101
- * (4096 on x86_64) for efficient mmap; smaller values work
102
- * but waste a whole page per region.
103
- * @throws Error if the prebuild is not loaded, or if memfd_create/mmap fail.
81
+ * Allocate a fresh anonymous shared-memory region. `size` SHOULD be a multiple
82
+ * of the page size — smaller values work but waste a whole page per region.
83
+ *
84
+ * @throws if the prebuild is not loaded, or memfd_create/mmap fail.
104
85
  */
105
86
  static create(size: number): SharedBuffer;
106
87
  /**
107
- * Map an existing shared-memory fd into this process. The fd is dup'd
108
- * — the caller retains ownership of their copy and may close it.
88
+ * Map an existing shared-memory fd — typically received via SCM_RIGHTS — into
89
+ * this process. The fd is dup'd, so the caller keeps ownership of their copy.
109
90
  *
110
- * @param fd file descriptor for a shared-memory object (typically
111
- * received via SCM_RIGHTS from a parent process).
112
- * @param size MUST match the sender's view of the region size; the
113
- * kernel does not check this for memfd, so a mismatch
114
- * results in silent partial maps.
91
+ * `size` MUST match the sender's view of the region: the kernel does not check
92
+ * it for a memfd, so a mismatch silently produces a partial map.
115
93
  */
116
94
  static fromFd(fd: number, size: number): SharedBuffer;
117
95
  /** Region size in bytes. */
118
96
  get byteLength(): number;
119
97
  /**
120
- * File descriptor of the backing memfd. Pass this to a child process
121
- * via Gio.SubprocessLauncher.take_fd() (pre-spawn) or SCM_RIGHTS
122
- * (post-spawn) so the child can call `SharedBuffer.fromFd(fd, size)`.
98
+ * File descriptor of the backing memfd — hand it to a child via
99
+ * `Gio.SubprocessLauncher.take_fd()` pre-spawn or SCM_RIGHTS post-spawn.
123
100
  */
124
101
  get fd(): number;
125
102
  /** True if this region has been released via close(). */
126
103
  get closed(): boolean;
127
104
  /**
128
- * Release the underlying memfd + mmap explicitly. Idempotent. The
129
- * backing store survives in any other process that still has the fd
130
- * mapped.
105
+ * Drop this view of the region. Idempotent. NOT a release: the memfd and mmap go
106
+ * away with the native GObject's destructor, so this only makes later access
107
+ * throw rather than segfault. The backing store also survives in any other
108
+ * process still holding the fd mapped.
131
109
  */
132
110
  close(): void;
133
111
  getUint8(offset: number): number;
@@ -139,64 +117,39 @@ export declare class SharedBuffer {
139
117
  getBigUint64LE(offset: number): bigint;
140
118
  setBigUint64LE(offset: number, v: bigint): void;
141
119
  /**
142
- * Read a byte range out as a Uint8Array. ONE-TIME COPY — modifications
143
- * to the returned array do NOT propagate back to the region. Use
144
- * `writeBytes()` to commit changes back, or `viewBytes()` for a
145
- * zero-copy view.
120
+ * Read a byte range as a Uint8Array. ONE-TIME COPY — modifications do NOT
121
+ * propagate back; use `writeBytes()` to commit changes.
146
122
  */
147
123
  readBytes(offset: number, length: number): Uint8Array;
148
124
  /**
149
- * Return a fresh `Uint8Array` containing the bytes at `[offset, offset+length)`.
150
- * Same semantics as `readBytes()` — kept under this name because
151
- * downstream tooling (`Buffer.from`, `node:crypto`'s `Hash.update`,
152
- * `fs.writeSync`) calls into this method via duck-typing and the
153
- * `viewBytes` name reads more naturally there.
154
- *
155
- * **NOT zero-copy in current GJS.** GJS's `byteArray.fromGBytes`
156
- * (`refs/gjs/gjs/byteArray.cpp::from_gbytes_func`) allocates a fresh
157
- * `JS::ArrayBuffer` and memcpy's the GBytes data into it — by design,
158
- * for alignment + immutability reasons.
159
- *
160
- * **Why no internal fix is possible**: a true zero-copy view would need
161
- * `JS::NewExternalArrayBuffer` against the mmap pointer, but that JSAPI
162
- * call requires a `JSContext*` which GJS does not expose to
163
- * GObject-introspected `.so` plugins. The fix has to land in GJS
164
- * itself (e.g. a `byteArray.fromGBytesShared` helper). Tracked under
165
- * status/upstream-patch-candidates.md.
166
- *
167
- * Modifications to the returned array therefore do NOT propagate back
168
- * to the region — use `writeBytes()` to commit changes.
125
+ * Identical to `readBytes()`, under a second name because downstream tooling
126
+ * (`Buffer.from`, `node:crypto`'s `Hash.update`, `fs.writeSync`) duck-types into
127
+ * it and `viewBytes` reads naturally there.
169
128
  *
170
- * @throws Error if `byteArray.fromGBytes` is not available (GJS < 1.66).
129
+ * Despite the name it is NOT zero-copy, and cannot be made so from here: GJS's
130
+ * `byteArray.fromGBytes` (`refs/gjs/gjs/byteArray.cpp::from_gbytes_func`)
131
+ * allocates a fresh `JS::ArrayBuffer` and memcpys into it, deliberately, for
132
+ * alignment and immutability. A real view needs `JS::NewExternalArrayBuffer`
133
+ * over the mmap pointer, which needs a `JSContext*` that GJS does not hand to
134
+ * introspected `.so` plugins — so the fix belongs in GJS (a
135
+ * `byteArray.fromGBytesShared`). Tracked in status/upstream-patch-candidates.md.
171
136
  */
172
137
  viewBytes(offset: number, length: number): Uint8Array;
173
138
  /**
174
- * Return a `Buffer` containing the bytes at `[offset, offset+length)`.
175
- * The Buffer is a fresh allocation (see `viewBytes()` for the "not
176
- * zero-copy in current GJS" caveat — the limitation is in GJS'
177
- * `byteArray.fromGBytes`, not bypassable from a `.so` plugin without
178
- * upstream patching GJS) — `buf.writeUInt32LE(...)`, `buf.subarray(...)`,
179
- * `buf.toString('hex')`, `createHash().update(buf)` all work, but
180
- * writes do NOT propagate back to the shared region.
139
+ * The bytes at `[offset, offset+length)` as a `Buffer`, so `writeUInt32LE`,
140
+ * `subarray`, `toString('hex')` and `createHash().update()` all work — on a
141
+ * COPY, per `viewBytes()`, so writes do not reach the shared region.
181
142
  *
182
- * Requires `globalThis.Buffer` to be registered (via
183
- * `@gjsify/buffer/register`) — otherwise throws. Consumers running
184
- * under the standard gjsify CLI bundle have Buffer registered via
185
- * `--globals auto`; for ad-hoc scripts, add an explicit
186
- * `import '@gjsify/buffer/register'` at the entry point.
143
+ * Needs `globalThis.Buffer` registered, which `--globals auto` does for the
144
+ * standard CLI bundle; an ad-hoc script needs an explicit
145
+ * `import '@gjsify/buffer/register'`.
187
146
  *
188
- * Return type is generic to avoid an import dependency on
189
- * `@gjsify/buffer` — the concrete return is a `Buffer` (subclass of
190
- * `Uint8Array`). Default `T = Uint8Array` is always safe;
191
- * `toBuffer<Buffer>()` gets the full Buffer surface.
192
- *
193
- * @param offset byte offset into the shared region. Defaults to 0.
194
- * @param length byte length. Defaults to `byteLength - offset`.
147
+ * The return type is generic only to avoid a dependency on `@gjsify/buffer`; the
148
+ * concrete value is always a `Buffer`, so `toBuffer<Buffer>()` gets the full
149
+ * surface and the `Uint8Array` default is always safe.
195
150
  */
196
151
  toBuffer<T extends Uint8Array = Uint8Array>(offset?: number, length?: number): T;
197
- /**
198
- * Write a byte range into the region. memcpy on the C side.
199
- */
152
+ /** Write a byte range into the region. memcpy on the C side. */
200
153
  writeBytes(offset: number, data: Uint8Array): void;
201
154
  /** @internal Internal escape-hatch for atomics + worker_threads transfer. */
202
155
  get _nativeHandle(): NativeSharedBuffer;
@@ -205,10 +158,9 @@ export declare class SharedBuffer {
205
158
  /**
206
159
  * Atomic operations against a `SharedBuffer`. Memory order: SEQ_CST.
207
160
  *
208
- * **Cannot be used with `Atomics.*` built-ins** — those reject anything
209
- * that isn't a typed-array view over a real `SharedArrayBuffer`. This
210
- * namespace mirrors the most common Atomics surface against our
211
- * memfd-backed regions instead.
161
+ * The `Atomics.*` built-ins are not an option: GJS does not expose `Atomics` at all
162
+ * (verified on gjs 1.88.1), and a `SharedBuffer` is not a typed-array view in any
163
+ * case. This namespace mirrors the common surface over the memfd-backed region.
212
164
  */
213
165
  export declare const atomics: {
214
166
  /** `[*(int32_t*)(sb+offset)] += v`, returns previous value. */
@@ -238,11 +190,11 @@ export declare const atomics: {
238
190
  notify32(sb: SharedBuffer, offset: number, count: number): number;
239
191
  };
240
192
  /**
241
- * Unix-domain socket pair + SCM_RIGHTS fd-transfer helper.
193
+ * Unix-domain socket pair + SCM_RIGHTS fd transfer.
242
194
  *
243
- * **Internal API.** Exposed for `@gjsify/worker_threads` to wire up the
244
- * cross-process SharedBuffer transfer at `Worker` spawn time. Direct
245
- * consumers should use `Worker.postMessage(value, [sb])` instead.
195
+ * @internal for `@gjsify/worker_threads` to wire up the cross-process SharedBuffer
196
+ * transfer at `Worker` spawn time; direct consumers use
197
+ * `Worker.postMessage(value, [sb])`.
246
198
  */
247
199
  export declare const fdChannel: {
248
200
  makePair(): {
@@ -250,23 +202,16 @@ export declare const fdChannel: {
250
202
  childFd: number;
251
203
  };
252
204
  /**
253
- * Send one fd over an open SOCK_SEQPACKET pair via SCM_RIGHTS. Returns
254
- * `true` on success, `false` on `sendmsg()` failure (errno preserved on
255
- * the calling thread — caller surfaces the error in whatever shape
256
- * makes sense for the situation).
205
+ * Send one fd over an open SOCK_SEQPACKET pair via SCM_RIGHTS. `false` on
206
+ * `sendmsg()` failure, with errno left on the calling thread for the caller
207
+ * to shape into an error.
257
208
  */
258
209
  sendFd(socketFd: number, fdToSend: number, tag: number): boolean;
259
- /**
260
- * Blocking recv of one fd. Returns the received fd + tag, or null on
261
- * orderly EOF.
262
- */
210
+ /** Blocking recv of one fd + tag, or `null` on orderly EOF. */
263
211
  recvFd(socketFd: number): {
264
212
  fd: number;
265
213
  tag: number;
266
214
  } | null;
267
- /**
268
- * close(2) on a fd previously created by `makePair()` (or any fd, really).
269
- * Idempotent — closing an already-closed fd is fine.
270
- */
215
+ /** close(2) on any fd. Idempotent: the shim reports success on EBADF too. */
271
216
  closeFd(fd: number): void;
272
217
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/sab-native",
3
- "version": "0.36.0",
3
+ "version": "0.37.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",
@@ -68,8 +68,8 @@
68
68
  "@girs/gio-2.0": "^4.1.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@gjsify/cli": "^0.36.0",
72
- "@gjsify/unit": "^0.36.0",
71
+ "@gjsify/cli": "^0.37.0",
72
+ "@gjsify/unit": "^0.37.0",
73
73
  "@ts-for-gir/cli": "^4.1.0",
74
74
  "@types/node": "^25.9.2",
75
75
  "typescript": "^6.0.3"
@@ -85,10 +85,10 @@
85
85
  },
86
86
  "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/node/sab-native#readme",
87
87
  "optionalDependencies": {
88
- "@gjsify/sab-native-linux-arm64": "0.36.0",
89
- "@gjsify/sab-native-linux-ppc64": "0.36.0",
90
- "@gjsify/sab-native-linux-riscv64": "0.36.0",
91
- "@gjsify/sab-native-linux-s390x": "0.36.0",
92
- "@gjsify/sab-native-linux-x64": "0.36.0"
88
+ "@gjsify/sab-native-linux-arm64": "0.37.0",
89
+ "@gjsify/sab-native-linux-ppc64": "0.37.0",
90
+ "@gjsify/sab-native-linux-riscv64": "0.37.0",
91
+ "@gjsify/sab-native-linux-s390x": "0.37.0",
92
+ "@gjsify/sab-native-linux-x64": "0.37.0"
93
93
  }
94
94
  }