@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,461 @@
1
+ /*
2
+ * sab-helpers.c — see sab-helpers.h for the contract.
3
+ *
4
+ * Linux-only. Compile with -D_GNU_SOURCE so memfd_create, MFD_CLOEXEC,
5
+ * and the FUTEX_*_PRIVATE constants are visible.
6
+ */
7
+
8
+ #include "sab-helpers.h"
9
+
10
+ #include <errno.h>
11
+ #include <fcntl.h>
12
+ #include <linux/futex.h>
13
+ #include <stdatomic.h>
14
+ #include <stdint.h>
15
+ #include <string.h>
16
+ #include <sys/mman.h>
17
+ #include <sys/socket.h>
18
+ #include <sys/syscall.h>
19
+ #include <sys/time.h>
20
+ #include <sys/types.h>
21
+ #include <sys/un.h>
22
+ #include <time.h>
23
+ #include <unistd.h>
24
+
25
+ /* glibc < 2.27 doesn't expose memfd_create() as a libc wrapper. We use the
26
+ * raw syscall to stay portable across the prebuild distro matrix. */
27
+ #ifndef MFD_CLOEXEC
28
+ # define MFD_CLOEXEC 0x0001U
29
+ #endif
30
+
31
+ static int
32
+ gjsify_memfd_create (const char *name, unsigned int flags)
33
+ {
34
+ return (int) syscall (SYS_memfd_create, name, flags);
35
+ }
36
+
37
+ /* ────────────────────────────────────────────────────────────────────── *
38
+ * GjsifySabRegion: opaque shared-memory region
39
+ * ────────────────────────────────────────────────────────────────────── */
40
+
41
+ struct _GjsifySabRegion {
42
+ void *ptr;
43
+ gsize size;
44
+ gint fd;
45
+ /* Refcount for GBytes-borrow lifetimes (read_bytes returns a GBytes that
46
+ * pins the region until the bytes object is released). Strong count is
47
+ * always ≥ 1 (the JS-side SharedBuffer owns one); each outstanding GBytes
48
+ * adds one more. */
49
+ gint refcount;
50
+ };
51
+
52
+ static GjsifySabRegion *
53
+ region_ref (GjsifySabRegion *region)
54
+ {
55
+ g_atomic_int_inc (&region->refcount);
56
+ return region;
57
+ }
58
+
59
+ static void
60
+ region_unref (gpointer data)
61
+ {
62
+ GjsifySabRegion *region = (GjsifySabRegion *) data;
63
+ if (!g_atomic_int_dec_and_test (&region->refcount)) return;
64
+
65
+ if (region->ptr != NULL && region->ptr != MAP_FAILED) {
66
+ munmap (region->ptr, region->size);
67
+ }
68
+ if (region->fd >= 0) {
69
+ close (region->fd);
70
+ }
71
+ g_slice_free (GjsifySabRegion, region);
72
+ }
73
+
74
+ GjsifySabRegion *
75
+ gjsify_sab_region_new_anonymous (gsize size)
76
+ {
77
+ if (size == 0) { errno = EINVAL; return NULL; }
78
+
79
+ int fd = gjsify_memfd_create ("gjsify-sab", MFD_CLOEXEC);
80
+ if (fd < 0) return NULL;
81
+
82
+ if (ftruncate (fd, (off_t) size) < 0) {
83
+ int e = errno;
84
+ close (fd);
85
+ errno = e;
86
+ return NULL;
87
+ }
88
+
89
+ void *ptr = mmap (NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
90
+ if (ptr == MAP_FAILED) {
91
+ int e = errno;
92
+ close (fd);
93
+ errno = e;
94
+ return NULL;
95
+ }
96
+
97
+ GjsifySabRegion *region = g_slice_new0 (GjsifySabRegion);
98
+ region->ptr = ptr;
99
+ region->size = size;
100
+ region->fd = fd;
101
+ region->refcount = 1;
102
+ return region;
103
+ }
104
+
105
+ GjsifySabRegion *
106
+ gjsify_sab_region_new_from_fd (gint fd, gsize size)
107
+ {
108
+ if (fd < 0 || size == 0) { errno = EINVAL; return NULL; }
109
+
110
+ /* dup so the caller can close their copy without unmapping ours. */
111
+ int dup_fd = fcntl (fd, F_DUPFD_CLOEXEC, 0);
112
+ if (dup_fd < 0) return NULL;
113
+
114
+ void *ptr = mmap (NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, dup_fd, 0);
115
+ if (ptr == MAP_FAILED) {
116
+ int e = errno;
117
+ close (dup_fd);
118
+ errno = e;
119
+ return NULL;
120
+ }
121
+
122
+ GjsifySabRegion *region = g_slice_new0 (GjsifySabRegion);
123
+ region->ptr = ptr;
124
+ region->size = size;
125
+ region->fd = dup_fd;
126
+ region->refcount = 1;
127
+ return region;
128
+ }
129
+
130
+ void
131
+ gjsify_sab_region_free (GjsifySabRegion *region)
132
+ {
133
+ if (region == NULL) return;
134
+ region_unref (region);
135
+ }
136
+
137
+ gint gjsify_sab_region_get_fd (const GjsifySabRegion *region) { return region->fd; }
138
+ gsize gjsify_sab_region_get_size (const GjsifySabRegion *region) { return region->size; }
139
+
140
+ /* ────────────────────────────────────────────────────────────────────── *
141
+ * Plain read / write
142
+ * ────────────────────────────────────────────────────────────────────── */
143
+
144
+ #define BOUNDS_CHECK(reg, off, sz) do { \
145
+ if (G_UNLIKELY ((off) + (sz) > (reg)->size)) { \
146
+ g_error ("gjsify-sab: out-of-bounds access (offset=%" G_GSIZE_FORMAT \
147
+ ", size=%zu, region=%" G_GSIZE_FORMAT ")", (gsize)(off), (size_t)(sz), \
148
+ (reg)->size); \
149
+ } \
150
+ } while (0)
151
+
152
+ guint8
153
+ gjsify_sab_region_read_u8 (const GjsifySabRegion *region, gsize offset)
154
+ {
155
+ BOUNDS_CHECK (region, offset, 1);
156
+ return ((const guint8 *) region->ptr)[offset];
157
+ }
158
+
159
+ void
160
+ gjsify_sab_region_write_u8 (GjsifySabRegion *region, gsize offset, guint8 v)
161
+ {
162
+ BOUNDS_CHECK (region, offset, 1);
163
+ ((guint8 *) region->ptr)[offset] = v;
164
+ }
165
+
166
+ guint32
167
+ gjsify_sab_region_read_u32_le (const GjsifySabRegion *region, gsize offset)
168
+ {
169
+ BOUNDS_CHECK (region, offset, 4);
170
+ guint32 v;
171
+ memcpy (&v, (const guint8 *) region->ptr + offset, 4);
172
+ return GUINT32_FROM_LE (v);
173
+ }
174
+
175
+ void
176
+ gjsify_sab_region_write_u32_le (GjsifySabRegion *region, gsize offset, guint32 v)
177
+ {
178
+ BOUNDS_CHECK (region, offset, 4);
179
+ guint32 le = GUINT32_TO_LE (v);
180
+ memcpy ((guint8 *) region->ptr + offset, &le, 4);
181
+ }
182
+
183
+ gint32
184
+ gjsify_sab_region_read_i32_le (const GjsifySabRegion *region, gsize offset)
185
+ {
186
+ BOUNDS_CHECK (region, offset, 4);
187
+ guint32 u;
188
+ memcpy (&u, (const guint8 *) region->ptr + offset, 4);
189
+ u = GUINT32_FROM_LE (u);
190
+ gint32 i;
191
+ memcpy (&i, &u, 4);
192
+ return i;
193
+ }
194
+
195
+ void
196
+ gjsify_sab_region_write_i32_le (GjsifySabRegion *region, gsize offset, gint32 v)
197
+ {
198
+ BOUNDS_CHECK (region, offset, 4);
199
+ guint32 u;
200
+ memcpy (&u, &v, 4);
201
+ u = GUINT32_TO_LE (u);
202
+ memcpy ((guint8 *) region->ptr + offset, &u, 4);
203
+ }
204
+
205
+ guint64
206
+ gjsify_sab_region_read_u64_le (const GjsifySabRegion *region, gsize offset)
207
+ {
208
+ BOUNDS_CHECK (region, offset, 8);
209
+ guint64 v;
210
+ memcpy (&v, (const guint8 *) region->ptr + offset, 8);
211
+ return GUINT64_FROM_LE (v);
212
+ }
213
+
214
+ void
215
+ gjsify_sab_region_write_u64_le (GjsifySabRegion *region, gsize offset, guint64 v)
216
+ {
217
+ BOUNDS_CHECK (region, offset, 8);
218
+ guint64 le = GUINT64_TO_LE (v);
219
+ memcpy ((guint8 *) region->ptr + offset, &le, 8);
220
+ }
221
+
222
+ /* ────────────────────────────────────────────────────────────────────── *
223
+ * Bulk read / write (zero-copy for read, memcpy for write)
224
+ * ────────────────────────────────────────────────────────────────────── */
225
+
226
+ GBytes *
227
+ gjsify_sab_region_read_bytes (GjsifySabRegion *region, gsize offset, gsize length)
228
+ {
229
+ BOUNDS_CHECK (region, offset, length);
230
+ /* GBytes wraps the mmap'd memory without copying. We hand the GBytes a
231
+ * region_ref so the mmap survives until the bytes object is released. */
232
+ region_ref (region);
233
+ return g_bytes_new_with_free_func ((const guint8 *) region->ptr + offset,
234
+ length,
235
+ region_unref,
236
+ region);
237
+ }
238
+
239
+ void
240
+ gjsify_sab_region_write_bytes (GjsifySabRegion *region, gsize offset, GBytes *data)
241
+ {
242
+ gsize len = 0;
243
+ gconstpointer src = g_bytes_get_data (data, &len);
244
+ BOUNDS_CHECK (region, offset, len);
245
+ if (len > 0) memcpy ((guint8 *) region->ptr + offset, src, len);
246
+ }
247
+
248
+ /* ────────────────────────────────────────────────────────────────────── *
249
+ * Atomics: __atomic_* builtins with SEQ_CST memory order
250
+ * ────────────────────────────────────────────────────────────────────── */
251
+
252
+ static inline int32_t *
253
+ i32_ptr (const GjsifySabRegion *region, gsize offset)
254
+ {
255
+ BOUNDS_CHECK (region, offset, 4);
256
+ return (int32_t *) ((guint8 *) region->ptr + offset);
257
+ }
258
+
259
+ gint32
260
+ gjsify_sab_region_atomic_add_i32 (GjsifySabRegion *region, gsize offset, gint32 v)
261
+ {
262
+ /* fetch_add: returns the value BEFORE the addition. */
263
+ return __atomic_fetch_add (i32_ptr (region, offset), v, __ATOMIC_SEQ_CST);
264
+ }
265
+
266
+ gint32
267
+ gjsify_sab_region_atomic_sub_i32 (GjsifySabRegion *region, gsize offset, gint32 v)
268
+ {
269
+ return __atomic_fetch_sub (i32_ptr (region, offset), v, __ATOMIC_SEQ_CST);
270
+ }
271
+
272
+ gint32
273
+ gjsify_sab_region_atomic_load_i32 (const GjsifySabRegion *region, gsize offset)
274
+ {
275
+ return __atomic_load_n (i32_ptr (region, offset), __ATOMIC_SEQ_CST);
276
+ }
277
+
278
+ void
279
+ gjsify_sab_region_atomic_store_i32 (GjsifySabRegion *region, gsize offset, gint32 v)
280
+ {
281
+ __atomic_store_n (i32_ptr (region, offset), v, __ATOMIC_SEQ_CST);
282
+ }
283
+
284
+ gint32
285
+ gjsify_sab_region_atomic_xchg_i32 (GjsifySabRegion *region, gsize offset, gint32 v)
286
+ {
287
+ return __atomic_exchange_n (i32_ptr (region, offset), v, __ATOMIC_SEQ_CST);
288
+ }
289
+
290
+ gboolean
291
+ gjsify_sab_region_atomic_cmpxchg_i32 (GjsifySabRegion *region,
292
+ gsize offset,
293
+ gint32 *expected,
294
+ gint32 desired)
295
+ {
296
+ /* weak=FALSE so spurious failures don't happen — JS caller expects a
297
+ * strong CAS. */
298
+ return __atomic_compare_exchange_n (i32_ptr (region, offset),
299
+ expected, desired,
300
+ FALSE,
301
+ __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)
302
+ ? TRUE : FALSE;
303
+ }
304
+
305
+ /* ────────────────────────────────────────────────────────────────────── *
306
+ * Futex wait / wake
307
+ * ────────────────────────────────────────────────────────────────────── */
308
+
309
+ gint
310
+ gjsify_sab_region_futex_wait (GjsifySabRegion *region,
311
+ gsize offset,
312
+ gint32 expected,
313
+ gint64 timeout_ms)
314
+ {
315
+ BOUNDS_CHECK (region, offset, 4);
316
+ int32_t *addr = (int32_t *) ((guint8 *) region->ptr + offset);
317
+
318
+ struct timespec ts;
319
+ struct timespec *ts_ptr = NULL;
320
+ if (timeout_ms >= 0) {
321
+ ts.tv_sec = timeout_ms / 1000;
322
+ ts.tv_nsec = (timeout_ms % 1000) * 1000000L;
323
+ ts_ptr = &ts;
324
+ }
325
+
326
+ /* FUTEX_WAIT (no _PRIVATE flag) so the kernel keys by underlying
327
+ * physical page rather than the calling process's virtual address.
328
+ * SharedBuffer regions are mmap'd at different addresses in each
329
+ * Worker, but back the same physical pages via memfd_create —
330
+ * FUTEX_WAIT_PRIVATE would fail to match across processes. */
331
+ long ret = syscall (SYS_futex, addr, FUTEX_WAIT,
332
+ (int) expected, ts_ptr, NULL, 0);
333
+ if (ret == 0) return 0; /* woken via FUTEX_WAKE */
334
+ int e = errno;
335
+ if (e == EAGAIN) return -1; /* value didn't match expected */
336
+ if (e == ETIMEDOUT) return -2;
337
+ if (e == EINTR) return -3;
338
+ return -e;
339
+ }
340
+
341
+ gint
342
+ gjsify_sab_region_futex_wake (GjsifySabRegion *region,
343
+ gsize offset,
344
+ gint count)
345
+ {
346
+ BOUNDS_CHECK (region, offset, 4);
347
+ int32_t *addr = (int32_t *) ((guint8 *) region->ptr + offset);
348
+ long ret = syscall (SYS_futex, addr, FUTEX_WAKE, count,
349
+ NULL, NULL, 0);
350
+ return (gint) ret;
351
+ }
352
+
353
+ /* ────────────────────────────────────────────────────────────────────── *
354
+ * IPC side-channel: socketpair + SCM_RIGHTS
355
+ * ────────────────────────────────────────────────────────────────────── */
356
+
357
+ gboolean
358
+ gjsify_sab_socketpair (gint *parent_fd, gint *child_fd)
359
+ {
360
+ int sv[2];
361
+ if (socketpair (AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sv) < 0) {
362
+ return FALSE;
363
+ }
364
+ *parent_fd = sv[0];
365
+ *child_fd = sv[1];
366
+ return TRUE;
367
+ }
368
+
369
+ gboolean
370
+ gjsify_sab_send_fd (gint socket_fd, gint fd_to_send, guint32 tag)
371
+ {
372
+ struct msghdr msg;
373
+ struct iovec iov;
374
+ union {
375
+ char buf[CMSG_SPACE (sizeof (int))];
376
+ struct cmsghdr align;
377
+ } cmsg_buf;
378
+
379
+ /* Payload: 4-byte big-endian tag. */
380
+ guint32 tag_be = GUINT32_TO_BE (tag);
381
+
382
+ memset (&msg, 0, sizeof msg);
383
+ memset (&cmsg_buf, 0, sizeof cmsg_buf);
384
+
385
+ iov.iov_base = &tag_be;
386
+ iov.iov_len = sizeof tag_be;
387
+ msg.msg_iov = &iov;
388
+ msg.msg_iovlen = 1;
389
+ msg.msg_control = cmsg_buf.buf;
390
+ msg.msg_controllen = sizeof cmsg_buf.buf;
391
+
392
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR (&msg);
393
+ cmsg->cmsg_level = SOL_SOCKET;
394
+ cmsg->cmsg_type = SCM_RIGHTS;
395
+ cmsg->cmsg_len = CMSG_LEN (sizeof (int));
396
+ memcpy (CMSG_DATA (cmsg), &fd_to_send, sizeof (int));
397
+
398
+ ssize_t n;
399
+ do {
400
+ n = sendmsg (socket_fd, &msg, MSG_NOSIGNAL);
401
+ } while (n < 0 && errno == EINTR);
402
+ return n >= 0;
403
+ }
404
+
405
+ gint
406
+ gjsify_sab_recv_fd (gint socket_fd, guint32 *tag)
407
+ {
408
+ struct msghdr msg;
409
+ struct iovec iov;
410
+ union {
411
+ char buf[CMSG_SPACE (sizeof (int))];
412
+ struct cmsghdr align;
413
+ } cmsg_buf;
414
+
415
+ guint32 tag_be = 0;
416
+
417
+ memset (&msg, 0, sizeof msg);
418
+ memset (&cmsg_buf, 0, sizeof cmsg_buf);
419
+
420
+ iov.iov_base = &tag_be;
421
+ iov.iov_len = sizeof tag_be;
422
+ msg.msg_iov = &iov;
423
+ msg.msg_iovlen = 1;
424
+ msg.msg_control = cmsg_buf.buf;
425
+ msg.msg_controllen = sizeof cmsg_buf.buf;
426
+
427
+ ssize_t n;
428
+ do {
429
+ n = recvmsg (socket_fd, &msg, MSG_CMSG_CLOEXEC);
430
+ } while (n < 0 && errno == EINTR);
431
+
432
+ if (n == 0) return 0; /* orderly EOF */
433
+ if (n < 0) return -1; /* error — errno preserved */
434
+
435
+ *tag = GUINT32_FROM_BE (tag_be);
436
+
437
+ /* Extract the fd from the control message. */
438
+ for (struct cmsghdr *cm = CMSG_FIRSTHDR (&msg); cm != NULL; cm = CMSG_NXTHDR (&msg, cm)) {
439
+ if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) {
440
+ int fd;
441
+ memcpy (&fd, CMSG_DATA (cm), sizeof (int));
442
+ return fd;
443
+ }
444
+ }
445
+
446
+ /* No fd in the message — protocol violation. */
447
+ errno = EBADMSG;
448
+ return -1;
449
+ }
450
+
451
+ gboolean
452
+ gjsify_sab_close_fd (gint fd)
453
+ {
454
+ if (fd < 0) return FALSE;
455
+ int r;
456
+ do {
457
+ r = close (fd);
458
+ } while (r < 0 && errno == EINTR);
459
+ /* EBADF: already closed (or never opened) — caller's intent satisfied. */
460
+ return r == 0 || errno == EBADF;
461
+ }
@@ -0,0 +1,150 @@
1
+ /*
2
+ * Tiny C shim around Linux shared-memory + atomics primitives that Vala
3
+ * can't express cleanly:
4
+ *
5
+ * - memfd_create(2) — anonymous shared-memory fd (no path needed)
6
+ * - mmap(2) / munmap(2) — MAP_SHARED region creation
7
+ * - SYS_futex (FUTEX_WAIT_PRIVATE / FUTEX_WAKE_PRIVATE) — wait/notify
8
+ * - __atomic_* GCC builtins — load/store/add/cmpxchg with SEQ_CST
9
+ * - socketpair(AF_UNIX, SOCK_SEQPACKET) — IPC side-channel for fd-passing
10
+ * - sendmsg/recvmsg + SCM_RIGHTS — cross-process fd transfer
11
+ *
12
+ * Same design as @gjsify/http2-native: opaque struct (full definition in .c),
13
+ * `gjsify_sab_<verb>` naming, `_free` paired with `_new`. Buffer ownership
14
+ * stays C-side; JS only sees the GObject wrapper from shared-buffer.vala
15
+ * and the GBytes objects returned by read_bytes (which borrow the mmap'd
16
+ * memory, so callers can do zero-copy reads via imports.byteArray.toArray).
17
+ *
18
+ * Reference: man 2 memfd_create, man 2 mmap, man 2 futex, man 3 cmsg.
19
+ */
20
+
21
+ #ifndef GJSIFY_SAB_HELPERS_H
22
+ #define GJSIFY_SAB_HELPERS_H
23
+
24
+ #include <glib.h>
25
+ #include <glib-object.h>
26
+
27
+ G_BEGIN_DECLS
28
+
29
+ /* ────────────────────────────────────────────────────────────────────── *
30
+ * Shared-memory region (opaque)
31
+ * ────────────────────────────────────────────────────────────────────── */
32
+
33
+ typedef struct _GjsifySabRegion GjsifySabRegion;
34
+
35
+ /**
36
+ * gjsify_sab_region_new_anonymous:
37
+ * @size: byte length of the region (multiple-of-page strongly preferred)
38
+ *
39
+ * Allocate a fresh memfd, ftruncate it to @size, and mmap it MAP_SHARED.
40
+ * Region owns the fd and will close+munmap it on free. Returns %NULL on
41
+ * any syscall failure (errno preserved).
42
+ */
43
+ GjsifySabRegion *gjsify_sab_region_new_anonymous (gsize size);
44
+
45
+ /**
46
+ * gjsify_sab_region_new_from_fd:
47
+ * @fd: caller-owned fd referring to a shared-memory object (typically
48
+ * received via SCM_RIGHTS from a parent process). Will be dup'd —
49
+ * caller retains ownership of the original fd.
50
+ * @size: byte length of the region (must match the sender's view)
51
+ *
52
+ * mmap the fd MAP_SHARED into the calling process's address space.
53
+ * Returns %NULL on dup() or mmap() failure (errno preserved).
54
+ */
55
+ GjsifySabRegion *gjsify_sab_region_new_from_fd (gint fd, gsize size);
56
+
57
+ void gjsify_sab_region_free (GjsifySabRegion *region);
58
+
59
+ gint gjsify_sab_region_get_fd (const GjsifySabRegion *region);
60
+ gsize gjsify_sab_region_get_size (const GjsifySabRegion *region);
61
+
62
+ /* ────────────────────────────────────────────────────────────────────── *
63
+ * Plain read / write (no memory barriers — see _atomic_* below for SEQ_CST)
64
+ * ────────────────────────────────────────────────────────────────────── *
65
+ *
66
+ * All multi-byte accessors are little-endian on the wire. On x86_64 /
67
+ * aarch64 (the native arches) this is a no-op; on s390x / ppc64 the C
68
+ * shim swaps via __builtin_bswap*. Bounds-check `offset + sizeof(T) <=
69
+ * size`; on overflow we g_error() (programmer error, fail-fast).
70
+ */
71
+ guint8 gjsify_sab_region_read_u8 (const GjsifySabRegion *region, gsize offset);
72
+ void gjsify_sab_region_write_u8 (GjsifySabRegion *region, gsize offset, guint8 v);
73
+ guint32 gjsify_sab_region_read_u32_le (const GjsifySabRegion *region, gsize offset);
74
+ void gjsify_sab_region_write_u32_le(GjsifySabRegion *region, gsize offset, guint32 v);
75
+ gint32 gjsify_sab_region_read_i32_le (const GjsifySabRegion *region, gsize offset);
76
+ void gjsify_sab_region_write_i32_le(GjsifySabRegion *region, gsize offset, gint32 v);
77
+ guint64 gjsify_sab_region_read_u64_le (const GjsifySabRegion *region, gsize offset);
78
+ void gjsify_sab_region_write_u64_le(GjsifySabRegion *region, gsize offset, guint64 v);
79
+
80
+ /* Bulk: return a fresh GBytes that borrows the mmap'd memory (no copy).
81
+ * Caller must release the GBytes BEFORE freeing the region — we wrap with
82
+ * g_bytes_new_with_free_func(ptr, length, region_ref, region_unref) so the
83
+ * region is reference-counted via a tiny refcount in the shim. */
84
+ GBytes *gjsify_sab_region_read_bytes (GjsifySabRegion *region, gsize offset, gsize length);
85
+ void gjsify_sab_region_write_bytes(GjsifySabRegion *region, gsize offset, GBytes *data);
86
+
87
+ /* ────────────────────────────────────────────────────────────────────── *
88
+ * Atomics — __atomic_* builtins, memory_order = SEQ_CST
89
+ * ────────────────────────────────────────────────────────────────────── *
90
+ *
91
+ * `cmpxchg32`: weak compare-and-swap. *expected is updated to the actual
92
+ * value on failure (standard C11 pattern). Returns TRUE on success.
93
+ */
94
+ gint32 gjsify_sab_region_atomic_add_i32 (GjsifySabRegion *region, gsize offset, gint32 v);
95
+ gint32 gjsify_sab_region_atomic_sub_i32 (GjsifySabRegion *region, gsize offset, gint32 v);
96
+ gint32 gjsify_sab_region_atomic_load_i32 (const GjsifySabRegion *region, gsize offset);
97
+ void gjsify_sab_region_atomic_store_i32 (GjsifySabRegion *region, gsize offset, gint32 v);
98
+ gint32 gjsify_sab_region_atomic_xchg_i32 (GjsifySabRegion *region, gsize offset, gint32 v);
99
+ gboolean gjsify_sab_region_atomic_cmpxchg_i32 (GjsifySabRegion *region, gsize offset, gint32 *expected, gint32 desired);
100
+
101
+ /* ────────────────────────────────────────────────────────────────────── *
102
+ * Futex wait / wake (Linux SYS_futex, FUTEX_*_PRIVATE flavour)
103
+ * ────────────────────────────────────────────────────────────────────── *
104
+ *
105
+ * `futex_wait`:
106
+ * - Compares *(int32_t*)(ptr+offset) against `expected`.
107
+ * - If equal: blocks until woken or timeout (`timeout_ms` < 0 = infinite).
108
+ * - If not equal: returns immediately (no block).
109
+ * Returns:
110
+ * 0 = woken via FUTEX_WAKE
111
+ * -1 = value did not match expected (EAGAIN) — no wait happened
112
+ * -2 = timed out (ETIMEDOUT)
113
+ * -3 = interrupted by signal (EINTR) — caller may retry
114
+ * other negative = generic errno, sign-flipped
115
+ *
116
+ * `futex_wake`:
117
+ * Wakes up to @count waiters on the address. Returns number actually woken.
118
+ */
119
+ gint gjsify_sab_region_futex_wait (GjsifySabRegion *region, gsize offset, gint32 expected, gint64 timeout_ms);
120
+ gint gjsify_sab_region_futex_wake (GjsifySabRegion *region, gsize offset, gint count);
121
+
122
+ /* ────────────────────────────────────────────────────────────────────── *
123
+ * IPC side-channel: AF_UNIX/SOCK_SEQPACKET pair + SCM_RIGHTS fd transfer
124
+ * ────────────────────────────────────────────────────────────────────── *
125
+ *
126
+ * Used by @gjsify/worker_threads to attach the fd of a SharedBuffer to a
127
+ * postMessage() call. The parent calls _socketpair() at worker spawn,
128
+ * inherits the child end via Gio.SubprocessLauncher.take_fd(child_fd, 3),
129
+ * then send_fd() each SharedBuffer's fd over the parent end. Child runs a
130
+ * recv_fd() loop on its end to pick them up in lockstep with the JSON IPC.
131
+ *
132
+ * Tag is a 4-byte sequence number the sender encodes so the receiver can
133
+ * map the received fd back to the JSON placeholder {__sab: <tag>, size: N}.
134
+ */
135
+ gboolean gjsify_sab_socketpair (gint *parent_fd, gint *child_fd);
136
+ gboolean gjsify_sab_send_fd (gint socket_fd, gint fd_to_send, guint32 tag);
137
+
138
+ /* recv_fd: returns received fd, or -1 on error (errno preserved).
139
+ * Returns 0 on orderly EOF (peer closed socket). *tag receives the
140
+ * sender-encoded tag value. */
141
+ gint gjsify_sab_recv_fd (gint socket_fd, guint32 *tag);
142
+
143
+ /* close_fd: just `close(2)` wrapped so JS callers don't have to drag in
144
+ * GioUnix's typelib for a single syscall. Returns TRUE on success
145
+ * (or EBADF — already closed is harmless), FALSE on other errno. */
146
+ gboolean gjsify_sab_close_fd (gint fd);
147
+
148
+ G_END_DECLS
149
+
150
+ #endif /* GJSIFY_SAB_HELPERS_H */