diff --git a/configs/emscripten.r b/configs/emscripten.r index 9842756afe..dd1a144f63 100644 --- a/configs/emscripten.r +++ b/configs/emscripten.r @@ -175,6 +175,17 @@ ldflags: compose [ {--pre-js prep/include/node-preload.js} ]) + ; The default build will create an emscripten module named "Module", which + ; holds various emscripten state (including the memory heap) and service + ; routines. If everyone built their projects like this, you would not be + ; able to load more than one at a time due to the name collision. So + ; we use the "Modularize" option to get a callback with a parameter that + ; is the module object when it is ready. This also simplifies the loading + ; process of registering notifications for being loaded and ready. + ; https://emscripten.org/docs/getting_started/FAQ.html#can-i-use-multiple-emscripten-compiled-programs-on-one-web-page + ; + {-s MODULARIZE=1 -s 'EXPORT_NAME="r3_module_promiser"'} + (if debug-javascript-extension [ {-s ASSERTIONS=1} ] else [ diff --git a/extensions/javascript/load-r3.js b/extensions/javascript/load-r3.js index ff4dfd9be8..f4e3b17f1c 100644 --- a/extensions/javascript/load-r3.js +++ b/extensions/javascript/load-r3.js @@ -81,15 +81,20 @@ // // https://medium.com/@tkssharma/javascript-module-pattern-b4b5012ada9f // -// Two global objects are exported. One is `Module`, which is how Emscripten -// expects to get its configuration parameters (as properties of that global -// object), so the startup function must initialize it with all the necessary -// properties and callbacks. -// -// The other object is `reb` which is the container for the API. The reason -// all the APIs are in an object (e.g. `reb.Elide()` instead of `rebElide()`) -// is because Node.js doesn't allow global functions, so the only way to get -// an API would be through something like `let reb = require('rebol')`. +// Only one object is exported, the `reb` object. It is the container for +// the API. The reason all the APIs are in an object (e.g. `reb.Elide()` +// instead of `rebElide()` as needed in C) is because Node.js doesn't allow +// global functions. So the only way to get an API would be through something +// like `let reb = require('rebol')`. +// +// The `reb.m` member holds the Emscripten module object. This contains the +// WebAssembly heap and other service routines, and all of the "unwrapped" +// raw API exports--which take integer parameters as heap addresses only. +// (The reb.XXX routines are wrapped to speak in terms of types like strings +// or arrays.) By default Emscripten would make this a global variable +// called `Module`, but using the `MODULARIZE=1` option it will give us a +// factory function that passes the module as a parameter so we can place it +// wherever we like. // // It may look like reb.Startup() takes two parameters. But if you read all // the way to the bottom of the file, you'll see `console` is passed in with @@ -102,10 +107,22 @@ // off. If you have that problem, comment out the function temporarily. // -var reb = {} // This aggregator is where we put all the Rebol APIs +// We can make an aggregator here for all the Rebol APIs, which is global for +// the browser... e.g. this is actually `window.reb`. But that doesn't do +// a Web Worker much good in threaded builds, since they have no `window`. +// Even though they share the same heap, they make their own copies of all +// the API wrappers in their own `reb` as `self.reb`. +// +var reb = {} -var Module // Emscripten expects this to be global and set up with options +// !!! This `reb.Startup` function gets overwritten by the wrapped version of +// the internal API's `reb.Startup`, which is then invoked during the load +// process. It works...but, for clarity the internal version might should be +// changed to something like `Startup_internal`, and the C version can then +// simply `#define rebStartup RL_Startup_internal` since it has no parallel +// to this loading step. +// reb.Startup = function(console_in, config_in) { // only ONE arg, see above! @@ -136,6 +153,62 @@ else config = default_config +// The factory function for MODULARIZE=1 in Emscripten takes an object as a +// parameter for the defaults. Since we don't need the defaults once the +// actual module is loaded, we reuse the same variable for the defaults as we +// do the ultimately loaded module. For documentation on options: +// +// https://emscripten.org/docs/api_reference/module.html#affecting-execution +// +reb.m = { + // + // For errors like: + // + // "table import 1 has a larger maximum size 37c than the module's + // declared maximum 890" + // + // The total memory must be bumped up. These large sizes occur in debug + // builds with lots of assertions and symbol tables. Note that the size + // may appear smaller than the maximum in the error message, as previous + // tables (e.g. table import 0 in the case above) can consume memory. + // + // !!! Messing with this setting never seemed to help. See the emcc + // parameter ALLOW_MEMORY_GROWTH for another possibility. + // + /* TOTAL_MEMORY: 16 * 1024 * 1024, */ + + locateFile: function(s) { + // + // function for finding %libr3.wasm (Note: memoryInitializerPrefixURL + // for bytecode was deprecated) + // + // https://stackoverflow.com/q/46332699 + // + config.info("reb.m.locateFile() asking for .wasm address of " + s) + + let stem = s.substr(0, s.indexOf('.')) + let suffix = s.substr(s.indexOf('.')) + + // Although we rename the files to add the Git Commit Hash before + // uploading them to S3, it seems that for some reason the .js hard + // codes the name the file was built under in this request. :-/ + // So even if the request was for `libr3-xxxxx.js` it will be asked + // in this routine as "Where is `libr3.wasm` + // + // For the moment, sanity check to libr3. But it should be `rebol`, + // or any name you choose to build with. + // + if (stem != "libr3") + throw Error("Unknown libRebol stem: " + stem) + + if (suffix == ".worker.js") + return URL.createObjectURL(workerJsBlob) + + return libRebolComponentURL(suffix) + } +} + + //=//// PICK BUILD BASED ON BROWSER CAPABILITIES //////////////////////////=// // // The JavaScript extension can be built two different ways for the browser. @@ -212,17 +285,17 @@ let reb_args = "[" for (let i = 0; i < js_args.length; i++) { let a = decodeURIComponent(js_args[i]).split("=") // makes array - if (a.length == 1) { // simple switch with no arguments, e.g. ?debug - if (a[0] == 'debug') { - is_debug = true - } else if (a[0] == 'local') { - base_dir = "./" - } else if (a[0] == 'remote') { - base_dir = "https://metaeducation.s3.amazonaws.com/travis-builds/" - } else if (a[0] == 'tracing_on') { - config.tracing_on = true - } else - reb_args += a[0] + ": true " // look like being set to true + if (a.length == 1) { // simple switch with no arguments, e.g. ?debug + if (a[0] == 'debug') { + is_debug = true + } else if (a[0] == 'local') { + base_dir = "./" + } else if (a[0] == 'remote') { + base_dir = "https://metaeducation.s3.amazonaws.com/travis-builds/" + } else if (a[0] == 'tracing_on') { + config.tracing_on = true + } else + reb_args += a[0] + ": true " // look like being set to true } else if (a.length = 2) { // combination key/val, e.g. "git_commit=" if (a[0] == 'git_commit') { @@ -407,61 +480,6 @@ let prefetch_worker_js_promiser = () => new Promise( ) -Module = { // Note that this is assigning a global - // - // For errors like: - // - // "table import 1 has a larger maximum size 37c than the module's - // declared maximum 890" - // - // The total memory must be bumped up. These large sizes occur in debug - // builds with lots of assertions and symbol tables. Note that the size - // may appear smaller than the maximum in the error message, as previous - // tables (e.g. table import 0 in the case above) can consume memory. - // - // !!! Messing with this setting never seemed to help. See the emcc - // parameter ALLOW_MEMORY_GROWTH for another possibility. - // - /* TOTAL_MEMORY: 16 * 1024 * 1024, */ - - locateFile: function(s) { - // - // function for finding %libr3.wasm (Note: memoryInitializerPrefixURL - // for bytecode was deprecated) - // - // https://stackoverflow.com/q/46332699 - // - config.info("Module.locateFile() asking for .wasm address of " + s) - - let stem = s.substr(0, s.indexOf('.')) - let suffix = s.substr(s.indexOf('.')) - - // Although we rename the files to add the Git Commit Hash before - // uploading them to S3, it seems that for some reason the .js hard - // codes the name the file was built under in this request. :-/ - // So even if the request was for `libr3-xxxxx.js` it will be asked - // in this routine as "Where is `libr3.wasm` - // - // For the moment, sanity check to libr3. But it should be `rebol`, - // or any name you choose to build with. - // - if (stem != "libr3") - throw Error("Unknown libRebol stem: " + stem) - - if (suffix == ".worker.js") - return URL.createObjectURL(workerJsBlob) - - return libRebolComponentURL(suffix) - }, - - // This is a callback that happens sometime after you load the emscripten - // library (%libr3.js in this case). It's turned into a promise instead - // of a callback. Sanity check it's not used prior by making it a string. - // - onRuntimeInitialized: "" -} - - //=// CONVERTING CALLBACKS TO PROMISES /////////////////////////////////////=// // // https://stackoverflow.com/a/22519785 @@ -490,16 +508,6 @@ if (document.readyState == "loading") { dom_content_loaded_promise = Promise.resolve() } -let runtime_init_promise = new Promise(function(resolve, reject) { - // - // The load of %libr3.js will at some point will trigger a call to - // onRuntimeInitialized(). We set it up so that when it does, it will - // resolve this promise (used to trigger a .then() step). - // - Module.onRuntimeInitialized = resolve -}) - - let load_rebol_scripts = function(defer) { let scripts = document.querySelectorAll("script[type='text/rebol']") let promise = Promise.resolve(null) @@ -552,23 +560,25 @@ let load_rebol_scripts = function(defer) { //=//// MAIN PROMISE CHAIN ////////////////////////////////////////////////=// return assign_git_commit_promiser(os_id) // sets git_commit - .then(function () { + .then(() => { config.log("prefetching worker...") - return prefetch_worker_js_promiser() // no-op in the non-pthread build + return prefetch_worker_js_promiser() // no-op in the non-pthread build }) - .then(function() { + .then(() => { - load_js_promiser(libRebolComponentURL(".js")) // needs git_commit + return load_js_promiser(libRebolComponentURL(".js")) // needs git_commit - }).then(function() { + }).then(() => { // we now know r3_module_promiser is available config.info('Loading/Running ' + libRebolComponentURL(".js") + '...') if (use_asyncify) - config.warn("The Asyncify build is biggers/slower, be patient...") + config.warn("The Asyncify build is bigger/slower, be patient...") + + return r3_module_promiser(reb.m) // at first, `reb.m` is defaults... - return runtime_init_promise + }).then(module => { // "Modularized" emscripten passes us the module - }).then(function() { // emscripten's onRuntimeInitialized() has no args + reb.m = module // overwrite the defaults with the instantiated module config.info('Executing Rebol boot code...') @@ -622,12 +632,13 @@ return assign_git_commit_promiser(os_id) // sets git_commit "for-each collation builtin-extensions", "[load-extension collation]" ) - }).then(()=>load_rebol_scripts(false)) + }).then(() => load_rebol_scripts(false)) .then(dom_content_loaded_promise) - .then(()=>load_rebol_scripts(true)) - .then(()=>{ - let code = me.innerText.trim() - if (code) eval(code) + .then(() => load_rebol_scripts(true)) + .then(() => { + let code = me.innerText.trim() + if (code) + eval(code) }) //=//// END ANONYMOUS CLOSURE USED AS MODULE //////////////////////////////=// diff --git a/extensions/javascript/mod-javascript.c b/extensions/javascript/mod-javascript.c index 2c0456df6b..01a48420a7 100644 --- a/extensions/javascript/mod-javascript.c +++ b/extensions/javascript/mod-javascript.c @@ -414,7 +414,7 @@ EXTERN_C intptr_t RL_rebPromise(REBFLGS flags, void *p, va_list *vaptr) #ifdef USE_ASYNCIFY EM_ASM( - { setTimeout(function() { _RL_rebIdle_internal(); }, 0); } + { setTimeout(function() { reb.m._RL_rebIdle_internal(); }, 0); } ); // note `_RL` (leading underscore means no cwrap) #else pthread_mutex_lock(&PG_Promise_Mutex); @@ -880,7 +880,7 @@ REB_R JavaScript_Dispatcher(REBFRM *f) MAIN_THREAD_EM_ASM( // blocking call { reb.RunNative_internal($0, $1); // `;` is necessary here - _RL_rebTakeAwaitLock_internal(); + reb.m._RL_rebTakeAwaitLock_internal(); }, native_id, // => $0 frame_id // => $1 @@ -1185,12 +1185,16 @@ REBNATIVE(js_eval_p) MAIN_THREAD_EM_ASM( { eval(UTF8ToString($0)) }, utf8 - ); - else + ); // !!! ...should be an else clause here... + // !!! However, there's an emscripten bug, so use two `if`s instead + // https://github.com/emscripten-core/emscripten/issues/11539 + // + if (not REF(local)) MAIN_THREAD_EM_ASM( { (1,eval)(UTF8ToString($0)) }, utf8 ); + return Init_Void(D_OUT); } diff --git a/extensions/javascript/prep-libr3-js.reb b/extensions/javascript/prep-libr3-js.reb index 1a94cd1d14..c3ff6a6c1f 100644 --- a/extensions/javascript/prep-libr3-js.reb +++ b/extensions/javascript/prep-libr3-js.reb @@ -208,17 +208,29 @@ e-cwrap/emit { * calling it `r.Run()`, if one wanted). Additionally, module support * in browsers is rolling out, although not fully mainstream yet. */ - var reb /* local definition only if not using modules */ /* Could use ENVIRONMENT_IS_NODE here, but really the test should be for * if the system supports modules (someone with an understanding of the - * state of browser modules should look at this). Note `Module.exports` - * seems not to be defined, even in the node version. + * state of browser modules should look at this). */ if (typeof module !== 'undefined') reb = module.exports /* add to what you get with require('rebol') */ - else - reb = {} /* build a new dictionary to use reb.Xxx() if in browser */ + else { + /* !!! In browser, `reb` is a global (window.reb) set by load-r3.js + * But it would be better if we "modularized" and let the caller + * name the module, with `reb` just being a default. However, + * letting them name it creates a lot of issues within EM_ASM + * calls from the C code. Also, the worker wants to use the same + * name. Punt for now. Note `self` must be used instead of `window` + * as `window` exists only on the main thread (`self` is a synonym). + */ + if (typeof window !== 'undefined') + reb = window.reb /* main, load-r3.js made it (has reb.m) */ + else { + reb = self.reb = {} /* worker, make our own API container */ + reb.m = self /* module exports are at global scope on worker? */ + } + } } to-js-type: func [ @@ -397,7 +409,7 @@ map-each-api [ continue ] - enter: copy {_RL_rebEnterApi_internal();} + enter: {reb.m._RL_rebEnterApi_internal();} if false [ ; It can be useful for debugging to see the API entry points; ; using console.error() adds a stack trace to it. @@ -478,7 +490,11 @@ map-each-api [ */ HEAP32[(va>>2) + (argc + 1)] = va + 4 - a = _RL_$(this.quotes, HEAP32[va>>2], va + 4 * (argc + 1)) + a = reb.m._RL_$( + this.quotes, + HEAP32[va>>2], + va + 4 * (argc + 1) + ) stackRestore(stack) @@ -491,7 +507,7 @@ map-each-api [ } api ] else [ e-cwrap/emit cscape/with { - reb.$ = cwrap_tolerant( /* vs. Module.cwrap() */ + reb.$ = cwrap_tolerant( /* vs. R3Module.cwrap() */ 'RL_$', $, [ $(Js-Param-Types), @@ -548,8 +564,8 @@ e-cwrap/emit { else throw Error("Unknown array type in reb.Binary " + typeof array) - let binary = _RL_rebUninitializedBinary_internal(view.length) - let head = _RL_rebBinaryHead_internal(binary) + let binary = reb.m._RL_rebUninitializedBinary_internal(view.length) + let head = reb.m._RL_rebBinaryHead_internal(binary) writeArrayToMemory(view, head) /* uses Int8Array.set() on HEAP8 */ return binary @@ -561,10 +577,10 @@ e-cwrap/emit { * https://stackoverflow.com/a/53605865 */ reb.Bytes = function(binary) { - let ptr = _RL_rebBinaryAt_internal(binary) - let size = _RL_rebBinarySizeAt_internal(binary) + let ptr = reb.m._RL_rebBinaryAt_internal(binary) + let size = reb.m._RL_rebBinarySizeAt_internal(binary) - var view = new Uint8Array(Module.HEAPU8.buffer, ptr, size) + var view = new Uint8Array(reb.m.HEAPU8.buffer, ptr, size) /* Copy method: https://stackoverflow.com/a/22114687/211160 */ @@ -581,9 +597,9 @@ e-cwrap/emit { * the ACTION! (turned into an integer) */ - let RL_JS_NATIVES = {} /* !!! would a Map be more performant? */ - let RL_JS_CANCELABLES = new Set() /* American spelling has one 'L' */ - const RL_JS_ERROR_HALTED = Error("Halted by Escape, reb.Halt(), or HALT") + reb.JS_NATIVES = {} /* !!! would a Map be more performant? */ + reb.JS_CANCELABLES = new Set() /* American spelling has one 'L' */ + reb.JS_ERROR_HALTED = Error("Halted by Escape, reb.Halt(), or HALT") /* If we just used raw ES6 Promises there would be no way for a signal to * cancel them. Whether it was a setTimeout(), a fetch(), or otherwise... @@ -628,7 +644,7 @@ e-cwrap/emit { } else { resolve(val) - RL_JS_CANCELABLES.delete(cancelable) + reb.JS_CANCELABLES.delete(cancelable) } }) promise.catch((error) => { @@ -637,7 +653,7 @@ e-cwrap/emit { } else { reject(error) - RL_JS_CANCELABLES.delete(cancelable) + reb.JS_CANCELABLES.delete(cancelable) } }) @@ -654,20 +670,20 @@ e-cwrap/emit { } wasCanceled = true - reject(RL_JS_ERROR_HALTED) + reject(reb.JS_ERROR_HALTED) /* !!! Supposedly it is safe to iterate and delete at the - * same time. If not, RL_JS_CANCELABLES would need to be + * same time. If not, reb.JS_CANCELABLES would need to be * copied by the iteration to allow this deletion: * * https://stackoverflow.com/q/28306756/ */ - RL_JS_CANCELABLES.delete(cancelable) + reb.JS_CANCELABLES.delete(cancelable) } }) cancelable.cancel = cancel - RL_JS_CANCELABLES.add(cancelable) /* reb.Halt() calls if in set */ + reb.JS_CANCELABLES.add(cancelable) /* reb.Halt() calls if in set */ return cancelable } @@ -684,26 +700,26 @@ e-cwrap/emit { * as the return result for a JS-AWAITER, but the user can explicitly * request augmentation for promises that they manually `await`. */ - RL_JS_CANCELABLES.forEach(promise => { + reb.JS_CANCELABLES.forEach(promise => { promise.cancel() }) } reb.RegisterId_internal = function(id, fn) { - if (id in RL_JS_NATIVES) + if (id in reb.JS_NATIVES) throw Error("Already registered " + id + " in JS_NATIVES table") - RL_JS_NATIVES[id] = fn + reb.JS_NATIVES[id] = fn } reb.UnregisterId_internal = function(id) { - if (!(id in RL_JS_NATIVES)) + if (!(id in reb.JS_NATIVES)) throw Error("Can't delete " + id + " in JS_NATIVES table") - delete RL_JS_NATIVES[id] + delete reb.JS_NATIVES[id] } reb.RunNative_internal = function(id, frame_id) { - if (!(id in RL_JS_NATIVES)) + if (!(id in reb.JS_NATIVES)) throw Error("Can't dispatch " + id + " in JS_NATIVES table") let resolver = function(res) { @@ -738,8 +754,8 @@ e-cwrap/emit { ) } - RL_JS_NATIVES[frame_id] = res /* stow result */ - _RL_rebSignalResolveNative_internal(frame_id) + reb.JS_NATIVES[frame_id] = res /* stow result */ + reb.m._RL_rebSignalResolveNative_internal(frame_id) } let rejecter = function(rej) { @@ -759,11 +775,11 @@ e-cwrap/emit { if (typeof rej == "number") console.log("Suspicious numeric throw() in JS-AWAITER"); - RL_JS_NATIVES[frame_id] = rej /* stow result */ - _RL_rebSignalRejectNative_internal(frame_id) + reb.JS_NATIVES[frame_id] = rej /* stow result */ + reb.m._RL_rebSignalRejectNative_internal(frame_id) } - let native = RL_JS_NATIVES[id] + let native = reb.JS_NATIVES[id] if (native.is_awaiter) { /* * There is no built in capability of ES6 promises to cancel, but @@ -795,7 +811,7 @@ e-cwrap/emit { } reb.GetNativeResult_internal = function(frame_id) { - var result = RL_JS_NATIVES[frame_id] /* resolution or rejection */ + var result = reb.JS_NATIVES[frame_id] /* resolution or rejection */ reb.UnregisterId_internal(frame_id); if (typeof result == "function") /* needed to empower emterpreter */ @@ -809,36 +825,36 @@ e-cwrap/emit { } reb.GetNativeError_internal = function(frame_id) { - var result = RL_JS_NATIVES[frame_id] /* resolution or rejection */ + var result = reb.JS_NATIVES[frame_id] /* resolution or rejection */ reb.UnregisterId_internal(frame_id) - if (result == RL_JS_ERROR_HALTED) + if (result == reb.JS_ERROR_HALTED) return 0 /* in halt state, can't run more code, will throw! */ return reb.Value("make error!", reb.T(String(result))) } reb.ResolvePromise_internal = function(promise_id, rebval) { - if (!(promise_id in RL_JS_NATIVES)) + if (!(promise_id in reb.JS_NATIVES)) throw Error( "Can't find promise_id " + promise_id + " in JS_NATIVES" ) - RL_JS_NATIVES[promise_id][0](rebval) /* [0] is resolve() */ + reb.JS_NATIVES[promise_id][0](rebval) /* [0] is resolve() */ reb.UnregisterId_internal(promise_id); } reb.RejectPromise_internal = function(promise_id, throw_id) { - if (!(throw_id in RL_JS_NATIVES)) /* frame_id of throwing awaiter */ + if (!(throw_id in reb.JS_NATIVES)) /* frame_id of throwing awaiter */ throw Error( "Can't find throw_id " + throw_id + " in JS_NATIVES" ) - let error = RL_JS_NATIVES[throw_id] /* typically JS Error() Object */ + let error = reb.JS_NATIVES[throw_id] /* typically a JS Error() obj */ reb.UnregisterId_internal(throw_id) - if (!(promise_id in RL_JS_NATIVES)) + if (!(promise_id in reb.JS_NATIVES)) throw Error( "Can't find promise_id " + promise_id + " in JS_NATIVES" ) - RL_JS_NATIVES[promise_id][1](error) /* [1] is reject() */ + reb.JS_NATIVES[promise_id][1](error) /* [1] is reject() */ reb.UnregisterId_internal(promise_id) } @@ -865,7 +881,7 @@ e-cwrap/emit { if false [ ; Only used if DEBUG_JAVASCRIPT_SILENT_TRACE (how to know here?) e-cwrap/emit { reb.GetSilentTrace_internal = function() { - return UTF8ToString(_RL_rebGetSilentTrace_internal()) + return UTF8ToString(reb.m._RL_rebGetSilentTrace_internal()) } } ] @@ -1000,7 +1016,7 @@ e-node-preload: (make-emitter ) e-node-preload/emit { - var Module = {}; + var R3Module = {}; console.log("Yes we're getting a chance to preload...") console.log(__dirname + '/libr3.bytecode') var fs = require('fs'); @@ -1008,10 +1024,10 @@ e-node-preload/emit { /* We don't want the direct result, but want the ArrayBuffer * Hence the .buffer (?) */ - Module.emterpreterFile = + R3Module.emterpreterFile = fs.readFileSync(__dirname + '/libr3.bytecode').buffer - console.log(Module.emterpreterFile) + console.log(R3Module.emterpreterFile) } e-node-preload/write-emitted