Detect nested usage of callUserCallback. NFC
Detect cases where `callUserCallback` is called while already running
user code. This doesn't change the behaviour, but warns about
(currently) incorrect usage of `callUserCallback`.
Split out from #17441 which fixes a real bug in glfw where
`callUserCallback` could cause the runtime to exit mid-program.
This bug only effects programs that are built with `EXIT_RUNTIME` but
its serious enough we probably want to look at more robust solution.
For now, issuing a warning seems like a good first step.
diff --git a/src/library.js b/src/library.js
index 2ebdd0c..c15cce6 100644
--- a/src/library.js
+++ b/src/library.js
@@ -3502,6 +3502,12 @@
return;
}
try {
+#if ASSERTIONS
+ if (userCodeEntriesOnStack != 0) {
+ warnOnce('nested call to callUserCallback detected (This should only be used to enter from the event loop)');
+ }
+ userCodeEntriesOnStack += 1;
+#endif
func();
#if EXIT_RUNTIME || USE_PTHREADS
#if USE_PTHREADS && !EXIT_RUNTIME
@@ -3512,6 +3518,11 @@
} catch (e) {
handleException(e);
}
+#if ASSERTIONS
+ finally {
+ userCodeEntriesOnStack -= 1;
+ }
+#endif
},
$maybeExit__deps: ['exit', '$handleException',
diff --git a/src/postamble.js b/src/postamble.js
index 777164c..42d141b 100644
--- a/src/postamble.js
+++ b/src/postamble.js
@@ -161,6 +161,11 @@
abortWrapperDepth += 2;
#endif
+#if ASSERTIONS
+ // TODO(sbc): Have `callMain` use `callUserCallback` to avoid duplicating
+ // this tracking.
+ userCodeEntriesOnStack += 1;
+#endif
#if STANDALONE_WASM
entryFunction();
// _start (in crt1.c) will call exit() if main return non-zero. So we know
@@ -189,6 +194,9 @@
return handleException(e);
#endif // !PROXY_TO_PTHREAD
} finally {
+#if ASSERTIONS
+ userCodeEntriesOnStack -= 1;
+#endif
calledMain = true;
#if ABORT_ON_WASM_EXCEPTIONS
diff --git a/src/preamble.js b/src/preamble.js
index d374cbb..3e52012 100644
--- a/src/preamble.js
+++ b/src/preamble.js
@@ -368,6 +368,11 @@
var dependenciesFulfilled = null; // overridden to take different actions when all run dependencies are fulfilled
#if ASSERTIONS
var runDependencyTracking = {};
+// Keeps track of how many times we have entered user code in the current stack.
+// For example, this starts of at 0, and is set to 1 when we enter main. This
+// enables callUserCallback to issue a warning when it is called from within
+// user code (its designed to be used to enter user code from the event loop).
+var userCodeEntriesOnStack = 0;
#endif
function getUniqueRunDependency(id) {
diff --git a/tests/other/test_nested_user_callback.c b/tests/other/test_nested_user_callback.c
new file mode 100644
index 0000000..053a65e
--- /dev/null
+++ b/tests/other/test_nested_user_callback.c
@@ -0,0 +1,36 @@
+#include <assert.h>
+#include <emscripten.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+void myatexit() {
+ puts("myatexit\n");
+}
+
+int main() {
+ atexit(myatexit);
+
+ puts("in main");
+
+ EM_ASM({
+ // Without this Push, the runtime will exit after the user callback.
+ runtimeKeepalivePush();
+ // Should generate a warning because we are already within user code.
+ callUserCallback(() => out("in user callback"));
+ runtimeKeepalivePop();
+
+ assert(!runtimeExited);
+ out('before: ' + runtimeExited);
+ callUserCallback(() => out("in user callback; without runtimeKeepalivePush"));
+ // callUserCallback without runtimeKeepalivePush will result in the runtime
+ // exiting at the end of the user callback.
+ // So we should never get here:
+ assert(false);
+ });
+
+ puts("returning from main");
+ // Should never get here since the EM_ASM block above causes application to
+ // exit.
+ assert(0);
+ return 0;
+}
diff --git a/tests/other/test_nested_user_callback.out b/tests/other/test_nested_user_callback.out
new file mode 100644
index 0000000..988cea9
--- /dev/null
+++ b/tests/other/test_nested_user_callback.out
@@ -0,0 +1,6 @@
+in main
+nested call to callUserCallback detected (This should only be used to enter from the event loop)
+in user callback
+before: false
+in user callback; without runtimeKeepalivePush
+myatexit
diff --git a/tests/test_other.py b/tests/test_other.py
index 1b236bd..0ed1ac7 100644
--- a/tests/test_other.py
+++ b/tests/test_other.py
@@ -11299,11 +11299,16 @@
self.do_runf(test_file('hello_world.c'), 'hello, world!')
def test_runtime_keepalive(self):
- self.uses_es6 = True
self.set_setting('DEFAULT_LIBRARY_FUNCS_TO_INCLUDE', ['$runtimeKeepalivePush', '$runtimeKeepalivePop', '$callUserCallback'])
self.set_setting('EXIT_RUNTIME')
self.do_other_test('test_runtime_keepalive.cpp')
+ def test_nested_user_callback(self):
+ self.set_setting('DEFAULT_LIBRARY_FUNCS_TO_INCLUDE', ['$runtimeKeepalivePush', '$runtimeKeepalivePop', '$callUserCallback'])
+ self.set_setting('EXIT_RUNTIME')
+ self.set_setting('ASSERTIONS')
+ self.do_other_test('test_nested_user_callback.c')
+
def test_em_asm_side_module(self):
err = self.expect_fail([EMCC, '-sSIDE_MODULE', test_file('hello_world_em_asm.c')])
self.assertContained('EM_ASM is not supported in side modules', err)