packages feed

jsaddle-wkwebview-0.9.10.0: cbits/WKWebView-cbits.m

#include "HsFFI.h"
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

BOOL openApp(NSURL * url);

@interface JSaddleHandler : NSObject<WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate>

@property (nonatomic, assign) HsStablePtr startHandler;
@property (nonatomic, assign) HsStablePtr resultHandler;
@property (nonatomic, assign) HsStablePtr syncHandler;
@property (nonatomic, assign) void (^completionHandler)(NSString *result);

- (instancetype)initHandler:(HsStablePtr)startHandler resultHandler:(HsStablePtr)resultHandler syncHandler:(HsStablePtr)syncHandler;

@end

// 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

- (instancetype)initHandler:(HsStablePtr)startHandler resultHandler:(HsStablePtr)resultHandler syncHandler:(HsStablePtr)syncHandler
{
    self = [super init];
    if (self) {
        _startHandler = startHandler;
        _resultHandler = resultHandler;
        _syncHandler = syncHandler;
    }
    return self;
}

// The stable-pointer checks in the three handler methods below cover the
// window between removeJSaddleHandler freeing them (setting them to NULL) and
// the last WebKit delivery already in flight: dereferencing a freed
// StablePtr is undefined, ignoring a message for a detached context is not.
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
    if (jsaddleCallbacks.jsaddleStart && self.startHandler)
        jsaddleCallbacks.jsaddleStart(self.startHandler);
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    NSString * s = (NSString *) message.body;
    if (jsaddleCallbacks.jsaddleResult && self.resultHandler)
        jsaddleCallbacks.jsaddleResult(self.resultHandler, [s UTF8String]);
}

- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt
    defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler
{
    if([prompt isEqualToString:@"JSaddleSync"]) {
        if (jsaddleCallbacks.jsaddleSyncResult && self.syncHandler) {
            _completionHandler = completionHandler;
            jsaddleCallbacks.jsaddleSyncResult(self.syncHandler, (void *)self, [defaultText UTF8String]);
        }
        else
            completionHandler(@"");   // detached context: unblock the page
    }
    else {
        // TODO decide what if anything should go here
    }
}

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
    NSURL *url = navigationAction.request.URL;

    if(!navigationAction.targetFrame && ![url.scheme isEqualToString:@"file"]) {
        if(openApp(url))
            decisionHandler(WKNavigationActionPolicyCancel);
         else
            decisionHandler(WKNavigationActionPolicyAllow);
    }
    else
        decisionHandler(WKNavigationActionPolicyAllow);
}

@end

// Returns the handler (retained: the caller's reference, released by
// removeJSaddleHandler) so a host that tears a webview down can detach it
// again — see removeJSaddleHandler.
JSaddleHandler * addJSaddleHandler(WKWebView *webView, HsStablePtr startHandler, HsStablePtr resultHandler, HsStablePtr syncHandler) {
    JSaddleHandler * handler = [[JSaddleHandler alloc] initHandler:startHandler resultHandler:resultHandler syncHandler:syncHandler];
    [[[webView configuration] userContentController] addScriptMessageHandler:handler name:@"jsaddle"];
#ifdef USE_UIKIT
    webView.scrollView.bounces = NO;
#endif
    webView.navigationDelegate = handler;
    webView.UIDelegate = handler;
    return handler;
}

// Detach a handler installed by addJSaddleHandler and free the three stable
// pointers it holds.  Those pointers keep their closures — and through them the
// context's whole callback graph (for a reflex application, the entire live
// network) — alive for the life of the PROCESS, which in ghci mode means every
// reloaded instance's network stays reachable: measured at ~30MB of live heap
// leaked per reload.
//
// On the MAIN QUEUE, because that is where all three callbacks are delivered
// from: running the detach there is what makes it mutually exclusive with a
// callback that is mid-flight, so the frees cannot race a deRefStablePtr.
// Asynchronously, because the caller may be a Haskell thread that the main
// thread is (or is about to be) waiting on — a dispatch_sync from the jsaddle
// sync round trip's own producer would deadlock.  The block retains both
// objects, so the host may release the webview as soon as this returns.
// @onDetached@ is run (once) after the handler is detached; freeing the three
// stable pointers is its job, not this file's.  Calling the RTS directly here
// is not an option: in ghci mode this file is compiled into a plain dylib with
// no RTS to link against, so hs_free_stable_ptr is an undefined symbol — the
// same constraint that makes every Haskell call out of this file go through the
// registered callback table (see WKWebView-callbacks.h).
void removeJSaddleHandler(WKWebView *webView, JSaddleHandler *handler, HsStablePtr onDetached) {
    dispatch_async(dispatch_get_main_queue(), ^{
        [[[webView configuration] userContentController] removeScriptMessageHandlerForName:@"jsaddle"];
        if (webView.navigationDelegate == handler) webView.navigationDelegate = nil;
        if (webView.UIDelegate == handler) webView.UIDelegate = nil;
        // Clear before the pointers are freed, so a delivery WebKit had already
        // queued behind this block sees a detached handler (the methods above
        // check) rather than a dangling stable pointer.
        handler.startHandler = NULL;
        handler.resultHandler = NULL;
        handler.syncHandler = NULL;
        [handler release];   // balances addJSaddleHandler's +1
        if (jsaddleCallbacks.callIO)
            jsaddleCallbacks.callIO(onDetached);
    });
}

void evaluateJavaScript(WKWebView *webView, const char * _Nonnull js) {
    NSString *jsString = [[NSString alloc] initWithCString:js encoding:NSUTF8StringEncoding];
    dispatch_async(dispatch_get_main_queue(), ^{
        [webView evaluateJavaScript:jsString completionHandler:NULL];
        [jsString release];
    });
}

void completeSync(JSaddleHandler *handler, const char * _Nonnull s) {
    NSString *string = [[NSString alloc] initWithCString:s encoding:NSUTF8StringEncoding];
    handler.completionHandler(string);
    handler.completionHandler = NULL;
    [string release];
}

void loadHTMLStringWithBaseURL(WKWebView *webView, const char * _Nonnull html, const char * _Nonnull url) {
  NSString *htmlString = [NSString stringWithCString:html encoding:NSUTF8StringEncoding];
  NSURL *baseURL = [NSURL URLWithString:[NSString stringWithCString:url encoding:NSUTF8StringEncoding]];
  dispatch_async(dispatch_get_main_queue(), ^{
      [webView loadHTMLString:htmlString baseURL:baseURL];
    });
}

const char * mainBundleResourcePathC() {
    NSBundle *main = [NSBundle mainBundle];
    if(!main) return NULL;
    NSString *path = main.resourcePath;
    if(!path) return NULL;
    return [path UTF8String];
}

void loadBundleFile(WKWebView *webView, const char * _Nonnull file, const char * _Nonnull allowing) {
    NSBundle *main = [NSBundle mainBundle];
    if(main) {
        NSString *path = main.resourcePath;
        if(path) {
            NSURL *fileUrl = [NSURL fileURLWithPath:[NSString stringWithFormat: @"%@/%@", path, [NSString stringWithCString:file encoding:NSUTF8StringEncoding]]];
            NSURL *allowingUrl = [NSURL fileURLWithPath:[NSString stringWithFormat: @"%@/%@", path, [NSString stringWithCString:allowing encoding:NSUTF8StringEncoding]]];
            [webView loadFileURL:fileUrl allowingReadAccessToURL:allowingUrl];
        }
    }
}