From 7ee4468a18f18dd2269fc422220fea4dc77017c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20S=C3=B8rvig?= Date: Fri, 21 May 2021 11:14:34 +0200 Subject: [PATCH] wasm: enable MODULARIZE option and set EXPORT_NAME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make Emscripten generate a global constructor function ("createQtAppInstance()") instead of a global javascript module object. This enables more fine-grained control over module instantiation; previously the module object would be created when the runtime javascript was evaluated, and the number of emscripten module/instances was limited to one per page. Set EXPORT_NAME to “createQtAppInstance” which avoids collisions with other non-Qt Emscripten modules on the same page. A further improvement would be to include the app name in EXPORT_NAME, but this is not done at this time. Update the code in qtloader.js to call the constructor function instead of working on a global module object. The qtloader.js API is functional before the wasm and Emscripten modules have been instantiated; store properties and forward to the Emscripten module when it's created. Change-Id: I12c49a5b9a4a932bbc46fcc5e5ecc453fd0fe7f0 Reviewed-by: Lorn Potter --- cmake/QtWasmHelpers.cmake | 10 ++++ mkspecs/wasm-emscripten/qmake.conf | 4 +- src/plugins/platforms/wasm/qtloader.js | 63 ++++++++++++++++---------- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/cmake/QtWasmHelpers.cmake b/cmake/QtWasmHelpers.cmake index bc5e455fcd1..ecea701042a 100644 --- a/cmake/QtWasmHelpers.cmake +++ b/cmake/QtWasmHelpers.cmake @@ -7,6 +7,16 @@ function (qt_internal_setup_wasm_target_properties wasmTarget) "SHELL:-s USE_WEBGL2=1" "--bind" "SHELL:-s FETCH=1") + + # Enable MODULARIZE and set EXPORT_NAME, which makes it possible to + # create application instances using a global constructor function, + # e.g. let app_instance = await createQtAppInstance(). + # (as opposed to MODULARIZE=0, where Emscripten creates a global app + # instance object at Javascript eval time) + target_link_options("${wasmTarget}" INTERFACE + "SHELL:-s MODULARIZE=1" + "SHELL:-s EXPORT_NAME=createQtAppInstance") + target_compile_options("${wasmTarget}" INTERFACE --bind) # Hardcode wasm memory size. Emscripten does not currently support memory growth diff --git a/mkspecs/wasm-emscripten/qmake.conf b/mkspecs/wasm-emscripten/qmake.conf index ad66ff39820..cc3bc8064e2 100644 --- a/mkspecs/wasm-emscripten/qmake.conf +++ b/mkspecs/wasm-emscripten/qmake.conf @@ -41,7 +41,9 @@ EMCC_COMMON_LFLAGS += \ -s ERROR_ON_UNDEFINED_SYMBOLS=1 \ -s EXTRA_EXPORTED_RUNTIME_METHODS=[\"UTF16ToString\",\"stringToUTF16\"] \ --bind \ - -s FETCH=1 + -s FETCH=1 \ + -s MODULARIZE=1 \ + -s EXPORT_NAME=createQtAppInstance # The -s arguments can also be used with release builds, # but are here in debug for clarity. diff --git a/src/plugins/platforms/wasm/qtloader.js b/src/plugins/platforms/wasm/qtloader.js index b159cd63ab2..14a22fc0f1c 100644 --- a/src/plugins/platforms/wasm/qtloader.js +++ b/src/plugins/platforms/wasm/qtloader.js @@ -128,10 +128,18 @@ // Sets the logical font dpi for the application. -var Module = {} - function QtLoader(config) { + // The Emscripten module and module configuration object. The module + // object is created in completeLoadEmscriptenModule(). + self.module = undefined; + self.moduleConfig = {}; + + // Qt properties. These are propagated to the Emscripten module after + // it has been created. + self.qtCanvasElements = undefined; + self.qtFontDpi = 96; + function webAssemblySupported() { return typeof WebAssembly !== "undefined" } @@ -341,7 +349,7 @@ function QtLoader(config) // The wasm binary has been compiled into a module during resource download, // and is ready to be instantiated. Define the instantiateWasm callback which // emscripten will call to create the instance. - Module.instantiateWasm = function(imports, successCallback) { + self.moduleConfig.instantiateWasm = function(imports, successCallback) { WebAssembly.instantiate(wasmModule, imports).then(function(instance) { successCallback(instance, wasmModule); }, function(error) { @@ -351,27 +359,27 @@ function QtLoader(config) return {}; }; - Module.locateFile = Module.locateFile || function(filename) { + self.moduleConfig.locateFile = self.moduleConfig.locateFile || function(filename) { return config.path + filename; }; // Attach status callbacks - Module.setStatus = Module.setStatus || function(text) { + self.moduleConfig.setStatus = self.moduleConfig.setStatus || function(text) { // Currently the only usable status update from this function // is "Running..." if (text.startsWith("Running")) setStatus("Running"); }; - Module.monitorRunDependencies = Module.monitorRunDependencies || function(left) { + self.moduleConfig.monitorRunDependencies = self.moduleConfig.monitorRunDependencies || function(left) { // console.log("monitorRunDependencies " + left) }; // Attach standard out/err callbacks. - Module.print = Module.print || function(text) { + self.moduleConfig.print = self.moduleConfig.print || function(text) { if (config.stdoutEnabled) console.log(text) }; - Module.printErr = Module.printErr || function(text) { + self.moduleConfig.printErr = self.moduleConfig.printErr || function(text) { // Filter out OpenGL getProcAddress warnings. Qt tries to resolve // all possible function/extension names at startup which causes // emscripten to spam the console log with warnings. @@ -387,12 +395,12 @@ function QtLoader(config) // Emscripten will typically call printErr with the error text // as well. Note that emscripten may also throw exceptions from // async callbacks. These should be handled in window.onerror by user code. - Module.onAbort = Module.onAbort || function(text) { + self.moduleConfig.onAbort = self.moduleConfig.onAbort || function(text) { publicAPI.crashed = true; publicAPI.exitText = text; setStatus("Exited"); }; - Module.quit = Module.quit || function(code, exception) { + self.moduleConfig.quit = self.moduleConfig.quit || function(code, exception) { if (exception.name == "ExitStatus") { // Clean exit with code publicAPI.exitText = undefined @@ -404,17 +412,20 @@ function QtLoader(config) setStatus("Exited"); }; - // Set environment variables - Module.preRun = Module.preRun || [] - Module.preRun.push(function() { + self.moduleConfig.preRun = self.moduleConfig.preRun || [] + self.moduleConfig.preRun.push(function(module) { + // Set environment variables for (var [key, value] of Object.entries(config.environment)) { ENV[key.toUpperCase()] = value; } + // Propagate Qt module properties + module.qtCanvasElements = self.qtCanvasElements; + module.qtFontDpi = self.qtFontDpi; }); - Module.mainScriptUrlOrBlob = new Blob([emscriptenModuleSource], {type: 'text/javascript'}); + self.moduleConfig.mainScriptUrlOrBlob = new Blob([emscriptenModuleSource], {type: 'text/javascript'}); - Module.qtCanvasElements = config.canvasElements; + self.qtCanvasElements = config.canvasElements; config.restart = function() { @@ -438,9 +449,13 @@ function QtLoader(config) publicAPI.exitText = undefined; publicAPI.crashed = false; - // Finally evaluate the emscripten application script, which will - // reference the global Module object created above. - self.eval(emscriptenModuleSource); // ES5 indirect global scope eval + // Load the Emscripten application module. This is done by eval()'ing the + // javascript runtime generated by Emscripten, and then calling + // createQtAppInstance(), which was added to the global scope. + eval(emscriptenModuleSource); + createQtAppInstance(self.moduleConfig).then(function(module) { + self.module = module; + }); } function setErrorContent() { @@ -544,31 +559,31 @@ function QtLoader(config) function addCanvasElement(element) { if (publicAPI.status == "Running") - Module.qtAddCanvasElement(element); + self.module.qtAddCanvasElement(element); else console.log("Error: addCanvasElement can only be called in the Running state"); } function removeCanvasElement(element) { if (publicAPI.status == "Running") - Module.qtRemoveCanvasElement(element); + self.module.qtRemoveCanvasElement(element); else console.log("Error: removeCanvasElement can only be called in the Running state"); } function resizeCanvasElement(element) { if (publicAPI.status == "Running") - Module.qtResizeCanvasElement(element); + self.module.qtResizeCanvasElement(element); } function setFontDpi(dpi) { - Module.qtFontDpi = dpi; + self.qtFontDpi = dpi; if (publicAPI.status == "Running") - Module.qtSetFontDpi(dpi); + self.qtSetFontDpi(dpi); } function fontDpi() { - return Module.qtFontDpi; + return self.qtFontDpi; } setStatus("Created");