Skip to content

jsaddle-wkwebview: registered ObjC callbacks + objc-in-library flag (GHCi support) - #170

Merged
hamishmack merged 2 commits into
masterfrom
hkm/wkwebview-objc-callbacks
Aug 2, 2026
Merged

hamishmack merged 2 commits into
masterfrom
hkm/wkwebview-objc-callbacks

Conversation

@hamishmack

Copy link
Copy Markdown
Member

Makes jsaddle-wkwebview usable from GHCi on toolchains where the RTS linker loads package archives (e.g. a static aarch64-darwin GHC). Two changes, one commit:

  1. The Objective-C 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.
  2. A new manual objc-in-library flag (default True) 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 with ghci -L<dir> -l<name>), with -objc-in-library keeping a second, dead copy out of the archive. But a dylib cannot name RTS-loaded foreign exports — hence (1): FunPtrs created with foreign 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. registerJSaddleCallbacks is idempotent (MVar-guarded) and called from both entry points — jsaddleMain' and run' — 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 be static there) and is declared in the new cbits/WKWebView-callbacks.h, which every ObjC file now includes instead of writing out extern declarations.

Testing

  • jsaddle-wkwebview builds clean on macOS/aarch64, GHC 9.14, both with the flag on (default) and with --flags=-objc-in-library.
  • The documented dylib recipe was run end to end: cbits-cocoa/WKWebView-AppDelegate.m compiles and links as a standalone dylib, exports jsaddle_wk_set_callbacks / jsaddleCallbacks, and has no undefined references to jsaddleStart / jsaddleResult / jsaddleSyncResult / callIO / callWith* — i.e. exactly the property that lets dyld load it without the Haskell side being linked in.
  • In daily use in leksah, both compiled (flag on) and under leksah.sh --ghci (flag off + preloaded dylib), where this is what makes an interpreted GUI session possible at all.
  • Not covered by CI: this repo's Haskell-CI is Linux-only, so it never compiles the wkwebview ObjC.

Only the include-app-delegate: False and iOS/UIKit paths are untested here — they are wired the same way (if flag(objc-in-library) around the cxx-sources, with -DUSE_COCOA/-DUSE_UIKIT deliberately left outside it so the CPP flags are unaffected by the new flag).

…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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 registerJSaddleCallbacks to build FunPtr wrappers for the Haskell callbacks and register them with Objective-C via jsaddle_wk_set_callbacks.
  • Introduce and wire a new manual Cabal flag objc-in-library (default True) to conditionally compile Objective-C sources into the library.
  • Replace direct extern calls in ObjC sources with calls through the jsaddleCallbacks table (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.

Comment on lines +5 to 8
// 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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@hamishmack
hamishmack merged commit ea49142 into master Aug 2, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants