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
32 changes: 18 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.
Comment thread
sbc100 marked this conversation as resolved.
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,13 @@ 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 useRuntimeKeepaliveStack()
// If there is no existing scheduler then we are transitioning from
// inactive to active and we add to runtime keepalive counter.
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 @@ -422,7 +430,6 @@ LibraryJSEventLoop = {
#if RUNTIME_DEBUG
dbg('main loop exiting');
#endif
{{{ runtimeKeepalivePop() }}}
#if !MINIMAL_RUNTIME
maybeExit();
#endif
Expand All @@ -433,10 +440,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 useRuntimeKeepaliveStack() {
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 (useRuntimeKeepaliveStack()) {
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 (useRuntimeKeepaliveStack()) {
return 'runtimeKeepalivePop();';
} else {
return '';
}
}

// Some web APIs like TextDecoder.decode() and XMLHttpRequest.send() do not
Expand Down Expand Up @@ -1291,4 +1301,5 @@ addToCompileTimeContext({
nodeWWDetection,
wasmWorkerDetection,
pthreadDetection,
useRuntimeKeepaliveStack,
});
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": 267525,
"a.out.nodebug.wasm": 588311,
"total": 855850,
"total": 855836,
"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 @@ -13429,6 +13429,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