From e71992b874539c5ecf1785a2d779fdeafa1a4fde Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Mon, 2 Oct 2023 14:36:35 -0700 Subject: [PATCH] feat: allow for customizing emcc arguments in plugin-wasm (#372) Fixes #371 --- .../docs/interfaces/WasmPluginOptions.md | 26 ++++- packages/plugin-wasm/src/options.ts | 23 ++++- packages/plugin-wasm/src/plugin.ts | 97 ++++++++++++------- 3 files changed, 107 insertions(+), 39 deletions(-) diff --git a/packages/plugin-wasm/docs/interfaces/WasmPluginOptions.md b/packages/plugin-wasm/docs/interfaces/WasmPluginOptions.md index 31e8b93f..e7f9182d 100644 --- a/packages/plugin-wasm/docs/interfaces/WasmPluginOptions.md +++ b/packages/plugin-wasm/docs/interfaces/WasmPluginOptions.md @@ -6,13 +6,37 @@ ### emsdkDir - `Optional` **emsdkDir**: `string` + `Optional` **emsdkDir**: `string` \| () => `string` The path to the Emscripten SDK. If undefined, the plugin will walk up the directory structure to find the nearest `emsdk` directory. ___ +### emccArgs + + `Optional` **emccArgs**: `string`[] \| () => `string`[] + +The array of additional arguments to pass to `emcc`. If undefined, the plugin will +use the following default set of arguments, which are tuned for (and known to work +with) Emscripten versions 2.0.34 and 3.1.46, among others. +``` + -Wall + -Os + -s STRICT=1 + -s MALLOC=emmalloc + -s FILESYSTEM=0 + -s MODULARIZE=1 + -s SINGLE_FILE=1 + -s EXPORT_ES6=1 + -s USE_ES6_IMPORT_META=0 + -s ENVIRONMENT='web,webview,worker' + -s EXPORTED_FUNCTIONS=['_malloc','_getMaxOutputIndices','_getInitialTime','_getFinalTime','_getSaveper','_runModelWithBuffers'] + -s EXPORTED_RUNTIME_METHODS=['cwrap'] +``` + +___ + ### outputJsPath `Optional` **outputJsPath**: `string` diff --git a/packages/plugin-wasm/src/options.ts b/packages/plugin-wasm/src/options.ts index e9388f97..257f7e1d 100644 --- a/packages/plugin-wasm/src/options.ts +++ b/packages/plugin-wasm/src/options.ts @@ -5,7 +5,28 @@ export interface WasmPluginOptions { * The path to the Emscripten SDK. If undefined, the plugin will walk up the directory * structure to find the nearest `emsdk` directory. */ - emsdkDir?: string + emsdkDir?: string | (() => string) + + /** + * The array of additional arguments to pass to `emcc`. If undefined, the plugin will + * use the following default set of arguments, which are tuned for (and known to work + * with) Emscripten versions 2.0.34 and 3.1.46, among others. + * ``` + * -Wall + * -Os + * -s STRICT=1 + * -s MALLOC=emmalloc + * -s FILESYSTEM=0 + * -s MODULARIZE=1 + * -s SINGLE_FILE=1 + * -s EXPORT_ES6=1 + * -s USE_ES6_IMPORT_META=0 + * -s ENVIRONMENT='web,webview,worker' + * -s EXPORTED_FUNCTIONS=['_malloc','_getMaxOutputIndices','_getInitialTime','_getFinalTime','_getSaveper','_runModelWithBuffers'] + * -s EXPORTED_RUNTIME_METHODS=['cwrap'] + * ``` + */ + emccArgs?: string[] | (() => string[]) /** * The path of the resulting JS file (containing the embedded Wasm model). If undefined, diff --git a/packages/plugin-wasm/src/plugin.ts b/packages/plugin-wasm/src/plugin.ts index 0be50aec..d38d2f6f 100644 --- a/packages/plugin-wasm/src/plugin.ts +++ b/packages/plugin-wasm/src/plugin.ts @@ -19,27 +19,6 @@ class WasmPlugin implements Plugin { async postGenerateC(context: BuildContext, cContent: string): Promise { context.log('info', ' Generating WebAssembly module') - // Locate the Emscripten SDK directory - let emsdkDir: string - if (this.options?.emsdkDir) { - // Try the configured directory - emsdkDir = this.options.emsdkDir - if (!existsSync(emsdkDir)) { - throw new Error(`Invalid emsdk directory '${emsdkDir}'`) - } - } else { - // Walk up the directory structure to find the nearest `emsdk` directory - emsdkDir = await findUp('emsdk', { type: 'directory' }) - if (emsdkDir === undefined) { - throw new Error('Could not find emsdk directory') - } - } - - // XXX: On Windows, we need to use Windows-specific commands; need to revisit - const isWin = process.platform === 'win32' - const emccCmd = isWin ? 'emcc.bat' : 'emcc' - const emccCmdPath = joinPath(emsdkDir, 'upstream', 'emscripten', emccCmd) - // If `outputJsPath` is undefined, write `wasm-model.js` to the prep dir const stagedOutputJsFile = 'wasm-model.js' let outputJsPath: string @@ -56,7 +35,7 @@ class WasmPlugin implements Plugin { const stagedOutputJsPath = context.prepareStagedFile('model', stagedOutputJsFile, outputJsDir, outputJsFile) // Generate the Wasm binary (wrapped in a JS file) - await buildWasm(context, emccCmdPath, context.config.prepDir, stagedOutputJsPath) + await buildWasm(context, context.config.prepDir, stagedOutputJsPath, this.options) // context.log('info', ' Done!') @@ -69,10 +48,35 @@ class WasmPlugin implements Plugin { */ async function buildWasm( context: BuildContext, - emccCmdPath: string, prepDir: string, - outputJsPath: string + outputJsPath: string, + options?: WasmPluginOptions ): Promise { + // Locate the Emscripten SDK directory + let emsdkDir: string + if (options?.emsdkDir) { + // Try the configured directory + if (typeof options.emsdkDir === 'function') { + emsdkDir = options.emsdkDir() + } else { + emsdkDir = options.emsdkDir + } + if (!existsSync(emsdkDir)) { + throw new Error(`Invalid emsdk directory '${emsdkDir}'`) + } + } else { + // Walk up the directory structure to find the nearest `emsdk` directory + emsdkDir = await findUp('emsdk', { type: 'directory' }) + if (emsdkDir === undefined) { + throw new Error('Could not find emsdk directory') + } + } + + // XXX: On Windows, we need to use Windows-specific commands; need to revisit + const isWin = process.platform === 'win32' + const emccCmd = isWin ? 'emcc.bat' : 'emcc' + const emccCmdPath = joinPath(emsdkDir, 'upstream', 'emscripten', emccCmd) + // Use Emscripten to compile the C model into a Wasm blob packaged inside // an ES6 module. We use `SINGLE_FILE=1` to include the Wasm directly // inside the JS file as a base64-encoded string. This increases the @@ -98,19 +102,38 @@ async function buildWasm( addArg('-Ibuild') addArg('-o') addArg(outputJsPath) - addArg('-Wall') - addArg('-Os') - addFlag('STRICT=1') - addFlag('MALLOC=emmalloc') - addFlag('FILESYSTEM=0') - addFlag('MODULARIZE=1') - addFlag('SINGLE_FILE=1') - addFlag('EXPORT_ES6=1') - addFlag('USE_ES6_IMPORT_META=0') - addFlag( - `EXPORTED_FUNCTIONS=['_malloc','_getMaxOutputIndices','_getInitialTime','_getFinalTime','_getSaveper','_runModelWithBuffers']` - ) - addFlag(`EXPORTED_RUNTIME_METHODS=['cwrap']`) + if (options?.emccArgs !== undefined) { + let argsArray: string[] + if (typeof options.emccArgs === 'function') { + argsArray = options.emccArgs() + } else { + argsArray = options.emccArgs + } + argsArray.forEach(addArg) + } else { + addArg('-Wall') + addArg('-Os') + addFlag('STRICT=1') + addFlag('MALLOC=emmalloc') + addFlag('FILESYSTEM=0') + addFlag('MODULARIZE=1') + addFlag('SINGLE_FILE=1') + addFlag('EXPORT_ES6=1') + addFlag('USE_ES6_IMPORT_META=0') + // Note: The following argument is used to override the default list of supported environments. + // The problem is that the default list includes "node", but we can't use `USE_ES6_IMPORT_META=0` + // if "node" is included in the list. We want `USE_ES6_IMPORT_META=0` because using 1 causes + // problems with our init code since we also use `SINGLE_FILE=1` (inlined wasm). The bottom + // line is that if we omit "node" from this list, the wasm will still work fine in both browser + // and Node.js contexts (tested in Emscripten 2.0.34 and 3.1.46). + addFlag(`ENVIRONMENT='web,webview,worker'`) + addFlag( + `EXPORTED_FUNCTIONS=['_malloc','_getMaxOutputIndices','_getInitialTime','_getFinalTime','_getSaveper','_runModelWithBuffers']` + ) + addFlag(`EXPORTED_RUNTIME_METHODS=['cwrap']`) + } + + // context.log('verbose', ` emcc args: ${args}`) await context.spawnChild(prepDir, command, args, { // Ignore unhelpful Emscripten SDK cache messages