diff --git a/ChangeLog.md b/ChangeLog.md index 612d47b20afa4..3960939981677 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,8 @@ See docs/process.md for more on how version tagging works. 4.0.20 (in development) ----------------------- +- Added `emscripten_remove_callback` function in `html5.h` in order to be + able to remove a single callback. (#25535) 4.0.19 - 11/04/25 ----------------- diff --git a/site/source/docs/api_reference/html5.h.rst b/site/source/docs/api_reference/html5.h.rst index 23155eefd4307..40e0536aab3ad 100644 --- a/site/source/docs/api_reference/html5.h.rst +++ b/site/source/docs/api_reference/html5.h.rst @@ -90,6 +90,50 @@ The ``useCapture`` parameter maps to ``useCapture`` in `EventTarget.addEventLis Most functions return the result using the type :c:data:`EMSCRIPTEN_RESULT`. Zero and positive values denote success. Negative values signal failure. None of the functions fail or abort by throwing a JavaScript or C++ exception. If a particular browser does not support the given feature, the value :c:data:`EMSCRIPTEN_RESULT_NOT_SUPPORTED` will be returned at the time the callback is registered. +Remove callback function +------------------------ + +In order to remove a callback, previously set via a ``emscripten_set_some_callback`` call, there is a dedicated and generic function for this purpose: + + .. code-block:: cpp + + EMSCRIPTEN_RESULT emscripten_remove_callback( + const char *target, // ID of the target HTML element. + void *userData, // User-defined data (passed to the callback). + int eventTypeId, // The event type ID (EMSCRIPTEN_EVENT_XXX). + void *callback // Callback function. + ); + + +The ``target``, ``userData`` and ``callback`` parameters are the same parameters provided in ``emscripten_set_some_callback`` with the only difference being that, since this function applies to all types of callbacks, the type of ``callback`` is ``void *``. + +The ``eventTypeId`` represents the event type, the same Id received in the callback functions. + + .. code-block:: cpp + + // Example + + bool my_mouse_callback_1(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) { + // ... + } + + bool my_mouse_callback_2(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) { + // ... + } + + void main() { + + // 1. set callbacks for mouse down and mouse move + emscripten_set_mousedown_callback("#mydiv", 0, my_mouse_callback_1); + emscripten_set_mousedown_callback("#mydiv", (void *) 34, my_mouse_callback_2); + emscripten_set_mousemove_callback("#mydiv", 0, my_mouse_callback_1); + + // 2. remove these callbacks + emscripten_remove_callback("#mydiv", 0, EMSCRIPTEN_EVENT_MOUSEDOWN, my_mouse_callback_1); + emscripten_remove_callback("#mydiv", (void *) 34, EMSCRIPTEN_EVENT_MOUSEDOWN, my_mouse_callback_2); + emscripten_remove_callback("#mydiv", 0, EMSCRIPTEN_EVENT_MOUSEMOVE, my_mouse_callback_1); + } + Callback functions ------------------ diff --git a/src/lib/libhtml5.js b/src/lib/libhtml5.js index 0d110367b4fe9..30b08f54584bf 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 @@ -850,6 +893,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: deviceMotionEventHandlerFunc, useCapture @@ -936,6 +981,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: orientationChangeEventHandlerFunc, useCapture @@ -1047,6 +1094,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: fullscreenChangeEventhandlerFunc, useCapture @@ -1548,6 +1597,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: pointerlockChangeEventHandlerFunc, useCapture @@ -1592,6 +1643,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: pointerlockErrorEventHandlerFunc, useCapture @@ -1746,6 +1799,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: visibilityChangeEventHandlerFunc, useCapture @@ -1864,6 +1919,8 @@ var LibraryHTML5 = { allowsDeferredCalls: eventTypeString == 'touchstart' || eventTypeString == 'touchend', #endif eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: touchEventHandlerFunc, useCapture @@ -1950,6 +2007,8 @@ var LibraryHTML5 = { allowsDeferredCalls: true, #endif eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: gamepadEventHandlerFunc, useCapture @@ -2036,6 +2095,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: beforeUnloadEventHandlerFunc, useCapture @@ -2089,6 +2150,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 d00be138946d8..f579ae57175af 100644 --- a/src/lib/libhtml5_webgl.js +++ b/src/lib/libhtml5_webgl.js @@ -440,6 +440,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 e2b5f43371de6..53c5f5fb685c4 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -732,6 +732,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/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 0d1f74f824ac2..ffc74c8ee5782 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 245483, - "a.out.nodebug.wasm": 574042, - "total": 819525, + "a.out.js": 245865, + "a.out.nodebug.wasm": 574039, + "total": 819904, "sent": [ "IMG_Init", "IMG_Load", @@ -741,6 +741,7 @@ "emscripten_promise_resolve", "emscripten_promise_then", "emscripten_random", + "emscripten_remove_callback", "emscripten_request_animation_frame", "emscripten_request_animation_frame_loop", "emscripten_request_fullscreen", diff --git a/test/test_browser.py b/test/test_browser.py index 1686fd3cd9c2e..b8875180b162c 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -2667,6 +2667,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; +}