Skip to content

Feature | Harden and modularize the native interop layer #260

@freakdaniel

Description

@freakdaniel

Impact

High

Problem / Motivation

The native layer has grown into a high-risk area: platform window files are large and mix window lifecycle, WebView setup, custom schemes, JS injection, dialogs, notifications, threading, and interop ownership. This makes native bugs hard to isolate and increases the chance of crashes, leaks, ABI drift, and platform-specific regressions

Current behavior

  • Platform/Windows/Window.cpp, Platform/Linux/Window.cpp, and Platform/Mac/Window.mm are large monoliths with many responsibilities
  • Exports.cpp directly dereferences InfiniFrameWindow* in most exports and has no consistent status/error contract
  • Native code sometimes reports fatal setup problems via UI dialogs and exit(0) instead of returning errors to managed code
  • String/buffer ownership differs across Windows/Linux/macOS and is manually encoded in several places
  • Test-only exports (Exports.Tests.cpp) are compiled into the native shared library through TEST_SOURCES
  • WebView custom scheme, CORS, and Blazor shim logic are embedded inside platform window setup
  • .clang-tidy and sanitizer intent exist, but static analysis/sanitizer coverage is not clearly enforced across native CI

Expected behavior

The native layer should have clear ownership rules, safer C ABI boundaries, smaller platform modules, test-only code excluded from production artifacts, and repeatable native quality gates

Proposed solution

Introduce a native hardening/refactor initiative:

  • Split platform window implementations into focused modules: window lifecycle, WebView host, custom schemes, JS interop, dialogs, notifications, monitor utilities, and UI-thread dispatch
  • Add a small C ABI guard/error layer so exports validate instance, output pointers, and invalid arguments consistently
  • Replace process termination paths with error propagation to managed code
  • Centralize native allocation/free contracts for strings, string arrays, and callback response buffers
  • Move Exports.Tests.cpp into a dedicated test-only native target or compile it behind an explicit test flag
  • Extract custom scheme response/CORS/header creation into reusable helpers shared by platform implementations where possible
  • Store/remove native event subscriptions deterministically, especially WebView2 tokens
  • Wire clang-tidy, clang-format --check, and native sanitizer builds into CI where platform support allows

How will suggestion looks on prod:

src/InfiniFrame.Native/
  Core/
    InfiniFrame.h
    InfiniFrameWindow.h
    InfiniFrameInitParams.h
    InfiniFrameDialog.h
    InfiniFrameWindowImpl.h

  Interop/
    Exports.cpp
    ExportGuards.h
    NativeResult.h
    NativeString.h
    NativeBuffer.h
    InitParamsReader.h

  Shared/
    CustomSchemeResponse.h
    WebMessagePayload.h
    WindowGeometry.h
    UiThreadDispatcher.h

  Platform/
    Windows/
      Window.Win32.cpp
      WindowProc.Win32.cpp
      WebView2Host.cpp
      WebView2Settings.cpp
      WebView2CustomSchemes.cpp
      WebView2JsInterop.cpp
      Dialog.Win32.cpp
      Notifications.WinToast.cpp
      Monitors.Win32.cpp
      DarkMode.cpp
      Dpi.Win32.cpp

    Linux/
      Window.Gtk.cpp
      WebKitGtkHost.cpp
      WebKitGtkSettings.cpp
      WebKitGtkCustomSchemes.cpp
      WebKitGtkJsInterop.cpp
      Dialog.Gtk.cpp
      Notifications.LibNotify.cpp
      Monitors.Gtk.cpp
      UiDispatcher.Gtk.cpp

    Mac/
      Window.Cocoa.mm
      WKWebViewHost.mm
      WKWebViewSettings.mm
      WKCustomSchemes.mm
      WKJsInterop.mm
      Dialog.Cocoa.mm
      Notifications.UserNotifications.mm
      Monitors.Cocoa.mm
      UiDispatcher.Cocoa.mm
      Delegates/
        AppDelegate.mm
        WindowDelegate.mm
        NavigationDelegate.mm
        UiDelegate.mm
        UrlSchemeHandler.mm

  Tests/
    Exports.Tests.cpp

Comparsion

Before:

  Platform/Windows/Window.cpp
    - HWND lifecycle
    - WebView2 creation
    - WebView2 settings
    - custom schemes
    - JS interop
    - Blazor patches
    - event subscriptions
    - notifications
    - monitors
    - UI-thread invoke
    - window state

After:

  Window.Win32.cpp
    - create/show/close/move/resize window only

  WebView2Host.cpp
    - create WebView2 environment/controller only

  WebView2CustomSchemes.cpp
    - scheme registration and response handling only

  WebView2JsInterop.cpp
    - JS injection and web message callbacks only

  Notifications.WinToast.cpp
    - notifications only

What about PIMPL

PIMPL would not break at all. Right now PIMPL looks like:

// Core/InfiniFrameWindow.h
class InfiniFrameWindow {
    struct Impl;
    std::unique_ptr<Impl> m_impl;
};

So the correct solution after refactor must be:

Core/
  InfiniFrameWindow.h          # public-ish API, only forward-declares Impl
  InfiniFrameWindowImpl.h      # shared internal base state

Platform/Windows/
  WindowImpl.Win32.h           # private header, defines InfiniFrameWindow::Impl
  Window.Win32.cpp
  WebView2Host.cpp
  WebView2CustomSchemes.cpp
  WebView2JsInterop.cpp
  Dialog.Win32.cpp

So Impl just moving from single and overcoded .cpp to private platform header:

// Platform/Windows/WindowImpl.Win32.h
#pragma once

#include "Core/InfiniFrameWindow.h"
#include "Core/InfiniFrameWindowImpl.h"

struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl {
    HWND _hWnd = nullptr;
    wil::com_ptr<ICoreWebView2Controller> _webviewController;
    wil::com_ptr<ICoreWebView2> _webviewWindow;
    // ...
};

And different Windows .cpp can see/work around it:

// Platform/Windows/WebView2CustomSchemes.cpp
#include "WindowImpl.Win32.h"

void InfiniFrameWindow::Impl::RegisterCustomSchemes() {
    // access _webviewWindow, _customSchemeNames, etc.
}

What are really prohibited in this solution:

Core/InfiniFrameWindow.h
  - place HWND / GtkWidget / WKWebView into it
  - show entire Impl in public header
  - create a single Impl, which contain entire platforms headers

A little compromise about it is:

Public boundary:
  Core/InfiniFrameWindow.h

Private shared state:
  Core/InfiniFrameWindowImpl.h

Private platform implementation:
  Platform/Windows/WindowImpl.Win32.h
  Platform/Linux/WindowImpl.Gtk.h
  Platform/Mac/WindowImpl.Cocoa.h

Alternatives considered

Keep fixing issues individually. This works for urgent crash bugs, but does not reduce the underlying complexity that keeps producing ownership, callback, and platform divergence defects

Use case

Maintainers need to safely add native features such as new WebView options, custom scheme behavior, packaging/AOT support, or platform-specific window functionality without re-auditing thousands of lines of mixed lifecycle and interop code each time

Technical proposal

  • Production InfiniFrame.Native no longer exports InfiniWindowTests_* symbols
  • C ABI exports have consistent precondition guards and documented return/error behavior
  • Common.h ErrorCode / Result is either used meaningfully or removed in favor of a chosen error model
  • Native ownership helpers cover string returns, string arrays, and custom scheme response buffers
  • Platform Window files are reduced by extracting at least custom schemes, JS interop, and UI-thread invocation
  • Native CI includes formatting/static-analysis checks and at least one sanitizer/native smoke configuration
  • Existing managed and Playwright scenarios continue passing

Related issues

This refactor is related to several existing native/interop issues:

Checklist

  • I searched existing issues
  • This is not a support question

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions