Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ See docs/process.md for more on how version tagging works.

4.0.17 (in development)
-----------------------
- Mutable Wasm globals can now be exported from native code. Currently these
cannot be declared in C/C++ but can be defined and exported in assembly code.
This currently only works for mutable globals since immutables are already
(and continue to be) exported as plain JS numbers. (#25530)
- Minimum Firefox version was bumped up to Firefox 68 ESR, since older Firefox
versions are not able to run the parallel browser harness: (#25493)
- Firefox: v65 -> v68
Expand Down
61 changes: 42 additions & 19 deletions src/lib/libdylink.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,24 +230,33 @@ var LibraryDylink = {
}
#endif

GOT[symName] ||= new WebAssembly.Global({'value': '{{{ POINTER_WASM_TYPE }}}', 'mutable': true});
if (replace || GOT[symName].value == 0) {

var existingEntry = GOT[symName] && GOT[symName].value != 0;
if (replace || !existingEntry) {
#if DYLINK_DEBUG == 2
dbg(`updateGOT: before: ${symName} : ${GOT[symName].value}`);
#endif
var newValue;
if (typeof value == 'function') {
GOT[symName].value = {{{ to64('addFunction(value)') }}};
newValue = {{{ to64('addFunction(value)') }}};
#if DYLINK_DEBUG == 2
dbg(`updateGOT: FUNC: ${symName} : ${GOT[symName].value}`);
#endif
} else if (typeof value == {{{ POINTER_JS_TYPE }}}) {
GOT[symName].value = value;
newValue = value;
} else {
err(`unhandled export type for '${symName}': ${typeof value}`);
// The GOT can only contain addresses (i.e data addresses or function
// addresses so we currently ignore other types export here.
#if DYLINK_DEBUG
dbg(`updateGOT: ignoring ${symName} due to its type: ${typeof value}`);
#endif
continue;
}
#if DYLINK_DEBUG == 2
dbg(`updateGOT: after: ${symName} : ${GOT[symName].value} (${value})`);
dbg(`updateGOT: after: ${symName} : ${newValue} (${value})`);
#endif
GOT[symName] ||= new WebAssembly.Global({'value': '{{{ POINTER_WASM_TYPE }}}', 'mutable': true});
GOT[symName].value = newValue;
}
#if DYLINK_DEBUG
else if (GOT[symName].value != value) {
Expand All @@ -260,29 +269,43 @@ var LibraryDylink = {
#endif
},

$isImmutableGlobal__internal: true,
$isImmutableGlobal: (val) => {
if (val instanceof WebAssembly.Global) {
try {
val.value = val.value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this particular method of checking for the presence of the value property? Wouldn't it be easier to do hasOwnProperty or something?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is only checking for mutability, which is not something that the JS spec exposes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right. Since we expose immutable globals as numbers, should we have some kind of assertion that there aren't any immutable globals exported as wasm globals?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right. Since we expose immutable globals as numbers, should we have some kind of assertion that there aren't any immutable globals exported as wasm globals?

But we do have such exports. All data symbols (addresses) are exported in this way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I guess you are saying, do we have tests that confirm if data exports are working? I assume we have many such tests.. basically any test that does EMSCRIPTEN_KEEPALIVE on a data symbol, or included a data symbols in EXPORTED_FUNCTIONS.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More like, we expect everything exported as a WebAssembly.Global to be mutable (because up until now, all data exports are immutable and exported as numbers and we never exported anything as a mutable global other than the stack pointer). So if we ever see an immutable WebAssembly.Global it would be an error, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but this function (isImmutableGlobal) is used to disciminate bewtween those two types of exports.

Type 1: Mutable globals that we are exporting with the intent that they are exported as first class globals (e.g. __stack_pointer.
Type 2: Immutable globals that are exported as addresses, and are visible on the outside a simply numbers. These one also get relocated (+= __memory_base) so they can be used to index into the HEAP directly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. at this point in the code we expect to see both types.

} catch {
return true;
}
}
return false;
},

// Applies relocations to exported things.
$relocateExports__internal: true,
$relocateExports__deps: ['$updateGOT'],
$relocateExports__deps: ['$updateGOT', '$isImmutableGlobal'],
$relocateExports__docs: '/** @param {boolean=} replace */',
$relocateExports: (exports, memoryBase, replace) => {
var relocated = {};

for (var e in exports) {
var value = exports[e];
function relocateExport(name, value) {
#if SPLIT_MODULE
// Do not modify exports synthesized by wasm-split
if (e.startsWith('%')) {
relocated[e] = value
continue;
if (name.startsWith('%')) {
return value;
}
#endif
// Detect wasm global exports. These represent data addresses
// Detect immuable wasm global exports. These represent data addresses
// which are relative to `memoryBase`
if (value instanceof WebAssembly.Global) {
value = value.value;
value += {{{ to64('memoryBase') }}};
if (isImmutableGlobal(value)) {
return value.value + {{{ to64('memoryBase') }}};
}
relocated[e] = value;

// Return unmodified value (no relocation required).
return value;
}

var relocated = {};
for (var e in exports) {
relocated[e] = relocateExport(e, exports[e])
}
updateGOT(relocated, replace);
return relocated;
Expand Down
4 changes: 2 additions & 2 deletions src/settings_internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
// underscore.
var WASM_EXPORTS = [];

// Similar to above but only includes the global/data symbols.
var WASM_GLOBAL_EXPORTS = [];
// Similar to above but only includes the data symbols (address exports).
var DATA_EXPORTS = [];

// An array of all symbols exported from all the side modules specified on the
// command line.
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_hello_dylink.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"a.out.js": 26912,
"a.out.js.gz": 11470,
"a.out.js": 26919,
"a.out.js.gz": 11469,
"a.out.nodebug.wasm": 18567,
"a.out.nodebug.wasm.gz": 9199,
"total": 45479,
"total_gz": 20669,
"total": 45486,
"total_gz": 20668,
"sent": [
"__heap_base",
"__indirect_function_table",
Expand Down
4 changes: 2 additions & 2 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"a.out.js": 245822,
"a.out.js": 245829,
"a.out.nodebug.wasm": 597746,
"total": 843568,
"total": 843575,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down
43 changes: 43 additions & 0 deletions test/core/test_wasm_global.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include <stdio.h>
#include <emscripten/em_asm.h>
#include <emscripten.h>

__asm__(
".section .data.my_global,\"\",@\n"
".globl my_global\n"
".globaltype my_global, i32\n"
"my_global:\n"
);

int get_global() {
int val;
// Without volatile here this test fails in O1 and above.
__asm__ volatile ("global.get my_global\n"
"local.set %0\n" : "=r" (val));
return val;
}

void set_global(int val) {
__asm__("local.get %0\n"
"global.set my_global\n" : : "r" (val));
}

int main() {
printf("in main: %d\n", get_global());
set_global(42);
printf("new value: %d\n", get_global());
EM_ASM({
// With the ESM integration, the Wasm global be exported as a regular
// number. Otherwise it will be a WebAssembly.Global object.
#ifdef ESM_INTEGRATION
assert(typeof _my_global == 'number', typeof _my_global);
out('from js:', _my_global);
_my_global += 1
#else
assert(typeof _my_global == 'object', typeof _my_global);
out('from js:', _my_global.value);
_my_global.value += 1
#endif
});
printf("done: %d\n", get_global());
}
4 changes: 4 additions & 0 deletions test/core/test_wasm_global.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
in main: 0
new value: 42
from js: 42
done: 43
13 changes: 13 additions & 0 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9677,6 +9677,19 @@ def test_externref_emjs(self, dynlink):
self.set_setting('MAIN_MODULE', 2)
self.do_core_test('test_externref_emjs.c')

@parameterized({
'': [False],
'dylink': [True],
})
@no_esm_integration('https://github.com/emscripten-core/emscripten/issues/25543')
def test_wasm_global(self, dynlink):
if dynlink:
self.check_dylink()
self.set_setting('MAIN_MODULE', 2)
if self.get_setting('WASM_ESM_INTEGRATION'):
self.cflags.append('-DESM_INTEGRATION')
self.do_core_test('test_wasm_global.c', cflags=['-sEXPORTED_FUNCTIONS=_main,_my_global'])

def test_syscall_intercept(self):
self.do_core_test('test_syscall_intercept.c')

Expand Down
2 changes: 1 addition & 1 deletion tools/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,7 @@ def metadce(js_file, wasm_file, debug_info, last):
exports = settings.WASM_EXPORTS
else:
# Ignore exported wasm globals. Those get inlined directly into the JS code.
exports = sorted(set(settings.WASM_EXPORTS) - set(settings.WASM_GLOBAL_EXPORTS))
exports = sorted(set(settings.WASM_EXPORTS) - set(settings.DATA_EXPORTS))

extra_info = '{ "exports": [' + ','.join(f'["{asmjs_mangle(x)}", "{x}"]' for x in exports) + ']}'

Expand Down
16 changes: 8 additions & 8 deletions tools/emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def update_settings_glue(wasm_file, metadata, base_metadata):
settings.WASM_EXPORTS = base_metadata.all_exports
else:
settings.WASM_EXPORTS = metadata.all_exports
settings.WASM_GLOBAL_EXPORTS = list(metadata.global_exports.keys())
settings.DATA_EXPORTS = list(metadata.data_exports.keys())
settings.HAVE_EM_ASM = bool(settings.MAIN_MODULE or len(metadata.em_asm_consts) != 0)

# start with the MVP features, and add any detected features.
Expand Down Expand Up @@ -269,9 +269,9 @@ def trim_asm_const_body(body):
return body


def create_global_exports(global_exports):
def create_data_exports(data_exports):
lines = []
for k, v in global_exports.items():
for k, v in data_exports.items():
if shared.is_internal_global(k):
continue

Expand Down Expand Up @@ -408,11 +408,11 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True, base_metadat
other_exports = base_metadata.other_exports
# We want the real values from the final metadata but we only want to
# include names from the base_metadata. See phase_link() in link.py.
global_exports = {k: v for k, v in metadata.global_exports.items() if k in base_metadata.global_exports}
data_exports = {k: v for k, v in metadata.data_exports.items() if k in base_metadata.data_exports}
else:
function_exports = metadata.function_exports
other_exports = metadata.other_exports
global_exports = metadata.global_exports
data_exports = metadata.data_exports

if settings.ASYNCIFY == 1:
function_exports['asyncify_start_unwind'] = webassembly.FuncType([webassembly.Type.I32], [])
Expand All @@ -421,7 +421,7 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True, base_metadat
function_exports['asyncify_stop_rewind'] = webassembly.FuncType([], [])

parts = [pre]
parts += create_module(metadata, function_exports, global_exports, other_exports,
parts += create_module(metadata, function_exports, data_exports, other_exports,
forwarded_json['librarySymbols'], forwarded_json['nativeAliases'])
parts.append(post)
settings.ALIASES = list(forwarded_json['nativeAliases'].keys())
Expand Down Expand Up @@ -1008,10 +1008,10 @@ def create_receiving(function_exports, other_exports, library_symbols, aliases):
return '\n'.join(receiving)


def create_module(metadata, function_exports, global_exports, other_exports, library_symbols, aliases):
def create_module(metadata, function_exports, data_exports, other_exports, library_symbols, aliases):
module = []
module.append(create_receiving(function_exports, other_exports, library_symbols, aliases))
module.append(create_global_exports(global_exports))
module.append(create_data_exports(data_exports))

sending = create_sending(metadata, library_symbols)
if settings.WASM_ESM_INTEGRATION:
Expand Down
25 changes: 17 additions & 8 deletions tools/extract_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,16 @@ def get_main_reads_params(module, export_map):
return True


def get_global_exports(module, exports):
global_exports = {}
def get_data_exports(module, exports):
data_exports = {}
for export in exports:
if export.kind == webassembly.ExternType.GLOBAL:
g = module.get_global(export.index)
global_exports[export.name] = str(get_global_value(g))
return global_exports
# Data symbols (addresses) are exported as immutable Wasm globals.
# mutable globals are handled via get_other_exports
if not g.mutable:
data_exports[export.name] = str(get_global_value(g))
return data_exports


def get_function_exports(module):
Expand All @@ -260,8 +263,14 @@ def get_function_exports(module):
def get_other_exports(module):
rtn = []
for e in module.get_exports():
if e.kind not in (webassembly.ExternType.FUNC, webassembly.ExternType.GLOBAL):
rtn.append(e.name)
if e.kind == webassembly.ExternType.FUNC:
continue
if e.kind == webassembly.ExternType.GLOBAL:
g = module.get_global(e.index)
# Immutable globals are handled specially. See get_data_exports
if not g.mutable:
continue
rtn.append(e.name)
return rtn


Expand Down Expand Up @@ -311,7 +320,7 @@ class Metadata:
features: List[str]
invoke_funcs: List[str]
main_reads_params: bool
global_exports: Dict[str, str]
data_exports: Dict[str, str]
function_exports: Dict[str, webassembly.FuncType]
other_exports: List[str]
all_exports: List[str]
Expand Down Expand Up @@ -348,7 +357,7 @@ def extract_metadata(filename):
metadata.em_js_funcs = em_js_funcs
metadata.features = features
metadata.main_reads_params = get_main_reads_params(module, export_map)
metadata.global_exports = get_global_exports(module, exports)
metadata.data_exports = get_data_exports(module, exports)

read_module_imports(module, metadata)

Expand Down