-
Notifications
You must be signed in to change notification settings - Fork 5
Description
While working on the event library, I was wondering if it was possible to pass in direct functions rather than registering one by name and then passing the returned id into callback functions. So, I came up with this small example of an approach that can do just that.
It's impossible (as far as I can tell) to pass functions from C and call them from JavaScript. However, that can be circumvented by having a function in C that can call it on behalf of JS. The main problem comes in passing the arguments. Originally, I wanted the functions to have a similar structure to the current implementation where each value is passed as its own argument. However, WASM doesn't support varadic (variable number) arguments. So, the only approach I could think of is to pass an array of the arguments along with the length. With that, I came up with the following implementation:
C
typedef void (CallbackFunc(int*, int));
__attribute__((export_name("callbackCaller")))
void callback_caller(CallbackFunc* callback_func, int* parameters, int len) {
callback_func(parameters, len);
free(parameters);
}
This implementation expects that parameters is allocated on the heap and will simply call the provided function pointer with the parameters list and length before freeing the parameters list and returning.
For example usage I made this:
C
__attribute__((import_name("testSingleShotTimer")))
void test_single_shot_timer(CallbackFunc* callback, int timeout_ms);
__attribute__((import_name("testMouseMovement")))
void test_mouse_movement(CallbackFunc* callback);
void print_hello(int* parameters, int len) {
printf("hello!\n");
test_single_shot_timer(print_hello, 1000);
}
void mouse_event(int* parameters, int len) {
assert(len == 2);
printf("(%d, %d)\n", parameters[0], parameters[1]);
}
__attribute__((export_name("test")))
void test() {
test_single_shot_timer(print_hello, 1000);
test_mouse_movement(mouse_event);
}
TS
testSingleShotTimer(mod: IWasmModule|IWasmModuleAsync, funcPointer: number, timeoutMS: number) {
setTimeout(
async () => {
await mod.callC(["callbackCaller", funcPointer, 0, 0])
},
timeoutMS
)
}
testMouseMovement(mod: IWasmModule|IWasmModuleAsync, funcPointer: number) {
const SIZE = 4*2; //two ints (x, y)
document.addEventListener("mousemove", async (e) => {
const listPointer = await mod.malloc(SIZE);
//insert (x, y) into the allocated list
mod.wasmMem.mem32.set([e.pageX, e.pageY], listPointer/4);
await mod.callC(["callbackCaller", funcPointer, listPointer, 2]);
})
}
The above code makes two example event handlers: timeout and mouse move. The first one just needs to call the function while the second one allocates and fills the parameter list with the x and y positions. Ideally, the malloc, list filling, and call to the callbackCaller function would be handled by a TS function, but I did it manually for this example.
Pros:
- Callback functions no longer need to be given a named export
- Easier to set up for the user since they can just pass the function directly
- Produces easier-to-read code since you can see and jump to the function rather than track an ID
Cons:
- Probably slower due to indirect function calls and list-filling
- Slightly more complex event handler functions since it uses a list instead of flat parameters
- Requires some form of manual memory management in TS to pass the parameter list
- The example approach I used used malloc which requires an additional WASM call
- Another approach could be to use some fixed buffer that's just filled though this would be more complicated since it needs to be able to handle being called multiple times before the first one returns
- As an example: while working on the event library, I've found that postEvent can immediately call the event listeners attached to it. So, an event can be called which calls the callback. From there, the callback could call some JS/TS code to post an event which then calls the callback handler function again without returning first.