From b818c4fac3d4f2f9c00d752d999904470abaea2b Mon Sep 17 00:00:00 2001 From: Brian Dickens Date: Mon, 26 Feb 2024 13:14:54 -0500 Subject: [PATCH] Add specifier capture to the `reb` in JS-NATIVE This makes it so that each JS-NATIVE receives a slightly tweaked version of the `reb` object, which reports a specifier custom to that native. Hence lookup by functions like `reb.Value()` will see the arguments of that native inside the JavaScript body of that function! --- extensions/javascript/mod-javascript.c | 70 ++++++++++--------- extensions/javascript/tools/prep-libr3-js.reb | 35 ++++++++-- src/core/a-lib.c | 20 ++++++ 3 files changed, 87 insertions(+), 38 deletions(-) diff --git a/extensions/javascript/mod-javascript.c b/extensions/javascript/mod-javascript.c index a4dc9bbb04..09d4849dad 100644 --- a/extensions/javascript/mod-javascript.c +++ b/extensions/javascript/mod-javascript.c @@ -839,38 +839,49 @@ DECLARE_NATIVE(js_native) // Init_Logic(Details_At(details, IDX_JS_NATIVE_IS_AWAITER), REF(awaiter)); - // The generation of the function called by JavaScript. It takes no - // arguments, as giving it arguments would make calling it more complex - // as well as introduce several issues regarding mapping legal Rebol - // names to names for JavaScript parameters. libRebol APIs must be used - // to access the arguments out of the frame. + //=//// MAKE ASCII SOURCE FOR JAVASCRIPT FUNCTION ///////////////////////=// + + // 1. A JS-AWAITER can only be triggered from Rebol on the worker thread + // as part of a rebPromise(). Making it an async function means it + // will return an ES6 Promise, and allows use of the AWAIT JavaScript + // feature in the body: + // + // https://javascript.info/async-await + // + // Using plain return within an async function returns a fulfilled + // promise while using AWAIT causes the execution to pause and return + // a pending promise. When that promise is fulfilled it will jump back + // in and pick up code on the line after that AWAIT. + // + // 2. We do not try to auto-translate the Rebol arguments into JS args. + // That would make calling it more complex, and introduce several + // issues of mapping Rebol names to legal JavaScript identifiers. + // + // Instead, the function receives an updated `reb` API interface, that + // is intended to "shadow" the global `reb` interface and override it + // during the body of the function. This local `reb` has a specifier + // for the JS-NATIVE's frame, such that when reb.Value("argname") + // is called, this reb passes that specifier through to API_rebValue(), + // and the argument can be resolved this way. + // + // !!! There should be some customization here where if the interface + // was imported via another name than `reb`, it would be used here. + // + // 3. WebAssembly cannot hold onto JavaScript objects directly. So we + // need to store the created function somewhere we can find it later + // when it's time to invoke it. This is done by making a table that + // maps a numeric ID (that we *can* hold onto) to the corresponding + // JavaScript function entity. DECLARE_MOLD (mo); Push_Mold(mo); Append_Ascii(mo->series, "let f = "); // variable we store function in - // A JS-AWAITER can only be triggered from Rebol on the worker thread as - // part of a rebPromise(). Making it an async function means it will - // return an ES6 Promise, and allows use of the AWAIT JavaScript feature - // in the body: - // - // https://javascript.info/async-await - // - // Using plain return within an async function returns a fulfilled promise - // while using AWAIT causes the execution to pause and return a pending - // promise. When that promise is fulfilled it will jump back in and - // pick up code on the line after that AWAIT. - // if (REF(awaiter)) - Append_Ascii(mo->series, "async "); + Append_Ascii(mo->series, "async "); // run inside rebPromise() [1] - // We do not try to auto-translate the Rebol arguments into JS args. It - // would make calling it more complex, and introduce several issues of - // mapping Rebol names to legal JavaScript identifiers. reb.Arg() or - // reb.ArgR() must be used to access the arguments out of the frame. - // - Append_Ascii(mo->series, "function () {"); + Append_Ascii(mo->series, "function (reb) {"); // just one arg [2] Append_String(mo->series, source); Append_Ascii(mo->series, "};\n"); // end `function() {` @@ -882,22 +893,17 @@ DECLARE_NATIVE(js_native) Byte id_buf[60]; // !!! Why 60? Copied from MF_Integer() REBINT len = Emit_Integer(id_buf, native_id); - // Rebol cannot hold onto JavaScript objects directly, so there has to be - // a table mapping some numeric ID (that we *can* hold onto) to the - // corresponding JS function entity. - // - Append_Ascii(mo->series, "reb.RegisterId_internal("); + Append_Ascii(mo->series, "reb.RegisterId_internal("); // put in table [3] Append_Ascii_Len(mo->series, s_cast(id_buf), len); Append_Ascii(mo->series, ", f);\n"); - // The javascript code for registering the function body is now the last - // thing in the mold buffer. Get a pointer to it. - // Term_Binary(mo->series); // !!! is this necessary? const char *js = cs_cast(Binary_At(mo->series, mo->base.size)); TRACE("Registering native_id %ld", cast(long, native_id)); + //=//// RUN FUNCTION GENERATION (ALSO ADDS TO TABLE) ////////////////////=// + // The table mapping IDs to JavaScript objects only exists on the main // thread. So in the pthread build, if we're on the worker we have to // synchronously wait on the registration. (Continuing without blocking diff --git a/extensions/javascript/tools/prep-libr3-js.reb b/extensions/javascript/tools/prep-libr3-js.reb index b35034fdf2..ff89491f37 100644 --- a/extensions/javascript/tools/prep-libr3-js.reb +++ b/extensions/javascript/tools/prep-libr3-js.reb @@ -508,7 +508,7 @@ for-each-api [ HEAP32[(packed>>2) + argc] = reb.END a = reb.m._API_$( - 0, /* null specifier, just to start */ + this.getSpecifierRef(), /* "virtual", overridden in shadow */ packed, 0 /* null vaptr means `p` is array of `const void*` */ ) @@ -522,6 +522,15 @@ for-each-api [ ] e-cwrap/emit { + /* + * JavaScript lacks the idea of "virtual fields" which you can mention in + * methods in a base class, but then shadow in a derived class such that + * the base methods see the updates. But if you override a function then + * that override will effectively be "virtual", such that the methods + * will call it. + */ + reb.getSpecifierRef = function() { return 0 } + reb.R = reb.RELEASING reb.Q = reb.QUOTING reb.U = reb.UNQUOTING @@ -731,7 +740,7 @@ e-cwrap/emit { delete reb.JS_NATIVES[id] } - reb.RunNative_internal = function(id, frame_id) { + reb.RunNative_internal = function(id, level_id) { if (!(id in reb.JS_NATIVES)) throw Error("Can't dispatch " + id + " in JS_NATIVES table") @@ -763,7 +772,7 @@ e-cwrap/emit { ) } - reb.m._API_rebResolveNative_internal(frame_id, result_id) + reb.m._API_rebResolveNative_internal(level_id, result_id) } let rejecter = function(rej) { @@ -789,7 +798,19 @@ e-cwrap/emit { else error_id = reb.JavaScriptError(rej) - reb.m._API_rebRejectNative_internal(frame_id, error_id) + reb.m._API_rebRejectNative_internal(level_id, error_id) + } + + /* + * The shadowing object is a variant of the `reb` API object which + * knows what frame it's in. + */ + let reb_shadow = { + specifier_ref: ( + reb.m._API_rebAllocSpecifierRefFromLevel_internal(level_id) + ), + getSpecifierRef: function() { return this.specifier_ref }, + __proto__: reb } let native = reb.JS_NATIVES[id] @@ -798,7 +819,7 @@ e-cwrap/emit { * There is no built in capability of ES6 promises to cancel, but * we make the promise given back cancelable. */ - let promise = reb.Cancelable(native()) + let promise = reb.Cancelable(native(reb_shadow)) promise.then(resolver).catch(rejecter) /* cancel causes reject */ /* resolve() or reject() cannot be signaled yet...JavaScript does @@ -813,7 +834,7 @@ e-cwrap/emit { } else { try { - resolver(native()) + resolver(native(reb_shadow)) } catch(e) { rejecter(e) @@ -821,6 +842,8 @@ e-cwrap/emit { /* resolve() or reject() guaranteed to be signaled in this case */ } + + reb.m._API_rebFree(reb_shadow.specifier_ref) } reb.ResolvePromise_internal = function(promise_id, rebval) { diff --git a/src/core/a-lib.c b/src/core/a-lib.c index eb835b28e8..ad2bce55f8 100644 --- a/src/core/a-lib.c +++ b/src/core/a-lib.c @@ -2904,6 +2904,26 @@ RebolSpecifier* API_rebSpecifierFromLevel_internal(void* level_) } +// +// rebAllocSpecifierRefFromLevel_internal: API +// +// This bridges being able to do a pointer-to-pointer in JavaScript without +// needing to use low-level Webassembly byte fiddling. +// +RebolSpecifier** API_rebAllocSpecifierRefFromLevel_internal(void* level_) +{ + ENTER_API; + + RebolSpecifier** ref = cast( + RebolSpecifier**, + API_rebAllocBytes(sizeof(RebolSpecifier*)) + ); + + *ref = API_rebSpecifierFromLevel_internal(level_); + return ref; +} + + // // rebCollateExtension_internal: API //