Read native handle methods as function values

Methods of Web objects held in untyped code (Request, Response, Headers,
ReadableStream, its reader, AbortSignal) answered member calls only by
static name, so a property read such as hono's `raw[key]()` or
`typeof req.json` saw undefined. A handle class now lists the methods its
invoke answers; a read returns one shared function per method that calls
invoke on its `this` and raises Node's ERR_INVALID_THIS TypeError (or a
rejected promise for the async operations) on another receiver. The in
operator sees the methods and the modeled properties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-23 01:13:24 -07:00
co-authored by Claude Opus 5.5
parent 88d15a8ea7
commit 2f903f0255
4 changed files with 275 additions and 9 deletions
+23
View File
@@ -1472,6 +1472,19 @@ ScrDyn *scr_dyn_visit_view(const ScrDyn *capsule);
* (scr_http_dyn_install / scr_net_dyn_install — emitted main() calls
* them exactly when the unit is linked, the scr_net_install story), so
* this always-linked core never references gated units. */
/* A method a handle class answers through invoke, read as a value
* (`typeof req.json`, `raw[key]()`): the read answers one function per
* (tag, method) that calls invoke on its `this`. length is the method's
* Function.prototype.length; rejects marks a method whose call on another
* receiver answers a rejected promise instead of throwing (the async
* WebIDL operations: Body's json(), ReadableStream's cancel()). */
typedef struct ScrDynHandleMethod {
const char *name;
uint32_t length;
bool rejects;
const char *invalid_this; /* NULL = the class's (ScrDynHandleOps) */
} ScrDynHandleMethod;
typedef struct ScrDynHandleOps {
const char *cls; /* Node's constructor name — error texts and dynCheck's "got ..." */
void *(*retain)(void *h);
@@ -1537,6 +1550,16 @@ typedef struct ScrDynHandleOps {
* stream unit builds the handle's Transform from these operations on its
* first stream use. NULL = the class is not a stream. */
const struct ScrDynHandleTransform *transform;
/* The class's methods as values, terminated by a NULL name; NULL = a
* property read answers no methods (member calls still reach invoke).
* See ScrDynHandleMethod. */
const ScrDynHandleMethod *methods;
/* The TypeError (code ERR_INVALID_THIS) a method value called on another
* receiver raises; NULL = WebIDL's "Illegal invocation". */
const char *invalid_this;
/* A promise rejected with the pending exception (+1), for the rejects
* methods; the owning unit supplies it, since promises are a gated unit. */
ScrDyn *(*rejected)(void);
} ScrDynHandleOps;
/* The operations a Transform handle gives the stream unit (scr_stream.c):
+56 -8
View File
@@ -4990,7 +4990,7 @@ static ScrDyn *sf_request_get(void *ptr, const char *key, size_t len) {
if (default_value) { ScrStr *s = scr_str_new(default_value, strlen(default_value)); ScrDyn *v = scr_dyn_new_str(s); scr_str_release(s); return v; }
if (sf_name(key, len, "keepalive") || sf_name(key, len, "isReloadNavigation") || sf_name(key, len, "isHistoryNavigation")) return scr_dyn_new_bool(false);
if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") || sf_name(key, len, "headers")) return sf_response_get(r->payload, key, len);
return scr_dyn_undefined();
return NULL;
}
static ScrDyn *sf_request_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) {
SfRequest *r = ptr;
@@ -5495,33 +5495,73 @@ static bool sf_no_set(void *ptr, const char *key, size_t len,
return false;
}
/* The members invoke answers, as values (ScrDynHandleMethod), with Node's
* lengths and the brand check each one runs on another receiver. */
static const ScrDynHandleMethod sf_signal_methods[] = {
{"throwIfAborted", 0, false,
"Value of \"this\" must be of type AbortSignal"},
{"addEventListener", 2, false},
{"removeEventListener", 2, false}, {"dispatchEvent", 1, false},
{NULL, 0, false}};
static const ScrDynHandleMethod sf_stream_methods[] = {
{"cancel", 0, true}, {"getReader", 0, false}, {NULL, 0, false}};
static const ScrDynHandleMethod sf_reader_methods[] = {
{"read", 0, true}, {"releaseLock", 0, false}, {"cancel", 0, true},
{NULL, 0, false}};
static const ScrDynHandleMethod sf_body_methods[] = {
{"json", 0, true}, {"text", 0, true}, {"bytes", 0, true},
{"arrayBuffer", 0, true}, {NULL, 0, false}};
static const ScrDynHandleMethod sf_headers_methods[] = {
{"append", 2, false}, {"delete", 1, false}, {"entries", 0, false},
{"forEach", 1, false}, {"get", 1, false}, {"getSetCookie", 0, false},
{"has", 1, false}, {"keys", 0, false}, {"set", 2, false},
{"values", 0, false}, {NULL, 0, false}};
static ScrDyn *sf_rejected(void) {
ScrPromise *p = scr_promise_settled_ref(NULL, &scr_dyn_retain_v,
&scr_dyn_release_v, NULL);
ScrDyn *out = scr_dyn_new_promise(p);
scr_promise_release(p);
return out;
}
static const ScrDynHandleOps sf_signal_ops = {
"AbortSignal", &sf_signal_retain_v, &sf_signal_release_v,
&sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL};
&sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL,
.methods = sf_signal_methods,
.invalid_this = "Value of \"this\" must be of type EventTarget"};
static const ScrDynHandleOps sf_abort_controller_ops = {
"AbortController", &sf_signal_retain_v, &sf_signal_release_v,
&sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL};
static const ScrDynHandleOps sf_stream_ops = {
"ReadableStream", &sf_stream_retain_v, &sf_stream_release_v,
&sf_stream_invoke, &sf_stream_get, &sf_no_set, NULL};
&sf_stream_invoke, &sf_stream_get, &sf_no_set, NULL,
.methods = sf_stream_methods,
.invalid_this = "Value of \"this\" must be of type ReadableStream",
.rejected = &sf_rejected};
static const ScrDynHandleOps sf_reader_ops = {
"ReadableStreamDefaultReader", &sf_reader_handle_retain_v,
&sf_reader_handle_release_v, &sf_reader_invoke, &sf_reader_get,
&sf_no_set, NULL};
&sf_no_set, NULL, .methods = sf_reader_methods,
.invalid_this =
"Value of \"this\" must be of type ReadableStreamDefaultReader",
.rejected = &sf_rejected};
static const ScrDynHandleOps sf_controller_ops = {
"ReadableStreamDefaultController", &sf_stream_retain_v,
&sf_stream_release_v, &sf_controller_invoke, &sf_controller_get,
&sf_no_set, NULL};
static const ScrDynHandleOps sf_request_ops = {
"Request", &sf_request_retain_v, &sf_request_release_v,
&sf_request_invoke, &sf_request_get, &sf_no_set, NULL};
&sf_request_invoke, &sf_request_get, &sf_no_set, NULL,
.methods = sf_body_methods, .rejected = &sf_rejected};
static const ScrDynHandleOps sf_response_ops = {
"Response", &sf_response_retain_v, &sf_response_release_v,
&sf_response_invoke, &sf_response_get, &sf_no_set, NULL};
&sf_response_invoke, &sf_response_get, &sf_no_set, NULL,
.methods = sf_body_methods, .rejected = &sf_rejected};
static const ScrDynHandleOps sf_headers_ops = {
"Headers", &sf_headers_retain_v, &sf_headers_release_v,
&sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL, NULL,
&sf_headers_step};
&sf_headers_step, .methods = sf_headers_methods};
static void sf_teardown(void) {
while (sf_live) {
@@ -5572,9 +5612,17 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, const ScrDyn *valu
(void)ptr; (void)key; (void)len; (void)value;
return false;
}
static const ScrDynHandleMethod sf_signal_methods[] = {
{"throwIfAborted", 0, false,
"Value of \"this\" must be of type AbortSignal"},
{"addEventListener", 2, false},
{"removeEventListener", 2, false}, {"dispatchEvent", 1, false},
{NULL, 0, false}};
static const ScrDynHandleOps sf_signal_ops = {
"AbortSignal", &sf_signal_retain_v, &sf_signal_release_v,
&sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL };
&sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL,
.methods = sf_signal_methods,
.invalid_this = "Value of \"this\" must be of type EventTarget" };
static const ScrDynHandleOps sf_abort_controller_ops = {
"AbortController", &sf_signal_retain_v, &sf_signal_release_v,
&sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL };
+86 -1
View File
@@ -4975,19 +4975,103 @@ ScrDyn *scr_dyn_promise_key_get(const ScrDyn *d, const ScrStr *k) {
return method ? method : scr_dyn_retain(scr_dyn_undefined());
}
/* ── handle methods as values (ScrDynHandleMethod) ──────────────────
* One function per (tag, method), as Node's prototype methods are shared
* by every instance. The function calls invoke on its `this` when that is
* a handle of its tag, and otherwise raises the class's ERR_INVALID_THIS
* TypeError (or answers a promise rejected with it), as the WebIDL brand
* checks do. closure->fn holds the method entry, caps[0] the tag. */
static SCR_TL ScrDyn **scr_dynh_method_values[SCR_DYNH_CAPACITY];
static SCR_TL bool scr_dynh_method_values_registered;
static void scr_dynh_method_values_cleanup(void) {
for (size_t tag = 0; tag < SCR_DYNH_CAPACITY; tag++) {
ScrDyn **values = scr_dynh_method_values[tag];
if (!values) continue;
scr_dynh_method_values[tag] = NULL;
const ScrDynHandleMethod *methods = scr_dynh_ops[tag]->methods;
for (size_t i = 0; methods[i].name; i++)
if (values[i]) scr_dyn_release(values[i]);
free(values);
}
}
static ScrDyn *scr_dynh_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
const ScrDynHandleMethod *method = closure->fn;
ScrDynHandleTag tag = (ScrDynHandleTag)scr_box_get_f64(closure->caps[0]);
const ScrDynHandleOps *ops = scr_dyn_handle_ops(tag);
ScrDyn *self = scr_dyn_this_get();
ScrDyn *result;
if (self->kind == SCR_DYN_HANDLE && self->v.handle.tag == tag) {
result = ops->invoke(self->v.handle.ptr, self, method->name, args, argc, method->name);
} else {
const char *message = method->invalid_this ? method->invalid_this
: ops->invalid_this ? ops->invalid_this
: "Illegal invocation";
scr_throw_error_msg_code(SCR_ERR_TYPE, message, strlen(message), "ERR_INVALID_THIS");
result = method->rejects && ops->rejected ? ops->rejected() : NULL;
}
scr_dyn_release(self);
return result;
}
/* The method `key` of the handle's class as a function value (+1), or NULL
* when the class has no such method. */
static ScrDyn *scr_dynh_method_value(ScrDynHandleTag tag, const ScrDynHandleOps *ops, const char *key, size_t len) {
const ScrDynHandleMethod *methods = ops->methods;
if (!methods || !ops->invoke) return NULL;
for (size_t i = 0; methods[i].name; i++) {
if (strlen(methods[i].name) != len || memcmp(methods[i].name, key, len) != 0) continue;
if (!scr_dynh_method_values[tag]) {
size_t count = i + 1;
while (methods[count].name) count++;
scr_dynh_method_values[tag] = calloc(count, sizeof(ScrDyn *));
if (!scr_dynh_method_values[tag]) scr_trap("scriptc: out of memory\n");
if (!scr_dynh_method_values_registered) {
scr_dynh_method_values_registered = true;
scr_atexit(scr_dynh_method_values_cleanup);
}
}
ScrDyn **slot = &scr_dynh_method_values[tag][i];
if (!*slot) {
ScrClosure *closure = scr_closure_new((void *)&methods[i], 1);
closure->caps[0] = scr_box_new(SCR_BOX_F64);
scr_box_set_f64(closure->caps[0], (double)tag);
*slot = scr_dyn_new_func(closure, scr_dynh_method_call, methods[i].length, "", methods[i].name);
}
return scr_dyn_retain(*slot);
}
return NULL;
}
ScrDyn *scr_dyn_handle_key_get(const ScrDyn *d, const ScrStr *k) {
const ScrDynHandleOps *ops = scr_dyn_handle_ops(d->v.handle.tag);
ScrDynHandleTag tag = d->v.handle.tag;
const ScrDynHandleOps *ops = scr_dyn_handle_ops(tag);
ScrDyn *r = ops->get(d->v.handle.ptr, k->data, k->len);
if (scr_exc_pending()) {
scr_dyn_release(r);
return NULL;
}
if (!r) r = scr_dynh_method_value(tag, ops, k->data, k->len);
/* Unmodeled names answer undefined — the checked-dynamic tree's own-property stance
* (real-but-unmodeled members throw "not supported yet" inside ops->get instead;
* SEMANTICS.md documents the remainder). */
return r ? r : scr_dyn_retain(scr_dyn_undefined());
}
/* `key in handle`: the class's methods, and the properties its get models. */
static bool scr_dyn_handle_in(const ScrDyn *d, const ScrStr *k) {
const ScrDynHandleOps *ops = scr_dyn_handle_ops(d->v.handle.tag);
const ScrDynHandleMethod *methods = ops->invoke ? ops->methods : NULL;
for (size_t i = 0; methods && methods[i].name; i++)
if (strlen(methods[i].name) == k->len && memcmp(methods[i].name, k->data, k->len) == 0) return true;
if (!methods) return false;
ScrDyn *value = ops->get(d->v.handle.ptr, k->data, k->len);
if (!value) return scr_dyn_object_proto_name(k);
scr_dyn_release(value);
return true;
}
/* ── the ambient receiver (scr_runtime.h's design note) ───────────────
* A strictly nested push/pop stack: firing sites bind the owner around
* each listener call, dyn OBJ method dispatch binds the object, and the
@@ -9196,6 +9280,7 @@ bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) {
}
if (v->kind == SCR_DYN_PROXY) return scr_dyn_proxy_has(v, key);
if (v->kind == SCR_DYN_BYTES) return scr_dyn_bytes_in(v, key);
if (v->kind == SCR_DYN_HANDLE) return scr_dyn_handle_in(v, key);
if (scr_dyn_has_key(v, key)) return true;
if (scr_exc_pending()) return false;
if (v->kind == SCR_DYN_OBJ) return scr_dyn_builtin_inherited(v, key);
+110
View File
@@ -0,0 +1,110 @@
// Methods of native Web objects read as values in untyped code: hono's
// HonoRequest calls `raw[key]()` with a computed body-method name, and code
// probes `typeof x.json` or `"get" in headers`. Each method is one shared
// function that runs on its `this`; another receiver fails Node's brand check.
const report = (label, e) => console.log(label, e.name, e.message, e.code);
// An untyped holder, as values reach a JavaScript library.
const untyped = (value) => {
const holder = JSON.parse("{}");
holder.value = value;
return holder.value;
};
async function bodies() {
const holder = JSON.parse("{}");
holder.req = new Request("http://example.test/books", { method: "POST", body: '{"title":"Dune"}' });
const cache = {};
const cachedBody = (key) => {
const raw = holder.req;
if (cache[key]) return cache[key];
return (cache[key] = raw[key]());
};
console.log(await cachedBody("json"));
for (const key of ["text", "arrayBuffer", "bytes"]) {
const res = new Response("abc");
const value = await res[key]();
console.log(key, typeof value, value instanceof ArrayBuffer, value.length ?? value.byteLength);
}
}
function shape() {
const req = JSON.parse("{}");
req.value = new Request("http://example.test/");
const r = req.value;
console.log(typeof r.json, typeof r.text, typeof r.blobby, typeof r.url);
console.log("json" in r, "url" in r, "toString" in r, "missing" in r);
const other = JSON.parse("{}");
other.res = new Response("x");
console.log(r.json === r.json, r.json === other.res.json, r.json === r.text);
console.log(r.json.name, r.json.length, r.arrayBuffer.name);
const box = { h: untyped(new Headers({ a: "1" })) };
console.log(box.h.append.length, box.h.forEach.length, box.h.get.name, "has" in box.h, "nope" in box.h);
const name = "get";
console.log(box.h[name]("a"), box.h["has"]("b"));
box.h["set"]("b", "2");
const entries = [];
box.h["forEach"]((v, k) => entries.push(k + "=" + v));
console.log(entries.join(","));
}
async function receivers() {
const box = {
h: untyped(new Headers({ a: "1" })),
req: untyped(new Request("http://example.test/", { method: "POST", body: "x" })),
};
const get = box.h.get;
console.log(get.call(box.h, "a"), get.bind(box.h)("a"), get.apply(box.h, ["a"]));
try {
get("a");
} catch (e) {
report("unbound get", e);
}
try {
get.call(box.req, "a");
} catch (e) {
report("foreign get", e);
}
const text = box.req.text;
const pending = text();
console.log("text returns", pending instanceof Promise);
await pending.then(
() => console.log("unexpected"),
(e) => report("unbound text", e),
);
await text.call(box.h).catch((e) => report("foreign text", e));
console.log(await text.call(box.req));
const signal = { s: untyped(new AbortController().signal) };
const throwIfAborted = signal.s.throwIfAborted;
try {
throwIfAborted();
} catch (e) {
report("unbound throwIfAborted", e);
}
try {
signal.s.addEventListener.call(box.h, "abort", () => {});
} catch (e) {
report("foreign addEventListener", e);
}
}
async function streams() {
const box = { body: untyped(new Response("stream").body) };
const key = "getReader";
const reader = box.body[key]();
const read = reader.read;
const first = await read.call(reader);
console.log(first.done, new TextDecoder().decode(first.value));
await read().catch((e) => report("unbound read", e));
try {
box.body.getReader.call(reader);
} catch (e) {
report("foreign getReader", e);
}
}
(async () => {
await bodies();
shape();
await receivers();
await streams();
})();