webview-zig
webview-zig copied to clipboard
Callback function pointers do not allow for multiple callbacks
Description
From my understanding of Zig, struct types inside functions are only created once per distinct comptime
arguments. In other words, the struct type created in each call to dispatch
and bind
is actually the same every time. This means the same callback
global is overwritten on each call when using function pointers -- the latest dispatch
/bind
call replaces the function pointer given in previous calls.
This means you cannot dispatch/bind two different function pointers at once.
Example
const std = @import("std");
const print = std.log.debug;
const WebView = @import("webview").WebView;
pub fn main() !void {
const webview = WebView.create(true, null);
defer {
webview.terminate();
webview.destroy();
}
webview.dispatch(&callback1, null);
webview.dispatch(&callback2, null);
webview.bind("binding1", &binding1, null);
webview.bind("binding2", &binding2, null);
webview.init(
\\binding1();
\\binding2();
);
webview.setHtml(""); // needed to run javascript
webview.run();
}
pub fn callback1(_: WebView, _: ?*anyopaque) void {
print("callback1", .{});
}
pub fn callback2(_: WebView, _: ?*anyopaque) void {
print("callback2", .{});
}
pub fn binding1(_: [:0]const u8, _: [:0]const u8, _: ?*anyopaque) void {
print("binding1", .{});
}
pub fn binding2(_: [:0]const u8, _: [:0]const u8, _: ?*anyopaque) void {
print("binding2", .{});
}
Expected output
debug: callback1
debug: callback2
debug: binding1
debug: binding2
Actual output
debug: callback2
debug: callback2
debug: binding2
debug: binding2
Notice that this doesn't happen when binding the functions directly, only with function pointers, because passing in a function creates a new struct type that doesn't use a global. But function pointers are the only option when callbacks are determined at runtime, so this issue can still get in the way.
Potential Fixes
1 - Limit functionality
The easiest fix would be to remove support for function pointer callbacks, or to only allow "raw" function pointers passed directly to the WebView API.
Pros:
- Easy fix
- Gives the user full control
Cons:
- Puts more work on the user to define functions for the C API.
2 - Use arg
as the function pointer
We could pass the function pointer as the arg
to webview_dispatch
/webview_bind
. Then when function
is called, it would cast arg
back to a function pointer and call it.
Pros:
- Easy fix
Cons:
- Removes the user's ability to use
arg
- Would create a discrepancy with non-pointer callbacks, as
arg
can still be used there
- Would create a discrepancy with non-pointer callbacks, as
3 - Extend the user's arg
to store the function pointer
We could create a struct, say WrappedArg
, that stores both the function pointer and the user-provided argument, the address of which is passed as the arg
to the API, allowing the user to still use their own arg
.
However, this WrappedArg
would have to exist as long as the callback is in use. For example, it can't be created in bind
's stack frame, because that address would be invalid by the time the callback is used. This means the user would either have to provide an Allocator
or a location to store the WrappedArg
. Either way, the user would be responsible for memory management.
Pros:
- Keeps all functionality that the API should have
Cons:
- Puts responsibility on the user to manage memory
- Might complicate the API for users
4 - Provide higher-level abstraction
Going a bit off-topic, this wrapper is pretty thin -- there's not much difference to using the C API directly. A higher-level abstraction could provide much more:
- Start webview in another thread
- Manage callbacks
- Similar to option 3 + storing the
Allocator
inWebView
- Possibly remove
?*anyopaque
in favor of generics?- Could stop double indirection when user's
arg
is a pointer to another object
- Could stop double indirection when user's
- Translating to/from JSON so any normal Zig function can seamlessly bound to JS would be great
- Similar to option 3 + storing the
- Translate
webview_error_t
to Zig error unions
Pros:
- Would be a huge step up from a thin wrapper
Cons:
- Would be a lot of work to design and implement well
It's up to you what you want to go with. I think I'll take a crack at a higher-level abstraction in my fork to see where that leads, because I'm already doing something similar for a project I'm working on. I'll let you know if I get anywhere with it!