From b91b08ad18c5c60c389cb339cd8d0f4dfbba85b8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 11 Oct 2026 22:25:22 -0700 Subject: [PATCH] Mac/iOS: register delegate protocols when the runtime doesn't know them objc_getProtocol only returns protocols that some loaded image references. When it returned null, our managed delegate classes silently didn't conform to WKScriptMessageHandler, WKNavigationDelegate and ASWebAuthenticationPresentationContextProviding, which breaks WebKit on iOS 26. Now, if the protocol is missing, we register one with the same name and the method descriptions we implement. The runtime checks conformance by name. Fixes #90 Co-Authored-By: Claude Opus 5.5 --- ...henticationPresentationContextProviding.cs | 10 ++-- .../Macios/Interop/Libobjc.cs | 8 ++++ .../Macios/Interop/NSObject.cs | 47 +++++++++++++++++++ .../Interop/WebKit/WKNavigationDelegate.cs | 13 ++--- .../Interop/WebKit/WKScriptMessageHandler.cs | 11 ++--- 5 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls.WebView.Core/Macios/Interop/AuthenticationServices/ASWebAuthenticationPresentationContextProviding.cs b/src/Avalonia.Controls.WebView.Core/Macios/Interop/AuthenticationServices/ASWebAuthenticationPresentationContextProviding.cs index 6ce41daf..e9a7f6ad 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/Interop/AuthenticationServices/ASWebAuthenticationPresentationContextProviding.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/Interop/AuthenticationServices/ASWebAuthenticationPresentationContextProviding.cs @@ -17,13 +17,13 @@ static ASWebAuthenticationPresentationContextProviding() var delegateClass = AllocateClassPair("ManagedASWebAuthenticationPresentationContextProviding"); - if (Libobjc.objc_getProtocol("ASWebAuthenticationPresentationContextProviding") is var protocol and > 0) - { - AddProtocol(delegateClass, protocol); - } + var presentationAnchorSel = Libobjc.sel_getUid("presentationAnchorForWebAuthenticationSession:"); + + AddProtocol(delegateClass, "ASWebAuthenticationPresentationContextProviding", + new ProtocolMethod(presentationAnchorSel, "@@:@", true)); AddMethod(delegateClass, - Libobjc.sel_getUid("presentationAnchorForWebAuthenticationSession:"), + presentationAnchorSel, new IntPtr(s_presentationAnchorForWebAuthenticationSession), "@@:@"); diff --git a/src/Avalonia.Controls.WebView.Core/Macios/Interop/Libobjc.cs b/src/Avalonia.Controls.WebView.Core/Macios/Interop/Libobjc.cs index 92a90308..30208851 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/Interop/Libobjc.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/Interop/Libobjc.cs @@ -34,6 +34,14 @@ internal static unsafe partial class Libobjc [LibraryImport(libobjc, StringMarshalling = StringMarshalling.Utf8)] public static partial IntPtr objc_getProtocol(string selector); + [LibraryImport(libobjc, StringMarshalling = StringMarshalling.Utf8)] + public static partial IntPtr objc_allocateProtocol(string name); + [LibraryImport(libobjc)] + public static partial void objc_registerProtocol(IntPtr protocol); + [LibraryImport(libobjc)] + public static partial void protocol_addProtocol(IntPtr protocol, IntPtr addition); + [LibraryImport(libobjc, StringMarshalling = StringMarshalling.Utf8)] + public static partial void protocol_addMethodDescription(IntPtr protocol, IntPtr selector, string types, byte isRequiredMethod, byte isInstanceMethod); [LibraryImport(libobjc)] public static partial byte class_addProtocol(IntPtr basePtr, IntPtr protocol); [LibraryImport(libobjc, StringMarshalling = StringMarshalling.Utf8)] diff --git a/src/Avalonia.Controls.WebView.Core/Macios/Interop/NSObject.cs b/src/Avalonia.Controls.WebView.Core/Macios/Interop/NSObject.cs index 71b72d13..95082b39 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/Interop/NSObject.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/Interop/NSObject.cs @@ -92,6 +92,51 @@ protected static void AddProtocol(IntPtr classHandle, IntPtr protocol) } } + /// + /// Adds the protocol to the class. + /// If the runtime doesn't know the protocol yet, registers one with the same name and the given methods. + /// + /// + /// objc_getProtocol only finds protocols that some loaded image references. + /// On iOS 26, WebKit protocols are not always registered, and WebKit rejects handlers that don't conform. + /// The runtime checks conformance by protocol name, so a protocol with the same name is enough for conformsToProtocol:. + /// Method descriptions are still added, so code that inspects the protocol sees the same methods as in the original one. + /// + protected static void AddProtocol(IntPtr classHandle, string protocolName, params ReadOnlySpan methods) + { + var protocol = Libobjc.objc_getProtocol(protocolName); + if (protocol == IntPtr.Zero) + { + protocol = Libobjc.objc_allocateProtocol(protocolName); + if (protocol == IntPtr.Zero) + { + // Another thread or image registered it in the meantime. + protocol = Libobjc.objc_getProtocol(protocolName); + } + else + { + if (Libobjc.objc_getProtocol("NSObject") is var nsObjectProtocol and not 0) + { + Libobjc.protocol_addProtocol(protocol, nsObjectProtocol); + } + foreach (var method in methods) + { + Libobjc.protocol_addMethodDescription(protocol, method.Selector, method.Types, + method.Required ? (byte)1 : (byte)0, 1); + } + Libobjc.objc_registerProtocol(protocol); + } + } + + if (protocol == IntPtr.Zero) + { + Logger.TryGet(LogEventLevel.Warning, "WebView")?.Log(null, "Failed to resolve {Protocol} protocol", protocolName); + return; + } + + AddProtocol(classHandle, protocol); + } + protected static void AddMethod(IntPtr classHandle, IntPtr selector, IntPtr methodHandler, string types) { if (classHandle == IntPtr.Zero) @@ -266,6 +311,8 @@ protected internal unsafe IntPtr GetSuperRef() return new IntPtr(_superRef); } + protected readonly record struct ProtocolMethod(IntPtr Selector, string Types, bool Required); + [StructLayout (LayoutKind.Sequential)] private struct ObjcSuper { public IntPtr Handle; diff --git a/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs b/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs index cb6954cc..2bacd04e 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs @@ -18,15 +18,16 @@ static WKNavigationDelegate() { var delegateClass = AllocateClassPair("ManagedWKNavigationDelegate"); - if (Libobjc.objc_getProtocol("WKNavigationDelegate") is var protocol and > 0) - { - AddProtocol(delegateClass, protocol); - } - var willPresentNotificationSel = Libobjc.sel_getUid("webView:didFinishNavigation:"); + var didReceiveNotificationResponse = Libobjc.sel_getUid("webView:decidePolicyForNavigationAction:decisionHandler:"); + + // All WKNavigationDelegate methods are optional. + AddProtocol(delegateClass, "WKNavigationDelegate", + new ProtocolMethod(willPresentNotificationSel, "v@:@@", false), + new ProtocolMethod(didReceiveNotificationResponse, "v@:@@@?", false)); + AddMethod(delegateClass, willPresentNotificationSel, new IntPtr(s_willPresentNotification), "v@:@@"); - var didReceiveNotificationResponse = Libobjc.sel_getUid("webView:decidePolicyForNavigationAction:decisionHandler:"); AddMethod(delegateClass, didReceiveNotificationResponse, new IntPtr(s_decidePolicyForNavigationAction), "v@:@@@"); RegisterManagedMembers(delegateClass); diff --git a/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKScriptMessageHandler.cs b/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKScriptMessageHandler.cs index 08045d8c..4cbd5f99 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKScriptMessageHandler.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKScriptMessageHandler.cs @@ -19,13 +19,12 @@ static WKScriptMessageHandler() { var delegateClass = AllocateClassPair("ManagedWKScriptMessageHandler"); - if (Libobjc.objc_getProtocol("WKScriptMessageHandler") is var protocol and > 0) - { - AddProtocol(delegateClass, protocol); - } + var didReceiveScriptMessageSel = Libobjc.sel_getUid("userContentController:didReceiveScriptMessage:"); + + AddProtocol(delegateClass, "WKScriptMessageHandler", + new ProtocolMethod(didReceiveScriptMessageSel, "v@:@@", true)); - var willPresentNotificationSel = Libobjc.sel_getUid("userContentController:didReceiveScriptMessage:"); - AddMethod(delegateClass, willPresentNotificationSel, new IntPtr(s_didReceiveScriptMessage), "v@:@@"); + AddMethod(delegateClass, didReceiveScriptMessageSel, new IntPtr(s_didReceiveScriptMessage), "v@:@@"); RegisterManagedMembers(delegateClass);