diff options
Diffstat (limited to 'src/plugins/platforms/wasm/qtloader.js')
-rw-r--r-- | src/plugins/platforms/wasm/qtloader.js | 275 |
1 files changed, 225 insertions, 50 deletions
diff --git a/src/plugins/platforms/wasm/qtloader.js b/src/plugins/platforms/wasm/qtloader.js index 8ec4cb0ca4..dc7f4583da 100644 --- a/src/plugins/platforms/wasm/qtloader.js +++ b/src/plugins/platforms/wasm/qtloader.js @@ -9,27 +9,62 @@ * - environment: { [name:string] : string } * environment variables set on the instance * - onExit: (exitStatus: { text: string, code?: number, crashed: bool }) => void - * called when the application has exited for any reason. exitStatus.code is defined in - * case of a normal application exit. This is not called on exit with return code 0, as - * the program does not shutdown its runtime and technically keeps running async. + * called when the application has exited for any reason. There are two cases: + * aborted: crashed is true, text contains an error message. + * exited: crashed is false, code contians the exit code. + * + * Note that by default Emscripten does not exit when main() returns. This behavior + * is controlled by the EXIT_RUNTIME linker flag; set "-s EXIT_RUNTIME=1" to make + * Emscripten tear down the runtime and exit when main() returns. + * * - containerElements: HTMLDivElement[] * Array of host elements for Qt screens. Each of these elements is mapped to a QScreen on * launch. * - fontDpi: number * Specifies font DPI for the instance * - onLoaded: () => void - * Called when the module has loaded. + * Called when the module has loaded, at the point in time where any loading placeholder + * should be hidden and the application window should be shown. * - entryFunction: (emscriptenConfig: object) => Promise<EmscriptenModule> * Qt always uses emscripten's MODULARIZE option. This is the MODULARIZE entry function. + * - module: Promise<WebAssembly.Module> + * The module to create the instance from (optional). Specifying the module allows optimizing + * use cases where several instances are created from a single WebAssembly source. + * - qtdir: string + * Path to Qt installation. This path will be used for loading Qt shared libraries and plugins. + * The path is set to 'qt' by default, and is relative to the path of the web page's html file. + * This property is not in use when static linking is used, since this build mode includes all + * libraries and plugins in the wasm file. + * - preload: [string]: Array of file paths to json-encoded files which specifying which files to preload. + * The preloaded files will be downloaded at application startup and copied to the in-memory file + * system provided by Emscripten. + * + * Each json file must contain an array of source, destination objects: + * [ + * { + * "source": "path/to/source", + * "destination": "/path/to/destination" + * }, + * ... + * ] + * The source path is relative to the html file path. The destination path must be + * an absolute path. + * + * $QTDIR may be used as a placeholder for the "qtdir" configuration property (see @qtdir), for instance: + * "source": "$QTDIR/plugins/imageformats/libqjpeg.so" + * - localFonts.requestPermission: bool + * Whether Qt should request for local fonts access permission on startup (default false). + * - localFonts.familiesCollection string + * Specifies a collection of local fonts to load. Possible values are: + * "NoFontFamilies" : Don't load any font families + * "DefaultFontFamilies" : A subset of available font families; currently the "web-safe" fonts (default). + * "AllFontFamilies" : All local font families (not reccomended) + * - localFonts.extraFamilies: [string] + * Adds additional font families to be loaded at startup. * - * @return Promise<{ - * instance: EmscriptenModule, - * exitStatus?: { text: string, code?: number, crashed: bool } - * }> + * @return Promise<instance: EmscriptenModule> * The promise is resolved when the module has been instantiated and its main function has been - * called. The returned exitStatus is defined if the application crashed or exited immediately - * after its entry function has been called. Otherwise, config.onExit will get called at a - * later time when (and if) the application exits. + * called. * * @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten for * EmscriptenModule @@ -38,12 +73,15 @@ async function qtLoad(config) { const throwIfEnvUsedButNotExported = (instance, config) => { - const environment = config.environment; + const environment = config.qt.environment; if (!environment || Object.keys(environment).length === 0) return; - const isEnvExported = typeof instance.ENV === 'object'; - if (!isEnvExported) - throw new Error('ENV must be exported if environment variables are passed'); + const descriptor = Object.getOwnPropertyDescriptor(instance, 'ENV'); + const isEnvExported = typeof descriptor.value === 'object'; + if (!isEnvExported) { + throw new Error('ENV must be exported if environment variables are passed, ' + + 'add it to the QT_WASM_EXTRA_EXPORTED_METHODS CMake target property'); + } }; if (typeof config !== 'object') @@ -51,13 +89,22 @@ async function qtLoad(config) if (typeof config.qt !== 'object') throw new Error('config.qt is required, expected an object'); if (typeof config.qt.entryFunction !== 'function') - config.qt.entryFunction = window.createQtAppInstance; + throw new Error('config.qt.entryFunction is required, expected a function'); + + config.qt.qtdir ??= 'qt'; + config.qt.preload ??= []; config.qtContainerElements = config.qt.containerElements; delete config.qt.containerElements; config.qtFontDpi = config.qt.fontDpi; delete config.qt.fontDpi; + // Make Emscripten not call main(); this gives us more control over + // the startup sequence. + const originalNoInitialRun = config.noInitialRun; + const originalArguments = config.arguments; + config.noInitialRun = true; + // Used for rejecting a failed load's promise where emscripten itself does not allow it, // like in instantiateWasm below. This allows us to throw in case of a load error instead of // hanging on a promise to entry function, which emscripten unfortunately does. @@ -65,11 +112,11 @@ async function qtLoad(config) const circuitBreaker = new Promise((_, reject) => { circuitBreakerReject = reject; }); // If module async getter is present, use it so that module reuse is possible. - if (config.qt.modulePromise) { + if (config.qt.module) { config.instantiateWasm = async (imports, successCallback) => { try { - const module = await config.qt.modulePromise; + const module = await config.qt.module; successCallback( await WebAssembly.instantiate(module, imports), module); } catch (e) { @@ -77,58 +124,186 @@ async function qtLoad(config) } } } - - const originalPreRun = config.preRun; - config.preRun = instance => - { - originalPreRun?.(); - + const preloadFetchHelper = async (path) => { + const response = await fetch(path); + if (!response.ok) + throw new Error("Could not fetch preload file: " + path); + return response.json(); + } + const filesToPreload = (await Promise.all(config.qt.preload.map(preloadFetchHelper))).flat(); + const qtPreRun = (instance) => { + // Copy qt.environment to instance.ENV throwIfEnvUsedButNotExported(instance, config); for (const [name, value] of Object.entries(config.qt.environment ?? {})) instance.ENV[name] = value; - }; - config.onRuntimeInitialized = () => config.qt.onLoaded?.(); + // Preload files from qt.preload + const makeDirs = (FS, filePath) => { + const parts = filePath.split("/"); + let path = "/"; + for (let i = 0; i < parts.length - 1; ++i) { + const part = parts[i]; + if (part == "") + continue; + path += part + "/"; + try { + FS.mkdir(path); + } catch (error) { + const EEXIST = 20; + if (error.errno != EEXIST) + throw error; + } + } + } - // This is needed for errors which occur right after resolving the instance promise but - // before exiting the function (i.e. on call to main before stack unwinding). - let loadTimeException = undefined; - // We don't want to issue onExit when aborted - let aborted = false; - const originalQuit = config.quit; - config.quit = (code, exception) => - { - originalQuit?.(code, exception); + const extractFilenameAndDir = (path) => { + const parts = path.split('/'); + const filename = parts.pop(); + const dir = parts.join('/'); + return { + filename: filename, + dir: dir + }; + } + const preloadFile = (file) => { + makeDirs(instance.FS, file.destination); + const source = file.source.replace('$QTDIR', config.qt.qtdir); + const filenameAndDir = extractFilenameAndDir(file.destination); + instance.FS.createPreloadedFile(filenameAndDir.dir, filenameAndDir.filename, source, true, true); + } + const isFsExported = typeof instance.FS === 'object'; + if (!isFsExported) + throw new Error('FS must be exported if preload is used'); + filesToPreload.forEach(preloadFile); + } + + if (!config.preRun) + config.preRun = []; + config.preRun.push(qtPreRun); + + const originalOnRuntimeInitialized = config.onRuntimeInitialized; + config.onRuntimeInitialized = () => { + originalOnRuntimeInitialized?.(); + config.qt.onLoaded?.(); + } - if (exception) - loadTimeException = exception; - if (!aborted && code !== 0) { + const originalLocateFile = config.locateFile; + config.locateFile = filename => { + const originalLocatedFilename = originalLocateFile ? originalLocateFile(filename) : filename; + if (originalLocatedFilename.startsWith('libQt6')) + return `${config.qt.qtdir}/lib/${originalLocatedFilename}`; + return originalLocatedFilename; + } + + let onExitCalled = false; + const originalOnExit = config.onExit; + config.onExit = code => { + originalOnExit?.(); + + if (!onExitCalled) { + onExitCalled = true; config.qt.onExit?.({ - text: exception.message, code, crashed: false }); } - }; + } const originalOnAbort = config.onAbort; config.onAbort = text => { originalOnAbort?.(); - - aborted = true; - config.qt.onExit?.({ - text, - crashed: true - }); + + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + text, + crashed: true + }); + } }; // Call app/emscripten module entry function. It may either come from the emscripten // runtime script or be customized as needed. - const instance = await Promise.race( - [circuitBreaker, config.qt.entryFunction(config)]); - if (loadTimeException && loadTimeException.name !== 'ExitStatus') - throw loadTimeException; + let instance; + try { + instance = await Promise.race( + [circuitBreaker, config.qt.entryFunction(config)]); + + // Call main after creating the instance. We've opted into manually + // calling main() by setting noInitialRun in the config. Thie Works around + // issue where Emscripten suppresses all exceptions thrown during main. + if (!originalNoInitialRun) + instance.callMain(originalArguments); + } catch (e) { + // If this is the exception thrown by app.exec() then that is a normal + // case and we suppress it. + if (e == "unwind") // not much to go on + return; + + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + text: e.message, + crashed: true + }); + } + throw e; + } return instance; } + +// Compatibility API. This API is deprecated, +// and will be removed in a future version of Qt. +function QtLoader(qtConfig) { + + const warning = 'Warning: The QtLoader API is deprecated and will be removed in ' + + 'a future version of Qt. Please port to the new qtLoad() API.'; + console.warn(warning); + + let emscriptenConfig = qtConfig.moduleConfig || {} + qtConfig.moduleConfig = undefined; + const showLoader = qtConfig.showLoader; + qtConfig.showLoader = undefined; + const showError = qtConfig.showError; + qtConfig.showError = undefined; + const showExit = qtConfig.showExit; + qtConfig.showExit = undefined; + const showCanvas = qtConfig.showCanvas; + qtConfig.showCanvas = undefined; + if (qtConfig.canvasElements) { + qtConfig.containerElements = qtConfig.canvasElements + qtConfig.canvasElements = undefined; + } else { + qtConfig.containerElements = qtConfig.containerElements; + qtConfig.containerElements = undefined; + } + emscriptenConfig.qt = qtConfig; + + let qtloader = { + exitCode: undefined, + exitText: "", + loadEmscriptenModule: _name => { + try { + qtLoad(emscriptenConfig); + } catch (e) { + showError?.(e.message); + } + } + } + + qtConfig.onLoaded = () => { + showCanvas?.(); + } + + qtConfig.onExit = exit => { + qtloader.exitCode = exit.code + qtloader.exitText = exit.text; + showExit?.(); + } + + showLoader?.("Loading"); + + return qtloader; +}; |