@scriptc/runtime 0.0.32 → 0.0.34
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/package.json +1 -1
- package/src/scr_async.c +54 -5
- package/src/scr_bytes.c +17 -0
- package/src/scr_ffi.c +139 -0
- package/src/scr_ffi_queue.c +379 -0
- package/src/scr_runtime.h +100 -0
package/package.json
CHANGED
package/src/scr_async.c
CHANGED
|
@@ -2198,6 +2198,22 @@ void scr_loop_set_watch(bool (*pending)(void), void (*dispatch)(void), int (*pol
|
|
|
2198
2198
|
scr_watch_pollfd_fn = pollfd;
|
|
2199
2199
|
}
|
|
2200
2200
|
|
|
2201
|
+
/* The foreign-FFI queue hook (scr_ffi.c when a format-5 descriptor exists).
|
|
2202
|
+
* Its pending count includes live registrations (reffed by default) and
|
|
2203
|
+
* queued deliveries; dispatch runs exactly one callback per loop turn. */
|
|
2204
|
+
static bool (*scr_ffi_pending_fn)(void) = NULL;
|
|
2205
|
+
static bool (*scr_ffi_dispatch_fn)(void) = NULL;
|
|
2206
|
+
static int (*scr_ffi_pollfd_fn)(void) = NULL;
|
|
2207
|
+
static void (*scr_ffi_stop_fn)(void) = NULL;
|
|
2208
|
+
|
|
2209
|
+
void scr_loop_set_ffi(bool (*pending)(void), bool (*dispatch)(void),
|
|
2210
|
+
int (*pollfd)(void), void (*stop)(void)) {
|
|
2211
|
+
scr_ffi_pending_fn = pending;
|
|
2212
|
+
scr_ffi_dispatch_fn = dispatch;
|
|
2213
|
+
scr_ffi_pollfd_fn = pollfd;
|
|
2214
|
+
scr_ffi_stop_fn = stop;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2201
2217
|
/* The stream hook (scr_stream.c, when linked): `pending` keeps the loop
|
|
2202
2218
|
* alive while deferred stream ticks exist, `dispatch` drains them at the
|
|
2203
2219
|
* TOP of every turn — before the events/net stations, the closest
|
|
@@ -2295,6 +2311,12 @@ bool scr_loop_run(ScrPromise *top_level) {
|
|
|
2295
2311
|
if (scr_exc_pending()) return false;
|
|
2296
2312
|
if (dispatched) continue;
|
|
2297
2313
|
}
|
|
2314
|
+
/* Foreign native callbacks are macrotasks. Deliver one, then restart at
|
|
2315
|
+
* the microtask checkpoint before considering the next queued post. */
|
|
2316
|
+
if (scr_ffi_dispatch_fn != NULL && scr_ffi_dispatch_fn()) {
|
|
2317
|
+
if (scr_exc_pending()) return false;
|
|
2318
|
+
if (scr_ready_len > 0 || scr_nt_head != NULL || scr_nunhandled > 0) continue;
|
|
2319
|
+
}
|
|
2298
2320
|
/* Stream tick dispatch (scr_stream.c, when linked): the deferred
|
|
2299
2321
|
* next-tick emissions ('data' flow kicks, 'readable'/'end'/'finish'/
|
|
2300
2322
|
* 'drain'/'error'/'close') fire now, FIRST — the nextTick station.
|
|
@@ -2363,6 +2385,7 @@ bool scr_loop_run(ScrPromise *top_level) {
|
|
|
2363
2385
|
(scr_net_pending_fn != NULL && scr_net_pending_fn()) ||
|
|
2364
2386
|
(scr_dgram_pending_fn != NULL && scr_dgram_pending_fn()) ||
|
|
2365
2387
|
(scr_watch_pending_fn != NULL && scr_watch_pending_fn()) ||
|
|
2388
|
+
(scr_ffi_pending_fn != NULL && scr_ffi_pending_fn()) ||
|
|
2366
2389
|
scr_fs_renames_pending();
|
|
2367
2390
|
if (held) {
|
|
2368
2391
|
scr_children_poll();
|
|
@@ -2382,6 +2405,7 @@ bool scr_loop_run(ScrPromise *top_level) {
|
|
|
2382
2405
|
bool net = scr_net_pending_fn != NULL && scr_net_pending_fn();
|
|
2383
2406
|
bool dgram = scr_dgram_pending_fn != NULL && scr_dgram_pending_fn();
|
|
2384
2407
|
bool watch = scr_watch_pending_fn != NULL && scr_watch_pending_fn();
|
|
2408
|
+
bool ffi = scr_ffi_pending_fn != NULL && scr_ffi_pending_fn();
|
|
2385
2409
|
bool renames = scr_fs_renames_pending();
|
|
2386
2410
|
/* Timer liveness counts only REF'd timers: an unref'd timer stays in
|
|
2387
2411
|
* the heap (and fires if the loop runs on for other reasons) but does
|
|
@@ -2389,7 +2413,7 @@ bool scr_loop_run(ScrPromise *top_level) {
|
|
|
2389
2413
|
* Children follow the same rule: an unref'd child is still REAPED
|
|
2390
2414
|
* while the loop runs (kids drives the sweeps and sleeps above) but
|
|
2391
2415
|
* only reffed ones keep the process alive. */
|
|
2392
|
-
if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !scr_children_reffed_pending() && !io && !events && !net && !dgram && !watch && !renames) break;
|
|
2416
|
+
if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !scr_children_reffed_pending() && !io && !events && !net && !dgram && !watch && !ffi && !renames) break;
|
|
2393
2417
|
/* Sleep to the earliest deadline, then run every due timer (each may
|
|
2394
2418
|
* enqueue microtasks, which the next iteration drains first). Who
|
|
2395
2419
|
* sleeps depends on what is pending:
|
|
@@ -2430,11 +2454,11 @@ bool scr_loop_run(ScrPromise *top_level) {
|
|
|
2430
2454
|
* on EINTR), so they re-impose a coarser cap — bounded Ctrl-C and
|
|
2431
2455
|
* socket latency during a fetch, without the reap-granularity
|
|
2432
2456
|
* cost. */
|
|
2433
|
-
else if ((evw || net || dgram || watch) && due > now + SCR_SIGNAL_POLL_MS) due = now + SCR_SIGNAL_POLL_MS;
|
|
2457
|
+
else if ((evw || net || dgram || watch || ffi) && due > now + SCR_SIGNAL_POLL_MS) due = now + SCR_SIGNAL_POLL_MS;
|
|
2434
2458
|
scr_io_poll_fn(due > now ? due - now : 0);
|
|
2435
2459
|
now = scr_now_ms();
|
|
2436
2460
|
if (scr_ready_len > 0) continue; /* io callbacks woke fibers */
|
|
2437
|
-
} else if (evw || net || dgram || watch) {
|
|
2461
|
+
} else if (evw || net || dgram || watch || ffi) {
|
|
2438
2462
|
#if defined(_WIN32) || defined(__wasi__)
|
|
2439
2463
|
/* The win32 arm, and WASI hosts whose poll_oneoff adapters do not
|
|
2440
2464
|
* reliably wake for a closed inherited stdin pipe: the sleep is a capped nanosleep and
|
|
@@ -2449,6 +2473,7 @@ bool scr_loop_run(ScrPromise *top_level) {
|
|
|
2449
2473
|
* WaitForMultipleObjects over WSAEVENTs, or IOCP. */
|
|
2450
2474
|
if (evw && due > now + SCR_SIGNAL_POLL_MS) due = now + SCR_SIGNAL_POLL_MS;
|
|
2451
2475
|
if ((net || dgram || watch) && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
|
|
2476
|
+
if (ffi && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
|
|
2452
2477
|
if (kids && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
|
|
2453
2478
|
if (due > now) {
|
|
2454
2479
|
double wait = due - now;
|
|
@@ -2465,7 +2490,7 @@ bool scr_loop_run(ScrPromise *top_level) {
|
|
|
2465
2490
|
* events are pending); unrepresentable children keep the ~1ms reap cap
|
|
2466
2491
|
* instead. Dispatch happens at the next turn's top — the poll only
|
|
2467
2492
|
* decides how long to sleep. */
|
|
2468
|
-
struct pollfd fds[
|
|
2493
|
+
struct pollfd fds[7];
|
|
2469
2494
|
int nfds = 0;
|
|
2470
2495
|
int evfds[2];
|
|
2471
2496
|
int nev = evw && scr_events_pollfds_fn != NULL ? scr_events_pollfds_fn(evfds) : 0;
|
|
@@ -2510,6 +2535,16 @@ bool scr_loop_run(ScrPromise *top_level) {
|
|
|
2510
2535
|
due = now + SCR_SIGNAL_POLL_MS;
|
|
2511
2536
|
}
|
|
2512
2537
|
}
|
|
2538
|
+
if (ffi) {
|
|
2539
|
+
int ffd = scr_ffi_pollfd_fn != NULL ? scr_ffi_pollfd_fn() : -1;
|
|
2540
|
+
if (ffd >= 0) {
|
|
2541
|
+
fds[nfds].fd = ffd;
|
|
2542
|
+
fds[nfds].events = POLLIN;
|
|
2543
|
+
fds[nfds++].revents = 0;
|
|
2544
|
+
} else if (due > now + SCR_CHILD_POLL_MS) {
|
|
2545
|
+
due = now + SCR_CHILD_POLL_MS;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2513
2548
|
if (kids) {
|
|
2514
2549
|
int cfd = scr_children_wake_fd();
|
|
2515
2550
|
if (cfd >= 0) {
|
|
@@ -2618,7 +2653,21 @@ bool scr_loop_run(ScrPromise *top_level) {
|
|
|
2618
2653
|
* them as leaks. Uncaught callback throws still return above for main
|
|
2619
2654
|
* to report through the existing exceptional teardown path. */
|
|
2620
2655
|
scr_timers_teardown();
|
|
2621
|
-
/*
|
|
2656
|
+
/* No reffed foreign registration or queued invocation remains at ordinary
|
|
2657
|
+
* exhaustion. Disarm posting before exit listeners/global teardown so a
|
|
2658
|
+
* straggling native thread becomes a safe silent drop. */
|
|
2659
|
+
if (scr_ffi_stop_fn != NULL) scr_ffi_stop_fn();
|
|
2660
|
+
/* Retained native FFI registrations are deliberately NOT torn down
|
|
2661
|
+
* here: process 'exit' listeners run after the loop returns (inline in
|
|
2662
|
+
* main, before any atexit handler) and may legitimately release a
|
|
2663
|
+
* registration or pump a native library one last time — exactly like
|
|
2664
|
+
* the process.exit() path, where the ledger is also still intact. On
|
|
2665
|
+
* returns that reach atexit, the sweep scr_ffi_retain registered (LIFO
|
|
2666
|
+
* before the RC audit) then drops the ledger's references. It does NOT
|
|
2667
|
+
* run on process.exit(): scr_process_exit ends in _Exit, skipping every
|
|
2668
|
+
* atexit handler — the sweep and the RC audit alike — and leaves the
|
|
2669
|
+
* ledger to the OS. */
|
|
2670
|
+
/* Unref'd children the loop never reaped: release the
|
|
2622
2671
|
* registry's references (their listeners never fire — the process is
|
|
2623
2672
|
* exiting, Node's behavior; the OS reparents the children). */
|
|
2624
2673
|
scr_children_teardown();
|
package/src/scr_bytes.c
CHANGED
|
@@ -63,6 +63,15 @@ ScrBytes *scr_bytes_new(ScrBytesElem elem, double n) {
|
|
|
63
63
|
return scr_bytes_alloc(elem, (size_t)t);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
ScrBytes *scr_bytes_from_data(const uint8_t *data, size_t len) {
|
|
67
|
+
if (data == NULL && len != 0) {
|
|
68
|
+
scr_trap("scriptc: native callback passed a NULL span with nonzero length\n");
|
|
69
|
+
}
|
|
70
|
+
ScrBytes *b = scr_bytes_alloc(SCR_BYTES_U8, len);
|
|
71
|
+
if (len != 0) memcpy(b->data, data, len);
|
|
72
|
+
return b;
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
ScrBytes *scr_bytes_copy(const ScrBytes *src) {
|
|
67
76
|
ScrBytes *b = scr_bytes_alloc(src->elem, src->len);
|
|
68
77
|
memcpy(b->data, src->data, src->len * scr_bytes_elem_size(src->elem));
|
|
@@ -410,6 +419,10 @@ static const char scr_hex_digits[] = "0123456789abcdef";
|
|
|
410
419
|
* — what Buffer.prototype.toString("utf8") does. Output re-encodes as
|
|
411
420
|
* (now valid) UTF-8: worst case 3 bytes per input byte. */
|
|
412
421
|
static ScrStr *scr_bytes_decode_utf8(const uint8_t *in, size_t n) {
|
|
422
|
+
if (in == NULL && n != 0) {
|
|
423
|
+
scr_trap("scriptc: native callback passed a NULL span with nonzero length\n");
|
|
424
|
+
}
|
|
425
|
+
if (n > (SIZE_MAX - 1) / 3) scr_bytes_oom();
|
|
413
426
|
char *out = malloc(n * 3 + 1);
|
|
414
427
|
if (!out) scr_bytes_oom();
|
|
415
428
|
size_t o = 0;
|
|
@@ -478,6 +491,10 @@ static ScrStr *scr_bytes_decode_utf8(const uint8_t *in, size_t n) {
|
|
|
478
491
|
return s;
|
|
479
492
|
}
|
|
480
493
|
|
|
494
|
+
ScrStr *scr_str_from_utf8_lossy(const uint8_t *bytes, size_t len) {
|
|
495
|
+
return scr_bytes_decode_utf8(bytes, len);
|
|
496
|
+
}
|
|
497
|
+
|
|
481
498
|
/* WHATWG TextDecoder.decode (utf-8, default options): the SAME maximal-
|
|
482
499
|
* subpart replacement decode as toString("utf8") above, with one
|
|
483
500
|
* difference — a leading UTF-8 BOM is stripped (ignoreBOM defaults to
|
package/src/scr_ffi.c
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#include "scr_runtime.h"
|
|
2
|
+
|
|
3
|
+
#include <stdlib.h>
|
|
4
|
+
|
|
5
|
+
/* The process-global retained-registration ledger. Format-4 script-thread
|
|
6
|
+
* tables use the lock-free path in this always-linked unit. Format-5 foreign
|
|
7
|
+
* tables install an optional teardown hook owned by scr_ffi_queue.c, keeping
|
|
8
|
+
* all queue/thread machinery out of unrelated binaries. */
|
|
9
|
+
static ScrFfiTable *scr_ffi_tables;
|
|
10
|
+
static bool scr_ffi_exit_registered;
|
|
11
|
+
|
|
12
|
+
static void scr_ffi_oom(void) { scr_trap("scriptc: out of memory\n"); }
|
|
13
|
+
|
|
14
|
+
void scr_ffi_link(ScrFfiTable *table) {
|
|
15
|
+
if (!table->linked) {
|
|
16
|
+
table->linked = true;
|
|
17
|
+
table->next = scr_ffi_tables;
|
|
18
|
+
scr_ffi_tables = table;
|
|
19
|
+
}
|
|
20
|
+
if (!scr_ffi_exit_registered) {
|
|
21
|
+
scr_ffi_exit_registered = true;
|
|
22
|
+
scr_atexit(scr_ffi_teardown_all);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
void scr_ffi_teardown(ScrFfiTable *table) {
|
|
27
|
+
if (table->teardown != NULL) {
|
|
28
|
+
table->teardown(table);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
/* Disarm the raw trampoline slot FIRST: a closure release below cannot
|
|
32
|
+
* run script code today, but the slot must never dangle over freed
|
|
33
|
+
* entries — a native exit-path invocation takes the NULL trap instead. */
|
|
34
|
+
if (table->slot != NULL) *table->slot = NULL;
|
|
35
|
+
for (size_t i = 0; i < table->len; i++) {
|
|
36
|
+
scr_closure_release(table->entries[i]);
|
|
37
|
+
}
|
|
38
|
+
free(table->entries);
|
|
39
|
+
table->entries = NULL;
|
|
40
|
+
table->len = 0;
|
|
41
|
+
table->cap = 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void scr_ffi_teardown_all(void) {
|
|
45
|
+
for (ScrFfiTable *table = scr_ffi_tables; table != NULL; table = table->next) {
|
|
46
|
+
scr_ffi_teardown(table);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
void scr_ffi_retain(ScrFfiTable *table, ScrClosure *callback) {
|
|
51
|
+
scr_ffi_link(table);
|
|
52
|
+
if (table->len == table->cap) {
|
|
53
|
+
size_t cap = table->cap == 0 ? 4 : table->cap * 2;
|
|
54
|
+
if (cap < table->cap || cap > SIZE_MAX / sizeof *table->entries) scr_ffi_oom();
|
|
55
|
+
ScrClosure **entries = realloc(table->entries, cap * sizeof *entries);
|
|
56
|
+
if (entries == NULL) scr_ffi_oom();
|
|
57
|
+
table->entries = entries;
|
|
58
|
+
table->cap = cap;
|
|
59
|
+
}
|
|
60
|
+
table->entries[table->len++] = scr_closure_retain(callback);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Raw singleton registration runs in two halves around the native set
|
|
64
|
+
* call. The first half pins the incoming closure and records the slot,
|
|
65
|
+
* but leaves the CURRENT registration untouched: a native setter may
|
|
66
|
+
* flush the outgoing callback one last time mid-replace, and it must
|
|
67
|
+
* dispatch a live closure. An empty slot arms immediately so a native
|
|
68
|
+
* fire-on-subscribe during the set call reaches the new closure. */
|
|
69
|
+
void scr_ffi_retain_slot(ScrFfiTable *table, ScrClosure **slot, ScrClosure *callback) {
|
|
70
|
+
table->slot = slot;
|
|
71
|
+
scr_ffi_retain(table, callback);
|
|
72
|
+
if (*slot == NULL) *slot = callback;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* The second half, after the native call returns: point the slot at the
|
|
76
|
+
* new registration and retire every pin it superseded. A callback pumped
|
|
77
|
+
* during the native call may have released or replaced this registration
|
|
78
|
+
* already (nested rawSet/rawRemove) — commit only a still-live entry. */
|
|
79
|
+
void scr_ffi_commit_slot(ScrFfiTable *table, ScrClosure *callback) {
|
|
80
|
+
bool live = false;
|
|
81
|
+
for (size_t i = 0; i < table->len; i++) {
|
|
82
|
+
if (table->entries[i] == callback) {
|
|
83
|
+
live = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!live) return;
|
|
88
|
+
*table->slot = callback;
|
|
89
|
+
bool kept = false;
|
|
90
|
+
size_t i = 0;
|
|
91
|
+
while (i < table->len) {
|
|
92
|
+
ScrClosure *entry = table->entries[i];
|
|
93
|
+
if (entry == callback && !kept) {
|
|
94
|
+
kept = true;
|
|
95
|
+
i++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
/* Swap-remove BEFORE releasing so the table stays consistent while
|
|
99
|
+
* the release runs; the swapped-in tail entry is re-examined at i. */
|
|
100
|
+
table->len--;
|
|
101
|
+
table->entries[i] = table->entries[table->len];
|
|
102
|
+
scr_closure_release(entry);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Pre-call validation for an explicit release: emitted before the native
|
|
107
|
+
* removal call, so releasing an unregistered value traps before native
|
|
108
|
+
* code can act on the bogus pointer pair. */
|
|
109
|
+
void scr_ffi_require(ScrFfiTable *table, ScrClosure *callback) {
|
|
110
|
+
for (size_t i = 0; i < table->len; i++) {
|
|
111
|
+
if (table->entries[i] == callback) return;
|
|
112
|
+
}
|
|
113
|
+
scr_trap("scriptc: releasing a native callback registration that does not exist\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
void scr_ffi_release(ScrFfiTable *table, ScrClosure *callback) {
|
|
117
|
+
for (size_t i = 0; i < table->len; i++) {
|
|
118
|
+
if (table->entries[i] != callback) continue;
|
|
119
|
+
ScrClosure *owned = table->entries[i];
|
|
120
|
+
table->len--;
|
|
121
|
+
if (i != table->len) table->entries[i] = table->entries[table->len];
|
|
122
|
+
/* If the raw slot dispatched to this registration and no duplicate
|
|
123
|
+
* pin remains, disarm it — the trampoline must not reach a released
|
|
124
|
+
* closure. */
|
|
125
|
+
if (table->slot != NULL && *table->slot == callback) {
|
|
126
|
+
bool remaining = false;
|
|
127
|
+
for (size_t j = 0; j < table->len; j++) {
|
|
128
|
+
if (table->entries[j] == callback) {
|
|
129
|
+
remaining = true;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!remaining) *table->slot = NULL;
|
|
134
|
+
}
|
|
135
|
+
scr_closure_release(owned);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
scr_trap("scriptc: releasing a native callback registration that does not exist\n");
|
|
139
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#include "scr_runtime.h"
|
|
2
|
+
|
|
3
|
+
#include <stdint.h>
|
|
4
|
+
#include <stdio.h>
|
|
5
|
+
#include <stdlib.h>
|
|
6
|
+
#include <string.h>
|
|
7
|
+
|
|
8
|
+
#ifdef _WIN32
|
|
9
|
+
#include <windows.h>
|
|
10
|
+
#elif defined(__wasi__)
|
|
11
|
+
/* Native FFI is rejected for WASI. This keeps direct runtime builds honest. */
|
|
12
|
+
#else
|
|
13
|
+
#include <fcntl.h>
|
|
14
|
+
#include <pthread.h>
|
|
15
|
+
#include <unistd.h>
|
|
16
|
+
#endif
|
|
17
|
+
|
|
18
|
+
typedef enum {
|
|
19
|
+
SCR_FFI_ARG_NONE = 0,
|
|
20
|
+
SCR_FFI_ARG_F64,
|
|
21
|
+
SCR_FFI_ARG_BOOL,
|
|
22
|
+
SCR_FFI_ARG_U8,
|
|
23
|
+
SCR_FFI_ARG_U32,
|
|
24
|
+
SCR_FFI_ARG_I32,
|
|
25
|
+
SCR_FFI_ARG_DATA,
|
|
26
|
+
} ScrFfiArgKind;
|
|
27
|
+
|
|
28
|
+
typedef struct {
|
|
29
|
+
ScrFfiArgKind kind;
|
|
30
|
+
union {
|
|
31
|
+
double f64;
|
|
32
|
+
uint32_t u32;
|
|
33
|
+
int32_t i32;
|
|
34
|
+
struct {
|
|
35
|
+
uint8_t *data;
|
|
36
|
+
size_t len;
|
|
37
|
+
} span;
|
|
38
|
+
} value;
|
|
39
|
+
} ScrFfiArg;
|
|
40
|
+
|
|
41
|
+
struct ScrFfiCall {
|
|
42
|
+
ScrFfiTable *table; /* opaque on the posting thread */
|
|
43
|
+
void *loop; /* reserved instance/loop identity */
|
|
44
|
+
ScrClosure *callback; /* opaque; the table owns its script RC pin */
|
|
45
|
+
ScrFfiDispatchFn dispatch;
|
|
46
|
+
size_t nargs;
|
|
47
|
+
ScrFfiArg *args;
|
|
48
|
+
struct ScrFfiCall *next;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
static ScrFfiCall *scr_ffi_head;
|
|
52
|
+
static ScrFfiCall *scr_ffi_tail;
|
|
53
|
+
static size_t scr_ffi_foreign_registrations;
|
|
54
|
+
static bool scr_ffi_installed;
|
|
55
|
+
static bool scr_ffi_stopping;
|
|
56
|
+
static int scr_ffi_wake_pipe[2] = {-1, -1};
|
|
57
|
+
|
|
58
|
+
#ifdef _WIN32
|
|
59
|
+
static SRWLOCK scr_ffi_lock = SRWLOCK_INIT;
|
|
60
|
+
static void scr_ffi_lock_enter(void) { AcquireSRWLockExclusive(&scr_ffi_lock); }
|
|
61
|
+
static void scr_ffi_lock_leave(void) { ReleaseSRWLockExclusive(&scr_ffi_lock); }
|
|
62
|
+
#elif defined(__wasi__)
|
|
63
|
+
static void scr_ffi_lock_enter(void) {}
|
|
64
|
+
static void scr_ffi_lock_leave(void) {}
|
|
65
|
+
#else
|
|
66
|
+
static pthread_mutex_t scr_ffi_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
67
|
+
static void scr_ffi_lock_enter(void) { (void)pthread_mutex_lock(&scr_ffi_lock); }
|
|
68
|
+
static void scr_ffi_lock_leave(void) { (void)pthread_mutex_unlock(&scr_ffi_lock); }
|
|
69
|
+
#endif
|
|
70
|
+
|
|
71
|
+
static void scr_ffi_oom(void) { scr_trap("scriptc: out of memory\n"); }
|
|
72
|
+
|
|
73
|
+
/* Foreign trampolines cannot enter the script exception machinery. Allocation
|
|
74
|
+
* failure and malformed native pointers are fatal process-boundary failures,
|
|
75
|
+
* reported without touching any runtime object. */
|
|
76
|
+
static void scr_ffi_foreign_fatal(const char *message) {
|
|
77
|
+
fputs(message, stderr);
|
|
78
|
+
abort();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static void *scr_ffi_foreign_alloc(size_t size) {
|
|
82
|
+
void *ptr = malloc(size == 0 ? 1 : size);
|
|
83
|
+
if (ptr == NULL) scr_ffi_foreign_fatal("scriptc: out of memory\n");
|
|
84
|
+
return ptr;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static void scr_ffi_call_free(ScrFfiCall *call) {
|
|
88
|
+
if (call == NULL) return;
|
|
89
|
+
for (size_t i = 0; i < call->nargs; i++) {
|
|
90
|
+
if (call->args[i].kind == SCR_FFI_ARG_DATA) free(call->args[i].value.span.data);
|
|
91
|
+
}
|
|
92
|
+
free(call->args);
|
|
93
|
+
free(call);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static bool scr_ffi_table_contains(const ScrFfiTable *table, const ScrClosure *callback) {
|
|
97
|
+
for (size_t i = 0; i < table->len; i++) {
|
|
98
|
+
if (table->entries[i] == callback) return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static void scr_ffi_table_retire(ScrFfiTable *table, ScrClosure *callback) {
|
|
104
|
+
if (table->retired_len == table->retired_cap) {
|
|
105
|
+
size_t cap = table->retired_cap == 0 ? 4 : table->retired_cap * 2;
|
|
106
|
+
if (cap < table->retired_cap || cap > SIZE_MAX / sizeof *table->retired) scr_ffi_oom();
|
|
107
|
+
ScrClosure **retired = realloc(table->retired, cap * sizeof *retired);
|
|
108
|
+
if (retired == NULL) scr_ffi_oom();
|
|
109
|
+
table->retired = retired;
|
|
110
|
+
table->retired_cap = cap;
|
|
111
|
+
}
|
|
112
|
+
table->retired[table->retired_len++] = callback;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static void scr_ffi_release_retired(ScrFfiTable *table) {
|
|
116
|
+
ScrClosure **retired = NULL;
|
|
117
|
+
size_t len = 0;
|
|
118
|
+
scr_ffi_lock_enter();
|
|
119
|
+
if (table->queued == 0 && table->retired_len > 0) {
|
|
120
|
+
retired = table->retired;
|
|
121
|
+
len = table->retired_len;
|
|
122
|
+
table->retired = NULL;
|
|
123
|
+
table->retired_len = 0;
|
|
124
|
+
table->retired_cap = 0;
|
|
125
|
+
}
|
|
126
|
+
scr_ffi_lock_leave();
|
|
127
|
+
for (size_t i = 0; i < len; i++) scr_closure_release(retired[i]);
|
|
128
|
+
free(retired);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
void scr_ffi_retain_foreign(ScrFfiTable *table, ScrClosure *callback) {
|
|
132
|
+
table->teardown = &scr_ffi_teardown_foreign;
|
|
133
|
+
scr_ffi_link(table);
|
|
134
|
+
scr_ffi_lock_enter();
|
|
135
|
+
if (table->len == table->cap) {
|
|
136
|
+
size_t cap = table->cap == 0 ? 4 : table->cap * 2;
|
|
137
|
+
if (cap < table->cap || cap > SIZE_MAX / sizeof *table->entries) scr_ffi_oom();
|
|
138
|
+
ScrClosure **entries = realloc(table->entries, cap * sizeof *entries);
|
|
139
|
+
if (entries == NULL) scr_ffi_oom();
|
|
140
|
+
table->entries = entries;
|
|
141
|
+
table->cap = cap;
|
|
142
|
+
}
|
|
143
|
+
table->entries[table->len++] = scr_closure_retain(callback);
|
|
144
|
+
scr_ffi_foreign_registrations++;
|
|
145
|
+
scr_ffi_lock_leave();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
void scr_ffi_require_foreign(ScrFfiTable *table, ScrClosure *callback) {
|
|
149
|
+
scr_ffi_lock_enter();
|
|
150
|
+
bool found = scr_ffi_table_contains(table, callback);
|
|
151
|
+
scr_ffi_lock_leave();
|
|
152
|
+
if (!found) scr_trap("scriptc: releasing a native callback registration that does not exist\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
void scr_ffi_release_foreign(ScrFfiTable *table, ScrClosure *callback) {
|
|
156
|
+
scr_ffi_lock_enter();
|
|
157
|
+
for (size_t i = 0; i < table->len; i++) {
|
|
158
|
+
if (table->entries[i] != callback) continue;
|
|
159
|
+
ScrClosure *owned = table->entries[i];
|
|
160
|
+
table->len--;
|
|
161
|
+
if (i != table->len) table->entries[i] = table->entries[table->len];
|
|
162
|
+
if (scr_ffi_foreign_registrations > 0) scr_ffi_foreign_registrations--;
|
|
163
|
+
bool deferred = table->queued > 0;
|
|
164
|
+
if (deferred) scr_ffi_table_retire(table, owned);
|
|
165
|
+
scr_ffi_lock_leave();
|
|
166
|
+
if (!deferred) scr_closure_release(owned);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
scr_ffi_lock_leave();
|
|
170
|
+
scr_trap("scriptc: releasing a native callback registration that does not exist\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ScrFfiCall *scr_ffi_call_new(ScrFfiTable *table, ScrClosure *callback,
|
|
174
|
+
ScrFfiDispatchFn dispatch, size_t nargs) {
|
|
175
|
+
ScrFfiCall *call = scr_ffi_foreign_alloc(sizeof *call);
|
|
176
|
+
call->table = table;
|
|
177
|
+
call->loop = table->loop;
|
|
178
|
+
call->callback = callback;
|
|
179
|
+
call->dispatch = dispatch;
|
|
180
|
+
call->nargs = nargs;
|
|
181
|
+
call->args = nargs == 0 ? NULL : scr_ffi_foreign_alloc(nargs * sizeof *call->args);
|
|
182
|
+
if (nargs > 0) memset(call->args, 0, nargs * sizeof *call->args);
|
|
183
|
+
call->next = NULL;
|
|
184
|
+
return call;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static ScrFfiArg *scr_ffi_call_arg(ScrFfiCall *call, size_t index) {
|
|
188
|
+
if (index >= call->nargs) scr_ffi_foreign_fatal("scriptc: invalid staged FFI callback argument\n");
|
|
189
|
+
return &call->args[index];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
void scr_ffi_call_set_f64(ScrFfiCall *call, size_t i, double v) {
|
|
193
|
+
ScrFfiArg *arg = scr_ffi_call_arg(call, i); arg->kind = SCR_FFI_ARG_F64; arg->value.f64 = v;
|
|
194
|
+
}
|
|
195
|
+
void scr_ffi_call_set_bool(ScrFfiCall *call, size_t i, uint8_t v) {
|
|
196
|
+
ScrFfiArg *arg = scr_ffi_call_arg(call, i); arg->kind = SCR_FFI_ARG_BOOL; arg->value.u32 = v != 0;
|
|
197
|
+
}
|
|
198
|
+
void scr_ffi_call_set_u8(ScrFfiCall *call, size_t i, uint8_t v) {
|
|
199
|
+
ScrFfiArg *arg = scr_ffi_call_arg(call, i); arg->kind = SCR_FFI_ARG_U8; arg->value.u32 = v;
|
|
200
|
+
}
|
|
201
|
+
void scr_ffi_call_set_u32(ScrFfiCall *call, size_t i, uint32_t v) {
|
|
202
|
+
ScrFfiArg *arg = scr_ffi_call_arg(call, i); arg->kind = SCR_FFI_ARG_U32; arg->value.u32 = v;
|
|
203
|
+
}
|
|
204
|
+
void scr_ffi_call_set_i32(ScrFfiCall *call, size_t i, int32_t v) {
|
|
205
|
+
ScrFfiArg *arg = scr_ffi_call_arg(call, i); arg->kind = SCR_FFI_ARG_I32; arg->value.i32 = v;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static void scr_ffi_call_copy_data(ScrFfiCall *call, size_t i,
|
|
209
|
+
const uint8_t *v, size_t len,
|
|
210
|
+
const char *null_message) {
|
|
211
|
+
if (v == NULL && len != 0) scr_ffi_foreign_fatal(null_message);
|
|
212
|
+
ScrFfiArg *arg = scr_ffi_call_arg(call, i);
|
|
213
|
+
arg->kind = SCR_FFI_ARG_DATA;
|
|
214
|
+
arg->value.span.len = len;
|
|
215
|
+
arg->value.span.data = len == 0 ? NULL : scr_ffi_foreign_alloc(len);
|
|
216
|
+
if (len > 0) memcpy(arg->value.span.data, v, len);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
void scr_ffi_call_copy_string(ScrFfiCall *call, size_t i,
|
|
220
|
+
const uint8_t *v, size_t len) {
|
|
221
|
+
scr_ffi_call_copy_data(call, i, v, len,
|
|
222
|
+
"scriptc: native callback passed a NULL string span with nonzero length\n");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
void scr_ffi_call_copy_bytes(ScrFfiCall *call, size_t i,
|
|
226
|
+
const uint8_t *v, size_t len) {
|
|
227
|
+
scr_ffi_call_copy_data(call, i, v, len,
|
|
228
|
+
"scriptc: native callback passed a NULL bytes span with nonzero length\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
void scr_ffi_call_copy_cstring(ScrFfiCall *call, size_t i, const char *v) {
|
|
232
|
+
if (v == NULL) scr_ffi_foreign_fatal("scriptc: native callback passed a NULL cstring\n");
|
|
233
|
+
scr_ffi_call_copy_data(call, i, (const uint8_t *)v, strlen(v),
|
|
234
|
+
"scriptc: native callback passed a NULL cstring\n");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
void scr_ffi_post(ScrFfiCall *call) {
|
|
238
|
+
scr_ffi_lock_enter();
|
|
239
|
+
if (scr_ffi_stopping || !scr_ffi_table_contains(call->table, call->callback)) {
|
|
240
|
+
scr_ffi_lock_leave();
|
|
241
|
+
scr_ffi_call_free(call);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
call->table->queued++;
|
|
245
|
+
if (scr_ffi_tail != NULL) scr_ffi_tail->next = call;
|
|
246
|
+
else scr_ffi_head = call;
|
|
247
|
+
scr_ffi_tail = call;
|
|
248
|
+
#if !defined(_WIN32) && !defined(__wasi__)
|
|
249
|
+
if (scr_ffi_wake_pipe[1] >= 0) {
|
|
250
|
+
ssize_t ignored = write(scr_ffi_wake_pipe[1], "f", 1);
|
|
251
|
+
(void)ignored; /* a full pipe is already readable */
|
|
252
|
+
}
|
|
253
|
+
#endif
|
|
254
|
+
scr_ffi_lock_leave();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
static const ScrFfiArg *scr_ffi_get_arg(const ScrFfiCall *call, size_t index) {
|
|
258
|
+
if (index >= call->nargs) scr_trap("scriptc: invalid staged FFI callback argument\n");
|
|
259
|
+
return &call->args[index];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
double scr_ffi_call_get_f64(const ScrFfiCall *call, size_t i) { return scr_ffi_get_arg(call, i)->value.f64; }
|
|
263
|
+
bool scr_ffi_call_get_bool(const ScrFfiCall *call, size_t i) { return scr_ffi_get_arg(call, i)->value.u32 != 0; }
|
|
264
|
+
double scr_ffi_call_get_u8(const ScrFfiCall *call, size_t i) { return (double)scr_ffi_get_arg(call, i)->value.u32; }
|
|
265
|
+
double scr_ffi_call_get_u32(const ScrFfiCall *call, size_t i) { return (double)scr_ffi_get_arg(call, i)->value.u32; }
|
|
266
|
+
double scr_ffi_call_get_i32(const ScrFfiCall *call, size_t i) { return (double)scr_ffi_get_arg(call, i)->value.i32; }
|
|
267
|
+
const uint8_t *scr_ffi_call_get_data(const ScrFfiCall *call, size_t i) { return scr_ffi_get_arg(call, i)->value.span.data; }
|
|
268
|
+
size_t scr_ffi_call_get_len(const ScrFfiCall *call, size_t i) { return scr_ffi_get_arg(call, i)->value.span.len; }
|
|
269
|
+
|
|
270
|
+
static bool scr_ffi_pending(void) {
|
|
271
|
+
bool pending;
|
|
272
|
+
scr_ffi_lock_enter();
|
|
273
|
+
pending = scr_ffi_foreign_registrations > 0 || scr_ffi_head != NULL;
|
|
274
|
+
scr_ffi_lock_leave();
|
|
275
|
+
return pending;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
static void scr_ffi_wake_drain(void) {
|
|
279
|
+
#if !defined(_WIN32) && !defined(__wasi__)
|
|
280
|
+
if (scr_ffi_wake_pipe[0] < 0) return;
|
|
281
|
+
char buf[64];
|
|
282
|
+
while (read(scr_ffi_wake_pipe[0], buf, sizeof buf) > 0) {}
|
|
283
|
+
#endif
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
static bool scr_ffi_dispatch(void) {
|
|
287
|
+
scr_ffi_lock_enter();
|
|
288
|
+
ScrFfiCall *call = scr_ffi_head;
|
|
289
|
+
if (call != NULL) {
|
|
290
|
+
scr_ffi_head = call->next;
|
|
291
|
+
if (scr_ffi_head == NULL) scr_ffi_tail = NULL;
|
|
292
|
+
}
|
|
293
|
+
scr_ffi_lock_leave();
|
|
294
|
+
if (call == NULL) return false;
|
|
295
|
+
|
|
296
|
+
call->dispatch(call->callback, call);
|
|
297
|
+
|
|
298
|
+
ScrFfiTable *table = call->table;
|
|
299
|
+
scr_ffi_lock_enter();
|
|
300
|
+
if (table->queued > 0) table->queued--;
|
|
301
|
+
/* Keep the wake fd readable while queued calls remain. Drain only while
|
|
302
|
+
* holding the same lock writers use, closing the post-vs-drain lost-wakeup
|
|
303
|
+
* race for the transition to an empty queue. */
|
|
304
|
+
bool more = scr_ffi_head != NULL;
|
|
305
|
+
if (!more) scr_ffi_wake_drain();
|
|
306
|
+
scr_ffi_lock_leave();
|
|
307
|
+
scr_ffi_call_free(call);
|
|
308
|
+
scr_ffi_release_retired(table);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
static int scr_ffi_pollfd(void) { return scr_ffi_wake_pipe[0]; }
|
|
313
|
+
|
|
314
|
+
void scr_ffi_install(void) {
|
|
315
|
+
if (scr_ffi_installed) return;
|
|
316
|
+
scr_ffi_installed = true;
|
|
317
|
+
#if !defined(_WIN32) && !defined(__wasi__)
|
|
318
|
+
if (pipe(scr_ffi_wake_pipe) == 0) {
|
|
319
|
+
for (int i = 0; i < 2; i++) {
|
|
320
|
+
(void)fcntl(scr_ffi_wake_pipe[i], F_SETFL, O_NONBLOCK);
|
|
321
|
+
(void)fcntl(scr_ffi_wake_pipe[i], F_SETFD, FD_CLOEXEC);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
scr_ffi_wake_pipe[0] = scr_ffi_wake_pipe[1] = -1;
|
|
325
|
+
}
|
|
326
|
+
#endif
|
|
327
|
+
scr_loop_set_ffi(&scr_ffi_pending, &scr_ffi_dispatch, &scr_ffi_pollfd,
|
|
328
|
+
&scr_ffi_stop);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
void scr_ffi_stop(void) {
|
|
332
|
+
scr_ffi_lock_enter();
|
|
333
|
+
if (scr_ffi_stopping) {
|
|
334
|
+
scr_ffi_lock_leave();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
scr_ffi_stopping = true;
|
|
338
|
+
ScrFfiCall *calls = scr_ffi_head;
|
|
339
|
+
scr_ffi_head = scr_ffi_tail = NULL;
|
|
340
|
+
for (ScrFfiCall *call = calls; call != NULL; call = call->next) {
|
|
341
|
+
if (call->table->queued > 0) call->table->queued--;
|
|
342
|
+
}
|
|
343
|
+
scr_ffi_wake_drain();
|
|
344
|
+
#if !defined(_WIN32) && !defined(__wasi__)
|
|
345
|
+
if (scr_ffi_wake_pipe[0] >= 0) close(scr_ffi_wake_pipe[0]);
|
|
346
|
+
if (scr_ffi_wake_pipe[1] >= 0) close(scr_ffi_wake_pipe[1]);
|
|
347
|
+
#endif
|
|
348
|
+
scr_ffi_wake_pipe[0] = scr_ffi_wake_pipe[1] = -1;
|
|
349
|
+
scr_ffi_lock_leave();
|
|
350
|
+
while (calls != NULL) {
|
|
351
|
+
ScrFfiCall *next = calls->next;
|
|
352
|
+
ScrFfiTable *table = calls->table;
|
|
353
|
+
scr_ffi_call_free(calls);
|
|
354
|
+
scr_ffi_release_retired(table);
|
|
355
|
+
calls = next;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
void scr_ffi_teardown_foreign(ScrFfiTable *table) {
|
|
360
|
+
scr_ffi_stop();
|
|
361
|
+
scr_ffi_lock_enter();
|
|
362
|
+
ScrClosure **entries = table->entries;
|
|
363
|
+
size_t len = table->len;
|
|
364
|
+
ScrClosure **retired = table->retired;
|
|
365
|
+
size_t retired_len = table->retired_len;
|
|
366
|
+
if (scr_ffi_foreign_registrations >= len) scr_ffi_foreign_registrations -= len;
|
|
367
|
+
table->entries = NULL;
|
|
368
|
+
table->len = 0;
|
|
369
|
+
table->cap = 0;
|
|
370
|
+
table->retired = NULL;
|
|
371
|
+
table->retired_len = 0;
|
|
372
|
+
table->retired_cap = 0;
|
|
373
|
+
table->queued = 0;
|
|
374
|
+
scr_ffi_lock_leave();
|
|
375
|
+
for (size_t i = 0; i < len; i++) scr_closure_release(entries[i]);
|
|
376
|
+
free(entries);
|
|
377
|
+
for (size_t i = 0; i < retired_len; i++) scr_closure_release(retired[i]);
|
|
378
|
+
free(retired);
|
|
379
|
+
}
|
package/src/scr_runtime.h
CHANGED
|
@@ -389,6 +389,9 @@ typedef struct ScrStr {
|
|
|
389
389
|
} ScrStr;
|
|
390
390
|
|
|
391
391
|
ScrStr *scr_str_new(const char *bytes, size_t len); /* returns +1 */
|
|
392
|
+
/* Native callback boundary: copy and WHATWG-decode a UTF-8 span, replacing
|
|
393
|
+
* malformed subsequences with U+FFFD. NULL with len == 0 is an empty span. */
|
|
394
|
+
ScrStr *scr_str_from_utf8_lossy(const uint8_t *bytes, size_t len); /* +1 */
|
|
392
395
|
|
|
393
396
|
/* Internal allocators for buffer builders (scr_json.c): a +1 string with
|
|
394
397
|
* UNINITIALIZED data (the builder fills bytes, then len and the NUL), and
|
|
@@ -1316,6 +1319,100 @@ static inline ScrClosure *scr_closure_retain(ScrClosure *c) {
|
|
|
1316
1319
|
|
|
1317
1320
|
void scr_closure_release(ScrClosure *c); /* releases the boxes; NULL-tolerant */
|
|
1318
1321
|
|
|
1322
|
+
/* ── outbound FFI retained callbacks (scr_ffi.c) ─────────────────────
|
|
1323
|
+
* One compiler-emitted table per retained callback descriptor. For
|
|
1324
|
+
* context-bearing descriptors, entries are counted rather than
|
|
1325
|
+
* deduplicated: registering the same closure twice requires two matching
|
|
1326
|
+
* releases. Raw singleton slots replace instead: commit_slot retires
|
|
1327
|
+
* every pin the new registration superseded — a duplicate of the same
|
|
1328
|
+
* closure included — so after any number of set calls exactly one
|
|
1329
|
+
* release is pending. The table owns one closure reference per entry and
|
|
1330
|
+
* joins a process-global teardown list on first use. Script-thread retained
|
|
1331
|
+
* callbacks do not contribute event-loop liveness; foreign registrations do,
|
|
1332
|
+
* and their queued invocations keep released closure pins alive until the
|
|
1333
|
+
* script-thread dispatch has finished. */
|
|
1334
|
+
typedef struct ScrFfiTable {
|
|
1335
|
+
ScrClosure **entries;
|
|
1336
|
+
size_t len;
|
|
1337
|
+
size_t cap;
|
|
1338
|
+
struct ScrFfiTable *next;
|
|
1339
|
+
bool linked;
|
|
1340
|
+
/* Raw retained singletons only: the compiler-emitted global the
|
|
1341
|
+
* trampoline dispatches through. Teardown and release disarm it so a
|
|
1342
|
+
* late native invocation hits the trampoline's NULL trap instead of a
|
|
1343
|
+
* freed closure. NULL for context-bearing descriptors. */
|
|
1344
|
+
ScrClosure **slot;
|
|
1345
|
+
/* Foreign descriptors only. The global FFI queue lock protects these
|
|
1346
|
+
* fields together with entries/len/cap. Retired pins were explicitly
|
|
1347
|
+
* released while a queued invocation could still name them; dispatch
|
|
1348
|
+
* drops them on the script thread after this table's queue reaches zero. */
|
|
1349
|
+
ScrClosure **retired;
|
|
1350
|
+
size_t retired_len;
|
|
1351
|
+
size_t retired_cap;
|
|
1352
|
+
size_t queued;
|
|
1353
|
+
/* Reserved process-loop identity captured by queued calls. Executables use
|
|
1354
|
+
* one global loop today; library mode can make this per instance later
|
|
1355
|
+
* without changing the post/call ABI. */
|
|
1356
|
+
void *loop;
|
|
1357
|
+
/* Optional format-5 teardown. NULL keeps format-4 tables on the compact
|
|
1358
|
+
* always-linked path; foreign tables point into scr_ffi_queue.c. */
|
|
1359
|
+
void (*teardown)(struct ScrFfiTable *table);
|
|
1360
|
+
} ScrFfiTable;
|
|
1361
|
+
|
|
1362
|
+
void scr_ffi_link(ScrFfiTable *table);
|
|
1363
|
+
void scr_ffi_retain(ScrFfiTable *table, ScrClosure *callback);
|
|
1364
|
+
void scr_ffi_retain_foreign(ScrFfiTable *table, ScrClosure *callback);
|
|
1365
|
+
/* Raw singleton registration, split around the native set call:
|
|
1366
|
+
* retain_slot pins the incoming closure BEFORE the call without touching
|
|
1367
|
+
* the current registration; commit_slot repoints the slot and retires the
|
|
1368
|
+
* superseded pins after the call returns. */
|
|
1369
|
+
void scr_ffi_retain_slot(ScrFfiTable *table, ScrClosure **slot, ScrClosure *callback);
|
|
1370
|
+
void scr_ffi_commit_slot(ScrFfiTable *table, ScrClosure *callback);
|
|
1371
|
+
/* Traps unless the registration exists — emitted BEFORE a native release
|
|
1372
|
+
* call so a bogus release cannot reach native code. */
|
|
1373
|
+
void scr_ffi_require(ScrFfiTable *table, ScrClosure *callback);
|
|
1374
|
+
void scr_ffi_release(ScrFfiTable *table, ScrClosure *callback);
|
|
1375
|
+
void scr_ffi_require_foreign(ScrFfiTable *table, ScrClosure *callback);
|
|
1376
|
+
void scr_ffi_release_foreign(ScrFfiTable *table, ScrClosure *callback);
|
|
1377
|
+
void scr_ffi_teardown(ScrFfiTable *table);
|
|
1378
|
+
void scr_ffi_teardown_all(void);
|
|
1379
|
+
|
|
1380
|
+
/* Format-5 foreign-thread delivery. A generated native trampoline creates a
|
|
1381
|
+
* plain malloc-backed call, stages scalar values or copied byte spans, and
|
|
1382
|
+
* posts it. It never touches closure RC, exception cells, fibers, or other
|
|
1383
|
+
* script-thread-only runtime state. The generated dispatch thunk runs later
|
|
1384
|
+
* on the event loop and materializes script strings/bytes there. */
|
|
1385
|
+
typedef struct ScrFfiCall ScrFfiCall;
|
|
1386
|
+
typedef void (*ScrFfiDispatchFn)(ScrClosure *callback, ScrFfiCall *call);
|
|
1387
|
+
|
|
1388
|
+
ScrFfiCall *scr_ffi_call_new(ScrFfiTable *table, ScrClosure *callback,
|
|
1389
|
+
ScrFfiDispatchFn dispatch, size_t nargs);
|
|
1390
|
+
void scr_ffi_call_set_f64(ScrFfiCall *call, size_t index, double value);
|
|
1391
|
+
void scr_ffi_call_set_bool(ScrFfiCall *call, size_t index, uint8_t value);
|
|
1392
|
+
void scr_ffi_call_set_u8(ScrFfiCall *call, size_t index, uint8_t value);
|
|
1393
|
+
void scr_ffi_call_set_u32(ScrFfiCall *call, size_t index, uint32_t value);
|
|
1394
|
+
void scr_ffi_call_set_i32(ScrFfiCall *call, size_t index, int32_t value);
|
|
1395
|
+
void scr_ffi_call_copy_cstring(ScrFfiCall *call, size_t index, const char *value);
|
|
1396
|
+
void scr_ffi_call_copy_string(ScrFfiCall *call, size_t index,
|
|
1397
|
+
const uint8_t *value, size_t len);
|
|
1398
|
+
void scr_ffi_call_copy_bytes(ScrFfiCall *call, size_t index,
|
|
1399
|
+
const uint8_t *value, size_t len);
|
|
1400
|
+
void scr_ffi_post(ScrFfiCall *call);
|
|
1401
|
+
|
|
1402
|
+
double scr_ffi_call_get_f64(const ScrFfiCall *call, size_t index);
|
|
1403
|
+
bool scr_ffi_call_get_bool(const ScrFfiCall *call, size_t index);
|
|
1404
|
+
double scr_ffi_call_get_u8(const ScrFfiCall *call, size_t index);
|
|
1405
|
+
double scr_ffi_call_get_u32(const ScrFfiCall *call, size_t index);
|
|
1406
|
+
double scr_ffi_call_get_i32(const ScrFfiCall *call, size_t index);
|
|
1407
|
+
const uint8_t *scr_ffi_call_get_data(const ScrFfiCall *call, size_t index);
|
|
1408
|
+
size_t scr_ffi_call_get_len(const ScrFfiCall *call, size_t index);
|
|
1409
|
+
|
|
1410
|
+
void scr_ffi_install(void);
|
|
1411
|
+
void scr_ffi_stop(void);
|
|
1412
|
+
void scr_ffi_teardown_foreign(ScrFfiTable *table);
|
|
1413
|
+
void scr_loop_set_ffi(bool (*pending)(void), bool (*dispatch)(void),
|
|
1414
|
+
int (*pollfd)(void), void (*stop)(void));
|
|
1415
|
+
|
|
1319
1416
|
/* ── unions ─────────────────────────────────────────────────────────
|
|
1320
1417
|
* A union value (`A | B`) is an IMMUTABLE tagged box: a refcounted header,
|
|
1321
1418
|
* the arm's tag (its index in the compiler's canonical arm order), and one
|
|
@@ -4735,6 +4832,9 @@ ScrJsval *scr_jsval_from_bytes(const ScrBytes *b);
|
|
|
4735
4832
|
* THROWS Node's "Invalid typed array length" RangeError catchably and
|
|
4736
4833
|
* returns NULL with the exception pending). */
|
|
4737
4834
|
ScrBytes *scr_bytes_new(ScrBytesElem elem, double n); /* +1 */
|
|
4835
|
+
/* Native callback boundary: exact owned u8 copy. NULL with len == 0 is an
|
|
4836
|
+
* empty span. */
|
|
4837
|
+
ScrBytes *scr_bytes_from_data(const uint8_t *data, size_t len); /* +1 */
|
|
4738
4838
|
|
|
4739
4839
|
/* `new Uint8Array(src)` / Buffer.from(u8): a same-elem-kind copy (the
|
|
4740
4840
|
* compiler fences cross-kind construction). Borrows src. Never throws. */
|