@scriptc/runtime 0.0.31 → 0.0.33

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scriptc/runtime",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "scriptc native runtime — C sources, compiled into every scriptc binary",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://scriptc.dev",
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[6];
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
- /* Same story for unref'd children the loop never reaped: release the
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_musl.c CHANGED
@@ -2,7 +2,7 @@
2
2
  * this TU together with -DSCR_MUSL). musl deliberately provides no libc
3
3
  * identification macro, so the target-selected project macro is the guard.
4
4
  *
5
- * The x86_64 ucontext implementation below is derived from libucontext
5
+ * The x86_64 and AArch64 ucontext implementations below are derived from libucontext
6
6
  * commit 49e671dd52ff6791295d8161ad3b6da7dc5f6f9d:
7
7
  * https://github.com/kaniini/libucontext
8
8
  *
@@ -27,8 +27,8 @@
27
27
  #include <ucontext.h>
28
28
  #endif
29
29
 
30
- #if !defined(__x86_64__)
31
- #error "scriptc's musl runtime currently supports x86_64 only"
30
+ #if !defined(__x86_64__) && !defined(__aarch64__)
31
+ #error "scriptc's musl runtime currently supports x86_64 and AArch64 only"
32
32
  #endif
33
33
 
34
34
  /* The scriptc runtime uses arc4random_buf as its infallible CSPRNG contract.
@@ -57,9 +57,10 @@ void arc4random_buf(void *buf, size_t n) {
57
57
 
58
58
  /* musl exposes the POSIX ucontext types but intentionally omits these legacy
59
59
  * functions. scriptc's async/generator fibers need only user-space register
60
- * swaps; they never use a context to mutate the process signal mask. This is
61
- * libucontext's fast (non-POSIX-signal-mask) x86_64 implementation, emitted
62
- * under the standard symbol names that musl leaves unresolved. */
60
+ * swaps; they never use a context to mutate the process signal mask. These are
61
+ * libucontext's fast (non-POSIX-signal-mask) implementations, emitted under
62
+ * the standard symbol names that musl leaves unresolved. */
63
+ #if defined(__x86_64__)
63
64
  _Static_assert(offsetof(ucontext_t, uc_mcontext.gregs) == 40,
64
65
  "unexpected musl x86_64 ucontext layout");
65
66
  _Static_assert(offsetof(ucontext_t, uc_mcontext.fpregs) == 224,
@@ -265,6 +266,169 @@ void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...) {
265
266
  #undef SCR_STR
266
267
  #undef SCR_STR_INNER
267
268
 
269
+ #elif defined(__aarch64__)
270
+
271
+ /* musl's AArch64 ucontext embeds the kernel sigcontext after a 128-byte
272
+ * signal mask. Keep the offsets pinned to the Zig 0.16 musl headers: the
273
+ * assembly below saves the ABI's callee-preserved GPR and SIMD state there. */
274
+ _Static_assert(offsetof(ucontext_t, uc_mcontext.regs[0]) == 184,
275
+ "unexpected musl AArch64 register layout");
276
+ _Static_assert(offsetof(ucontext_t, uc_mcontext.sp) == 432,
277
+ "unexpected musl AArch64 stack-pointer layout");
278
+ _Static_assert(offsetof(ucontext_t, uc_mcontext.pc) == 440,
279
+ "unexpected musl AArch64 program-counter layout");
280
+ _Static_assert(offsetof(ucontext_t, uc_mcontext.pstate) == 448,
281
+ "unexpected musl AArch64 processor-state layout");
282
+ _Static_assert(offsetof(ucontext_t, uc_mcontext.__reserved) == 464,
283
+ "unexpected musl AArch64 SIMD-state layout");
284
+
285
+ #define SCR_STR_INNER(x) #x
286
+ #define SCR_STR(x) SCR_STR_INNER(x)
287
+ #define SCR_A64_UC_REG(reg) (184 + ((reg) * 8))
288
+ #define SCR_A64_UC_SP 432
289
+ #define SCR_A64_UC_PC 440
290
+ #define SCR_A64_UC_PSTATE 448
291
+ #define SCR_A64_UC_FPSIMD 464
292
+
293
+ __asm__(
294
+ ".text\n"
295
+ ".p2align 2\n"
296
+ ".global getcontext\n"
297
+ "getcontext:\n"
298
+ " str xzr, [x0, #" SCR_STR(SCR_A64_UC_REG(0)) "]\n"
299
+ " stp x2, x3, [x0, #" SCR_STR(SCR_A64_UC_REG(2)) "]\n"
300
+ " str x30, [x0, #" SCR_STR(SCR_A64_UC_PC) "]\n"
301
+ " mov x2, sp\n"
302
+ " str x2, [x0, #" SCR_STR(SCR_A64_UC_SP) "]\n"
303
+ " str xzr, [x0, #" SCR_STR(SCR_A64_UC_PSTATE) "]\n"
304
+ " add x2, x0, #" SCR_STR(SCR_A64_UC_FPSIMD) "\n"
305
+ " stp q8, q9, [x2, #144]\n"
306
+ " stp q10, q11, [x2, #176]\n"
307
+ " stp q12, q13, [x2, #208]\n"
308
+ " stp q14, q15, [x2, #240]\n"
309
+ " mov x2, x0\n"
310
+ " mov x0, #0\n"
311
+ " stp x0, x1, [x2, #" SCR_STR(SCR_A64_UC_REG(0)) "]\n"
312
+ " stp x4, x5, [x2, #" SCR_STR(SCR_A64_UC_REG(4)) "]\n"
313
+ " stp x6, x7, [x2, #" SCR_STR(SCR_A64_UC_REG(6)) "]\n"
314
+ " stp x8, x9, [x2, #" SCR_STR(SCR_A64_UC_REG(8)) "]\n"
315
+ " stp x10, x11, [x2, #" SCR_STR(SCR_A64_UC_REG(10)) "]\n"
316
+ " stp x12, x13, [x2, #" SCR_STR(SCR_A64_UC_REG(12)) "]\n"
317
+ " stp x14, x15, [x2, #" SCR_STR(SCR_A64_UC_REG(14)) "]\n"
318
+ " stp x16, x17, [x2, #" SCR_STR(SCR_A64_UC_REG(16)) "]\n"
319
+ " stp x18, x19, [x2, #" SCR_STR(SCR_A64_UC_REG(18)) "]\n"
320
+ " stp x20, x21, [x2, #" SCR_STR(SCR_A64_UC_REG(20)) "]\n"
321
+ " stp x22, x23, [x2, #" SCR_STR(SCR_A64_UC_REG(22)) "]\n"
322
+ " stp x24, x25, [x2, #" SCR_STR(SCR_A64_UC_REG(24)) "]\n"
323
+ " stp x26, x27, [x2, #" SCR_STR(SCR_A64_UC_REG(26)) "]\n"
324
+ " stp x28, x29, [x2, #" SCR_STR(SCR_A64_UC_REG(28)) "]\n"
325
+ " str x30, [x2, #" SCR_STR(SCR_A64_UC_REG(30)) "]\n"
326
+ " ret\n"
327
+
328
+ ".p2align 2\n"
329
+ ".global setcontext\n"
330
+ "setcontext:\n"
331
+ " ldp x18, x19, [x0, #" SCR_STR(SCR_A64_UC_REG(18)) "]\n"
332
+ " ldp x20, x21, [x0, #" SCR_STR(SCR_A64_UC_REG(20)) "]\n"
333
+ " ldp x22, x23, [x0, #" SCR_STR(SCR_A64_UC_REG(22)) "]\n"
334
+ " ldp x24, x25, [x0, #" SCR_STR(SCR_A64_UC_REG(24)) "]\n"
335
+ " ldp x26, x27, [x0, #" SCR_STR(SCR_A64_UC_REG(26)) "]\n"
336
+ " ldp x28, x29, [x0, #" SCR_STR(SCR_A64_UC_REG(28)) "]\n"
337
+ " ldr x30, [x0, #" SCR_STR(SCR_A64_UC_REG(30)) "]\n"
338
+ " ldr x2, [x0, #" SCR_STR(SCR_A64_UC_SP) "]\n"
339
+ " mov sp, x2\n"
340
+ " add x2, x0, #" SCR_STR(SCR_A64_UC_FPSIMD) "\n"
341
+ " ldp q8, q9, [x2, #144]\n"
342
+ " ldp q10, q11, [x2, #176]\n"
343
+ " ldp q12, q13, [x2, #208]\n"
344
+ " ldp q14, q15, [x2, #240]\n"
345
+ " ldr x16, [x0, #" SCR_STR(SCR_A64_UC_PC) "]\n"
346
+ " ldp x2, x3, [x0, #" SCR_STR(SCR_A64_UC_REG(2)) "]\n"
347
+ " ldp x4, x5, [x0, #" SCR_STR(SCR_A64_UC_REG(4)) "]\n"
348
+ " ldp x6, x7, [x0, #" SCR_STR(SCR_A64_UC_REG(6)) "]\n"
349
+ " ldp x0, x1, [x0, #" SCR_STR(SCR_A64_UC_REG(0)) "]\n"
350
+ " br x16\n"
351
+
352
+ ".p2align 2\n"
353
+ ".global swapcontext\n"
354
+ "swapcontext:\n"
355
+ " str xzr, [x0, #" SCR_STR(SCR_A64_UC_REG(0)) "]\n"
356
+ " stp x2, x3, [x0, #" SCR_STR(SCR_A64_UC_REG(2)) "]\n"
357
+ " stp x4, x5, [x0, #" SCR_STR(SCR_A64_UC_REG(4)) "]\n"
358
+ " stp x6, x7, [x0, #" SCR_STR(SCR_A64_UC_REG(6)) "]\n"
359
+ " stp x8, x9, [x0, #" SCR_STR(SCR_A64_UC_REG(8)) "]\n"
360
+ " stp x10, x11, [x0, #" SCR_STR(SCR_A64_UC_REG(10)) "]\n"
361
+ " stp x12, x13, [x0, #" SCR_STR(SCR_A64_UC_REG(12)) "]\n"
362
+ " stp x14, x15, [x0, #" SCR_STR(SCR_A64_UC_REG(14)) "]\n"
363
+ " stp x16, x17, [x0, #" SCR_STR(SCR_A64_UC_REG(16)) "]\n"
364
+ " stp x18, x19, [x0, #" SCR_STR(SCR_A64_UC_REG(18)) "]\n"
365
+ " stp x20, x21, [x0, #" SCR_STR(SCR_A64_UC_REG(20)) "]\n"
366
+ " stp x22, x23, [x0, #" SCR_STR(SCR_A64_UC_REG(22)) "]\n"
367
+ " stp x24, x25, [x0, #" SCR_STR(SCR_A64_UC_REG(24)) "]\n"
368
+ " stp x26, x27, [x0, #" SCR_STR(SCR_A64_UC_REG(26)) "]\n"
369
+ " stp x28, x29, [x0, #" SCR_STR(SCR_A64_UC_REG(28)) "]\n"
370
+ " str x30, [x0, #" SCR_STR(SCR_A64_UC_REG(30)) "]\n"
371
+ " str x30, [x0, #" SCR_STR(SCR_A64_UC_PC) "]\n"
372
+ " mov x2, sp\n"
373
+ " str x2, [x0, #" SCR_STR(SCR_A64_UC_SP) "]\n"
374
+ " str xzr, [x0, #" SCR_STR(SCR_A64_UC_PSTATE) "]\n"
375
+ " add x2, x0, #" SCR_STR(SCR_A64_UC_FPSIMD) "\n"
376
+ " stp q8, q9, [x2, #144]\n"
377
+ " stp q10, q11, [x2, #176]\n"
378
+ " stp q12, q13, [x2, #208]\n"
379
+ " stp q14, q15, [x2, #240]\n"
380
+ " mov x0, x1\n"
381
+ " b setcontext\n"
382
+
383
+ ".p2align 2\n"
384
+ ".hidden scr_musl_context_trampoline\n"
385
+ "scr_musl_context_trampoline:\n"
386
+ " mov x0, x19\n"
387
+ " cbz x0, 1f\n"
388
+ " bl setcontext\n"
389
+ " brk #0\n"
390
+ "1:\n"
391
+ " mov w0, #0\n"
392
+ " bl exit\n"
393
+ " brk #0\n");
394
+
395
+ extern void scr_musl_context_trampoline(void);
396
+
397
+ void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...) {
398
+ greg_t *sp;
399
+ va_list va;
400
+ int i;
401
+
402
+ sp = (greg_t *)((uintptr_t)ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size);
403
+ sp -= argc < 8 ? 0 : argc - 8;
404
+ sp = (greg_t *)((uintptr_t)sp & ~(uintptr_t)15);
405
+
406
+ ucp->uc_mcontext.sp = (greg_t)(uintptr_t)sp;
407
+ ucp->uc_mcontext.pc = (greg_t)(uintptr_t)func;
408
+ ucp->uc_mcontext.regs[19] = (greg_t)(uintptr_t)ucp->uc_link;
409
+ ucp->uc_mcontext.regs[30] =
410
+ (greg_t)(uintptr_t)&scr_musl_context_trampoline;
411
+
412
+ va_start(va, argc);
413
+ for (i = 0; i < argc && i < 8; i++) {
414
+ ucp->uc_mcontext.regs[i] = va_arg(va, greg_t);
415
+ }
416
+ for (; i < argc; i++) {
417
+ *sp++ = va_arg(va, greg_t);
418
+ }
419
+ va_end(va);
420
+ }
421
+
422
+ #undef SCR_A64_UC_FPSIMD
423
+ #undef SCR_A64_UC_PSTATE
424
+ #undef SCR_A64_UC_PC
425
+ #undef SCR_A64_UC_SP
426
+ #undef SCR_A64_UC_REG
427
+ #undef SCR_STR
428
+ #undef SCR_STR_INNER
429
+
430
+ #endif /* architecture */
431
+
268
432
  #endif /* !SCR_LIB */
269
433
 
270
434
  #else /* !SCR_MUSL */
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. */
@@ -5493,18 +5593,25 @@ long scr_secure_ctx_live_count(void);
5493
5593
  #endif
5494
5594
 
5495
5595
  /* ── node:tls, the CA-store introspection slice (scr_tls_ca.c — its own
5496
- * unit and link gate; plain PEM-block bookkeeping, NO mbedTLS, so a
5497
- * getCACertificates-only binary never builds the archive; cc.ts also
5498
- * compiles it whenever scr_tls.c does). The HOST bundle (the
5499
- * /etc/ssl/cert.pem probe order scr_tls.c documents) stands in for both
5500
- * Node's compiled-in Mozilla roots ('bundled', rootCertificates) and the
5501
- * platform store ('system') the established SEMANTICS divergence,
5502
- * extended to introspection; 'extra' uses the NODE_EXTRA_CA_CERTS file
5503
- * captured before user code runs. Arrays are cached per type (+1 retained
5504
- * answers each call — Node's own caching, and the identity the suite pins
5505
- * with strictEqual). */
5596
+ * unit and link gate; NO mbedTLS, so a getCACertificates-only binary never
5597
+ * builds the archive; cc.ts also compiles it whenever scr_tls.c does). The
5598
+ * HOST roots (Windows' system certificate stores, the established bundle probe on
5599
+ * POSIX) stand in for both Node's compiled-in Mozilla roots ('bundled',
5600
+ * rootCertificates) and the platform store ('system') the established
5601
+ * SEMANTICS divergence, extended to introspection; 'extra' uses the
5602
+ * NODE_EXTRA_CA_CERTS file captured before user code runs. Arrays are cached
5603
+ * per type (+1 retained answers each call Node's own caching, and the
5604
+ * identity the suite pins with strictEqual). */
5506
5605
  void scr_tls_ca_install(void); /* snapshots NODE_EXTRA_CA_CERTS + file bytes */
5507
5606
  bool scr_tls_ca_extra_pem(const char **pem, size_t *len); /* borrowed launch snapshot */
5607
+ #ifdef _WIN32
5608
+ /* Enumerates Windows' trusted ROOT, intermediate CA, and TrustedPeople store
5609
+ * locations, yielding only certificates whose effective Windows EKU policy
5610
+ * permits TLS server authentication. DER is borrowed for the call. */
5611
+ typedef void (*ScrTlsCaWindowsCertFn)(void *ctx, const unsigned char *der, size_t len);
5612
+ bool scr_tls_ca_windows_cert_server_auth(const void *cert_context);
5613
+ void scr_tls_ca_windows_certs(ScrTlsCaWindowsCertFn fn, void *ctx);
5614
+ #endif
5508
5615
  ScrArr *scr_tls_ca_get(ScrStr *type); /* +1; throws ERR_INVALID_ARG_VALUE on unknown types */
5509
5616
  ScrArr *scr_tls_ca_root(void); /* +1; === getCACertificates("bundled") */
5510
5617
  /* Replaces the 'default' set: entries filter to their PEM certificate
package/src/scr_tls.c CHANGED
@@ -56,7 +56,7 @@
56
56
  * head buffers in the socket's write buffer until the handshake
57
57
  * establishes (the mid-connect buffering path, reused verbatim).
58
58
  * rejectUnauthorized:false maps to VERIFY_NONE; `ca` replaces the trust
59
- * anchors; with neither, /etc/ssl/cert.pem stands in for Node's bundled
59
+ * anchors; with neither, the host trust store stands in for Node's bundled
60
60
  * Mozilla roots (SEMANTICS.md documents the divergence). Verify
61
61
  * failures surface as Node-shaped 'error' messages on the request
62
62
  * ("self-signed certificate", "self-signed certificate in certificate
@@ -336,15 +336,15 @@ typedef struct ScrTlsCli {
336
336
  bool verify_recorded;
337
337
  } ScrTlsCli;
338
338
 
339
- /* The default trust anchors when no `ca` option is given: the system
340
- * bundle, standing in for Node's compiled-in Mozilla roots, plus
341
- * NODE_EXTRA_CA_CERTS. The system bundle is probed once across the distro
342
- * spellings /etc/ssl/cert.pem first (macOS ships it; Alpine links it),
343
- * then Debian/Ubuntu's ca-certificates.crt, Fedora/RHEL's ca-bundle.crt,
344
- * and openSUSE's ca-bundle.pem. On macOS only the first path exists, so
345
- * the probe order changes nothing there. A host with none leaves only the
346
- * optional extra chain. Windows is that host by construction (no PEM
347
- * bundle ships; the OS store is cert-database-shaped). */
339
+ /* The default trust anchors when no `ca` option is given: the host trust
340
+ * store, standing in for Node's compiled-in Mozilla roots, plus
341
+ * NODE_EXTRA_CA_CERTS. Windows certificates live as DER entries across the
342
+ * machine, enterprise, current-user, and policy ROOT/CA/TrustedPeople store
343
+ * locations; mbedTLS copies each successfully parsed entry into its own chain.
344
+ * POSIX hosts probe the established bundle spellings /etc/ssl/cert.pem
345
+ * first (macOS ships it; Alpine links it), then Debian/Ubuntu's
346
+ * ca-certificates.crt, Fedora/RHEL's ca-bundle.crt, and openSUSE's
347
+ * ca-bundle.pem. */
348
348
  static mbedtls_x509_crt scr_tls_system_roots;
349
349
  static bool scr_tls_system_roots_loaded = false;
350
350
 
@@ -356,6 +356,12 @@ static bool scr_tls_system_roots_loaded = false;
356
356
  static mbedtls_x509_crt scr_tls_override_roots;
357
357
  static uint64_t scr_tls_override_parsed_gen = 0;
358
358
 
359
+ #ifdef _WIN32
360
+ static void scr_tls_add_windows_ca(void *ctx, const unsigned char *der, size_t len) {
361
+ (void)mbedtls_x509_crt_parse_der((mbedtls_x509_crt *)ctx, der, len);
362
+ }
363
+ #endif
364
+
359
365
  static mbedtls_x509_crt *scr_tls_system_ca(void) {
360
366
  const char *pem = NULL;
361
367
  size_t pem_len = 0;
@@ -375,6 +381,12 @@ static mbedtls_x509_crt *scr_tls_system_ca(void) {
375
381
  if (!scr_tls_system_roots_loaded) {
376
382
  scr_tls_system_roots_loaded = true;
377
383
  mbedtls_x509_crt_init(&scr_tls_system_roots);
384
+ #ifdef _WIN32
385
+ /* The shared enumerator applies Windows' effective server-auth EKU
386
+ * policy before yielding DER. mbedTLS copies every successfully parsed
387
+ * entry, so the store contexts may die immediately after each callback. */
388
+ scr_tls_ca_windows_certs(scr_tls_add_windows_ca, &scr_tls_system_roots);
389
+ #else
378
390
  static const char *const bundles[] = {
379
391
  "/etc/ssl/cert.pem", /* macOS, Alpine */
380
392
  "/etc/ssl/certs/ca-certificates.crt", /* Debian/Ubuntu */
@@ -384,6 +396,7 @@ static mbedtls_x509_crt *scr_tls_system_ca(void) {
384
396
  for (size_t i = 0; i < sizeof bundles / sizeof bundles[0]; i++) {
385
397
  if (mbedtls_x509_crt_parse_file(&scr_tls_system_roots, bundles[i]) == 0) break;
386
398
  }
399
+ #endif
387
400
  const char *extra = NULL;
388
401
  size_t extra_len = 0;
389
402
  if (scr_tls_ca_extra_pem(&extra, &extra_len) && extra_len > 0) {
package/src/scr_tls_ca.c CHANGED
@@ -2,28 +2,33 @@
2
2
  * rootCertificates, and setDefaultCACertificates (scr_runtime.h has the
3
3
  * contracts). Its own link gate ("tlsca." libCalls), deliberately free
4
4
  * of mbedTLS: everything here is PEM-BLOCK bookkeeping over the host
5
- * bundle, so a program that only inspects the CA store keeps the lean
6
- * link line. scr_tls.c (when it links — cc.ts compiles this unit
7
- * alongside it) consults scr_tls_ca_default_override for its client
8
- * trust anchors, which is what makes setDefaultCACertificates LIVE
9
- * semantics: the next https/tls dial verifies against the replaced set.
5
+ * roots (plus crypt32 store access on Windows), so a program that only
6
+ * inspects the CA store never builds the TLS archive. scr_tls.c (when it
7
+ * links — cc.ts compiles this unit alongside it) consults
8
+ * scr_tls_ca_default_override for its client trust anchors, which is what
9
+ * makes setDefaultCACertificates LIVE semantics: the next https/tls dial
10
+ * verifies against the replaced set.
10
11
  *
11
12
  * Divergences, documented: the host bundle stands in for Node's
12
13
  * compiled-in Mozilla roots ('bundled' and rootCertificates) AND for the
13
- * platform store ('system') — the same /etc/ssl/cert.pem stance
14
- * scr_tls.c's anchors established; and set-time validation is PEM-BLOCK
15
- * shaped (a well-formed block with corrupt DER inside is kept here where
16
- * Node's X509 parse would drop it — such a cert then fails verification
17
- * instead, the same observable outcome one step later). Deduplication is
18
- * byte-exact over the extracted block where Node compares parsed X509
19
- * identities; re-encoded duplicates of one certificate stay distinct
20
- * entries, which no equality the tests observe can distinguish without a
21
- * parser. */
14
+ * platform store ('system') — Windows' system certificate stores and the POSIX
15
+ * bundle probe are the same sources scr_tls.c's anchors use; and set-time
16
+ * validation is PEM-BLOCK shaped (a well-formed block with corrupt DER
17
+ * inside is kept here where Node's X509 parse would drop it — such a cert
18
+ * then fails verification instead, the same observable outcome one step
19
+ * later). Deduplication is byte-exact over the extracted block where Node
20
+ * compares parsed X509 identities; re-encoded duplicates of one certificate
21
+ * stay distinct entries, which no equality the tests observe can distinguish
22
+ * without a parser. */
22
23
  #include "scr_runtime.h"
23
24
 
24
25
  #include <stdio.h>
25
26
  #include <stdlib.h>
26
27
  #include <string.h>
28
+ #ifdef _WIN32
29
+ #include <windows.h>
30
+ #include <wincrypt.h>
31
+ #endif
27
32
 
28
33
  /* The per-type caches: +1 held here for the process lifetime (an atexit
29
34
  * teardown releases them so the RC audit stays clean). 'default' also
@@ -154,9 +159,121 @@ bool scr_tls_ca_extra_pem(const char **pem, size_t *len) {
154
159
  return true;
155
160
  }
156
161
 
157
- /* The host bundle, probed in scr_tls.c's documented order. */
162
+ #ifdef _WIN32
163
+ /* Windows can restrict a certificate's purposes in a store property that is
164
+ * not part of its DER. CertGetEnhancedKeyUsage with flags 0 intersects that
165
+ * property with the encoded EKU extension. An empty result means either all
166
+ * purposes (CRYPT_E_NOT_FOUND) or no purposes (ERROR_SUCCESS); every other
167
+ * API failure is fail-closed too. */
168
+ bool scr_tls_ca_windows_cert_server_auth(const void *cert_context) {
169
+ PCCERT_CONTEXT cert = (PCCERT_CONTEXT)cert_context;
170
+ DWORD usage_len = 0;
171
+ if (!CertGetEnhancedKeyUsage(cert, 0, NULL, &usage_len) || usage_len == 0) {
172
+ return false;
173
+ }
174
+ CERT_ENHKEY_USAGE *usage = malloc((size_t)usage_len);
175
+ if (usage == NULL) scr_ca_oom();
176
+ if (!CertGetEnhancedKeyUsage(cert, 0, usage, &usage_len)) {
177
+ free(usage);
178
+ return false;
179
+ }
180
+ DWORD usage_error = GetLastError();
181
+ bool allowed = usage->cUsageIdentifier == 0 &&
182
+ usage_error == CRYPT_E_NOT_FOUND;
183
+ for (DWORD i = 0; !allowed && usage->rgpszUsageIdentifier != NULL &&
184
+ i < usage->cUsageIdentifier; i++) {
185
+ const char *oid = usage->rgpszUsageIdentifier[i];
186
+ allowed = oid != NULL &&
187
+ (strcmp(oid, szOID_PKIX_KP_SERVER_AUTH) == 0 ||
188
+ strcmp(oid, szOID_ANY_ENHANCED_KEY_USAGE) == 0);
189
+ }
190
+ free(usage);
191
+ return allowed;
192
+ }
193
+
194
+ static void scr_tls_ca_windows_certs_at(
195
+ DWORD location, const char *store_name, HCERTSTORE seen,
196
+ ScrTlsCaWindowsCertFn fn, void *ctx) {
197
+ HCERTSTORE store = CertOpenStore(
198
+ CERT_STORE_PROV_SYSTEM_A, 0, 0,
199
+ location | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG,
200
+ store_name);
201
+ if (store == NULL) return;
202
+ PCCERT_CONTEXT cert = NULL;
203
+ /* CertEnumCertificatesInStore frees the previous context on every call,
204
+ * including the final NULL result. Each callback must copy DER it keeps. */
205
+ while ((cert = CertEnumCertificatesInStore(store, cert)) != NULL) {
206
+ /* The logical stores overlap (notably CurrentUser inherits machine
207
+ * roots). CERT_STORE_ADD_NEW turns the memory store into a process-local
208
+ * identity set: only the first allowed context reaches the consumer. */
209
+ if (scr_tls_ca_windows_cert_server_auth(cert) &&
210
+ CertAddCertificateContextToStore(
211
+ seen, cert, CERT_STORE_ADD_NEW, NULL)) {
212
+ fn(ctx, cert->pbCertEncoded, (size_t)cert->cbCertEncoded);
213
+ }
214
+ }
215
+ (void)CertCloseStore(store, 0);
216
+ }
217
+
218
+ void scr_tls_ca_windows_certs(ScrTlsCaWindowsCertFn fn, void *ctx) {
219
+ /* Match Node's Windows system-certificate sources: roots and intermediate
220
+ * CAs from all five applicable locations, plus explicitly trusted server
221
+ * certificates from the three local-machine TrustedPeople locations.
222
+ * A memory store deduplicates their overlapping contents. */
223
+ typedef struct ScrTlsCaWindowsStore {
224
+ DWORD location;
225
+ const char *name;
226
+ } ScrTlsCaWindowsStore;
227
+ static const ScrTlsCaWindowsStore stores[] = {
228
+ {CERT_SYSTEM_STORE_LOCAL_MACHINE, "ROOT"},
229
+ {CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY, "ROOT"},
230
+ {CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE, "ROOT"},
231
+ {CERT_SYSTEM_STORE_CURRENT_USER, "ROOT"},
232
+ {CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY, "ROOT"},
233
+ {CERT_SYSTEM_STORE_LOCAL_MACHINE, "CA"},
234
+ {CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY, "CA"},
235
+ {CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE, "CA"},
236
+ {CERT_SYSTEM_STORE_CURRENT_USER, "CA"},
237
+ {CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY, "CA"},
238
+ {CERT_SYSTEM_STORE_LOCAL_MACHINE, "TrustedPeople"},
239
+ {CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY, "TrustedPeople"},
240
+ {CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE, "TrustedPeople"},
241
+ };
242
+ HCERTSTORE seen = CertOpenStore(
243
+ CERT_STORE_PROV_MEMORY, 0, 0, CERT_STORE_CREATE_NEW_FLAG, NULL);
244
+ if (seen == NULL) return;
245
+ for (size_t i = 0; i < sizeof stores / sizeof stores[0]; i++) {
246
+ scr_tls_ca_windows_certs_at(
247
+ stores[i].location, stores[i].name, seen, fn, ctx);
248
+ }
249
+ (void)CertCloseStore(seen, 0);
250
+ }
251
+
252
+ static void scr_ca_push_windows_cert(void *ctx, const unsigned char *der, size_t len) {
253
+ if (len > UINT32_MAX) return;
254
+ DWORD pem_cap = 0;
255
+ DWORD flags = CRYPT_STRING_BASE64HEADER | CRYPT_STRING_NOCR;
256
+ if (!CryptBinaryToStringA(der, (DWORD)len, flags, NULL, &pem_cap) ||
257
+ pem_cap == 0) {
258
+ return;
259
+ }
260
+ char *pem = malloc((size_t)pem_cap);
261
+ if (pem == NULL) scr_ca_oom();
262
+ DWORD pem_len = pem_cap;
263
+ if (CryptBinaryToStringA(der, (DWORD)len, flags, pem, &pem_len)) {
264
+ scr_arr_push_ref((ScrArr *)ctx, scr_str_new(pem, (size_t)pem_len));
265
+ }
266
+ free(pem);
267
+ }
268
+ #endif
269
+
270
+ /* The host roots, from the Windows logical store or the POSIX bundle probe
271
+ * shared with scr_tls.c. */
158
272
  static ScrArr *scr_ca_load_host_bundle(void) {
159
273
  ScrArr *arr = scr_arr_new(SCR_ELEM_STR, 128);
274
+ #ifdef _WIN32
275
+ scr_tls_ca_windows_certs(scr_ca_push_windows_cert, arr);
276
+ #else
160
277
  static const char *const bundles[] = {
161
278
  "/etc/ssl/cert.pem", /* macOS, Alpine */
162
279
  "/etc/ssl/certs/ca-certificates.crt", /* Debian/Ubuntu */
@@ -171,6 +288,7 @@ static ScrArr *scr_ca_load_host_bundle(void) {
171
288
  free(text);
172
289
  break; /* first bundle wins, like the anchor probe */
173
290
  }
291
+ #endif
174
292
  return arr;
175
293
  }
176
294
 
package/vendor/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  - Commit: 49e671dd52ff6791295d8161ad3b6da7dc5f6f9d
7
7
  - License: ISC (copyright and permission notice reproduced in `src/scr_musl.c`)
8
8
 
9
- The x86_64 register-save/restore and `makecontext` logic is adapted into `src/scr_musl.c`, rather than carried as a separate library, to supply the legacy `ucontext` functions that musl declares but does not implement. It is compiled only for the explicit `x86_64-linux-musl` target and uses musl's public `ucontext_t` layout. The signal-mask compatibility wrapper is deliberately not included: scriptc fibers perform user-space register swaps and do not change a per-fiber signal mask.
9
+ The x86_64 and AArch64 register-save/restore and `makecontext` logic is adapted into `src/scr_musl.c`, rather than carried as a separate library, to supply the legacy `ucontext` functions that musl declares but does not implement. It is compiled for the explicit `x86_64-linux-musl` and `aarch64-linux-musl` targets and uses musl's public `ucontext_t` layouts. The signal-mask compatibility wrapper is deliberately not included: scriptc fibers perform user-space register swaps and do not change a per-fiber signal mask.
10
10
 
11
11
  ## ryu/
12
12