Skip to content

Fix regression that broke calls to makeDynCall in JS library code. #12732

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 11, 2020
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
31 changes: 31 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ Current Trunk
input that perform all the normal post link steps such as finalizing and
optimizing the wasm file and generating the JavaScript and/or html that will
run it.
- Added emulation support and a build time warning for calling Wasm function
pointers from JS library files via the old syntax
{{{ makeDynCall('sig') }}} (ptr, arg1, arg2);
that was broken on Aug 31st 2020 (Emscripten 2.0.2). A build warning will now
be emitted if the old signature is used. Convert to using the new signature
{{{ makeDynCall('sig', 'ptr') }}} (arg1, arg2);
instead.

2.0.8: 10/24/2020
-----------------
Expand Down Expand Up @@ -134,6 +141,30 @@ Current Trunk

2.0.3: 09/10/2020
-----------------
- Breaking changes to calling Wasm function pointers from JavaScript:
1. It is no longer possible to directly call dynCall_sig(funcPtr, param) to
call a function pointer from JavaScript code. As a result, JavaScript code
outside all JS libraries (pre-js/post-js/EM_ASM/EM_JS/external JS code) can no
longer call a function pointer via static signature matching dynCall_sig, but
must instead use the dynamic binding function

dynCall(sig, ptr, args);

This carries a significant performance overhead. The function dynCall is not
available in -s MINIMAL_RUNTIME=1 builds.
2. old syntax for calling a Wasm function pointer from a JS library file used
to be

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

This syntax will no longer work, and until Emscripten <2.0.9 causes
a runtime error TypeError: WebAssembly.Table.get(): Argument 0 must be
convertible to a valid number.

New syntax for calling Wasm function pointers from JS library files is

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

- The native optimizer and the corresponding config setting
(`EMSCRIPTEN_NATIVE_OPTIMIZER`) have been removed (it was only relevant to
asmjs/fastcomp backend).
Expand Down
154 changes: 88 additions & 66 deletions src/parseTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// Various tools for parsing LLVM. Utilities of various sorts, that are
// specific to Emscripten (and hence not in utility.js).

var currentlyParsedFilename = '';

// Does simple 'macro' substitution, using Django-like syntax,
// {{{ code }}} will be replaced with |eval(code)|.
// NOTE: Be careful with that ret check. If ret is |0|, |ret ? ret.toString() : ''| would result in ''!
Expand All @@ -29,86 +31,91 @@ function processMacros(text) {
// Param filenameHint can be passed as a description to identify the file that is being processed, used
// to locate errors for reporting and for html files to stop expansion between <style> and </style>.
function preprocess(text, filenameHint) {
var fileExt = (filenameHint) ? filenameHint.split('.').pop().toLowerCase() : "";
var isHtml = (fileExt === 'html' || fileExt === 'htm') ? true : false;
var inStyle = false;
var lines = text.split('\n');
var ret = '';
var showStack = [];
var emptyLine = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
try {
if (line[line.length-1] === '\r') {
line = line.substr(0, line.length-1); // Windows will have '\r' left over from splitting over '\r\n'
}
if (isHtml && line.indexOf('<style') !== -1 && !inStyle) {
inStyle = true;
}
if (isHtml && line.indexOf('</style') !== -1 && inStyle) {
inStyle = false;
}
currentlyParsedFilename = filenameHint;
try {
var fileExt = (filenameHint) ? filenameHint.split('.').pop().toLowerCase() : "";
var isHtml = (fileExt === 'html' || fileExt === 'htm') ? true : false;
var inStyle = false;
var lines = text.split('\n');
var ret = '';
var showStack = [];
var emptyLine = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
try {
if (line[line.length-1] === '\r') {
line = line.substr(0, line.length-1); // Windows will have '\r' left over from splitting over '\r\n'
}
if (isHtml && line.indexOf('<style') !== -1 && !inStyle) {
inStyle = true;
}
if (isHtml && line.indexOf('</style') !== -1 && inStyle) {
inStyle = false;
}

if (!inStyle) {
var trimmed = line.trim()
if (trimmed[0] === '#') {
var first = trimmed.split(' ', 1)[0]
if (first == '#if' || first == '#ifdef') {
if (first == '#ifdef') {
warn('warning: use of #ifdef in js library. Use #if instead.');
if (!inStyle) {
var trimmed = line.trim()
if (trimmed[0] === '#') {
var first = trimmed.split(' ', 1)[0]
if (first == '#if' || first == '#ifdef') {
if (first == '#ifdef') {
warn('warning: use of #ifdef in js library. Use #if instead.');
}
var after = trimmed.substring(trimmed.indexOf(' '));
var truthy = !!eval(after);
showStack.push(truthy);
} else if (first === '#include') {
if (showStack.indexOf(false) === -1) {
var filename = line.substr(line.indexOf(' ')+1);
if (filename.indexOf('"') === 0) {
filename = filename.substr(1, filename.length - 2);
}
var included = read(filename);
var result = preprocess(included, filename);
if (result) {
ret += "// include: " + filename + "\n";
ret += result;
ret += "// end include: " + filename + "\n";
}
}
} else if (first === '#else') {
assert(showStack.length > 0);
showStack.push(!showStack.pop());
} else if (first === '#endif') {
assert(showStack.length > 0);
showStack.pop();
} else {
throw "Unknown preprocessor directive on line " + i + ': `' + line + '`';
}
var after = trimmed.substring(trimmed.indexOf(' '));
var truthy = !!eval(after);
showStack.push(truthy);
} else if (first === '#include') {
} else {
if (showStack.indexOf(false) === -1) {
var filename = line.substr(line.indexOf(' ')+1);
if (filename.indexOf('"') === 0) {
filename = filename.substr(1, filename.length - 2);
// Never emit more than one empty line at a time.
if (emptyLine && !line) {
continue;
}
var included = read(filename);
var result = preprocess(included, filename);
if (result) {
ret += "// include: " + filename + "\n";
ret += result;
ret += "// end include: " + filename + "\n";
ret += line + '\n';
if (!line) {
emptyLine = true;
} else {
emptyLine = false;
}
}
} else if (first === '#else') {
assert(showStack.length > 0);
showStack.push(!showStack.pop());
} else if (first === '#endif') {
assert(showStack.length > 0);
showStack.pop();
} else {
throw "Unknown preprocessor directive on line " + i + ': `' + line + '`';
}
} else {
} else { // !inStyle
if (showStack.indexOf(false) === -1) {
// Never emit more than one empty line at a time.
if (emptyLine && !line) {
continue;
}
ret += line + '\n';
if (!line) {
emptyLine = true;
} else {
emptyLine = false;
}
}
}
} else { // !inStyle
if (showStack.indexOf(false) === -1) {
ret += line + '\n';
}
} catch(e) {
printErr('parseTools.js preprocessor error in ' + filenameHint + ':' + (i+1) + ': \"' + line + '\"!');
throw e;
}
} catch(e) {
printErr('parseTools.js preprocessor error in ' + filenameHint + ':' + (i+1) + ': \"' + line + '\"!');
throw e;
}
assert(showStack.length == 0, 'preprocessing error in file '+ filenameHint + ', no matching #endif found (' + showStack.length + ' unmatched preprocessing directives on stack)');
return ret;
} finally {
currentlyParsedFilename = null;
}
assert(showStack.length == 0, 'preprocessing error in file '+ filenameHint + ', no matching #endif found (' + showStack.length + ' unmatched preprocessing directives on stack)');
return ret;
}

function removePointing(type, num) {
Expand Down Expand Up @@ -985,6 +992,21 @@ function asmFFICoercion(value, type) {

function makeDynCall(sig, funcPtr) {
assert(sig.indexOf('j') == -1);
if (funcPtr === undefined) {
printErr(`warning: ${currentlyParsedFilename}: Legacy use of {{{ makeDynCall("${sig}") }}}(funcPtr, arg1, arg2, ...). Starting from Emscripten 2.0.2 (Aug 31st 2020), syntax for makeDynCall has changed. New syntax is {{{ makeDynCall("${sig}", "funcPtr") }}}(arg1, arg2, ...). Please update to new syntax.`);
var ret = (sig[0] == 'v') ? 'return ' : '';
var args = [];
for(var i = 1; i < sig.length; ++i) {
args.push(`a${i}`);
}
args = args.join(', ');

if (USE_LEGACY_DYNCALLS) {
return `(function(cb, ${args}) { return getDynCaller("${sig}", cb)(${args}) })`;
} else {
return `(function(cb, ${args}) { return wasmTable.get(cb)(${args}) })`;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I love the clear error message, but actually handling the old form feels excessive to me. I suggest erroring the build, to force people to update? It's an internal API, and one that isn't hard to update for (with the nice new error message here).

Copy link
Collaborator Author

@juj juj Nov 10, 2020

Choose a reason for hiding this comment

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

I am surprised to see you say that this is an internal API.

If this was an internal API (only used in Emscripten system library_*.js files), then indeed this would be excessive and I would not have minded too much about this, but would just fix our own library JS code in Unity and move on. However, {{{ makeDynCall(..) }}} works and is used also in user-provided library_*.js files, which makes this a public API.

Also we should not have core syntax in library_*.js that is not supposed to be used by end users. That is because most of the library_*.js files that people author are monkeyed off of existing src/library_*.js files - that is what we write to the GitHub issue tracker so many times in the past when people ask how to achieve something ("take a look at an example in src/library_webgl.js" etc). If {{{ makeDynCall(..) }}} was an internal API, it should certainly have been blocked from functioning in user library_*.js files at all. But why should user library_*.js files be somehow tiered to a lesser feature set than our own core library_*.js files?

Historically, according to my understanding, there have been three ways to invoke a function pointer from JS code:

  1. dynCall_sig(): static dispatch, works from library_*.js files and EM_ASM, EM_JS, --pre-js-, --post-js and external <script>s.
  2. {{{ makeDynCall('sig) }}}: static dispatch, works from library_*.js files only. The "safe" and preferred way to do static dispatch since it works across all build modes (EMULATED_FUNCTION_POINTERS, USE_LEGACY_DYNCALLS).
  3. dynCall('sig'): dynamic dispatch, works from library_*.js files and EM_ASM, EM_JS, --pre-js-, --post-js and external <script>s. Carries a significant performance hit compared to static dispatch.

PR #12059 regressed all of the syntaxes 1., 2. and 3. above (#12733).

This PR fixes the regression (2).

I do not know how to fix (1) at the moment, that is blocking us to be able to update Unity to a newer compiler version.

The regression of (3) is that dynCall was removed from the build by default (which I agree is nice for code size), requiring library_*.js authors to now explicitly add a __deps field to it for it to work, or add it to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE explicitly, if one wishes to do dynCall() from EM_ASM, EM_JS, --pre-js-, --post-js or external <script>s.

This regression (3) is minor for Unity's use, since we can paper over that in the build system easily, so not sweating too much about it (but it is still a breaking change that was not mentioned in the ChangeLog for Emscripten 2.0.2).

Chatting with @sbc100 on #12733 I believe there was a misassumption that (3) would be the "one syntax to rule them all". Historically I moved us away from the dynamic dispatch dynCall in Jan 2017 because of its large impact observed on real-world performance (#4894). The benchmark in #12733 (comment) verifies that the performance landscape is still the same today. We should not be going back to that.

Unity Asset Store has hundreds of packages that advertise WebGL support, and we have thousands to tens of thousands of users that develop their own WebGL project; of all of those an unknown number of packages and game projects WebGL packages with library_*.js files to interop with Unity C#, C/C++ and JS. We cannot break any of them when updating, because it may not even be up to the project developer to be able to fix up the library_*.js issue, but they must receive an update from the Asset Store package author.

If we errored out here in this message, that would mean that possibly thousands of users would be blocked from developing their game project until plugin authors provided a fix. That is not possible for us.

Because of these reasons is why I am adding a very explicit warning about this that will prompt developers who see the warning to look at their own library_*.js files, and nudge their plugin vendors to fix up theirs.

See also the comment #12733 (comment).

Copy link
Member

Choose a reason for hiding this comment

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

I see, thanks @juj, I didn't realize this was used in user code so much. I'd assumed that low-level handling indirect calls is extremely rare (like it is in the emscripten JS libraries). Optimally user code would use something that does the low-level handling for it, but that's not the situation.

Given that, sounds good to support the old syntax.

if (USE_LEGACY_DYNCALLS) {
return `getDynCaller("${sig}", ${funcPtr})`;
} else {
Expand Down
5 changes: 5 additions & 0 deletions tests/library_test_old_dyncall_format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mergeInto(LibraryManager.library, {
callFunc: function(func, param1, param2) {
{{{ makeDynCall('vii') }}} (func, param1, param2);
}
});
13 changes: 13 additions & 0 deletions tests/test_old_dyncall_format.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include <stdio.h>

void foo(int param1, int param2)
{
if (param1 == 42 && param2 == 100) printf("Test passed!\n");
}

extern void callFunc(void (*foo)(int, int), int param1, int param2);

int main()
{
callFunc(foo, 42, 100);
}
5 changes: 5 additions & 0 deletions tests/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -9617,6 +9617,11 @@ def test_oformat(self):
err = self.expect_fail([EMCC, path_from_root('tests', 'hello_world.c'), '--oformat=foo'])
self.assertContained("error: invalid output format: `foo` (must be one of ['wasm', 'js', 'mjs', 'html', 'bare']", err)

# Tests that the old format of {{{ makeDynCall('sig') }}}(func, param1) works
def test_old_makeDynCall_syntax(self):
err = self.run_process([EMCC, path_from_root('tests', 'test_old_dyncall_format.c'), '--js-library', path_from_root('tests', 'library_test_old_dyncall_format.js')], stderr=PIPE).stderr
self.assertContained('syntax for makeDynCall has changed', err)

def test_post_link(self):
err = self.run_process([EMCC, path_from_root('tests', 'hello_world.c'), '--oformat=bare', '-o', 'bare.wasm'], stderr=PIPE).stderr
self.assertContained('--oformat=base/--post-link are experimental and subject to change', err)
Expand Down