mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
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:
co-authored by
Claude Opus 5.5
parent
88d15a8ea7
commit
2f903f0255
@@ -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):
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
Reference in New Issue
Block a user