Repository navigation
jsaddle-wkwebview: registered ObjC callbacks + objc-in-library flag (GHCi support) - #170
Conversation
…orts The Objective-C now calls back into Haskell through function pointers registered at runtime (jsaddle_wk_set_callbacks, driven by the new Internal.registerJSaddleCallbacks) instead of referencing the foreign exports as extern symbols, and a new manual objc-in-library cabal flag (default True) can exclude the ObjC from the compiled library entirely. Together these make the package usable from GHCi on toolchains where the RTS linker loads package archives (e.g. static aarch64-darwin GHC): the RTS linker can load ObjC objects but never registers their classes with the ObjC runtime — only dyld does — so such a session builds the ObjC as a dylib (cc -dynamiclib cbits-cocoa/WKWebView-AppDelegate.m -DUSE_COCOA -Icbits ...) and preloads it with ghci -L/-l, with -objc-in-library keeping a second (dead) copy out of the archive. A dylib cannot name RTS-loaded foreign exports, but FunPtrs made with foreign import ccall "wrapper" are callable from both worlds — and behave identically in an ordinarily compiled application, where the flag stays on and nothing else changes. The foreign exports are kept for compatibility.
There was a problem hiding this comment.
Pull request overview
This PR updates jsaddle-wkwebview to support GHCi on toolchains where the RTS linker loads package archives by switching the Objective-C→Haskell callback mechanism from dyld-visible extern foreign exports to a runtime-registered callback table of FunPtrs, and by adding an objc-in-library Cabal flag to optionally keep Objective-C out of the built library archive.
Changes:
- Add
registerJSaddleCallbacksto buildFunPtrwrappers for the Haskell callbacks and register them with Objective-C viajsaddle_wk_set_callbacks. - Introduce and wire a new manual Cabal flag
objc-in-library(defaultTrue) to conditionally compile Objective-C sources into the library. - Replace direct
externcalls in ObjC sources with calls through thejsaddleCallbackstable (with null-guards) and add the shared callbacks header.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| jsaddle-wkwebview/src/Language/Javascript/JSaddle/WKWebView/Internal.hs | Adds registerJSaddleCallbacks and registers callback FunPtrs before callbacks can fire. |
| jsaddle-wkwebview/src-ghc/Language/Javascript/JSaddle/WKWebView.hs | Ensures callbacks are registered before runInWKWebView can trigger them. |
| jsaddle-wkwebview/jsaddle-wkwebview.cabal | Adds objc-in-library flag and gates ObjC compilation on it. |
| jsaddle-wkwebview/cbits/WKWebView-cbits.m | Defines jsaddleCallbacks table + jsaddle_wk_set_callbacks and routes delegate callbacks through the table. |
| jsaddle-wkwebview/cbits/WKWebView-callbacks.h | Introduces shared callback-table struct declaration and extern jsaddleCallbacks. |
| jsaddle-wkwebview/cbits-uikit/ViewController.m | Switches to callback-table invocation for callWithWebView. |
| jsaddle-wkwebview/cbits-uikit/AppDelegate.m | Switches to callback-table invocation for various app delegate callbacks (null-guarded). |
| jsaddle-wkwebview/cbits-cocoa/AppDelegate.m | Switches to callback-table invocation for Cocoa app delegate callbacks (null-guarded). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // The Haskell callbacks come through the registered jsaddleCallbacks table | ||
| // (see cbits/WKWebView-callbacks.h; the table is defined in | ||
| // WKWebView-cbits.m, #included into this translation unit before this file). | ||
|
|
There was a problem hiding this comment.
Fixed in cd7b5b7 — cbits-cocoa/AppDelegate.m now #includes ../cbits/WKWebView-callbacks.h itself, like the UIKit sources already did. The header is #pragma once, so the composite translation unit including it twice costs nothing.
Verified: clang -fsyntax-only on cbits-cocoa/AppDelegate.m alone now exits 0 (it failed before), the library still builds with the flag on and off, and the composite dylib still links with no undefined references to the foreign exports.
…egate.m It used jsaddleCallbacks while relying on the composite translation unit #including WKWebView-cbits.m (and so the header) ahead of it, which made the file dependent on include order and impossible to compile on its own. The header is #pragma once, so including it directly costs nothing — and matches what the UIKit sources already do.
Makes
jsaddle-wkwebviewusable from GHCi on toolchains where the RTS linker loads package archives (e.g. a static aarch64-darwin GHC). Two changes, one commit:jsaddle_wk_set_callbacks, driven by the newInternal.registerJSaddleCallbacks) instead of referencing the foreign exports asexternsymbols.objc-in-libraryflag (defaultTrue) that can keep the ObjC out of the compiled library entirely.Why
Objective-C classes are only registered with the ObjC runtime when their image is loaded by dyld. GHC's RTS linker can load ObjC object files but never registers their classes — so in a GHCi session that loads this package's archive, the delegate classes simply don't exist and nothing works.
The fix is to load the ObjC as a dylib instead (
cc -dynamiclib cbits-cocoa/WKWebView-AppDelegate.m -DUSE_COCOA -Icbits …, preloaded withghci -L<dir> -l<name>), with-objc-in-librarykeeping a second, dead copy out of the archive. But a dylib cannot name RTS-loaded foreign exports — hence (1):FunPtrs created withforeign import ccall "wrapper"are callable from both worlds.What this means for existing users
Nothing changes. The flag defaults to
True, so an ordinarily compiled application builds exactly as before; the foreign exports are kept for compatibility; and the FunPtrs behave identically to the direct calls they replace.registerJSaddleCallbacksis idempotent (MVar-guarded) and called from both entry points —jsaddleMain'andrun'— before anything can fire a callback. Every call site is null-guarded, so an unregistered table is a no-op rather than a crash into a null pointer.The callbacks struct lives in
WKWebView-cbits.m(one definition; the iOS build compiles its sources as separate translation units, so it can't bestaticthere) and is declared in the newcbits/WKWebView-callbacks.h, which every ObjC file now includes instead of writing outexterndeclarations.Testing
jsaddle-wkwebviewbuilds clean on macOS/aarch64, GHC 9.14, both with the flag on (default) and with--flags=-objc-in-library.cbits-cocoa/WKWebView-AppDelegate.mcompiles and links as a standalone dylib, exportsjsaddle_wk_set_callbacks/jsaddleCallbacks, and has no undefined references tojsaddleStart/jsaddleResult/jsaddleSyncResult/callIO/callWith*— i.e. exactly the property that lets dyld load it without the Haskell side being linked in.leksah.sh --ghci(flag off + preloaded dylib), where this is what makes an interpreted GUI session possible at all.Only the
include-app-delegate: Falseand iOS/UIKit paths are untested here — they are wired the same way (if flag(objc-in-library)around thecxx-sources, with-DUSE_COCOA/-DUSE_UIKITdeliberately left outside it so the CPP flags are unaffected by the new flag).