Skip to content
Open
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: 17 additions & 14 deletions src/lib/libeventloop.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,16 +204,18 @@ LibraryJSEventLoop = {
Module['resumeMainLoop'] = MainLoop.resume;
MainLoop.init();`,
$MainLoop: {
running: false,
// The main loop tick function that will be called at each iteration.
// This will be non-null whenever a loop function is registered.
func: null,
// This will be non-null whenever a loop function is both registered and
// currently running.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the difference between "registered" and "registered and currently running"?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Registered meas somebody called emscripten_set_main_loop.

Running can be false if somebody calls emscripten_pause_main_loop, but even while pauses the callback is still "registered", because one can call emscripten_resume_main_loop to re-use the same callback.

scheduler: null,
// Each main loop is numbered with a ID in sequence order. Only one main
// loop can run at a time. This variable stores the ordinal number of the
// main loop that is currently allowed to run. All previous main loops
// will quit themselves. This is incremented whenever a new main loop is
// created.
currentlyRunningMainloop: 0,
// The main loop tick function that will be called at each iteration.
func: null,
// The argument that will be passed to the main loop. (of type void*)
arg: 0,
timingMode: 0,
Expand All @@ -224,9 +226,12 @@ LibraryJSEventLoop = {
postMainLoop: [],

pause() {
MainLoop.scheduler = null;
// Incrementing this signals the previous main loop that it's now become old, and it must return.
MainLoop.currentlyRunningMainloop++;
if (MainLoop.scheduler) {
MainLoop.scheduler = null;
// Incrementing this signals the previous main loop that it's now become old, and it must return.
MainLoop.currentlyRunningMainloop++;
{{{ runtimeKeepalivePop() }}}
}
},

resume() {
Expand Down Expand Up @@ -328,10 +333,11 @@ LibraryJSEventLoop = {
return 1; // Return non-zero on failure, can't set timing mode when there is no main loop.
}

if (!MainLoop.running) {
{{{ runtimeKeepalivePush() }}}
MainLoop.running = true;
#if runtimeKeepaliveStack()
if (!MainLoop.scheduler) {
runtimeKeepalivePush();
}
#endif
if (mode == {{{ cDefs.EM_TIMING_SETTIMEOUT }}}) {
MainLoop.scheduler = function MainLoop_scheduler_setTimeout() {
var timeUntilNextTick = Math.max(0, MainLoop.tickStartTime + value - _emscripten_get_now())|0;
Expand Down Expand Up @@ -413,6 +419,7 @@ LibraryJSEventLoop = {
#if ASSERTIONS
assert(!MainLoop.func, 'emscripten_set_main_loop: there can only be one main loop function at once')
#endif
MainLoop.pause();
MainLoop.func = iterFunc;
MainLoop.arg = arg;

Expand All @@ -422,7 +429,6 @@ LibraryJSEventLoop = {
#if RUNTIME_DEBUG
dbg('main loop exiting');
#endif
{{{ runtimeKeepalivePop() }}}
#if !MINIMAL_RUNTIME
maybeExit();
#endif
Expand All @@ -433,10 +439,7 @@ LibraryJSEventLoop = {

// We create the loop runner here but it is not actually running until
// _emscripten_set_main_loop_timing is called (which might happen at a
// later time). This member signifies that the current runner has not
// yet been started so that we can call runtimeKeepalivePush when it
// gets its timing set for the first time.
MainLoop.running = false;
// later time).
MainLoop.runner = function MainLoop_runner() {
if (ABORT) return;
if (MainLoop.queue.length > 0) {
Expand Down
19 changes: 15 additions & 4 deletions src/parseTools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1063,22 +1063,32 @@ function awaitIf(condition) {
return condition ? 'await ' : '';
}

function runtimeKeepaliveStack() {
return !(MINIMAL_RUNTIME || (EXIT_RUNTIME == 0 && PTHREADS == 0));
}

// Adds a call to runtimeKeepalivePush, if needed by the current build
// configuration.
// We skip this completely in MINIMAL_RUNTIME and also in builds that
// don't ever need to exit the runtime.
function runtimeKeepalivePush() {
if (MINIMAL_RUNTIME || (EXIT_RUNTIME == 0 && PTHREADS == 0)) return '';
return 'runtimeKeepalivePush();';
if (runtimeKeepaliveStack()) {
return 'runtimeKeepalivePush();';
} else {
return '';
}
}

// Adds a call to runtimeKeepalivePush, if needed by the current build
// configuration.
// We skip this completely in MINIMAL_RUNTIME and also in builds that
// don't ever need to exit the runtime.
function runtimeKeepalivePop() {
if (MINIMAL_RUNTIME || (EXIT_RUNTIME == 0 && PTHREADS == 0)) return '';
return 'runtimeKeepalivePop();';
if (runtimeKeepaliveStack()) {
return 'runtimeKeepalivePop();';
} else {
return '';
}
}

// Some web APIs like TextDecoder.decode() and XMLHttpRequest.send() do not
Expand Down Expand Up @@ -1284,6 +1294,7 @@ addToCompileTimeContext({
runIfWorkerThread,
runtimeKeepalivePop,
runtimeKeepalivePush,
runtimeKeepaliveStack,
splitI64,
to64,
toIndexType,
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": 267539,
"a.out.js": 267530,
"a.out.nodebug.wasm": 588309,
"total": 855848,
"total": 855839,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down
6 changes: 6 additions & 0 deletions test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,12 @@ def test_emscripten_api_infloop(self):
def test_emscripten_main_loop(self):
self.btest_exit('test_emscripten_main_loop.c')

def test_emscripten_main_loop_cancel_exit(self):
self.btest_exit('test_emscripten_main_loop_cancel_exit.c', cflags=['-sASSERTIONS=2'])

def test_emscripten_main_loop_cancel_force_exit(self):
self.btest_exit('test_emscripten_main_loop_cancel_force_exit.c', cflags=['-sASSERTIONS=2'])

@parameterized({
'': ([],),
# test pthreads + AUTO_JS_LIBRARIES mode as well
Expand Down
20 changes: 20 additions & 0 deletions test/test_emscripten_main_loop_cancel_exit.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2026 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*/

#include <stdio.h>
#include <stdlib.h>
#include <emscripten.h>

void loop(void) {
emscripten_cancel_main_loop();
exit(0);
}

int main(void) {
emscripten_set_main_loop(loop, -1, 0);
return 99;
}
20 changes: 20 additions & 0 deletions test/test_emscripten_main_loop_cancel_force_exit.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2026 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*/

#include <stdio.h>
#include <stdlib.h>
#include <emscripten.h>

void loop(void) {
emscripten_cancel_main_loop();
emscripten_force_exit(0);
}

int main(void) {
emscripten_set_main_loop(loop, -1, 0);
return 99;
}
14 changes: 14 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -13381,6 +13381,20 @@ def test_emscripten_set_immediate_loop(self):
def test_emscripten_main_loop(self, args):
self.do_runf('test_emscripten_main_loop.c', cflags=args)

@parameterized({
'': ([],),
'exit_runtime': (['-sEXIT_RUNTIME'],),
})
def test_emscripten_main_loop_cancel_exit(self, args):
self.do_runf('test_emscripten_main_loop_cancel_exit.c', cflags=['-sASSERTIONS=2'] + args)

@parameterized({
'': ([],),
'exit_runtime': (['-sEXIT_RUNTIME'],),
})
def test_emscripten_main_loop_cancel_force_exit(self, args):
self.do_runf('test_emscripten_main_loop_cancel_force_exit.c', cflags=['-sASSERTIONS=2'] + args)

def test_emscripten_main_loop_and_blocker(self):
self.do_runf('test_emscripten_main_loop_and_blocker.c')

Expand Down
Loading