diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 667ceced0..24f3578c3 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -174,6 +174,17 @@ typedef enum { SCR_DYNH_COUNT, } ScrDynHandleTag; +/* Every per-tag table (scr_dynh_ops in scr_json.c, the message clone hooks + * in scr_message.c) is sized by this fixed capacity, never by + * SCR_DYNH_COUNT. Appending a tag for a link-gated unit therefore changes + * no always-linked storage or code: a program that never boxes the new + * handle links byte-identical objects. Raising the capacity is a + * program-wide layout change; do it in a change of its own, measured as + * neutral, and only when the enum outgrows it. */ +#define SCR_DYNH_CAPACITY 128 +_Static_assert(SCR_DYNH_COUNT <= SCR_DYNH_CAPACITY, + "ScrDynHandleTag outgrew SCR_DYNH_CAPACITY: raise it in its own change"); + typedef struct ScrDyn ScrDyn; typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */ typedef struct ScrClosure ScrClosure; /* full definition below (C11 repeat) */ @@ -1162,7 +1173,25 @@ typedef struct ScrDynHandleOps { * lookup): +1 value, or NULL when the class has no member for `key` * (the read answers undefined). NULL hook = no symbol-keyed members. */ ScrDyn *(*symbol_get)(void *h, const ScrDyn *key); + /* Cross-thread structured clone (postMessage, workerData); NULL = + * DataCloneError. See ScrDynHandleTransfer. */ + const struct ScrDynHandleTransfer *transfer; } ScrDynHandleOps; + +/* How a handle crosses threads, supplied by the handle's owning unit so the + * message encoder (scr_message.c) needs no reference to gated units and no + * per-tag registry. share runs on the sending thread: it answers a payload + * that any thread may use, +1, or NULL when this handle cannot be cloned + * (with an exception pending, or DataCloneError is thrown). adopt runs on + * the receiving thread and answers a fresh box over the payload (+1; the + * message keeps its payload reference). drop releases the payload when the + * message is freed, whether or not it was delivered. All three must be + * callable from any thread. */ +typedef struct ScrDynHandleTransfer { + void *(*share)(void *h); + ScrDyn *(*adopt)(void *payload); + void (*drop)(void *payload); +} ScrDynHandleTransfer; ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection); /* ── promises in the checked-dynamic tree (SCR_DYN_PROMISE) ──────────────────────────── diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b23e78241..ab4a10dd8 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3649,7 +3649,7 @@ ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc) { * always-linked core never references gated units. A missing tag at use * is an internal error: emitted programs install a unit's ops whenever * they can box its handles. */ -static SCR_TL const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_COUNT]; +static SCR_TL const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_CAPACITY]; /* BroadcastChannel's EventTarget table (scr_et_broadcast_install). */ SCR_TL const ScrEtBroadcastOps *scr_et_broadcast_ops = NULL; diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 82428a6fe..7c969a2e3 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -23,6 +23,8 @@ typedef enum { SCR_MESSAGE_TYPED_REF, /* A transferred MessagePort: its channel end (+1), the side in number. */ SCR_MESSAGE_PORT, + /* A native handle cloned through its unit's ScrDynHandleTransfer. */ + SCR_MESSAGE_HANDLE, } ScrMessageKind; const ScrMessagePortOps *scr_message_port_ops = NULL; @@ -49,6 +51,9 @@ typedef struct { bool data_view; ScrDyn *published; /* SCR_MESSAGE_PUBLISHED: immortal root, owns nothing */ ScrChannel *channel; /* SCR_MESSAGE_PORT */ + /* SCR_MESSAGE_HANDLE: the owning unit's hooks and its payload (+1). */ + const ScrDynHandleTransfer *transfer; + void *payload; /* SCR_MESSAGE_TYPED_REF: the capsule's process-wide adapters and type key * (vtables, adapters and type keys are shared by every thread). */ struct { @@ -389,7 +394,20 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) break; } bool buffer = scr_buffer_storage_is(value); - if (value->kind == SCR_DYN_HANDLE && !buffer) goto unsupported; + if (value->kind == SCR_DYN_HANDLE && !buffer) { + const ScrDynHandleTransfer *transfer = scr_dyn_handle_ops_of(value)->transfer; + if (!transfer) goto unsupported; + void *payload = transfer->share(value->v.handle.ptr); + if (!payload) { + if (scr_exc_pending()) return SIZE_MAX; + goto unsupported; + } + node = &encoder->message->nodes[id]; + node->kind = SCR_MESSAGE_HANDLE; + node->transfer = transfer; + node->payload = payload; + break; + } const ScrBytes *bytes = buffer ? value->v.handle.ptr : value->v.bytes; size_t storage = scr_message_storage(encoder, bytes); /* Adding the storage node can reallocate the node array. */ @@ -765,6 +783,7 @@ ScrDyn *scr_message_decode(const ScrMessage *message) { case SCR_MESSAGE_PORT: values[i] = scr_message_port_ops->adopt(node->channel, (int)node->number); break; + case SCR_MESSAGE_HANDLE: values[i] = node->transfer->adopt(node->payload); break; } if (!values[i] || scr_exc_pending()) goto done; } @@ -820,6 +839,7 @@ void scr_message_free(ScrMessage *message) { free(node->data); scr_shared_release(node->shared); if (node->kind == SCR_MESSAGE_PORT) scr_message_port_ops->release(node->channel); + if (node->kind == SCR_MESSAGE_HANDLE) node->transfer->drop(node->payload); } free(message->nodes); free(message); diff --git a/packages/runtime/src/scr_message.test.c b/packages/runtime/src/scr_message.test.c index 4e8aa7d62..0c6bd7ae3 100644 --- a/packages/runtime/src/scr_message.test.c +++ b/packages/runtime/src/scr_message.test.c @@ -172,6 +172,115 @@ static void check_documents(void) { scr_message_free(message); } +/* A link-gated unit's handle crosses threads through the transfer hooks in + * its ops: share on the sender, adopt on the receiver, drop with the + * message. The test borrows the last capacity slot, which no unit uses, so + * it also proves that slots past SCR_DYNH_COUNT work. */ +#define TRANSFER_TAG ((ScrDynHandleTag)(SCR_DYNH_CAPACITY - 1)) +typedef struct { + _Atomic size_t rc; + int value; +} TransferBox; +static _Atomic int transfer_shares, transfer_adopts, transfer_drops; +static void *transfer_retain(void *h) { + ((TransferBox *)h)->rc++; + return h; +} +static void transfer_release(void *h) { + if (--((TransferBox *)h)->rc == 0) free(h); +} +static void *transfer_share(void *h) { + TransferBox *source = h; + if (source->value < 0) return NULL; + transfer_shares++; + TransferBox *copy = malloc(sizeof *copy); + copy->rc = 1; + copy->value = source->value; + return copy; +} +static ScrDyn *transfer_adopt(void *payload) { + transfer_adopts++; + return scr_dyn_new_handle(payload, TRANSFER_TAG); +} +static void transfer_drop(void *payload) { + transfer_drops++; + transfer_release(payload); +} +static const ScrDynHandleTransfer transfer_hooks = {transfer_share, transfer_adopt, transfer_drop}; +static const ScrDynHandleOps transfer_ops = { + .cls = "Transferable", + .retain = transfer_retain, + .release = transfer_release, + .transfer = &transfer_hooks, +}; +static const ScrDynHandleOps opaque_ops = { + .cls = "Opaque", + .retain = transfer_retain, + .release = transfer_release, +}; + +static void *receive_handles(void *data) { + scr_context_enter(1); + scr_init(); + scr_lib_init(0, NULL); + scr_dyn_handle_install(TRANSFER_TAG, &transfer_ops); + ScrDyn *root = scr_message_decode(data); + assert(root && !scr_exc_pending()); + ScrDyn *first = scr_dyn_obj_get(root, "first", 5); + assert(first->kind == SCR_DYN_HANDLE && first->v.handle.tag == TRANSFER_TAG); + assert(((TransferBox *)first->v.handle.ptr)->value == 7); + /* One handle reached twice decodes to one box. */ + assert(first == scr_dyn_obj_get(root, "again", 5)); + assert(transfer_adopts == 1); + scr_dyn_release(root); + scr_collect_cycles(); + scr_context_cleanup(); + return NULL; +} + +static ScrDyn *transfer_handle(int value, const ScrDynHandleOps *ops) { + scr_dyn_handle_install(TRANSFER_TAG, ops); + TransferBox *box = malloc(sizeof *box); + box->rc = 1; + box->value = value; + ScrDyn *handle = scr_dyn_new_handle(box, TRANSFER_TAG); + transfer_release(box); + return handle; +} + +static void check_handle_transfer(void) { + ScrDyn *root = scr_dyn_new_obj(); + ScrDyn *handle = transfer_handle(7, &transfer_ops); + scr_dyn_obj_set(root, "first", 5, scr_dyn_retain(handle)); + scr_dyn_obj_set(root, "again", 5, handle); + ScrMessage *message = scr_message_encode(root); + assert(message && !scr_exc_pending()); + assert(transfer_shares == 1); + scr_dyn_release(root); + pthread_t receiver; + assert(pthread_create(&receiver, NULL, receive_handles, message) == 0); + assert(pthread_join(receiver, NULL) == 0); + assert(transfer_drops == 0); + scr_message_free(message); + assert(transfer_drops == 1); + + /* A handle whose unit declines (share answers NULL) and a handle with no + * transfer hooks both throw DataCloneError, releasing what was shared. */ + root = scr_dyn_new_arr(); + scr_dyn_arr_push(root, transfer_handle(1, &transfer_ops)); + scr_dyn_arr_push(root, transfer_handle(-1, &transfer_ops)); + assert(!scr_message_encode(root) && scr_exc_pending()); + scr_exc_clear(); + assert(transfer_shares == 2 && transfer_drops == 2); + scr_dyn_release(root); + root = transfer_handle(3, &opaque_ops); + assert(!scr_message_encode(root) && scr_exc_pending()); + scr_exc_clear(); + assert(transfer_shares == 2); + scr_dyn_release(root); + scr_collect_cycles(); +} + int main(void) { scr_init(); scr_lib_init(0, NULL); @@ -188,6 +297,8 @@ int main(void) { check_getters(); check_rejection(); check_documents(); + check_handle_transfer(); scr_context_cleanup(); puts("message graphs preserve identity, sparse values, byte aliases and getter order"); + puts("native handles cross threads through their transfer hooks"); } diff --git a/packages/runtime/src/scr_message.test.ts b/packages/runtime/src/scr_message.test.ts index b2364a5d8..56b35f1ac 100644 --- a/packages/runtime/src/scr_message.test.ts +++ b/packages/runtime/src/scr_message.test.ts @@ -34,7 +34,8 @@ test.skipIf(process.platform === "win32")( env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, }); expect(result.stdout).toBe( - "message graphs preserve identity, sparse values, byte aliases and getter order\n", + "message graphs preserve identity, sparse values, byte aliases and getter order\n" + + "native handles cross threads through their transfer hooks\n", ); expect(result.stderr).toBe(""); } finally {