Skip to content

Commit

Permalink
Add specifier capture to the reb in JS-NATIVE
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
hostilefork committed Feb 26, 2024
1 parent f13248e commit b818c4f
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 38 deletions.
70 changes: 38 additions & 32 deletions extensions/javascript/mod-javascript.c
Original file line number Diff line number Diff line change
Expand Up @@ -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() {`

Expand All @@ -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
Expand Down
35 changes: 29 additions & 6 deletions extensions/javascript/tools/prep-libr3-js.reb
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ for-each-api [
HEAP32[(packed>>2) + argc] = reb.END

a = reb.m._API_$<Name>(
0, /* null specifier, just to start */
this.getSpecifierRef(), /* "virtual", overridden in shadow */
packed,
0 /* null vaptr means `p` is array of `const void*` */
)
Expand All @@ -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
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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) {
Expand All @@ -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]
Expand All @@ -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
Expand All @@ -813,14 +834,16 @@ e-cwrap/emit {
}
else {
try {
resolver(native())
resolver(native(reb_shadow))
}
catch(e) {
rejecter(e)
}

/* 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) {
Expand Down
20 changes: 20 additions & 0 deletions src/core/a-lib.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand Down

0 comments on commit b818c4f

Please sign in to comment.