Skip to content

Commit 7ff719c

Browse files
authored
Restore the dynCall() and dynCall_sig() API into the build (#13296)
* Restore the dynCall() and dynCall_sig() API into the build when -s USE_LEGACY_DYNCALLS=1 is passed. Add support for dynCall() in MINIMAL_RUNTIME builds (that was not implemented before). * Add ChangeLog entry. * Rename USE_LEGACY_DYNCALLS to DYNCALLS. * Inline getDynCaller. * Address review. * Merge new dynCall test with old. * Reformat parseTools.js
1 parent 110a856 commit 7ff719c

13 files changed

+113
-18
lines changed

ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ Current Trunk
2626
- Emscripten now builds a complete sysroot inside the EM_CACHE directory.
2727
This includes the system headers which get copied into place there rather
2828
than adding a sequence of extra include directories.
29+
- Add back support for calling the legacy dynCall_sig() API to invoke function
30+
pointers to wasm functions from JavaScript. Pass -s DYNCALLS=1
31+
to include that functionality in the build. This fixes a regression that
32+
started in Aug 31st 2020 (Emscripten 2.0.2) in #12059. Also implement
33+
support for dynCall() in MINIMAL_RUNTIME builds. (#13296)
2934
- `SDL2_ttf` now uses upstream compiled with `TTF_USE_HARFBUZ` flag.
3035
- The system for linking native libraries on demand (based on the symbols
3136
present in input object files) has been removed. Libraries such as libgl,
@@ -218,6 +223,7 @@ Current Trunk
218223

219224
{{{ makeDynCall('sig', 'ptr') }}} (arg1, arg2);
220225

226+
See PR #12059 for details.
221227
- The native optimizer and the corresponding config setting
222228
(`EMSCRIPTEN_NATIVE_OPTIMIZER`) have been removed (it was only relevant to
223229
asmjs/fastcomp backend).

emcc.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,10 @@ def default_setting(name, new_default):
12831283
if shared.Settings.CLOSURE_WARNINGS not in ['quiet', 'warn', 'error']:
12841284
exit_with_error('Invalid option -s CLOSURE_WARNINGS=%s specified! Allowed values are "quiet", "warn" or "error".' % shared.Settings.CLOSURE_WARNINGS)
12851285

1286+
# Include dynCall() function by default in DYNCALLS builds in classic runtime; in MINIMAL_RUNTIME, must add this explicitly.
1287+
if shared.Settings.DYNCALLS and not shared.Settings.MINIMAL_RUNTIME:
1288+
shared.Settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$dynCall']
1289+
12861290
if shared.Settings.MAIN_MODULE:
12871291
assert not shared.Settings.SIDE_MODULE
12881292
if shared.Settings.MAIN_MODULE == 1:
@@ -1331,7 +1335,7 @@ def default_setting(name, new_default):
13311335
if shared.Settings.ASYNCIFY:
13321336
# See: https://github.com/emscripten-core/emscripten/issues/12065
13331337
# See: https://github.com/emscripten-core/emscripten/issues/12066
1334-
shared.Settings.USE_LEGACY_DYNCALLS = 1
1338+
shared.Settings.DYNCALLS = 1
13351339
shared.Settings.EXPORTED_FUNCTIONS += ['_emscripten_stack_get_base',
13361340
'_emscripten_stack_get_end',
13371341
'_emscripten_stack_set_limits']

emscripten.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ def compute_minimal_runtime_initializer_and_exports(post, exports, receiving):
4444
exports_that_are_not_initializers = [x for x in exports if x not in WASM_INIT_FUNC]
4545
# In Wasm backend the exports are still unmangled at this point, so mangle the names here
4646
exports_that_are_not_initializers = [asmjs_mangle(x) for x in exports_that_are_not_initializers]
47+
48+
# Decide whether we should generate the global dynCalls dictionary for the dynCall() function?
49+
if shared.Settings.DYNCALLS and '$dynCall' in shared.Settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE and len([x for x in exports_that_are_not_initializers if x.startswith('dynCall_')]) > 0:
50+
exports_that_are_not_initializers += ['dynCalls = {}']
51+
4752
post = post.replace('/*** ASM_MODULE_EXPORTS_DECLARES ***/', 'var ' + ',\n '.join(exports_that_are_not_initializers) + ';')
4853

4954
# Generate assignments from all asm.js/wasm exports out to the JS variables above: e.g. a = asm['a']; b = asm['b'];
@@ -401,7 +406,7 @@ def finalize_wasm(infile, outfile, memfile, DEBUG):
401406
args.append('-g')
402407
if shared.Settings.WASM_BIGINT:
403408
args.append('--bigint')
404-
if shared.Settings.USE_LEGACY_DYNCALLS:
409+
if shared.Settings.DYNCALLS:
405410
# we need to add all dyncalls to the wasm
406411
modify_wasm = True
407412
else:
@@ -710,7 +715,11 @@ def create_receiving(exports):
710715
# WebAssembly.instantiate(Module["wasm"], imports).then((function(output) {
711716
# var asm = output.instance.exports;
712717
# _main = asm["_main"];
713-
receiving += [asmjs_mangle(s) + ' = asm["' + s + '"];' for s in exports_that_are_not_initializers]
718+
generate_dyncall_assignment = shared.Settings.DYNCALLS and '$dynCall' in shared.Settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE
719+
for s in exports_that_are_not_initializers:
720+
mangled = asmjs_mangle(s)
721+
dynCallAssignment = ('dynCalls["' + s.replace('dynCall_', '') + '"] = ') if generate_dyncall_assignment and mangled.startswith('dynCall_') else ''
722+
receiving += [dynCallAssignment + mangled + ' = asm["' + s + '"];']
714723
else:
715724
if shared.Settings.MINIMAL_RUNTIME:
716725
# In wasm2js exports can be directly processed at top level, i.e.

src/embind/embind.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,15 +1063,15 @@ var LibraryEmbind = {
10631063
},
10641064

10651065
$embind__requireFunction__deps: ['$readLatin1String', '$throwBindingError'
1066-
#if USE_LEGACY_DYNCALLS || !WASM_BIGINT
1066+
#if DYNCALLS || !WASM_BIGINT
10671067
, '$getDynCaller'
10681068
#endif
10691069
],
10701070
$embind__requireFunction: function(signature, rawFunction) {
10711071
signature = readLatin1String(signature);
10721072

10731073
function makeDynCaller() {
1074-
#if USE_LEGACY_DYNCALLS
1074+
#if DYNCALLS
10751075
return getDynCaller(signature, rawFunction);
10761076
#else
10771077
#if !WASM_BIGINT

src/library.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3699,21 +3699,28 @@ LibraryManager.library = {
36993699
});
37003700
},
37013701

3702-
#if USE_LEGACY_DYNCALLS || !WASM_BIGINT
3702+
#if DYNCALLS || !WASM_BIGINT
37033703
$dynCallLegacy: function(sig, ptr, args) {
37043704
#if ASSERTIONS
3705+
#if MINIMAL_RUNTIME
3706+
assert(typeof dynCalls !== 'undefined', 'Global dynCalls dictionary was not generated in the build! Pass -s DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=["$dynCall"] linker flag to include it!');
3707+
assert(sig in dynCalls, 'bad function pointer type - no table for sig \'' + sig + '\'');
3708+
#else
37053709
assert(('dynCall_' + sig) in Module, 'bad function pointer type - no table for sig \'' + sig + '\'');
3710+
#endif
37063711
if (args && args.length) {
37073712
// j (64-bit integer) must be passed in as two numbers [low 32, high 32].
37083713
assert(args.length === sig.substring(1).replace(/j/g, '--').length);
37093714
} else {
37103715
assert(sig.length == 1);
37113716
}
37123717
#endif
3713-
if (args && args.length) {
3714-
return Module['dynCall_' + sig].apply(null, [ptr].concat(args));
3715-
}
3716-
return Module['dynCall_' + sig].call(null, ptr);
3718+
#if MINIMAL_RUNTIME
3719+
var f = dynCalls[sig];
3720+
#else
3721+
var f = Module["dynCall_" + sig];
3722+
#endif
3723+
return args && args.length ? f.apply(null, [ptr].concat(args)) : f.call(null, ptr);
37173724
},
37183725
$dynCall__deps: ['$dynCallLegacy'],
37193726

@@ -3722,7 +3729,7 @@ LibraryManager.library = {
37223729
// back to this function if needed.
37233730
$getDynCaller__deps: ['$dynCall'],
37243731
$getDynCaller: function(sig, ptr) {
3725-
#if !USE_LEGACY_DYNCALLS
3732+
#if ASSERTIONS && !DYNCALLS
37263733
assert(sig.indexOf('j') >= 0, 'getDynCaller should only be called with i64 sigs')
37273734
#endif
37283735
var argCache = [];
@@ -3737,7 +3744,7 @@ LibraryManager.library = {
37373744
#endif
37383745

37393746
$dynCall: function(sig, ptr, args) {
3740-
#if USE_LEGACY_DYNCALLS
3747+
#if DYNCALLS
37413748
return dynCallLegacy(sig, ptr, args);
37423749
#else
37433750
#if !WASM_BIGINT

src/parseTools.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,13 +1006,13 @@ Please update to new syntax.`);
10061006
}
10071007
args = args.join(', ');
10081008

1009-
if (USE_LEGACY_DYNCALLS) {
1009+
if (DYNCALLS) {
10101010
return `(function(cb, ${args}) { return getDynCaller("${sig}", cb)(${args}) })`;
10111011
} else {
10121012
return `(function(cb, ${args}) { return wasmTable.get(cb)(${args}) })`;
10131013
}
10141014
}
1015-
if (USE_LEGACY_DYNCALLS) {
1015+
if (DYNCALLS) {
10161016
const dyncall = exportedAsmFunc(`dynCall_${sig}`);
10171017
if (sig.length > 1) {
10181018
let args = [];

src/settings.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,10 @@ var BINARYEN_EXTRA_PASSES = "";
11461146
// (This option was formerly called BINARYEN_ASYNC_COMPILATION)
11471147
var WASM_ASYNC_COMPILATION = 1;
11481148

1149+
// If set to 1, the dynCall() and dynCall_sig() API is made available
1150+
// to caller.
1151+
var DYNCALLS = 0;
1152+
11491153
// WebAssembly integration with JavaScript BigInt. When enabled we don't need
11501154
// to legalize i64s into pairs of i32s, as the wasm VM will use a BigInt where
11511155
// an i64 is used.

src/settings_internal.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,6 @@ var EXPECT_MAIN = 1;
175175
// MODULARIZE, and returned from the factory function.
176176
var EXPORT_READY_PROMISE = 1;
177177

178-
var USE_LEGACY_DYNCALLS = 0;
179-
180178
// struct_info that is either generated or cached
181179
var STRUCT_INFO = '';
182180

tests/core/test_dyncalls.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include <emscripten.h>
2+
#include <stdint.h>
3+
#include <stdio.h>
4+
5+
void vijdf(int i, int64_t j, double d, float f)
6+
{
7+
EM_ASM(console.log('vijdf: i='+$0+',jlo='+$1+',jhi='+$2+',d='+$3+',f='+$4), i, (uint32_t)j, (uint32_t)(((uint64_t)j) >> 32), d, f);
8+
}
9+
10+
int iii(int i, int j)
11+
{
12+
EM_ASM(console.log('iii: i='+$0+',j='+$1), i, j);
13+
return 42;
14+
}
15+
16+
17+
void test_dyncalls_vijdf(void(*)(int, int64_t, double, float));
18+
void test_dyncalls_iii(int(*)(int, int));
19+
20+
int main()
21+
{
22+
test_dyncalls_vijdf(&vijdf);
23+
test_dyncalls_iii(&iii);
24+
}

tests/core/test_dyncalls.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
mergeInto(LibraryManager.library, {
2+
test_dyncalls_vijdf: function(funcPtr) {
3+
// 1. Directly access a function pointer via a static signature (32-bit ABI)
4+
// (this is the fastest way to call a function pointer when the signature is statically known in WASM_BIGINT==0 builds)
5+
dynCall_vijdf(funcPtr, 1, /*lo=*/2, /*hi=*/3, 4, 5); // Available only in WASM_BIGINT != 2 builds
6+
7+
// 2. Access a function pointer using the convenience/legacy 'dynCall' function (32-bit ABI)
8+
// (this form should never be used, it is suboptimal for performance, but provided for legacy compatibility)
9+
dynCall('vijdf', funcPtr, [2, /*lo=*/3, /*hi=*/4, 5, 6]); // Available only in WASM_BIGINT != 2 builds
10+
},
11+
12+
test_dyncalls_iii: function(funcPtr) {
13+
// 1. Directly access a function pointer via a static signature (32-bit ABI)
14+
// (this is the fastest way to call a function pointer when the signature is statically known in WASM_BIGINT==0 builds)
15+
var ret = dynCall_iii(funcPtr, 1, 2); // Available only in WASM_BIGINT != 2 builds
16+
console.log('iii returned ' + ret);
17+
18+
// 2. Access a function pointer using the convenience/legacy 'dynCall' function (32-bit ABI)
19+
// (this form should never be used, it is suboptimal for performance, but provided for legacy compatibility)
20+
var ret = dynCall('iii', funcPtr, [2, 3]); // Available only in WASM_BIGINT != 2 builds
21+
console.log('iii returned ' + ret);
22+
}
23+
});

tests/core/test_dyncalls.out

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
vijdf: i=1,jlo=2,jhi=3,d=4,f=5
2+
vijdf: i=2,jlo=3,jhi=4,d=5,f=6
3+
iii: i=1,j=2
4+
iii returned 42
5+
iii: i=2,j=3
6+
iii returned 42

tests/test_core.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6233,7 +6233,6 @@ def test_EXTRA_EXPORTED_RUNTIME_METHODS(self):
62336233
self.set_setting('EXTRA_EXPORTED_RUNTIME_METHODS', ['dynCall', 'addFunction', 'lengthBytesUTF8', 'getTempRet0', 'setTempRet0'])
62346234
self.do_run_in_out_file_test('tests', 'core', 'EXTRA_EXPORTED_RUNTIME_METHODS.c')
62356235

6236-
@no_minimal_runtime('MINIMAL_RUNTIME does not blindly export all symbols to Module to save code size')
62376236
def test_dyncall_specific(self):
62386237
emcc_args = self.emcc_args[:]
62396238
for which, exported_runtime_methods in [
@@ -6246,6 +6245,13 @@ def test_dyncall_specific(self):
62466245
self.set_setting('EXTRA_EXPORTED_RUNTIME_METHODS', exported_runtime_methods)
62476246
self.do_run_in_out_file_test('tests', 'core', 'dyncall_specific.c')
62486247

6248+
self.set_setting('EXTRA_EXPORTED_RUNTIME_METHODS', [])
6249+
self.emcc_args = emcc_args + ['-s', 'DYNCALLS=1', '--js-library', path_from_root('tests', 'core', 'test_dyncalls.js')]
6250+
self.do_run_in_out_file_test('tests', 'core', 'test_dyncalls.c')
6251+
6252+
self.emcc_args += ['-s', 'MINIMAL_RUNTIME=1', '-s', 'DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=["$dynCall"]']
6253+
self.do_run_in_out_file_test('tests', 'core', 'test_dyncalls.c')
6254+
62496255
def test_getValue_setValue(self):
62506256
# these used to be exported, but no longer are by default
62516257
def test(output_prefix='', args=[], assert_returncode=0):
@@ -8273,6 +8279,14 @@ def test_gl_main_module(self):
82738279
self.set_setting('MAIN_MODULE')
82748280
self.do_runf(path_from_root('tests', 'core', 'test_gl_get_proc_address.c'))
82758281

8282+
@no_asan('asan does not yet work in MINIMAL_RUNTIME')
8283+
def test_dyncalls_minimal_runtime(self):
8284+
self.set_setting('DYNCALLS')
8285+
self.set_setting('MINIMAL_RUNTIME')
8286+
self.emcc_args += ['-s', 'DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=[$dynCall]']
8287+
self.emcc_args += ['--js-library', path_from_root('tests', 'core', 'test_dyncalls.js')]
8288+
self.do_run_in_out_file_test('tests', 'core', 'test_dyncalls.c')
8289+
82768290

82778291
# Generate tests for everything
82788292
def make_run(name, emcc_args, settings=None, env=None):

tools/shared.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@ def is_legal_sig(sig):
804804
@staticmethod
805805
def make_dynCall(sig, args):
806806
# wasm2c and asyncify are not yet compatible with direct wasm table calls
807-
if Settings.USE_LEGACY_DYNCALLS or not JS.is_legal_sig(sig):
807+
if Settings.DYNCALLS or not JS.is_legal_sig(sig):
808808
args = ','.join(args)
809809
if not Settings.MAIN_MODULE and not Settings.SIDE_MODULE:
810810
# Optimize dynCall accesses in the case when not building with dynamic

0 commit comments

Comments
 (0)