I am trying to use WebKitGtk from Swift on Linux.
Gtk requires a run loop running. So my idea was, let's create a global actor that runs the run loop and bind all wrapper classes for WebKit to that global actor. So far so good, but when using callback from C I need to convince the compiler that I am indeed on the correct custom executor.
// the global actor
@globalActor
actor GtkMainActor: GlobalActor {
static let shared = GtkMainActor.run()
nonisolated var unownedExecutor: UnownedSerialExecutor {
self.executor.asUnownedSerialExecutor()
}
let executor: GlibExecutor
private init(executor: GlibExecutor) {
self.executor = executor
}
static func run() -> Self {
let gtkQueue = DispatchQueue(label: "gtk-queue")
gtkQueue.async {
gtk_init(nil, nil)
let loop = g_main_loop_new(nil, 0)
print("Gtk loop created")
g_main_loop_run(loop)
print("Gtk loop finished")
g_main_loop_unref(loop)
}
return Self(executor: GlibExecutor(queue: gtkQueue))
}
}
// the custom executor that dispatches to the run loop
class GlibContext {
var job: UnownedJob!
var executor: UnownedSerialExecutor!
}
final class GlibExecutor: SerialExecutor {
let queue: DispatchQueue
init(queue: DispatchQueue) {
self.queue = queue
}
func checkIsolated() {
dispatchPrecondition(condition: .onQueue(queue))
}
func enqueue(_ job: consuming ExecutorJob) {
let context = GlibContext()
context.job = UnownedJob(job)
context.executor = self.asUnownedSerialExecutor()
let unmanaged = Unmanaged.passRetained(context)
let job : @convention(c) (UnsafeMutableRawPointer?) -> gboolean = { data in
let ctx = Unmanaged<GlibContext>.fromOpaque(data!).takeRetainedValue()
ctx.job!.runSynchronously(on: ctx.executor)
return G_SOURCE_REMOVE
}
g_idle_add(job, unmanaged.toOpaque())
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
return UnownedSerialExecutor(ordinary: self)
}
}
Now, webkit uses callbacks, those callbacks must be @convention(c)
of course, so I can't make them @GtkMainActor
, but obviously they will always be called from that queue/thread.
@GtkMainActor
class WebBrowser {
var view: UnsafeMutablePointer<WebKitWebView>!
var loadChangedHandler: gulong = 0
var onLoadFinished: ((String) -> Void)?
init() {
self.view = shim_webkit_web_view_new()
guard let view = self.view else {
fatalError("Failed to create web view")
}
self.loadChangedHandler = shim_webkit_web_view_connect_load_changed(view, loadChanged, Unmanaged.passUnretained(self).toOpaque())
}
deinit {
g_signal_handler_disconnect(self.view, self.loadChangedHandler)
g_object_unref(self.view)
print("WebBrowser deinit")
}
func loadUrl(url: String) {
url.withCString { cstring in
webkit_web_view_load_uri(self.view, cstring)
}
}
}
class Context {
let continuation : CheckedContinuation<String, Error>
init(continuation: CheckedContinuation<String, Error>) {
self.continuation = continuation
}
}
func loadChanged(source: UnsafeMutablePointer<WebKitWebView>!, event: WebKitLoadEvent, user_data: UnsafeMutableRawPointer!) {
let webView = Unmanaged<WebBrowser>.fromOpaque(user_data).takeUnretainedValue()
switch event {
case WEBKIT_LOAD_STARTED:
print("Load started")
case WEBKIT_LOAD_REDIRECTED:
print("Load redirected")
case WEBKIT_LOAD_COMMITTED:
print("Load committed")
case WEBKIT_LOAD_FINISHED:
print("Load finished")
GtkMainActor.shared.assumeIsolated { actor in
webView.onLoadFinished?(webView.uri!)
// !!! Error: Global actor 'GtkMainActor'-isolated
// property 'onLoadFinished' can not be referenced
// from a nonisolated context
}
default: fatalError("Unknown event: \(event)")
}
}
Here the relevant snippet from the last function:
let webview: WebBrowser = β¦
GtkMainActor.shared.assumeIsolated { actor in
webView.onLoadFinished?(webView.uri!)
// !!! Error: Global actor 'GtkMainActor'-isolated
// property 'onLoadFinished' can not be referenced
// from a nonisolated context
}
I don't understand why I am getting this error. Am I not telling the compiler: "I know we are on the correct actor, crash at runtime if I am wrong"?
I read through the various proposal around actors and serial executors, but I feel like I am misunderstanding something fundamental here...