Skip to content

Commit 49c12a9

Browse files
authored
STANDALONE_WASM option (#9461)
This adds an option to build "standalone" wasm files, that is, files that can be run without JavaScript. They idea is that they can be run in either A wasm server runtime like wasmer or wasmtime, where the supported APIs are wasi. A custom wasm embedding that uses wasm as plugins or dynamic libraries, where the supported APIs may include application-specific imports and exports, some subset of wasi, and other stuff (but probably not JS-specific things). This removes the old EMITTING_JS flag, which was only used to check whether to minify wasm imports and exports. The new STANDALONE_WASM flag also prevents such minification (since we can't assume the JS will be the only thing to run the wasm), but is more general, in that we may still emit JS with that flag, but when we do the JS is only a convenient way to run the code on the Web or in Node.js, as the wasm can also be run standalone. Note that SIDE_MODULE is an interesting case here: with wasm, a side module is just a wasm file (no JS), so we used to set EMITTING_JS to 0. However, we can't just set STANDALONE_WASM in that case, since the side module may expect to be linked with a main module that is not standalone, that is, that depends on JS (i.e. the side module may call things in the main module which are from JS). Aside from side modules, though, if the user says -o X.wasm (emit wasm, no JS file) then we do set STANDALONE_WASM, since that is the likely intention (otherwise, without this flag running such a wasm file would be incredibly difficult since it wasn't designed for it!). The main reason for needing a new flag here is that while we can use many wasi APIs by default, like fd_write, there are some changes that are bad for JS. The main one is Memory handling: it's better to create the Memory early in JS, both to avoid fragmentation issues on 32-bit, and to allow using the Memory by JS while the wasm is still loading (e.g. to set up files). For standalone wasm, though, we can't do that since there is no JS to create it for us, and indeed wasi runtimes expect the memory to be created and not imported. So STANDALONE_WASM basically means, "make an effort to make the wasm as standalone as possible, even that wouldn't be good for JS." Without this flag we do still try to do that, but not when it compromises JS size. This adds libstandalone_wasm which contains things in C to avoid using JS, like routing exit to wasi's proc_exit. There may be better ways to do some of those things, which I intend to look into as followups, but I think this is a good first step to get the flag landed in a working and tested state, in as small a PR as possible. This adds testing in the form of running standalone wasm files in wasmer and wasmtime, on Linux. I have some ideas about how to generalize that in a nicer way, but want to leave that for followups. This updates EMSCRIPTEN_METADATA - we need a new field to know whether a wasm is standalone or not, as the ABI is different.
1 parent 737485e commit 49c12a9

File tree

13 files changed

+232
-26
lines changed

13 files changed

+232
-26
lines changed

.circleci/config.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,16 @@ jobs:
420420
apt-get update -q
421421
apt-get install -q -y python3 cmake
422422
- checkout
423+
- run:
424+
name: get wasmer
425+
command: |
426+
curl https://get.wasmer.io -sSfL | sh
427+
- run:
428+
name: get wasmtime
429+
command: |
430+
wget https://github.com/CraneStation/wasmtime/releases/download/dev/wasmtime-dev-x86_64-linux.tar.xz
431+
tar -xf wasmtime-dev-x86_64-linux.tar.xz
432+
cp wasmtime-dev-x86_64-linux/wasmtime ~/
423433
- build-upstream
424434
test-upstream-wasm0:
425435
executor: bionic

ChangeLog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ Current Trunk
2323
- Module.abort is no longer exported by default. It can be exported in the normal
2424
way using `EXTRA_EXPORTED_RUNTIME_METHODS`, and as with other such changes in
2525
the past, forgetting to export it with show a clear error in `ASSERTIONS` mode.
26+
- Remove `EMITTING_JS` flag, and replace it with `STANDALONE_WASM`. That flag indicates
27+
that we want the wasm to be as standalone as possible. We may still emit JS in
28+
that case, but the JS would just be a convenient way to run the wasm on the Web
29+
or in Node.js.
2630

2731
v.1.38.44: 09/11/2019
2832
---------------------

emcc.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,9 +1697,14 @@ def check_human_readable_list(items):
16971697
if use_source_map(options):
16981698
exit_with_error('wasm2js does not support source maps yet (debug in wasm for now)')
16991699

1700-
# wasm outputs are only possible with a side wasm
17011700
if target.endswith(WASM_ENDINGS):
1702-
shared.Settings.EMITTING_JS = 0
1701+
# if the output is just a wasm file, it will normally be a standalone one,
1702+
# as there is no JS. an exception are side modules, as we can't tell at
1703+
# compile time whether JS will be involved or not - the main module may
1704+
# have JS, and the side module is expected to link against that.
1705+
# we also do not support standalone mode in fastcomp.
1706+
if shared.Settings.WASM_BACKEND and not shared.Settings.SIDE_MODULE:
1707+
shared.Settings.STANDALONE_WASM = 1
17031708
js_target = misc_temp_files.get(suffix='.js').name
17041709

17051710
if shared.Settings.EVAL_CTORS:
@@ -1764,6 +1769,19 @@ def check_human_readable_list(items):
17641769
if shared.Settings.MINIMAL_RUNTIME and not shared.Settings.WASM:
17651770
options.separate_asm = True
17661771

1772+
if shared.Settings.STANDALONE_WASM:
1773+
if not shared.Settings.WASM_BACKEND:
1774+
exit_with_error('STANDALONE_WASM is only available in the upstream wasm backend path')
1775+
if shared.Settings.USE_PTHREADS:
1776+
exit_with_error('STANDALONE_WASM does not support pthreads yet')
1777+
if shared.Settings.SIMD:
1778+
exit_with_error('STANDALONE_WASM does not support simd yet')
1779+
if shared.Settings.ALLOW_MEMORY_GROWTH:
1780+
exit_with_error('STANDALONE_WASM does not support memory growth yet')
1781+
# the wasm must be runnable without the JS, so there cannot be anything that
1782+
# requires JS legalization
1783+
shared.Settings.LEGALIZE_JS_FFI = 0
1784+
17671785
if shared.Settings.WASM_BACKEND:
17681786
if shared.Settings.SIMD:
17691787
newargs.append('-msimd128')
@@ -3012,7 +3030,8 @@ def do_binaryen(target, asm_target, options, memfile, wasm_binary_target,
30123030
wasm_file=wasm_binary_target,
30133031
expensive_optimizations=will_metadce(options),
30143032
minify_whitespace=optimizer.minify_whitespace,
3015-
debug_info=intermediate_debug_info)
3033+
debug_info=intermediate_debug_info,
3034+
emitting_js=not target.endswith(WASM_ENDINGS))
30163035
save_intermediate_with_wasm('postclean', wasm_binary_target)
30173036

30183037
def run_closure_compiler(final):

emscripten.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2321,6 +2321,8 @@ def debug_copy(src, dst):
23212321
cmd.append('--global-base=%s' % shared.Settings.GLOBAL_BASE)
23222322
if shared.Settings.SAFE_STACK:
23232323
cmd.append('--check-stack-overflow')
2324+
if shared.Settings.STANDALONE_WASM:
2325+
cmd.append('--standalone-wasm')
23242326
shared.print_compiler_stage(cmd)
23252327
stdout = shared.check_call(cmd, stdout=subprocess.PIPE).stdout
23262328
if write_source_map:

site/source/docs/tools_reference/emcc.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ Options that are modified or new in *emcc* are listed below:
443443
- <name> **.html** : HTML + separate JavaScript file (**<name>.js**; + separate **<name>.wasm** file if emitting WebAssembly).
444444
- <name> **.bc** : LLVM bitcode.
445445
- <name> **.o** : LLVM bitcode (same as .bc), unless in `WASM_OBJECT_FILES` mode, in which case it will contain a WebAssembly object.
446-
- <name> **.wasm** : WebAssembly without JavaScript support code ("standalone wasm").
446+
- <name> **.wasm** : WebAssembly without JavaScript support code ("standalone wasm"; this enables ``STANDALONE_WASM``).
447447

448448
.. note:: If ``--memory-init-file`` is used, a **.mem** file will be created in addition to the generated **.js** and/or **.html** file.
449449

src/library_wasi.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2019 The Emscripten Authors. All rights reserved.
3+
* Emscripten is available under two separate licenses, the MIT license and the
4+
* University of Illinois/NCSA Open Source License. Both these licenses can be
5+
* found in the LICENSE file.
6+
*/
7+
8+
mergeInto(LibraryManager.library, {
9+
proc_exit__deps: ['exit'],
10+
proc_exit: function(code) {
11+
return _exit(code);
12+
},
13+
});
14+

src/modules.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ var LibraryManager = {
152152
libraries.push('library_glemu.js');
153153
}
154154

155+
if (STANDALONE_WASM) {
156+
libraries.push('library_wasi.js');
157+
}
158+
155159
libraries = libraries.concat(additionalLibraries);
156160

157161
if (BOOTSTRAPPING_STRUCT_INFO) libraries = ['library_bootstrap_structInfo.js', 'library_formatString.js'];

src/preamble.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,12 @@ function createWasm() {
989989
exports = Asyncify.instrumentWasmExports(exports);
990990
#endif
991991
Module['asm'] = exports;
992+
#if STANDALONE_WASM
993+
// In pure wasm mode the memory is created in the wasm (not imported), and
994+
// then exported.
995+
// TODO: do not create a Memory earlier in JS
996+
updateGlobalBufferAndViews(exports['memory'].buffer);
997+
#endif
992998
#if USE_PTHREADS
993999
// Keep a reference to the compiled module so we can post it to the workers.
9941000
wasmModule = module;

src/settings.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,9 +1049,6 @@ var EMTERPRETIFY_SYNCLIST = [];
10491049
// whether js opts will be run, after the main compiler
10501050
var RUNNING_JS_OPTS = 0;
10511051

1052-
// whether we are emitting JS glue code
1053-
var EMITTING_JS = 1;
1054-
10551052
// whether we are in the generate struct_info bootstrap phase
10561053
var BOOTSTRAPPING_STRUCT_INFO = 0;
10571054

@@ -1074,6 +1071,31 @@ var USE_GLFW = 2;
10741071
// still make sense there, see that option for more details.
10751072
var WASM = 1;
10761073

1074+
// STANDALONE_WASM indicates that we want to emit a wasm file that can run without
1075+
// JavaScript. The file will use standard APIs such as wasi as much as possible
1076+
// to achieve that.
1077+
//
1078+
// This option does not guarantee that the wasm can be used by itself - if you
1079+
// use APIs with no non-JS alternative, we will still use those (e.g., OpenGL
1080+
// at the time of writing this). This gives you the option to see which APIs
1081+
// are missing, and if you are compiling for a custom wasi embedding, to add
1082+
// those to your embedding.
1083+
//
1084+
// We may still emit JS with this flag, but the JS should only be a convenient
1085+
// way to run the wasm on the Web or in Node.js, and you can run the wasm by
1086+
// itself without that JS (again, unless you use APIs for which there is no
1087+
// non-JS alternative) in a wasm runtime like wasmer or wasmtime.
1088+
//
1089+
// Note that even without this option we try to use wasi etc. syscalls as much
1090+
// as possible. What this option changes is that we do so even when it means
1091+
// a tradeoff with JS size. For example, when this option is set we do not
1092+
// import the Memory - importing it is useful for JS, so that JS can start to
1093+
// use it before the wasm is even loaded, but in wasi and other wasm-only
1094+
// environments the expectation is to create the memory in the wasm itself.
1095+
// Doing so prevents some possible JS optimizations, so we only do it behind
1096+
// this flag.
1097+
var STANDALONE_WASM = 0;
1098+
10771099
// Whether to use the WebAssembly backend that is in development in LLVM. You
10781100
// should not set this yourself, instead set EMCC_WASM_BACKEND=1 in the
10791101
// environment.
@@ -1638,4 +1660,5 @@ var LEGACY_SETTINGS = [
16381660
['PRECISE_I64_MATH', [1, 2], 'Starting from Emscripten 1.38.26, PRECISE_I64_MATH is always enabled (https://github.com/emscripten-core/emscripten/pull/7935)'],
16391661
['MEMFS_APPEND_TO_TYPED_ARRAYS', [1], 'Starting from Emscripten 1.38.26, MEMFS_APPEND_TO_TYPED_ARRAYS=0 is no longer supported. MEMFS no longer supports using JS arrays for file data (https://github.com/emscripten-core/emscripten/pull/7918)'],
16401662
['ERROR_ON_MISSING_LIBRARIES', [1], 'missing libraries are always an error now'],
1663+
['EMITTING_JS', [1], 'The new STANDALONE_WASM flag replaces this (replace EMITTING_JS=0 with STANDALONE_WASM=1)'],
16411664
];

system/lib/standalone_wasm.c

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2019 The Emscripten Authors. All rights reserved.
3+
* Emscripten is available under two separate licenses, the MIT license and the
4+
* University of Illinois/NCSA Open Source License. Both these licenses can be
5+
* found in the LICENSE file.
6+
*/
7+
8+
#include <emscripten.h>
9+
#include <errno.h>
10+
#include <stdio.h>
11+
#include <string.h>
12+
13+
#include <wasi/wasi.h>
14+
15+
/*
16+
* WASI support code. These are compiled with the program, and call out
17+
* using wasi APIs, which can be provided either by a wasi VM or by our
18+
* emitted JS.
19+
*/
20+
21+
// libc
22+
23+
void exit(int status) {
24+
__wasi_proc_exit(status);
25+
__builtin_unreachable();
26+
}
27+
28+
void abort() {
29+
exit(1);
30+
}
31+
32+
// Musl lock internals. As we assume wasi is single-threaded for now, these
33+
// are no-ops.
34+
35+
void __lock(void* ptr) {}
36+
void __unlock(void* ptr) {}
37+
38+
// Emscripten additions
39+
40+
void *emscripten_memcpy_big(void *restrict dest, const void *restrict src, size_t n) {
41+
// This normally calls out into JS which can do a single fast operation,
42+
// but with wasi we can't do that. As this is called when n >= 8192, we
43+
// can just split into smaller calls.
44+
// TODO optimize, maybe build our memcpy with a wasi variant, maybe have
45+
// a SIMD variant, etc.
46+
const int CHUNK = 8192;
47+
unsigned char* d = (unsigned char*)dest;
48+
unsigned char* s = (unsigned char*)src;
49+
while (n > 0) {
50+
size_t curr_n = n;
51+
if (curr_n > CHUNK) curr_n = CHUNK;
52+
memcpy(d, s, curr_n);
53+
d += CHUNK;
54+
s += CHUNK;
55+
n -= curr_n;
56+
}
57+
return dest;
58+
}
59+
60+
static const int WASM_PAGE_SIZE = 65536;
61+
62+
// Note that this does not support memory growth in JS because we don't update the JS
63+
// heaps. Wasm and wasi lack a good API for that.
64+
int emscripten_resize_heap(size_t size) {
65+
size_t result = __builtin_wasm_memory_grow(0, (size + WASM_PAGE_SIZE - 1) / WASM_PAGE_SIZE);
66+
return result != (size_t)-1;
67+
}

tests/test_other.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
raise Exception('do not run this file directly; do something like: tests/runner.py other')
3131

3232
from tools.shared import Building, PIPE, run_js, run_process, STDOUT, try_delete, listify
33-
from tools.shared import EMCC, EMXX, EMAR, EMRANLIB, PYTHON, FILE_PACKAGER, WINDOWS, MACOS, LLVM_ROOT, EMCONFIG, EM_BUILD_VERBOSE
33+
from tools.shared import EMCC, EMXX, EMAR, EMRANLIB, PYTHON, FILE_PACKAGER, WINDOWS, MACOS, LINUX, LLVM_ROOT, EMCONFIG, EM_BUILD_VERBOSE
3434
from tools.shared import CLANG, CLANG_CC, CLANG_CPP, LLVM_AR
3535
from tools.shared import NODE_JS, SPIDERMONKEY_ENGINE, JS_ENGINES, V8_ENGINE
3636
from tools.shared import WebAssembly
@@ -8287,16 +8287,22 @@ def run(args, expected):
82878287
run(['-s', 'TOTAL_MEMORY=32MB', '-s', 'ALLOW_MEMORY_GROWTH=1', '-s', 'BINARYEN=1'], (2 * 1024 * 1024 * 1024 - 65536) // 16384)
82888288
run(['-s', 'TOTAL_MEMORY=32MB', '-s', 'ALLOW_MEMORY_GROWTH=1', '-s', 'BINARYEN=1', '-s', 'WASM_MEM_MAX=128MB'], 2048 * 4)
82898289

8290-
def test_wasm_targets(self):
8290+
def test_wasm_target_and_STANDALONE_WASM(self):
8291+
# STANDALONE_WASM means we never minify imports and exports.
82918292
for opts, potentially_expect_minified_exports_and_imports in (
8292-
([], False),
8293-
(['-O2'], False),
8294-
(['-O3'], True),
8295-
(['-Os'], True),
8293+
([], False),
8294+
(['-O2'], False),
8295+
(['-O3'], True),
8296+
(['-O3', '-s', 'STANDALONE_WASM'], False),
8297+
(['-Os'], True),
82968298
):
8299+
if 'STANDALONE_WASM' in opts and not self.is_wasm_backend():
8300+
continue
8301+
# targeting .wasm (without .js) means we enable STANDALONE_WASM automatically, and don't minify imports/exports
82978302
for target in ('out.js', 'out.wasm'):
82988303
expect_minified_exports_and_imports = potentially_expect_minified_exports_and_imports and target.endswith('.js')
8299-
print(opts, potentially_expect_minified_exports_and_imports, target, ' => ', expect_minified_exports_and_imports)
8304+
standalone = target.endswith('.wasm') or 'STANDALONE_WASM' in opts
8305+
print(opts, potentially_expect_minified_exports_and_imports, target, ' => ', expect_minified_exports_and_imports, standalone)
83008306

83018307
self.clear()
83028308
run_process([PYTHON, EMCC, path_from_root('tests', 'hello_world.cpp'), '-o', target] + opts)
@@ -8308,13 +8314,33 @@ def test_wasm_targets(self):
83088314
exports = [line.strip().split(' ')[1].replace('"', '') for line in wast_lines if "(export " in line]
83098315
imports = [line.strip().split(' ')[2].replace('"', '') for line in wast_lines if "(import " in line]
83108316
exports_and_imports = exports + imports
8311-
print(exports)
8312-
print(imports)
8317+
print(' exports', exports)
8318+
print(' imports', imports)
83138319
if expect_minified_exports_and_imports:
83148320
assert 'a' in exports_and_imports
83158321
else:
83168322
assert 'a' not in exports_and_imports
8317-
assert 'memory' in exports_and_imports, 'some things are not minified anyhow'
8323+
assert 'memory' in exports_and_imports or 'fd_write' in exports_and_imports, 'some things are not minified anyhow'
8324+
# verify the wasm runs with the JS
8325+
if target.endswith('.js'):
8326+
self.assertContained('hello, world!', run_js('out.js'))
8327+
# verify the wasm runs in a wasm VM, without the JS
8328+
# TODO: more platforms than linux
8329+
if LINUX and standalone and self.is_wasm_backend():
8330+
WASMER = os.path.expanduser(os.path.join('~', '.wasmer', 'bin', 'wasmer'))
8331+
if os.path.isfile(WASMER):
8332+
print(' running in wasmer')
8333+
out = run_process([WASMER, 'run', 'out.wasm'], stdout=PIPE).stdout
8334+
self.assertContained('hello, world!', out)
8335+
else:
8336+
print('[WARNING - no wasmer]')
8337+
WASMTIME = os.path.expanduser(os.path.join('~', 'wasmtime'))
8338+
if os.path.isfile(WASMTIME):
8339+
print(' running in wasmtime')
8340+
out = run_process([WASMTIME, 'out.wasm'], stdout=PIPE).stdout
8341+
self.assertContained('hello, world!', out)
8342+
else:
8343+
print('[WARNING - no wasmtime]')
83188344

83198345
def test_wasm_targets_side_module(self):
83208346
# side modules do allow a wasm target

0 commit comments

Comments
 (0)