From 8ea768493731d3b3483e303e8592d3fc8163c9b0 Mon Sep 17 00:00:00 2001 From: Hamish Mackenzie Date: Sat, 18 Jul 2026 12:22:40 +1200 Subject: [PATCH 1/2] jsaddle-wkwebview: registered callbacks instead of extern foreign exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- jsaddle-wkwebview/cbits-cocoa/AppDelegate.m | 23 +++--- jsaddle-wkwebview/cbits-uikit/AppDelegate.m | 30 ++++---- .../cbits-uikit/ViewController.m | 5 +- jsaddle-wkwebview/cbits/WKWebView-callbacks.h | 32 +++++++++ jsaddle-wkwebview/cbits/WKWebView-cbits.m | 37 ++++++++-- jsaddle-wkwebview/jsaddle-wkwebview.cabal | 41 ++++++++--- .../Language/Javascript/JSaddle/WKWebView.hs | 6 +- .../Javascript/JSaddle/WKWebView/Internal.hs | 70 ++++++++++++++++++- 8 files changed, 197 insertions(+), 47 deletions(-) create mode 100644 jsaddle-wkwebview/cbits/WKWebView-callbacks.h diff --git a/jsaddle-wkwebview/cbits-cocoa/AppDelegate.m b/jsaddle-wkwebview/cbits-cocoa/AppDelegate.m index 543791f4..70db3e7c 100644 --- a/jsaddle-wkwebview/cbits-cocoa/AppDelegate.m +++ b/jsaddle-wkwebview/cbits-cocoa/AppDelegate.m @@ -2,10 +2,9 @@ #import #import -extern void callIO(HsStablePtr); -extern bool callWithCStringReturningBool(const char * _Nonnull, HsStablePtr); -extern void callWithCString(const char * _Nonnull, HsStablePtr); -extern void callWithWebView(WKWebView *, HsStablePtr); +// 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). @interface AppDelegate : NSObject @property (nonatomic, assign) IBOutlet NSWindow *window; @@ -41,7 +40,9 @@ -(id)initApp:(HsStablePtr)handler progName:(NSString *)progName { } -(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename { - bool b = callWithCStringReturningBool([filename UTF8String], global_applicationOpenFile); + bool b = jsaddleCallbacks.callWithCStringReturningBool + ? jsaddleCallbacks.callWithCStringReturningBool([filename UTF8String], global_applicationOpenFile) + : false; return b ? YES : NO; } @@ -52,8 +53,10 @@ -(void)applicationWillFinishLaunching:(NSNotification *)notification { } WKWebView *webView = [[WKWebView alloc] initWithFrame: [_window.contentView frame] configuration:theConfiguration]; [_window setContentView:webView]; - callWithWebView(webView, _handler); - callIO(global_willFinishLaunchingWithOptions); + if (jsaddleCallbacks.callWithWebView) + jsaddleCallbacks.callWithWebView((void *)webView, _handler); + if (jsaddleCallbacks.callIO) + jsaddleCallbacks.callIO(global_willFinishLaunchingWithOptions); // Listen to URL events NSAppleEventManager *manager = [NSAppleEventManager sharedAppleEventManager]; [manager @@ -65,14 +68,16 @@ -(void)applicationWillFinishLaunching:(NSNotification *)notification { - (void)applicationUniversalLink:(NSAppleEventDescriptor *)event { NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; - callWithCString([url UTF8String], global_applicationUniversalLink); + if (jsaddleCallbacks.callWithCString) + jsaddleCallbacks.callWithCString([url UTF8String], global_applicationUniversalLink); } -(void)applicationDidFinishLaunching:(NSNotification *)notification { [_window orderFrontRegardless]; [_window center]; [NSApp activateIgnoringOtherApps:YES]; - callIO(global_didFinishLaunchingWithOptions); + if (jsaddleCallbacks.callIO) + jsaddleCallbacks.callIO(global_didFinishLaunchingWithOptions); } -(BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)app { diff --git a/jsaddle-wkwebview/cbits-uikit/AppDelegate.m b/jsaddle-wkwebview/cbits-uikit/AppDelegate.m index 4fe856d7..b4ea1bde 100644 --- a/jsaddle-wkwebview/cbits-uikit/AppDelegate.m +++ b/jsaddle-wkwebview/cbits-uikit/AppDelegate.m @@ -3,9 +3,9 @@ #import #import -extern void callIO(HsStablePtr); -extern void callWithCString(const char * _Nonnull, HsStablePtr); -extern void callWithCIntCString(int n, const char * _Nonnull, HsStablePtr); +// The Haskell callbacks come through the registered jsaddleCallbacks table +// (see cbits/WKWebView-callbacks.h for why they are not extern symbols). +#include "../cbits/WKWebView-callbacks.h" @interface AppDelegate () @@ -35,7 +35,7 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Tells the delegate that the launch process has begun but that state restoration has not yet occurred. - callIO(global_willFinishLaunchingWithOptions); + if (jsaddleCallbacks.callIO) jsaddleCallbacks.callIO(global_willFinishLaunchingWithOptions); return YES; } @@ -70,45 +70,45 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [application registerForRemoteNotifications]; } } - callIO(global_didFinishLaunchingWithOptions); + if (jsaddleCallbacks.callIO) jsaddleCallbacks.callIO(global_didFinishLaunchingWithOptions); return YES; } - (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. - callIO(global_applicationWillResignActive); + if (jsaddleCallbacks.callIO) jsaddleCallbacks.callIO(global_applicationWillResignActive); } - (void)applicationDidEnterBackground:(UIApplication *)application { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - callIO(global_applicationDidEnterBackground); + if (jsaddleCallbacks.callIO) jsaddleCallbacks.callIO(global_applicationDidEnterBackground); } - (void)applicationWillEnterForeground:(UIApplication *)application { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. - callIO(global_applicationWillEnterForeground); + if (jsaddleCallbacks.callIO) jsaddleCallbacks.callIO(global_applicationWillEnterForeground); } - (void)applicationDidBecomeActive:(UIApplication *)application { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - callIO(global_applicationDidBecomeActive); + if (jsaddleCallbacks.callIO) jsaddleCallbacks.callIO(global_applicationDidBecomeActive); } - (void)applicationWillTerminate:(UIApplication *)application { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - callIO(global_applicationWillTerminate); + if (jsaddleCallbacks.callIO) jsaddleCallbacks.callIO(global_applicationWillTerminate); } - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { // Tells the delegate that the app successfully registered with Apple Push Notification service (APNs). NSString *deviceTokenString = [deviceToken base64EncodedStringWithOptions: 0]; - callWithCString([deviceTokenString UTF8String], global_didRegisterForRemoteNotificationsWithDeviceToken); + if (jsaddleCallbacks.callWithCString) jsaddleCallbacks.callWithCString([deviceTokenString UTF8String], global_didRegisterForRemoteNotificationsWithDeviceToken); } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { @@ -121,7 +121,7 @@ - (void)application:(UIApplication *)application didReceiveRemoteNotification:(N NSData *jsonData = [NSJSONSerialization dataWithJSONObject:userInfo options:0 error:&error]; if (jsonData) { NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - callWithCIntCString((int) application.applicationState, [jsonString UTF8String], global_applicationDidReceiveRemoteNotification); + if (jsaddleCallbacks.callWithCIntCString) jsaddleCallbacks.callWithCIntCString((int) application.applicationState, [jsonString UTF8String], global_applicationDidReceiveRemoteNotification); } else { NSLog(@"jsaddle-wkwebview didReceiveRemoteNotification handler: error while serialising push notification as JSON"); } @@ -131,19 +131,19 @@ - (void)application:(UIApplication *)application didReceiveRemoteNotification:(N - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { // Sent to the delegate when Apple Push Notification service cannot successfully complete the registration process. NSString *errorString = [error localizedDescription]; - callWithCString([errorString UTF8String], global_didFailToRegisterForRemoteNotificationsWithError); + if (jsaddleCallbacks.callWithCString) jsaddleCallbacks.callWithCString([errorString UTF8String], global_didFailToRegisterForRemoteNotificationsWithError); } - (void)applicationSignificantTimeChange:(UIApplication *)application { // Tells the delegate when there is a significant change in the time. - callIO(global_applicationSignificantTimeChange); + if (jsaddleCallbacks.callIO) jsaddleCallbacks.callIO(global_applicationSignificantTimeChange); } - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler { // TODO: Reroute universal links when they're clicked in-app. // https://developer.apple.com/reference/webkit/wknavigationdelegate/1455643-webview?language=objc if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb] && userActivity.webpageURL) { - callWithCString([userActivity.webpageURL.absoluteString UTF8String], global_applicationUniversalLink); + if (jsaddleCallbacks.callWithCString) jsaddleCallbacks.callWithCString([userActivity.webpageURL.absoluteString UTF8String], global_applicationUniversalLink); return YES; } return NO; diff --git a/jsaddle-wkwebview/cbits-uikit/ViewController.m b/jsaddle-wkwebview/cbits-uikit/ViewController.m index 9d91e684..2624df6b 100644 --- a/jsaddle-wkwebview/cbits-uikit/ViewController.m +++ b/jsaddle-wkwebview/cbits-uikit/ViewController.m @@ -1,6 +1,7 @@ #import "ViewController.h" -extern void callWithWebView(WKWebView *, HsStablePtr); +// See cbits/WKWebView-callbacks.h: Haskell callbacks are registered, not extern. +#include "../cbits/WKWebView-callbacks.h" @interface ViewController () @@ -27,7 +28,7 @@ - (void)loadView { - (void)viewDidLoad { [super viewDidLoad]; - callWithWebView(_webView, _handler); + if (jsaddleCallbacks.callWithWebView) jsaddleCallbacks.callWithWebView((void *)_webView, _handler); } diff --git a/jsaddle-wkwebview/cbits/WKWebView-callbacks.h b/jsaddle-wkwebview/cbits/WKWebView-callbacks.h new file mode 100644 index 00000000..43fbf108 --- /dev/null +++ b/jsaddle-wkwebview/cbits/WKWebView-callbacks.h @@ -0,0 +1,32 @@ +// Haskell callbacks for the jsaddle-wkwebview Objective-C. +// +// These are registered at runtime by the Haskell side +// (jsaddle_wk_set_callbacks, called from +// Language.Javascript.JSaddle.WKWebView.Internal.registerJSaddleCallbacks +// before anything can fire a callback) rather than linked as extern foreign +// exports. On toolchains where GHCi's RTS linker loads this package's +// archive, the foreign exports are not dyld-visible — and the ObjC, which +// must then be loaded by dyld for its classes to be registered with the +// Objective-C runtime (e.g. compiled into a dylib and preloaded with +// ghci -L -l; see the objc-in-library cabal flag), could not +// reference them by name. Function pointers created with +// `foreign import ccall "wrapper"` are callable from both worlds, and +// behave identically in an ordinarily compiled application. +#pragma once +#include "HsFFI.h" +#include + +typedef struct { + void (*jsaddleStart)(HsStablePtr); + void (*jsaddleResult)(HsStablePtr, const char * _Nonnull); + void (*jsaddleSyncResult)(HsStablePtr, void * _Nonnull handler, const char * _Nonnull); + void (*callIO)(HsStablePtr); + void (*callWithCString)(const char * _Nonnull, HsStablePtr); + bool (*callWithCStringReturningBool)(const char * _Nonnull, HsStablePtr); + void (*callWithWebView)(void * _Nonnull webView, HsStablePtr); + void (*callWithCIntCString)(int, const char * _Nonnull, HsStablePtr); +} jsaddle_wk_callbacks; + +// Defined in WKWebView-cbits.m (one definition; the iOS build compiles the +// sources as separate translation units, so it cannot be static there). +extern jsaddle_wk_callbacks jsaddleCallbacks; diff --git a/jsaddle-wkwebview/cbits/WKWebView-cbits.m b/jsaddle-wkwebview/cbits/WKWebView-cbits.m index 46f50c20..6149c022 100644 --- a/jsaddle-wkwebview/cbits/WKWebView-cbits.m +++ b/jsaddle-wkwebview/cbits/WKWebView-cbits.m @@ -15,9 +15,31 @@ - (instancetype)initHandler:(HsStablePtr)startHandler resultHandler:(HsStablePtr @end -extern void jsaddleStart(HsStablePtr); -extern void jsaddleResult(HsStablePtr, const char * _Nonnull result); -extern void jsaddleSyncResult(HsStablePtr, JSaddleHandler *, const char * _Nonnull result); +// The Haskell callbacks come through the registered jsaddleCallbacks table — +// see WKWebView-callbacks.h for why they are not extern symbol references. +#include "WKWebView-callbacks.h" + +jsaddle_wk_callbacks jsaddleCallbacks; // zero-initialised + +void jsaddle_wk_set_callbacks( + void (*jsaddleStart_)(HsStablePtr), + void (*jsaddleResult_)(HsStablePtr, const char * _Nonnull), + void (*jsaddleSyncResult_)(HsStablePtr, void * _Nonnull, const char * _Nonnull), + void (*callIO_)(HsStablePtr), + void (*callWithCString_)(const char * _Nonnull, HsStablePtr), + bool (*callWithCStringReturningBool_)(const char * _Nonnull, HsStablePtr), + void (*callWithWebView_)(void * _Nonnull, HsStablePtr), + void (*callWithCIntCString_)(int, const char * _Nonnull, HsStablePtr)) +{ + jsaddleCallbacks.jsaddleStart = jsaddleStart_; + jsaddleCallbacks.jsaddleResult = jsaddleResult_; + jsaddleCallbacks.jsaddleSyncResult = jsaddleSyncResult_; + jsaddleCallbacks.callIO = callIO_; + jsaddleCallbacks.callWithCString = callWithCString_; + jsaddleCallbacks.callWithCStringReturningBool = callWithCStringReturningBool_; + jsaddleCallbacks.callWithWebView = callWithWebView_; + jsaddleCallbacks.callWithCIntCString = callWithCIntCString_; +} @implementation JSaddleHandler @@ -34,13 +56,15 @@ - (instancetype)initHandler:(HsStablePtr)startHandler resultHandler:(HsStablePtr - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { - jsaddleStart(self.startHandler); + if (jsaddleCallbacks.jsaddleStart) + jsaddleCallbacks.jsaddleStart(self.startHandler); } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { NSString * s = (NSString *) message.body; - jsaddleResult(self.resultHandler, [s UTF8String]); + if (jsaddleCallbacks.jsaddleResult) + jsaddleCallbacks.jsaddleResult(self.resultHandler, [s UTF8String]); } - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt @@ -48,7 +72,8 @@ - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSSt { if([prompt isEqualToString:@"JSaddleSync"]) { _completionHandler = completionHandler; - jsaddleSyncResult(self.syncHandler, self, [defaultText UTF8String]); + if (jsaddleCallbacks.jsaddleSyncResult) + jsaddleCallbacks.jsaddleSyncResult(self.syncHandler, (void *)self, [defaultText UTF8String]); } else { // TODO decide what if anything should go here diff --git a/jsaddle-wkwebview/jsaddle-wkwebview.cabal b/jsaddle-wkwebview/jsaddle-wkwebview.cabal index e63df005..0e6c8a07 100644 --- a/jsaddle-wkwebview/jsaddle-wkwebview.cabal +++ b/jsaddle-wkwebview/jsaddle-wkwebview.cabal @@ -17,15 +17,30 @@ tested-with: GHC==9.12.2, GHC==9.10.1, GHC==9.8.4, GHC==9.6.7, GHC==9.4.8, GHC== -- #included by cbits-cocoa/WKWebView-AppDelegate.m (the single Cocoa -- translation unit) rather than compiled directly, so they must be --- shipped explicitly. +-- shipped explicitly. (WKWebView-callbacks.h is #included everywhere.) extra-source-files: cbits/WKWebView-cbits.m + cbits/WKWebView-callbacks.h cbits-cocoa/AppDelegate.m flag include-app-delegate description: Include default AppDelegate C sources. default: True +flag objc-in-library + description: + Compile the Objective-C into the Haskell library (the default). + Disable for GHCi sessions on toolchains where the RTS linker loads + this library's archive: Objective-C classes are only registered with + the runtime when their image is loaded by dyld, so such a session + must instead preload the ObjC compiled as a dylib (e.g. + cc -dynamiclib cbits-cocoa/WKWebView-AppDelegate.m -DUSE_COCOA + -Icbits ... and ghci -L -l); the Haskell side then + reaches it through dyld, and it calls back in through the FunPtrs + registered by registerJSaddleCallbacks. + default: True + manual: True + source-repository head type: git location: https://github.com/ghcjs/jsaddle @@ -67,14 +82,16 @@ library if os(ios) frameworks: UIKit, UserNotifications if flag(include-app-delegate) - cxx-sources: - cbits/WKWebView-cbits.m - cbits-uikit/AppDelegate.m - cbits-uikit/ViewController.m + if flag(objc-in-library) + cxx-sources: + cbits/WKWebView-cbits.m + cbits-uikit/AppDelegate.m + cbits-uikit/ViewController.m cpp-options: -DUSE_UIKIT else - cxx-sources: - cbits/WKWebView-cbits.m + if flag(objc-in-library) + cxx-sources: + cbits/WKWebView-cbits.m else frameworks: Cocoa if flag(include-app-delegate) @@ -84,9 +101,11 @@ library -- rejects the duplicate ObjC protocol metadata every ObjC -- object emits, so the package's ObjC code must form one -- archive member for dependents' TH splices to load it. - cxx-sources: - cbits-cocoa/WKWebView-AppDelegate.m + if flag(objc-in-library) + cxx-sources: + cbits-cocoa/WKWebView-AppDelegate.m cpp-options: -DUSE_COCOA else - cxx-sources: - cbits/WKWebView-cbits.m + if flag(objc-in-library) + cxx-sources: + cbits/WKWebView-cbits.m diff --git a/jsaddle-wkwebview/src-ghc/Language/Javascript/JSaddle/WKWebView.hs b/jsaddle-wkwebview/src-ghc/Language/Javascript/JSaddle/WKWebView.hs index 7cdd8229..03c01628 100644 --- a/jsaddle-wkwebview/src-ghc/Language/Javascript/JSaddle/WKWebView.hs +++ b/jsaddle-wkwebview/src-ghc/Language/Javascript/JSaddle/WKWebView.hs @@ -22,7 +22,8 @@ import qualified Data.Set as Set import Data.Word import Foreign.Marshal.Utils (fromBool) import Language.Javascript.JSaddle.WKWebView.Internal - (jsaddleMain, jsaddleMainHTMLWithBaseURL, jsaddleMainFile, WKWebView(..), mainBundleResourcePath) + (jsaddleMain, jsaddleMainHTMLWithBaseURL, jsaddleMainFile, WKWebView(..), + mainBundleResourcePath, registerJSaddleCallbacks) import System.Environment (getProgName) import Foreign.C.String (CString, withCString) import Foreign.C.Types (CBool, CInt) @@ -163,6 +164,9 @@ run' :: AppDelegateConfig -> (WKWebView -> IO ()) -> IO () run' cfg main = do + -- The ObjC side calls Haskell through registered FunPtrs (see Internal); + -- register before runInWKWebView can fire any of them. + registerJSaddleCallbacks handler <- newStablePtr main progName <- getProgName diff --git a/jsaddle-wkwebview/src/Language/Javascript/JSaddle/WKWebView/Internal.hs b/jsaddle-wkwebview/src/Language/Javascript/JSaddle/WKWebView/Internal.hs index 06f3bb4e..99f9e8ff 100644 --- a/jsaddle-wkwebview/src/Language/Javascript/JSaddle/WKWebView/Internal.hs +++ b/jsaddle-wkwebview/src/Language/Javascript/JSaddle/WKWebView/Internal.hs @@ -5,11 +5,13 @@ module Language.Javascript.JSaddle.WKWebView.Internal , jsaddleMainFile , WKWebView(..) , mainBundleResourcePath + , registerJSaddleCallbacks ) where -import Control.Monad (void, join) +import Control.Monad (void, join, unless) import Control.Concurrent (forkIO, forkOS) -import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar, putMVar, takeMVar) +import Control.Concurrent.MVar + (MVar, modifyMVar_, newEmptyMVar, newMVar, putMVar, takeMVar) import System.IO.Unsafe (unsafePerformIO) @@ -23,7 +25,7 @@ import Data.Text.Encoding (encodeUtf8) import Foreign.C.String (CString) import Foreign.C.Types (CBool(..), CInt(..)) -import Foreign.Ptr (Ptr, nullPtr) +import Foreign.Ptr (Ptr, FunPtr, nullPtr) import Foreign.StablePtr (StablePtr, newStablePtr, deRefStablePtr) import Language.Javascript.JSaddle (Results, Batch, JSM) @@ -53,6 +55,67 @@ foreign export ccall callWithCIntCString :: CInt -> CString -> StablePtr (CInt - foreign export ccall callWithCString :: CString -> StablePtr (CString -> IO ()) -> IO () foreign export ccall callWithCStringReturningBool :: CString -> StablePtr (CString -> IO CBool) -> IO CBool foreign export ccall callIO :: StablePtr (IO ()) -> IO () + +-- The Objective-C side calls back into Haskell through function pointers +-- registered with 'registerJSaddleCallbacks' rather than through the foreign +-- exports above (kept for compatibility): on toolchains where GHCi's RTS +-- linker loads this package's archive, the exports are not dyld-visible, so +-- ObjC compiled into a dylib (necessary there for its classes to be +-- registered with the ObjC runtime; see the objc-in-library cabal flag) +-- could not reference them. See cbits/WKWebView-callbacks.h. +foreign import ccall "wrapper" mkStablePtrIOCb + :: (StablePtr (IO ()) -> IO ()) + -> IO (FunPtr (StablePtr (IO ()) -> IO ())) +foreign import ccall "wrapper" mkResultCb + :: (StablePtr (Results -> IO ()) -> CString -> IO ()) + -> IO (FunPtr (StablePtr (Results -> IO ()) -> CString -> IO ())) +foreign import ccall "wrapper" mkSyncResultCb + :: (StablePtr (Results -> IO Batch) -> JSaddleHandler -> CString -> IO ()) + -> IO (FunPtr (StablePtr (Results -> IO Batch) -> JSaddleHandler -> CString -> IO ())) +foreign import ccall "wrapper" mkCStringCb + :: (CString -> StablePtr (CString -> IO ()) -> IO ()) + -> IO (FunPtr (CString -> StablePtr (CString -> IO ()) -> IO ())) +foreign import ccall "wrapper" mkCStringBoolCb + :: (CString -> StablePtr (CString -> IO CBool) -> IO CBool) + -> IO (FunPtr (CString -> StablePtr (CString -> IO CBool) -> IO CBool)) +foreign import ccall "wrapper" mkWebViewCb + :: (WKWebView -> StablePtr (WKWebView -> IO ()) -> IO ()) + -> IO (FunPtr (WKWebView -> StablePtr (WKWebView -> IO ()) -> IO ())) +foreign import ccall "wrapper" mkCIntCStringCb + :: (CInt -> CString -> StablePtr (CInt -> CString -> IO ()) -> IO ()) + -> IO (FunPtr (CInt -> CString -> StablePtr (CInt -> CString -> IO ()) -> IO ())) +foreign import ccall "jsaddle_wk_set_callbacks" c_jsaddleWkSetCallbacks + :: FunPtr (StablePtr (IO ()) -> IO ()) + -> FunPtr (StablePtr (Results -> IO ()) -> CString -> IO ()) + -> FunPtr (StablePtr (Results -> IO Batch) -> JSaddleHandler -> CString -> IO ()) + -> FunPtr (StablePtr (IO ()) -> IO ()) + -> FunPtr (CString -> StablePtr (CString -> IO ()) -> IO ()) + -> FunPtr (CString -> StablePtr (CString -> IO CBool) -> IO CBool) + -> FunPtr (WKWebView -> StablePtr (WKWebView -> IO ()) -> IO ()) + -> FunPtr (CInt -> CString -> StablePtr (CInt -> CString -> IO ()) -> IO ()) + -> IO () + +{-# NOINLINE callbacksRegistered #-} +callbacksRegistered :: MVar Bool +callbacksRegistered = unsafePerformIO (newMVar False) + +-- | Register the Haskell callbacks with the Objective-C side. Idempotent; +-- run before anything can trigger a callback (jsaddleMain' and run' call it). +-- The FunPtrs live for the life of the process. +registerJSaddleCallbacks :: IO () +registerJSaddleCallbacks = modifyMVar_ callbacksRegistered $ \done -> do + unless done $ do + start <- mkStablePtrIOCb jsaddleStart + result <- mkResultCb jsaddleResult + sync <- mkSyncResultCb jsaddleSyncResult + io <- mkStablePtrIOCb callIO + cstr <- mkCStringCb callWithCString + cstrb <- mkCStringBoolCb callWithCStringReturningBool + webv <- mkWebViewCb callWithWebView + cint <- mkCIntCStringCb callWithCIntCString + c_jsaddleWkSetCallbacks start result sync io cstr cstrb webv cint + return True + foreign import ccall addJSaddleHandler :: WKWebView -> StablePtr (IO ()) -> StablePtr (Results -> IO ()) -> StablePtr (Results -> IO Batch) -> IO () foreign import ccall loadHTMLStringWithBaseURL :: WKWebView -> CString -> CString -> IO () foreign import ccall loadBundleFile :: WKWebView -> CString -> CString -> IO () @@ -93,6 +156,7 @@ jsaddleMainFile url allowing f webView = jsaddleMain' :: JSM () -> WKWebView -> IO () -> IO () jsaddleMain' f webView loadHtml = do + registerJSaddleCallbacks ready <- newEmptyMVar (processResult, syncResult, start) <- runJavaScriptWithSerializer (Just wkWebViewBatchLock) (\batch -> From cd7b5b785fdcf7dd2454f80a2c776b7dee669b18 Mon Sep 17 00:00:00 2001 From: Hamish Mackenzie Date: Mon, 3 Aug 2026 10:19:21 +1200 Subject: [PATCH 2/2] jsaddle-wkwebview: include the callbacks header in cbits-cocoa/AppDelegate.m MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- jsaddle-wkwebview/cbits-cocoa/AppDelegate.m | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/jsaddle-wkwebview/cbits-cocoa/AppDelegate.m b/jsaddle-wkwebview/cbits-cocoa/AppDelegate.m index 70db3e7c..deb9a899 100644 --- a/jsaddle-wkwebview/cbits-cocoa/AppDelegate.m +++ b/jsaddle-wkwebview/cbits-cocoa/AppDelegate.m @@ -3,8 +3,11 @@ #import // 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). +// (see cbits/WKWebView-callbacks.h for why they are not extern symbols). The +// table itself is defined in WKWebView-cbits.m, which the composite Cocoa +// translation unit #includes before this file; the header is included here +// anyway (it is #pragma once) so this file does not depend on that order. +#include "../cbits/WKWebView-callbacks.h" @interface AppDelegate : NSObject @property (nonatomic, assign) IBOutlet NSWindow *window;