Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ migrate_working_dir/
/build/
/coverage/

# Native CEF host: build output + fetched CEF SDK (see native/build_cef_host.sh).
native/cef_host/build/
native/**/cef_binary_*/
# Native CEF host: build output + fetched CEF SDK. Path-independent so it keeps
# matching regardless of where the macOS package lives (see build_cef_host.sh).
**/cef_host/build/
**/cef_binary_*/

# Federated sub-packages are libraries too — no pubspec.lock.
/packages/*/pubspec.lock
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 0.1.3

* **Federated package structure** (no API change): `flutter_cef` is now a
federated plugin — the app-facing package plus
`flutter_cef_platform_interface` (the shared Dart types + method-channel
contract) and the endorsed `flutter_cef_macos` (Swift plugin + `cef_host`).
Consumers keep depending on `flutter_cef` and importing
`package:flutter_cef/flutter_cef.dart` exactly as before. A Windows or Linux
implementation can now be added as a sibling `flutter_cef_<os>` package — see
[`PORTING.md`](PORTING.md) for the contract and the platform-seam map.

## 0.1.2

* **Navigation scheme allowlist**: `CefWebView(allowedSchemes: {...})` restricts
Expand Down
122 changes: 122 additions & 0 deletions PORTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Porting flutter_cef to a new platform

`flutter_cef` is a **federated** Flutter plugin. macOS ships today
(`flutter_cef_macos`); Windows and Linux can be added as sibling
implementation packages without touching the app-facing API or the macOS
implementation. CEF itself is cross-platform — the work is re-implementing the
thin platform glue around it.

```
flutter_cef/ # app-facing package (re-exports the API)
packages/
flutter_cef_platform_interface/ # the shared Dart contract (no platform code)
flutter_cef_macos/ # the macOS implementation (reference)
flutter_cef_windows/ _linux/ # <- you add these
```

## 1. The Dart / package side (small)

The Dart `lib/` is already platform-neutral and the cross-platform contract is
the **method-channel protocol** (channel name + method names + event names),
so a new platform needs almost no Dart.

1. Create `packages/flutter_cef_<os>/` with a `pubspec.yaml`:
```yaml
name: flutter_cef_<os>
dependencies:
flutter_cef_platform_interface: { path: ../flutter_cef_platform_interface }
flutter:
plugin:
implements: flutter_cef
platforms:
<os>:
pluginClass: FlutterCefPlugin # your native host plugin
dartPluginClass: FlutterCef<Os> # endorses the channel instance
```
2. `lib/flutter_cef_<os>.dart` — endorse the default method-channel instance
(mirror `flutter_cef_macos`'s `FlutterCefMacos.registerWith`):
```dart
class FlutterCef<Os> {
static void registerWith() =>
FlutterCefPlatform.instance = MethodChannelFlutterCef();
}
```
3. Endorse it from the app-facing package: in `flutter_cef/pubspec.yaml` add
`platforms: <os>: default_package: flutter_cef_<os>` and a path dependency on
it (mirror the `macos:` entry).

That's the entire Dart side. Everything else is native.

## 2. The host plugin (your `FlutterCefPlugin` equivalent)

Reference: `packages/flutter_cef_macos/macos/Classes/` (Swift). The host plugin
lives in the Flutter app's process and:

- Registers a **platform texture** (a `FlutterTexture` on macOS) backed by a
shared GPU surface, and hands its id to the page.
- Spawns **one `cef_host` subprocess per view**, passing
`--url --width --height --dpr --iosurface-id --ipc --allowed-schemes` as argv.
- Relays method-channel calls → IPC opcodes to `cef_host`, and IPC events from
`cef_host` → `invokeMethod` back to Dart.

The method/event protocol it must implement is the same on every platform — see
the `case` labels in `FlutterCefPlugin.handle` (host→native verbs: `create`,
`navigate`, `loadTrusted`, `resize`, `dispose`, `pointer`, `key`, `reload`,
`executeJavaScript`, cookies, IME, …) and the `emit(...)` calls (native→Dart
events: `cursor`, `loadingState`, `title`, `url`, `consoleMessage`, `jsDialog`,
`cookies`, `imeCompositionBounds`, …).

## 3. The `cef_host` subprocess — the platform seams

Reference: `packages/flutter_cef_macos/native/cef_host/` (`main.mm` is the
browser process, `process_helper.mm` the CEF child processes). The CEF client /
app / handler logic, the browser-control functions, the navigation scheme
allowlist, and the **IPC opcode protocol** are platform-agnostic C++ and can be
reused verbatim. Only these seams are macOS-specific (file:line are into
`main.mm` at the time of writing):

| Seam | macOS reference | What your platform provides |
| --- | --- | --- |
| **Shared surface** — receive painted frames and present them to the host texture. CEF delivers either software `OnPaint` (CPU buffer) or `OnAcceleratedPaint` (a shared GPU texture handle). | `OnPaint` / `OnAcceleratedPaint` + the `IOSurface*` ops (~lines 346–450); `g_surface` (~133). macOS uses an IOSurface-backed `CVPixelBuffer`. | **Windows**: a shared D3D11 texture / DXGI keyed-mutex handle. **Linux**: shared memory or a DMA-buf, presented via the platform texture. Look the surface up by the `--iosurface-id`-equivalent handle the host passes. |
| **IPC transport** — a framed bidirectional byte stream to the host. Wire format: 4-byte big-endian length prefix, then `[opcode][payload]`. | `WriteAll`/`ReadAll` (207–235), `SendFrame` (~237–249), `ConnectUnixSocket` (1341+), the read loop (~1140). Unix domain socket. | **Windows**: a named pipe. **Linux**: a Unix domain socket (reuse as-is). Keep the same length-prefixed framing. |
| **App / run loop** — give CEF a host application + message loop, and a per-process cache dir. | `CefHostApplication : NSApplication<CefAppProtocol>` (304–330); `@autoreleasepool` + `sharedApplication` (1420+); `NSTemporaryDirectory()` cache; `_NSGetExecutablePath` (1335). | **Windows/Linux**: the platform's CEF message-loop integration (`CefRunMessageLoop` or OS loop) and a per-user, per-process cache path. |
| **Sandbox** — bring the child processes into the Chromium sandbox in a signed/release build. | `process_helper.mm`: `CefScopedSandboxContext` (release only); `settings.no_sandbox` toggled by `CEF_HOST_ADHOC` (1426/1433). | **Windows**: link the `cef_sandbox` static lib + `CefScopedSandboxContext`. **Linux**: the SUID / user-namespace sandbox helper. |
| **Framework / resource path** — point CEF at the CEF binary distribution. | `CefScopedLibraryLoader::LoadInMain`/`LoadInHelper`; `framework_dir_path` / `main_bundle_path` (1453/1466). | The equivalent paths for your bundle layout. |
| **Build + bundle + sign** | `native/build_cef_host.sh` (CMake, `CEF_MULTI_PROCESS` / `CEF_HOST_ADHOC` flags), `tool/bundle_cef_host.sh`. | A platform build that produces `cef_host` + the CEF runtime, and a bundling step into the host app. |

The fetched CEF distribution already ships per-platform binaries and a CMake
config (`cef_binary_*_{macosarm64,windows64,linux64}`), so most of
`build_cef_host.sh` and `CMakeLists.txt` is reusable — adjust the
platform-specific link libs and the surface/transport sources.

## 4. Recommended: extract a `core/` + `platform/` split *with* your port

The macOS `main.mm` currently keeps the portable CEF logic and the macOS glue in
one translation unit. The seams above are the natural cut line:

```
native/cef_host/
core/ # protocol (opcodes + framing), CEF client/app, browser control,
# navigation allowlist, dispatch loop — depends only on platform.h
platform/
mac/ # IOSurface/Metal, Cocoa app, Unix socket, sandbox, paths
<os>/ # your implementations of the same platform.h interface
```

This split is deliberately **not** done up front: a platform abstraction
designed without a second consumer tends to guess the seam wrong. Do it as the
first step of your port, using the table above as the contract — extract the
portable pieces into `core/` behind a small `platform.h`, implement `platform.h`
for macOS (moving the existing code) and for your OS, and you have a shared core
validated by two real platforms.

## Checklist

- [ ] `flutter_cef_<os>` package: pubspec (`implements`, `pluginClass`,
`dartPluginClass`) + `registerWith`.
- [ ] Endorse via `default_package` in the app-facing `flutter_cef/pubspec.yaml`.
- [ ] Host plugin: texture registration, subprocess spawn, channel ↔ IPC relay.
- [ ] `cef_host`: implement the five seams (surface, transport, app loop,
sandbox, paths); reuse the CEF logic + opcode protocol.
- [ ] Build + bundle + (release) signing for the platform.
- [ ] `example/` runs on the platform; pointer / keyboard / IME / navigation work.
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Embed a **live Chromium browser** (via the [Chromium Embedded Framework](https://bitbucket.org/chromiumembedded/cef/)) as a Flutter widget — rendered into a `Texture`, so it composites, transforms, clips, and zooms like any other widget, and **keeps rendering even when off-screen / not focused**. Pointer, scroll, and trackpad two-finger pans are forwarded by coordinate (pans are caught even when an ancestor opts into Flutter's trackpad gesture API, as canvas hosts do), and keyboard input reaches the page as real `keydown → keypress → keyup` events (Enter activates a focused button / submits a form, Space toggles a checkbox) — including platform IME composition for CJK / emoji and the ⌃⌘Space emoji picker. Text input is bound to the hosting `FlutterView` (as `EditableText` does), so it **works in multi-view / multi-window apps**; the page cursor drives a `MouseRegion`.

> Status: **experimental, macOS 12+ only** (CEF 144 runtime floor). Real Chromium (any site — JS/CSS/WebGL/video). **Multi-process by default** (GPU-accelerated OSR — `OnAcceleratedPaint` GPU compositing into a shared IOSurface, Retina-crisp; renderer/utility crashes isolated, so heavy SPAs like Google sign-in render and survive); `CEF_MULTI_PROCESS=OFF native/build_cef_host.sh` for the simpler single-process build. No mobile (iOS bans third-party engines); desktop by nature.
> Status: **experimental, macOS 12+ only** (CEF 144 runtime floor). Real Chromium (any site — JS/CSS/WebGL/video). **Multi-process by default** (GPU-accelerated OSR — `OnAcceleratedPaint` GPU compositing into a shared IOSurface, Retina-crisp; renderer/utility crashes isolated, so heavy SPAs like Google sign-in render and survive); `CEF_MULTI_PROCESS=OFF packages/flutter_cef_macos/native/build_cef_host.sh` for the simpler single-process build. No mobile (iOS bans third-party engines); desktop by nature.

```dart
import 'package:flutter_cef/flutter_cef.dart';
Expand Down Expand Up @@ -76,9 +76,11 @@ Same pattern JCEF (JetBrains) and CefSharp use to render Chromium into a non-nat
CEF (~200 MB) is **fetched**, not vendored. Build the renderer once:

```sh
# The macOS implementation lives in packages/flutter_cef_macos.
cd packages/flutter_cef_macos
native/build_cef_host.sh # fetches CEF + builds cef_host.app
export FLUTTER_CEF_HOST="$PWD/native/cef_host/build/cef_host.app/Contents/MacOS/cef_host"
cd example && flutter run -d macos
cd ../../example && flutter run -d macos
```

### Bundling into a distributable app
Expand All @@ -89,11 +91,12 @@ automatically (`$FLUTTER_CEF_HOST` → pod resources → `Contents/Frameworks`
`Contents/Helpers`). After `flutter build macos`, run:

```sh
path/to/flutter_cef/tool/bundle_cef_host.sh "build/macos/.../YourApp.app" "" "<signing-identity>"
packages/flutter_cef_macos/tool/bundle_cef_host.sh "build/macos/.../YourApp.app" "" "<signing-identity>"
```

or wire it as a Run Script build phase on your Runner target (snippet in
`tool/bundle_cef_host.sh`) so it runs before Xcode's code-sign phase. Your host
`packages/flutter_cef_macos/tool/bundle_cef_host.sh`) so it runs before Xcode's
code-sign phase. Your host
app **must not be App-Sandboxed** (CEF spawns the helper, shares a global
IOSurface, writes a cache); entitlements need
`com.apple.security.cs.disable-library-validation` + JIT — see
Expand All @@ -117,7 +120,8 @@ build flag, `CEF_HOST_ADHOC` (default `ON`):
The `OFF` posture only *validates* under correct **inside-out Developer-ID
signing** of the `cef_host` tree (deepest helper → `libcef_sandbox.dylib` + CEF
framework → host, depth-first, Hardened Runtime + trusted timestamp). Build it
with `CEF_HOST_ADHOC=OFF CODESIGN_ID="<Developer ID>" native/build_cef_host.sh`,
with `CEF_HOST_ADHOC=OFF CODESIGN_ID="<Developer ID>"
packages/flutter_cef_macos/native/build_cef_host.sh`,
or — when bundled into a host app — let the app's own signing re-sign the tree
with those entitlements. Ad-hoc/dev builds run unsandboxed by necessity (the
sandbox can't validate without proper signing), which is why `ON` is the default.
Expand Down Expand Up @@ -178,8 +182,11 @@ Next:
scheme handlers, a typed DevTools/CDP client (the inspector window already
ships via `openDevTools`; this is the programmatic CDP surface), and `CefPermissionHandler`
(WebRTC camera/mic prompts).
- **Windows / Linux** — the federated structure is ready; each needs its own host
+ shared-texture path.
- **Windows / Linux** — the package is **federated** (`flutter_cef` +
`flutter_cef_platform_interface` + `flutter_cef_macos`); a new platform is a
sibling `flutter_cef_<os>` package. The CEF logic + IPC protocol are portable;
each OS supplies its own host plugin + shared-texture / transport / sandbox
glue. See [`PORTING.md`](PORTING.md) for the full contract and seam map.

## Credits

Expand Down
9 changes: 9 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
include: package:flutter_lints/flutter.yaml

analyzer:
errors:
# Federation members are intentionally path deps: this repo is consumed from
# source (path / git), where path deps resolve the siblings automatically and
# hosted constraints would force every consumer to add dependency_overrides.
# `pub publish` still enforces the swap to hosted constraints — see the
# publish note in pubspec.yaml — so this only silences `flutter analyze`.
invalid_dependency: ignore

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
6 changes: 3 additions & 3 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ CEF (~200 MB) is fetched, not vendored — build the renderer once, point the
plugin at it, then run:

```sh
cd .. # package root
native/build_cef_host.sh # fetches CEF + builds cef_host.app (one-time)
cd ../packages/flutter_cef_macos # the macOS implementation package
native/build_cef_host.sh # fetches CEF + builds cef_host.app (one-time)
export FLUTTER_CEF_HOST="$PWD/native/cef_host/build/cef_host.app/Contents/MacOS/cef_host"
cd example && flutter run -d macos
cd ../../example && flutter run -d macos
```

Without `FLUTTER_CEF_HOST` (or a bundled `cef_host.app` — see the root
Expand Down
2 changes: 1 addition & 1 deletion example/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import FlutterMacOS
import Foundation

import flutter_cef
import flutter_cef_macos

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterCefPlugin.register(with: registry.registrar(forPlugin: "FlutterCefPlugin"))
Expand Down
10 changes: 5 additions & 5 deletions example/macos/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
PODS:
- flutter_cef (0.1.2):
- flutter_cef_macos (0.1.2):
- FlutterMacOS
- FlutterMacOS (1.0.0)

DEPENDENCIES:
- flutter_cef (from `Flutter/ephemeral/.symlinks/plugins/flutter_cef/macos`)
- flutter_cef_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_cef_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)

EXTERNAL SOURCES:
flutter_cef:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_cef/macos
flutter_cef_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_cef_macos/macos
FlutterMacOS:
:path: Flutter/ephemeral

SPEC CHECKSUMS:
flutter_cef: 923b658c344928eb53d81cf23367973f5737457f
flutter_cef_macos: 29ec435deecded6902b4ea56dcf9e134d165a25d
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1

PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
Expand Down
24 changes: 23 additions & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,21 @@ packages:
path: ".."
relative: true
source: path
version: "0.1.2"
version: "0.1.3"
flutter_cef_macos:
dependency: transitive
description:
path: "../packages/flutter_cef_macos"
relative: true
source: path
version: "0.1.3"
flutter_cef_platform_interface:
dependency: transitive
description:
path: "../packages/flutter_cef_platform_interface"
relative: true
source: path
version: "0.1.3"
flutter_driver:
dependency: transitive
description: flutter
Expand Down Expand Up @@ -177,6 +191,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
process:
dependency: transitive
description:
Expand Down
5 changes: 3 additions & 2 deletions lib/flutter_cef.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
/// The page renders off-screen in a `cef_host` subprocess into a shared
/// IOSurface and is shown as a [Texture] (so it composites/transforms/clips
/// like any widget). Pointer + keyboard input is forwarded by coordinate, and
/// the page cursor drives a [MouseRegion]. macOS only, for now.
/// the page cursor drives a [MouseRegion]. macOS only, for now (the package is
/// federated — see `flutter_cef_macos` and `PORTING.md`).
library;

export 'src/cef_events.dart'
export 'package:flutter_cef_platform_interface/flutter_cef_platform_interface.dart'
show
CefCookie,
CefConsoleMessage,
Expand Down
12 changes: 9 additions & 3 deletions lib/src/cef_web_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

import 'cef_events.dart';
import 'cef_input.dart';
import 'package:flutter_cef_platform_interface/flutter_cef_platform_interface.dart';

/// Controls one CEF browser session: navigate, drive history, run JavaScript,
/// forward input, and observe page state (loading, title, url, cursor). Backed
Expand All @@ -23,7 +22,10 @@ class CefWebController {
_installHandler();
}

static const MethodChannel _channel = MethodChannel('flutter_cef');
// The method channel of the endorsed platform implementation (the default
// MethodChannelFlutterCef on macOS). A platform plugin may swap the instance
// in its registerWith; this reflects whatever is current.
static MethodChannel get _channel => FlutterCefPlatform.instance.channel;
static int _counter = 0;
bool _disposed = false;

Expand Down Expand Up @@ -114,6 +116,10 @@ class CefWebController {
<String, void Function(String)>{};

static void _installHandler() {
// Process-global, installed once on the first controller and never torn
// down; it fans events out to live controllers by sessionId and is a no-op
// for disposed ones. It binds the channel of whatever FlutterCefPlatform
// instance is current at first install (one channel in practice).
if (_handlerInstalled) return;
_handlerInstalled = true;
_channel.setMethodCallHandler((call) async {
Expand Down
Loading
Loading