Skip to content

Commit

Permalink
Add option to use source phase imports for wasm module loading
Browse files Browse the repository at this point in the history
  • Loading branch information
sbc100 committed Dec 16, 2024
1 parent d0b04b9 commit d615b59
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 362 deletions.
548 changes: 254 additions & 294 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"private": true,
"devDependencies": {
"@babel/plugin-proposal-import-wasm-source": "^7.25.9",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.9.1",
"es-check": "^7.2.1",
Expand Down
66 changes: 36 additions & 30 deletions src/preamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,37 @@ function instrumentWasmTableWithAbort() {
}
#endif

#if LOAD_SOURCE_MAP
var wasmSourceMap;
#include "source_map_support.js"

function receiveSourceMapJSON(sourceMap) {
wasmSourceMap = new WasmSourceMap(sourceMap);
{{{ runIfMainThread("removeRunDependency('source-map');") }}}
}
#endif

#if (PTHREADS || WASM_WORKERS) && (LOAD_SOURCE_MAP || USE_OFFSET_CONVERTER)
// When using postMessage to send an object, it is processed by the structured
// clone algorithm. The prototype, and hence methods, on that object is then
// lost. This function adds back the lost prototype. This does not work with
// nested objects that has prototypes, but it suffices for WasmSourceMap and
// WasmOffsetConverter.
function resetPrototype(constructor, attrs) {
var object = Object.create(constructor.prototype);
return Object.assign(object, attrs);
}
#endif

#if USE_OFFSET_CONVERTER
var wasmOffsetConverter;
#include "wasm_offset_converter.js"
#endif

#if SOURCE_PHASE_IMPORTS
// import source wasmModule from './{{{ WASM_BINARY_FILE }}}';
var wasmModule = await WebAssembly.compileStreaming(readAsync('{{{ WASM_BINARY_FILE }}}'));
#else
function findWasmBinary() {
#if EXPORT_ES6 && USE_ES6_IMPORT_META && !SINGLE_FILE && !AUDIO_WORKLET
if (Module['locateFile']) {
Expand Down Expand Up @@ -669,16 +700,6 @@ async function getWasmBinary(binaryFile) {
return getBinarySync(binaryFile);
}

#if LOAD_SOURCE_MAP
var wasmSourceMap;
#include "source_map_support.js"
#endif

#if USE_OFFSET_CONVERTER
var wasmOffsetConverter;
#include "wasm_offset_converter.js"
#endif

#if SPLIT_MODULE
{{{ makeModuleReceiveWithVar('loadSplitModule', undefined, 'instantiateSync', true) }}}
var splitModuleProxyHandler = {
Expand Down Expand Up @@ -707,13 +728,6 @@ var splitModuleProxyHandler = {
};
#endif

#if LOAD_SOURCE_MAP
function receiveSourceMapJSON(sourceMap) {
wasmSourceMap = new WasmSourceMap(sourceMap);
{{{ runIfMainThread("removeRunDependency('source-map');") }}}
}
#endif

#if SPLIT_MODULE || !WASM_ASYNC_COMPILATION
function instantiateSync(file, info) {
var module;
Expand Down Expand Up @@ -761,18 +775,6 @@ function instantiateSync(file, info) {
}
#endif

#if (PTHREADS || WASM_WORKERS) && (LOAD_SOURCE_MAP || USE_OFFSET_CONVERTER)
// When using postMessage to send an object, it is processed by the structured
// clone algorithm. The prototype, and hence methods, on that object is then
// lost. This function adds back the lost prototype. This does not work with
// nested objects that has prototypes, but it suffices for WasmSourceMap and
// WasmOffsetConverter.
function resetPrototype(constructor, attrs) {
var object = Object.create(constructor.prototype);
return Object.assign(object, attrs);
}
#endif

#if WASM_ASYNC_COMPILATION
async function instantiateArrayBuffer(binaryFile, imports) {
try {
Expand Down Expand Up @@ -873,6 +875,7 @@ async function instantiateAsync(binary, binaryFile, imports) {
return instantiateArrayBuffer(binaryFile, imports);
}
#endif // WASM_ASYNC_COMPILATION
#endif // SOURCE_PHASE_IMPORTS

function getWasmImports() {
#if PTHREADS
Expand Down Expand Up @@ -1075,8 +1078,10 @@ function getWasmImports() {
}
#endif

#if SOURCE_PHASE_IMPORTS
return WebAssembly.instantiate(wasmModule, info);
#else
wasmBinaryFile ??= findWasmBinary();

#if WASM_ASYNC_COMPILATION
#if RUNTIME_DEBUG
dbg('asynchronously preparing wasm');
Expand Down Expand Up @@ -1108,6 +1113,7 @@ function getWasmImports() {
return receiveInstance(result[0]);
#endif
#endif // WASM_ASYNC_COMPILATION
#endif // SOURCE_PHASE_IMPORTS
}

#if !WASM_BIGINT
Expand Down
2 changes: 2 additions & 0 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2186,6 +2186,8 @@ var LEGACY_RUNTIME = false;
// [link]
var SIGNATURE_CONVERSIONS = [];

var SOURCE_PHASE_IMPORTS = false;

// For renamed settings the format is:
// [OLD_NAME, NEW_NAME]
// For removed settings (which now effectively have a fixed value and can no
Expand Down
42 changes: 12 additions & 30 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,11 @@ def test_emcc_generate_config(self, compiler):
@parameterized({
'': ([],),
'node': (['-sENVIRONMENT=node'],),
# load a worker before startup to check ES6 modules there as well
'pthreads': (['-pthread', '-sPTHREAD_POOL_SIZE=1'],),
'source_phase_imports': (['-sSOURCE_PHASE_IMPORTS', '-sENVIRONMENT=node'],),
})
def test_emcc_output_mjs(self, args):
def test_esm(self, args):
self.run_process([EMCC, '-o', 'hello_world.mjs',
'--extern-post-js', test_file('modularize_post_js.js'),
test_file('hello_world.c')] + args)
Expand All @@ -373,7 +376,7 @@ def test_emcc_output_mjs(self, args):
'node': (['-sENVIRONMENT=node'],),
})
@node_pthreads
def test_emcc_output_worker_mjs(self, args):
def test_esm_worker(self, args):
os.mkdir('subdir')
self.run_process([EMCC, '-o', 'subdir/hello_world.mjs',
'-sEXIT_RUNTIME', '-sPROXY_TO_PTHREAD', '-pthread', '-O1',
Expand All @@ -386,7 +389,7 @@ def test_emcc_output_worker_mjs(self, args):
self.assertContained('hello, world!', self.run_js('subdir/hello_world.mjs'))

@node_pthreads
def test_emcc_output_worker_mjs_single_file(self):
def test_esm_worker_single_file(self):
self.run_process([EMCC, '-o', 'hello_world.mjs', '-pthread',
'--extern-post-js', test_file('modularize_post_js.js'),
test_file('hello_world.c'), '-sSINGLE_FILE'])
Expand All @@ -395,15 +398,15 @@ def test_emcc_output_worker_mjs_single_file(self):
self.assertContained("new Worker(new URL('hello_world.mjs', import.meta.url), workerOptions)", src)
self.assertContained('hello, world!', self.run_js('hello_world.mjs'))

def test_emcc_output_mjs_closure(self):
def test_esm_closure(self):
self.run_process([EMCC, '-o', 'hello_world.mjs',
'--extern-post-js', test_file('modularize_post_js.js'),
test_file('hello_world.c'), '--closure=1'])
src = read_file('hello_world.mjs')
self.assertContained('new URL("hello_world.wasm", import.meta.url)', src)
self.assertContained('hello, world!', self.run_js('hello_world.mjs'))

def test_emcc_output_mjs_web_no_import_meta(self):
def test_esm_web_no_import_meta(self):
# Ensure we don't emit import.meta.url at all for:
# ENVIRONMENT=web + EXPORT_ES6 + USE_ES6_IMPORT_META=0
self.run_process([EMCC, '-o', 'hello_world.mjs',
Expand All @@ -413,46 +416,25 @@ def test_emcc_output_mjs_web_no_import_meta(self):
self.assertNotContained('import.meta.url', src)
self.assertContained('export default Module;', src)

def test_export_es6_implies_modularize(self):
def test_esm_implies_modularize(self):
self.run_process([EMCC, test_file('hello_world.c'), '-sEXPORT_ES6'])
src = read_file('a.out.js')
self.assertContained('export default Module;', src)

def test_export_es6_requires_modularize(self):
def test_esm_requires_modularize(self):
err = self.expect_fail([EMCC, test_file('hello_world.c'), '-sEXPORT_ES6', '-sMODULARIZE=0'])
self.assertContained('EXPORT_ES6 requires MODULARIZE to be set', err)

def test_export_es6_node_requires_import_meta(self):
def test_esm_node_requires_import_meta(self):
err = self.expect_fail([EMCC, test_file('hello_world.c'),
'-sENVIRONMENT=node', '-sEXPORT_ES6', '-sUSE_ES6_IMPORT_META=0'])
self.assertContained('EXPORT_ES6 and ENVIRONMENT=*node* requires USE_ES6_IMPORT_META to be set', err)

def test_export_es6_allows_export_in_post_js(self):
def test_esm_allows_export_in_post_js(self):
self.run_process([EMCC, test_file('hello_world.c'), '-O3', '-sEXPORT_ES6', '--post-js', test_file('export_module.js')])
src = read_file('a.out.js')
self.assertContained('export{doNothing};', src)

@parameterized({
'': (False,),
'package_json': (True,),
})
@parameterized({
'': ([],),
# load a worker before startup to check ES6 modules there as well
'pthreads': (['-pthread', '-sPTHREAD_POOL_SIZE=1'],),
})
def test_export_es6(self, package_json, args):
self.run_process([EMCC, test_file('hello_world.c'), '-sEXPORT_ES6',
'-o', 'hello.mjs'] + args)
# In ES6 mode we use MODULARIZE, so we must instantiate an instance of the
# module to run it.
create_file('runner.mjs', '''
import Hello from "./hello.mjs";
Hello();
''')

self.assertContained('hello, world!', self.run_js('runner.mjs'))

@parameterized({
'': ([],),
'pthreads': (['-pthread'],),
Expand Down
10 changes: 8 additions & 2 deletions tools/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,8 +516,13 @@ def version_split(v):
def transpile(filename):
config = {
'sourceType': 'script',
'targets': {}
'targets': {},
#'plugins': [
# '@babel/plugin-proposal-import-wasm-source'
#],
}
if settings.EXPORT_ES6:
config['sourceType'] = 'module'
if settings.MIN_CHROME_VERSION != UNSUPPORTED:
config['targets']['chrome'] = str(settings.MIN_CHROME_VERSION)
if settings.MIN_FIREFOX_VERSION != UNSUPPORTED:
Expand All @@ -533,7 +538,8 @@ def transpile(filename):
config_file = shared.get_temp_files().get('babel_config.json').name
logger.debug(config_json)
utils.write_file(config_file, config_json)
cmd = shared.get_npm_cmd('babel') + [filename, '-o', outfile, '--presets', '@babel/preset-env', '--config-file', config_file]
cmd = shared.get_npm_cmd('babel') + [filename, '-o', outfile,
'--plugins=@babel/plugin-proposal-import-wasm-source', '--presets', '@babel/preset-env', '--config-file', config_file]
check_call(cmd, cwd=path_from_root())
return outfile

Expand Down
9 changes: 3 additions & 6 deletions tools/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -1178,7 +1178,8 @@ def phase_linker_setup(options, state, newargs): # noqa: C901, PLR0912, PLR0915
settings.TRANSPILE = (settings.MIN_FIREFOX_VERSION < 79 or
settings.MIN_CHROME_VERSION < 85 or
settings.MIN_SAFARI_VERSION < 140000 or
settings.MIN_NODE_VERSION < 160000)
settings.MIN_NODE_VERSION < 160000 or
settings.SOURCE_PHASE_IMPORTS)

# https://caniuse.com/class: FF:45 CHROME:49 SAFARI:9
supports_es6_classes = (settings.MIN_FIREFOX_VERSION >= 45 and
Expand Down Expand Up @@ -2396,15 +2397,11 @@ def modularize():

# Multi-environment ES6 builds require an async function
async_emit = ''
if settings.EXPORT_ES6 and \
if settings.EXPORT_ES6 and (settings.SOURCE_PHASE_IMPORTS or \
settings.ENVIRONMENT_MAY_BE_NODE and \
settings.ENVIRONMENT_MAY_BE_WEB:
async_emit = 'async '

# TODO: Remove when https://bugs.webkit.org/show_bug.cgi?id=223533 is resolved.
if async_emit != '' and settings.EXPORT_NAME == 'config':
diagnostics.warning('emcc', 'EXPORT_NAME should not be named "config" when targeting Safari')

if settings.MODULARIZE == 'instance':
src = '''
export default async function init(moduleArg = {}) {
Expand Down

0 comments on commit d615b59

Please sign in to comment.