mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Size handle tables by a fixed capacity and let handles cross threads
Appending a ScrDynHandleTag used to grow the always-linked scr_dynh_ops table, which shifted .bss and code addresses in every program, including programs that never box the new handle (Blob and File paid this when they were added). The table is now sized by SCR_DYNH_CAPACITY (128; 52 tags are in use), so a link-gated unit can add a tag without changing any other program's executable. This change is the one layout shift that makes later tags free. Structured clone between threads (postMessage, workerData) rejected every handle other than MessagePorts and buffers. A handle's ops can now carry a ScrDynHandleTransfer (share on the sender, adopt on the receiver, drop with the message), which scr_message.c uses for any tag, so a gated unit such as a future WebAssembly.Module needs no reference from the message encoder and no per-tag registry. No handle declares the hooks yet; scr_message.test.c exercises them across threads through a test handle in the last capacity slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d3bfa69323
commit
4b78531ab4
@@ -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) ────────────────────────────
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user