Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------
Expand Down
44 changes: 44 additions & 0 deletions site/source/docs/api_reference/html5.h.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe slightly simpler phrasing:

In order to unregister a single event handler callback, call the following function:


.. 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
------------------
Expand Down
63 changes: 63 additions & 0 deletions src/lib/libhtml5.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -298,6 +317,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: keyEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -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) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, should we key the userData pointer into the removal? I.e. it would be better for this API to be

emscripten_remove_callback: (target, eventTypeId, callback)

The rationale is that the userData pointer is generally just data to the function call, and not identifying. I.e. it is more of the 'value' part in a key->value association.

It might be some function local value in the caller (e.g. a boolean for some behavior), and might result in user having to store the userData field in some global to be able to unregister. So requiring users to know to match the userData value could be a bit tedious.

(If users did specifically want to differentiate unregistrations based on what was seemingly a different userData value, they could do that by separating to a different function with a trampoline)

Also, this function would be good to follow the same _on_thread model, so that it can be called from pthreads symmetric to how registering on pthreads works.

The semantics of function emscripten_remove_callback are to remove all copies of callback registrations of the given function. I wonder if the function naming should somehow reflect that?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this makes sense to me, but in that case I think it would also be good it if we make it impossible to register the same callback function for the same event with different userData.

emscripten_set_click_callback(window, data1, 0, mycallback);   // success
emscripten_set_click_callback(window, data2, 0, mycallback);   // error `mycallback` is already registered as a click callback

If we didn't error in the second case, and allowed the same callback to be registred twice, there would be no way to unregister just one of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • if we want to error when registering the same callback with a different user data, then it means we need to store the user data (which we don't prior to this changes)
  • it also means that code that is currently working could fail after this change

I think what @juj suggested was to indeed un-register all callbacks that were registered with different data but with the same target/event_id/callback combination. That would have eliminated having to store the user data.

It sounds like you don't like this because then "there would be no way to unregister just one of them".

I am definitely not in favor of erroring when registering 2 identical callbacks with different user data, because from a developer point of view, this is absolutely not an error and I can imagine scenarios where that could be useful.

So I guess the solution is to leave the API and changes as implemented in this PR. That way we can un-register a single callback (which IS the point of this PR). So I am personally in favor of this.

I will now work on updating the documentation and fixing the "Code Size" failure.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am definitely not in favor of erroring when registering 2 identical callbacks with different user data, because from a developer point of view, this is absolutely not an error and I can imagine scenarios where that could be useful.

Can you give an example of when this might be useful?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This a theoretical example, because I am not implementing it this way at the moment, but imagine for GLFW, you have 2 windows (represented in C by GLFWwindow pointers and canvases in HTML) and you need to set mouse move callbacks to track the mouse movements.

In order to detect moving the mouse outside the canvas while the button is clicked (something my library supports where others don't) you cannot set the callback on each canvas, it has to be set on the window... so the callbacks would be:

emscripten_set_mousemove_callback(WINDOW, glfwWindow1, true, handle_move_callback);
emscripten_set_mousemove_callback(WINDOW, glfwWindow2, true, handle_move_callback);

Again I am not doing it this way, but that could be a way to implement it...

Copy link
Collaborator

Choose a reason for hiding this comment

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

Fair enough. If we do want to officially support that then we probably also want to officially support unregistering them individually (sadly)

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) =>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -599,6 +634,8 @@ var LibraryHTML5 = {
allowsDeferredCalls: true,
#endif
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: wheelHandlerFunc,
useCapture
Expand Down Expand Up @@ -674,6 +711,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: uiEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -721,6 +760,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: focusEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -779,6 +820,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: deviceOrientationEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -850,6 +893,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: deviceMotionEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -936,6 +981,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: orientationChangeEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -1047,6 +1094,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: fullscreenChangeEventhandlerFunc,
useCapture
Expand Down Expand Up @@ -1548,6 +1597,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: pointerlockChangeEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -1592,6 +1643,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: pointerlockErrorEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -1746,6 +1799,8 @@ var LibraryHTML5 = {
var eventHandler = {
target,
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: visibilityChangeEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -1864,6 +1919,8 @@ var LibraryHTML5 = {
allowsDeferredCalls: eventTypeString == 'touchstart' || eventTypeString == 'touchend',
#endif
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: touchEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -1950,6 +2007,8 @@ var LibraryHTML5 = {
allowsDeferredCalls: true,
#endif
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: gamepadEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -2036,6 +2095,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: beforeUnloadEventHandlerFunc,
useCapture
Expand Down Expand Up @@ -2089,6 +2150,8 @@ var LibraryHTML5 = {
var eventHandler = {
target: battery,
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: batteryEventHandlerFunc,
useCapture
Expand Down
2 changes: 2 additions & 0 deletions src/lib/libhtml5_webgl.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,8 @@ var LibraryHtml5WebGL = {
var eventHandler = {
target: findEventTarget(target),
eventTypeString,
eventTypeId,
userData,
callbackfunc,
handlerFunc: webGlEventHandlerFunc,
useCapture
Expand Down
1 change: 1 addition & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions system/include/emscripten/html5.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
5 changes: 3 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": 245483,
"a.out.js": 245865,
"a.out.nodebug.wasm": 574039,
"total": 819522,
"total": 819904,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2675,6 +2675,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'],),
Expand Down
Loading