diff --git a/src/lib/libhtml5.js b/src/lib/libhtml5.js
index b7363179d097b..119a9ed0e70b3 100644
--- a/src/lib/libhtml5.js
+++ b/src/lib/libhtml5.js
@@ -205,6 +205,25 @@ var LibraryHTML5 = {
return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
},
+ removeSingleHandler(eventHandler) {
+ if (!eventHandler.target) {
+#if ASSERTIONS
+ err('removeSingleHandler: the target element for event handler registration does not exist, when processing the following event handler registration:');
+ console.dir(eventHandler);
+#endif
+ return {{{ cDefs.EMSCRIPTEN_RESULT_UNKNOWN_TARGET }}};
+ }
+ for (var i = 0; i < JSEvents.eventHandlers.length; ++i) {
+ if (JSEvents.eventHandlers[i].target === eventHandler.target
+ && JSEvents.eventHandlers[i].eventTypeId === eventHandler.eventTypeId
+ && JSEvents.eventHandlers[i].callbackfunc === eventHandler.callbackfunc
+ && JSEvents.eventHandlers[i].userData === eventHandler.userData) {
+ JSEvents._removeHandler(i--);
+ }
+ }
+ return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
+ },
+
#if PTHREADS
getTargetThreadForEventCallback(targetThread) {
switch (targetThread) {
@@ -298,6 +317,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: keyEventHandlerFunc,
useCapture
@@ -412,6 +433,18 @@ var LibraryHTML5 = {
},
#endif
+ emscripten_remove_callback__proxy: 'sync',
+ emscripten_remove_callback__deps: ['$JSEvents', '$findEventTarget'],
+ emscripten_remove_callback: (target, userData, eventTypeId, callback) => {
+ var eventHandler = {
+ target: findEventTarget(target),
+ userData,
+ eventTypeId,
+ callbackfunc: callback,
+ };
+ return JSEvents.removeSingleHandler(eventHandler);
+ },
+
emscripten_set_keypress_callback_on_thread__proxy: 'sync',
emscripten_set_keypress_callback_on_thread__deps: ['$registerKeyEventCallback'],
emscripten_set_keypress_callback_on_thread: (target, userData, useCapture, callbackfunc, targetThread) =>
@@ -503,6 +536,8 @@ var LibraryHTML5 = {
allowsDeferredCalls: eventTypeString != 'mousemove' && eventTypeString != 'mouseenter' && eventTypeString != 'mouseleave', // Mouse move events do not allow fullscreen/pointer lock requests to be handled in them!
#endif
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: mouseEventHandlerFunc,
useCapture
@@ -599,6 +634,8 @@ var LibraryHTML5 = {
allowsDeferredCalls: true,
#endif
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: wheelHandlerFunc,
useCapture
@@ -674,6 +711,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: uiEventHandlerFunc,
useCapture
@@ -721,6 +760,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: focusEventHandlerFunc,
useCapture
@@ -779,6 +820,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: deviceOrientationEventHandlerFunc,
useCapture
@@ -849,6 +892,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: deviceMotionEventHandlerFunc,
useCapture
@@ -935,6 +980,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: orientationChangeEventHandlerFunc,
useCapture
@@ -1046,6 +1093,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: fullscreenChangeEventhandlerFunc,
useCapture
@@ -1547,6 +1596,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: pointerlockChangeEventHandlerFunc,
useCapture
@@ -1591,6 +1642,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: pointerlockErrorEventHandlerFunc,
useCapture
@@ -1745,6 +1798,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: visibilityChangeEventHandlerFunc,
useCapture
@@ -1863,6 +1918,8 @@ var LibraryHTML5 = {
allowsDeferredCalls: eventTypeString == 'touchstart' || eventTypeString == 'touchend',
#endif
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: touchEventHandlerFunc,
useCapture
@@ -1949,6 +2006,8 @@ var LibraryHTML5 = {
allowsDeferredCalls: true,
#endif
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: gamepadEventHandlerFunc,
useCapture
@@ -2035,6 +2094,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: beforeUnloadEventHandlerFunc,
useCapture
@@ -2088,6 +2149,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: battery,
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: batteryEventHandlerFunc,
useCapture
diff --git a/src/lib/libhtml5_webgl.js b/src/lib/libhtml5_webgl.js
index 63f8d2ad39624..ed8b4bf27577c 100644
--- a/src/lib/libhtml5_webgl.js
+++ b/src/lib/libhtml5_webgl.js
@@ -441,6 +441,8 @@ var LibraryHtml5WebGL = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
+ eventTypeId,
+ userData,
callbackfunc,
handlerFunc: webGlEventHandlerFunc,
useCapture
diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js
index 04e36d080ee3f..3078715a905e4 100644
--- a/src/lib/libsigs.js
+++ b/src/lib/libsigs.js
@@ -731,6 +731,7 @@ sigs = {
emscripten_promise_resolve__sig: 'vpip',
emscripten_promise_then__sig: 'ppppp',
emscripten_random__sig: 'f',
+ emscripten_remove_callback__sig: 'ippip',
emscripten_request_animation_frame__sig: 'ipp',
emscripten_request_animation_frame_loop__sig: 'vpp',
emscripten_request_fullscreen__sig: 'ipi',
diff --git a/system/include/emscripten/html5.h b/system/include/emscripten/html5.h
index 6fb863e2f77b9..667a0a9c6d1d4 100644
--- a/system/include/emscripten/html5.h
+++ b/system/include/emscripten/html5.h
@@ -420,6 +420,8 @@ EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target __attribute
void emscripten_html5_remove_all_event_listeners(void);
+EMSCRIPTEN_RESULT emscripten_remove_callback(const char *target __attribute__((nonnull)), void *userData, int eventTypeId, void *callback __attribute__((nonnull)));
+
#define EM_CALLBACK_THREAD_CONTEXT_MAIN_RUNTIME_THREAD ((pthread_t)0x1)
#define EM_CALLBACK_THREAD_CONTEXT_CALLING_THREAD ((pthread_t)0x2)
diff --git a/test/test_browser.py b/test/test_browser.py
index a9f9b71d86be2..6bdaa8facbf1f 100644
--- a/test/test_browser.py
+++ b/test/test_browser.py
@@ -2634,6 +2634,9 @@ def test_html5_core(self, opts):
self.cflags.append('--pre-js=pre.js')
self.btest_exit('test_html5_core.c', cflags=opts)
+ def test_html5_remove_callback(self):
+ self.btest_exit('test_html5_remove_callback.c')
+
@parameterized({
'': ([],),
'closure': (['-O2', '-g1', '--closure=1'],),
diff --git a/test/test_html5_remove_callback.c b/test/test_html5_remove_callback.c
new file mode 100644
index 0000000000000..b583775f89e39
--- /dev/null
+++ b/test/test_html5_remove_callback.c
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2025 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
+#include
+#include
+#include
+#include
+
+const char *emscripten_result_to_string(EMSCRIPTEN_RESULT result) {
+ if (result == EMSCRIPTEN_RESULT_SUCCESS) return "EMSCRIPTEN_RESULT_SUCCESS";
+ if (result == EMSCRIPTEN_RESULT_DEFERRED) return "EMSCRIPTEN_RESULT_DEFERRED";
+ if (result == EMSCRIPTEN_RESULT_NOT_SUPPORTED) return "EMSCRIPTEN_RESULT_NOT_SUPPORTED";
+ if (result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED) return "EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED";
+ if (result == EMSCRIPTEN_RESULT_INVALID_TARGET) return "EMSCRIPTEN_RESULT_INVALID_TARGET";
+ if (result == EMSCRIPTEN_RESULT_UNKNOWN_TARGET) return "EMSCRIPTEN_RESULT_UNKNOWN_TARGET";
+ if (result == EMSCRIPTEN_RESULT_INVALID_PARAM) return "EMSCRIPTEN_RESULT_INVALID_PARAM";
+ if (result == EMSCRIPTEN_RESULT_FAILED) return "EMSCRIPTEN_RESULT_FAILED";
+ if (result == EMSCRIPTEN_RESULT_NO_DATA) return "EMSCRIPTEN_RESULT_NO_DATA";
+ return "Unknown EMSCRIPTEN_RESULT!";
+}
+
+// Report API failure
+#define TEST_RESULT(x) if (ret != EMSCRIPTEN_RESULT_SUCCESS) printf("%s returned %s.\n", #x, emscripten_result_to_string(ret));
+
+// Like above above but also assert API success
+#define ASSERT_RESULT(x) TEST_RESULT(x); assert(ret == EMSCRIPTEN_RESULT_SUCCESS);
+
+char const *userDataToString(void *userData) {
+ return userData ? (char const *) userData : "nullptr";
+}
+
+bool key_callback_1(int eventType, const EmscriptenKeyboardEvent *e, void *userData) {
+ printf("key_callback_1: eventType=%d, userData=%s\n", eventType, userDataToString(userData));
+ return 0;
+}
+
+bool key_callback_2(int eventType, const EmscriptenKeyboardEvent *e, void *userData) {
+ printf("key_callback_2: eventType=%d, userData=%s\n", eventType, userDataToString(userData));
+ return 0;
+}
+
+bool mouse_callback_1(int eventType, const EmscriptenMouseEvent *e, void *userData) {
+ printf("mouse_callback_1: eventType=%d, userData=%s\n", eventType, userDataToString(userData));
+ return 0;
+}
+
+void checkCount(int count)
+{
+ int eventHandlersCount = EM_ASM_INT({ return JSEvents.eventHandlers.length; });
+ printf("Detected [%d] handlers\n", eventHandlersCount);
+ assert(count == eventHandlersCount);
+}
+
+void test_done() {}
+
+int main() {
+ bool useCapture = true;
+ void *userData3 = "3";
+
+ // first we check for invalid parameters
+ assert(emscripten_remove_callback("this_dom_element_does_not_exist", NULL, 0, key_callback_1) == EMSCRIPTEN_RESULT_UNKNOWN_TARGET);
+
+ checkCount(0);
+
+ EMSCRIPTEN_RESULT ret = emscripten_set_keypress_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, key_callback_1);
+ ASSERT_RESULT(emscripten_set_keypress_callback);
+ ret = emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, key_callback_1);
+ ASSERT_RESULT(emscripten_set_keydown_callback);
+ ret = emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, key_callback_1);
+ ASSERT_RESULT(emscripten_set_keyup_callback);
+
+ checkCount(3);
+
+ // removing keydown event
+ ret = emscripten_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYDOWN, key_callback_1);
+ ASSERT_RESULT(emscripten_remove_callback);
+
+ checkCount(2);
+
+ // adding another keypress callback on the same target
+ ret = emscripten_set_keypress_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, key_callback_2);
+ ASSERT_RESULT(emscripten_set_keypress_callback);
+
+ checkCount(3);
+
+ // adding another keypress callback on the same target with different user data
+ ret = emscripten_set_keypress_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, userData3, useCapture, key_callback_2);
+ ASSERT_RESULT(emscripten_set_keypress_callback);
+
+ checkCount(4);
+
+ // removing a combination that does not exist (no mouse_callback_1 registered)
+ ret = emscripten_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYPRESS, mouse_callback_1);
+ ASSERT_RESULT(emscripten_remove_callback);
+
+ checkCount(4);
+
+ // removing keypress / userData=NULL / key_callback_2
+ ret = emscripten_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYPRESS, key_callback_2);
+ ASSERT_RESULT(emscripten_remove_callback);
+
+ checkCount(3);
+
+ // removing keypress / userData=NULL / key_callback_1
+ ret = emscripten_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYPRESS, key_callback_1);
+ ASSERT_RESULT(emscripten_remove_callback);
+
+ checkCount(2);
+
+ // removing keypress / userData=3 / key_callback_2
+ ret = emscripten_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, userData3, EMSCRIPTEN_EVENT_KEYPRESS, key_callback_2);
+ ASSERT_RESULT(emscripten_remove_callback);
+
+ checkCount(1);
+
+ // adding the same mouse down callback to 2 different targets
+ ret = emscripten_set_mousedown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, mouse_callback_1);
+ ASSERT_RESULT(emscripten_set_mousedown_callback);
+ ret = emscripten_set_mousedown_callback("#canvas", NULL, useCapture, mouse_callback_1);
+ ASSERT_RESULT(emscripten_set_mousedown_callback);
+
+ checkCount(3);
+
+ // removing mousedown / userData=NULL / mouse_callback_1 on the window target
+ ret = emscripten_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_MOUSEDOWN, mouse_callback_1);
+ ASSERT_RESULT(emscripten_remove_callback);
+
+ checkCount(2);
+
+ return 0;
+}