From 93d53eae9d27fb946852309e14a612ce80c8bffe Mon Sep 17 00:00:00 2001 From: bkonyi Date: Fri, 30 Oct 2026 18:24:36 +0000 Subject: [PATCH 1/7] RFC 510.0000: Targeting Custom Embedders with Flutter Tools Proposes a first-class, isolate-based platform extension architecture for the Flutter CLI (`flutter_tools`) that decouples custom and embedded target platform tooling from the core Flutter SDK into project-scoped Dart packages (`package:flutter_tools_extension` and `package:flutter_tools_core`). --- ...ing-custom-embedders-with-flutter-tools.md | 1469 +++++++++++++++++ 1 file changed, 1469 insertions(+) create mode 100644 rfc/510.0000-targeting-custom-embedders-with-flutter-tools.md diff --git a/rfc/510.0000-targeting-custom-embedders-with-flutter-tools.md b/rfc/510.0000-targeting-custom-embedders-with-flutter-tools.md new file mode 100644 index 0000000..08e6ce9 --- /dev/null +++ b/rfc/510.0000-targeting-custom-embedders-with-flutter-tools.md @@ -0,0 +1,1469 @@ +--- +type: rfc +rfc: '510.0000' +title: Targeting Custom Embedders with Flutter Tools +description: > + Proposes a first-class, isolate-based platform extension mechanism for + the Flutter CLI (flutter_tools) that allows custom and embedded targets to + integrate seamlessly with standard Flutter workflows without forking the + Flutter SDK. +status: draft +created: 2026-10-27T00:00:00Z +updated: 2026-10-30T00:00:00Z +tags: + - 510-tool + - 450-embedded + - 440-plugins +authors: + - https://github.com/bkonyi +--- + +# RFC 510.0000: Targeting Custom Embedders with Flutter Tools + +## Summary + +This RFC proposes a first-class, isolate-based platform extension architecture for the Flutter CLI (`flutter_tools`). By decoupling target platform tooling from the core Flutter SDK into project-scoped Dart packages (`package:flutter_tools_extension` and `package:flutter_tools_core`) that communicate with the Flutter Tool over a structured JSON-RPC 2.0 protocol, third-party and custom embedder authors can hook their devices, build targets, platform templates, SDK artifacts, and diagnostics directly into standard `flutter` commands (`doctor`, `config`, `create`, `devices`, `emulators`, `run`, `attach`, `install`, `screenshot`, `build`, `assemble`, `precache`, and `clean`) without forking the Flutter repository or maintaining bespoke wrapper CLIs. + +## Overview + +Custom Flutter embeddings (such as WebOS, Tizen, automotive head units, and industrial displays) currently lack first-class access to the standard functionality of the `flutter` command-line tool. While the C/C++ Embedder API provides a stable boundary for integrating the Flutter runtime on non-standard operating systems, the developer tooling in `flutter_tools` has historically hard-coded assumptions around its six first-party target platforms (Android, iOS, web, Linux, macOS, and Windows). + +Consequently, embedder maintainers have been stuck between two difficult choices: + +1. **Maintaining a fork of the Flutter SDK or a bespoke wrapper CLI** (`flutter-elinux`, `tizen-flutter`, `flutter-pi` tooling) that duplicates command routing, argument parsing, and asset bundling while constantly drifting behind upstream SDK releases and breaking standard IDE integrations. +2. **Using `flutter custom-devices`**, which substitutes shell commands for basic device discovery, installation, and port forwarding via a static JSON configuration file, but cannot express custom build pipelines (`flutter build`, `flutter assemble`), platform scaffolding (`flutter create`), SDK artifact management (`flutter precache`), environment diagnostics (`flutter doctor`), or native plugin compilation. + +To support custom embedders and target platforms without forks, the Flutter Tool needs to be pluggable, providing embedder maintainers a way to hook into existing commands that require knowledge of the target platform. To achieve this, the Flutter Tool will support project-scoped tool extensions (simply referred to as "extensions" throughout the rest of this document) that run in separate Dart isolates and provide platform-specific implementations for key operations performed by the tool. + +Once implemented, the Tool Extension API will allow extensions to provide support for: + +* Downloading and caching necessary artifacts for the target platform (e.g., custom embeddings of the Flutter engine or cross-compilation toolchains) via `flutter precache` +* Interacting with custom devices (e.g., detection via `flutter devices`, deployment and hot reload via `flutter run` and `flutter attach`, standalone installation via `flutter install`, screenshot capture via `flutter screenshot`, and log streaming) +* Listing and launching device emulators via `flutter emulators` +* Registering high-level `flutter build ` subcommands and fine-grained `flutter assemble` build targets to handle compilation and bundling steps specific to the target platform +* Running custom `flutter doctor` checks to verify that the host build environment is set up for the target platform +* Registering custom feature flags and configuration options for use with `flutter config` +* Scaffolding platform runner directories for applications and plugins via `flutter create . --platforms=` +* Cleaning up extension-specific build outputs during `flutter clean` + +### Non-Goals + +* **Security Sandboxing**: This design focuses on process stability and isolating the tool from uncaught Dart exceptions in buggy extensions, not on providing an OS-level security sandbox for executing untrusted code (see [Section 3.3](#33-isolate-isolation-boundaries--process-wide-constraints)). Once dependencies are resolved via `flutter pub get`, extensions run with the same permissions as the Flutter Tool, matching the trust model of Dart build hooks (`package:hooks`) and analyzer plugins. +* **Non-Dart Extensions**: This proposal only supports extensions written in Dart to leverage the isolate-based communication model and shared packages. +* **Universal Dart CLI Extensibility**: While the underlying isolate JSON-RPC runner is generic, the scope of this document is limited to the Flutter Tool and does not aim to provide a general plugin system for all Dart CLI tools. (See [Out of Scope & Future Work](#out-of-scope--future-work) for follow-up first-party platform migration and `flutter custom-devices` deprecation plans.) + +--- + +## Usage Examples + +### Example 1: Implementing a Custom Embedder Extension + +An embedder maintainer creates a Dart package (e.g., `package:flutter_tools_extension_custom_embedder`), declares its static manifest in `extension/flutter_tools/config.yaml`, and implements the relevant services from `package:flutter_tools_extension`. + +```yaml +# extension/flutter_tools/config.yaml +name: flutter_tools_extension_custom_embedder +entrypoint: bin/flutter_tools_extension_custom_embedder.dart +host_platforms: + - linux + - macos +services: + - device + - build + - template +build_targets: + - custom-embedder +target_platforms: + - custom_embedder +``` + +```dart +// bin/flutter_tools_extension_custom_embedder.dart +import 'package:flutter_tools_extension/flutter_tools_extension.dart'; + +/// Builds a Flutter application bundle for the custom embedder target. +final class CustomEmbedderBuildTarget extends BuildTarget { + const CustomEmbedderBuildTarget() + : super( + description: 'Build a custom embedder bundle.', + outputDir: + '${BuiltInPlaceholders.outputDir}/custom_embedder/${BuiltInPlaceholders.buildMode}', + targetPlatform: 'custom-arm64', + ); + + /// The subcommand name registered under `flutter build`. + @override + String get name => 'custom-embedder'; + + /// Compiles and packages the application bundle inside + /// [ExtensionBuildContext.outputDir]. + @override + Future build(ExtensionBuildContext context) async { + final Uri bundleDir = context.outputDir.resolve('bundle/'); + return ExtensionBuildResult(executableUri: bundleDir); + } +} + +/// Exposes custom embedder build targets to the Flutter Tool. +final class CustomEmbedderBuildService extends BuildService { + /// High-level `flutter build` targets provided by this extension. + @override + List get buildTargets => const [ + CustomEmbedderBuildTarget(), + ]; +} + +/// Discovers and manages connected custom embedder hardware. +final class CustomEmbedderDeviceService extends DeviceService { + /// Queries device tooling or scans the network for target hardware. + @override + Future> getDevices() async { + return const [ + TargetDevice( + buildTarget: 'custom-embedder', + category: .mobile, + ephemeral: true, + id: 'embedder-01', + name: 'Custom Embedder Reference Board', + sdkNameAndVersion: 'CustomOS 2.1', + supportsInstall: false, + supportsScreenshot: false, + supportsScreenRecording: false, + targetPlatform: 'custom-arm64', + ), + ]; + } + + /// Checks whether [project] contains a `custom_embedder/` runner directory. + @override + Future isSupportedForProject({ + required String deviceId, + required ExtensionFlutterProject project, + }) async { + return true; + } + + /// Installs and launches [executableUri] on [deviceId] and forwards the + /// VM Service port to the host machine. + @override + Future launchApp({ + required ExtensionDebuggingOptions debuggingOptions, + required String deviceId, + required Uri executableUri, + }) async {} + + /// Returns the host-forwarded HTTP VM Service URI for hot reload and + /// DevTools. + @override + Future getVmServiceUri({ + required String deviceId, + int? deviceVmServicePort, + int? hostVmServicePort, + }) async { + return Uri.parse('http://127.0.0.1:41235/abcd1234efgh=/'); + } + + /// Terminates the running application and tears down port forwarding. + @override + Future stopApp({required String deviceId}) async {} +} + +/// Adds `--platforms=custom_embedder` support to `flutter create`. +final class CustomEmbedderTemplateService extends TemplateService { + /// Maps the `custom_embedder` platform identifier to its template directory. + @override + Map get appPlatformTemplates => const { + 'custom_embedder': + 'package:flutter_tools_extension_custom_embedder/templates/custom_embedder', + }; +} + +/// Isolate entrypoint spawned by the Flutter Tool via `Isolate.spawnUri`. +Future main(List args, Object? message) async { + await ToolExtensionEntryPoint.run( + message, + [ + CustomEmbedderBuildService(), + CustomEmbedderDeviceService(), + CustomEmbedderTemplateService(), + ], + ); +} +``` + +### Example 2: CLI Workflow for App Developers + +Once the extension is published to `pub.dev` or available locally in a workspace, application developers use standard `flutter` CLI commands: + +```sh +# 1. Enable the experimental tool extensions feature flag. +$ flutter config --enable-tool-extensions + +# 2. Create a standard Flutter project and add the embedder extension. +$ flutter create my_embedded_app +$ cd my_embedded_app +$ flutter pub add --dev flutter_tools_extension_custom_embedder + +# 3. Scaffold the custom platform runner directory in place. +$ flutter create . --platforms=custom_embedder + +# 4. Verify environment readiness and connected target hardware. +$ flutter doctor +$ flutter devices + +# 5. Run with hot reload, build a release bundle, and clean build outputs. +$ flutter run -d embedder-01 +$ flutter build custom-embedder --release +$ flutter clean +``` + +--- + +## Detailed Design + +### 1. Package Boundaries & Workspace Layout + +The extension framework splits contracts across three packages in the Flutter repository workspace: + +```mermaid +graph TD + subgraph "Host CLI Process (flutter_tools)" + FT["package:flutter_tools
(Extension Management, RPC Clients,
Host Subsystem Adapters)"] + FTC_Host["package:flutter_tools_core
(Shared Wire Models & Constants)"] + FT --> FTC_Host + end + + subgraph "Extension Isolate (Isolate.spawnUri)" + EXT["3P Extension Package
(e.g., flutter_tools_extension_tizen)"] + FTE["package:flutter_tools_extension
(ToolExtensionEntryPoint, *Service bases,
ExtensionDependencies, Testing Harness)"] + FTC_Ext["package:flutter_tools_core
(Shared Wire Models & Constants)"] + EXT --> FTE + EXT --> FTC_Ext + FTE --> FTC_Ext + end + + FT <== "JSON-RPC 2.0 over IsolateChannel
(SendPort / ReceivePort)" ==> FTE +``` + +1. **`package:flutter_tools_core`** (`packages/flutter_tools/packages/flutter_tools_core/`): + * Contains pure, immutable data transfer models and wire types for devices and emulators, build and assemble targets, doctor diagnostics, configuration flags, SDK artifacts, and extension manifest metadata. + * Depends only on `package:collection` and `package:meta` so it can be published to `pub.dev` without pulling internal `flutter_tools` implementation details into third-party packages. +2. **`package:flutter_tools_extension`** (`packages/flutter_tools/packages/flutter_tools_extension/`): + * Re-exports `package:flutter_tools_core` and provides the base service interfaces, build and assemble execution contexts, isolate entrypoint runner, injectable host-like dependencies (file system, logger, and process manager), and extension testing utilities (`package:flutter_tools_extension/testing.dart`). +3. **`package:flutter_tools`** (`packages/flutter_tools/`): + * Implements host-side extension discovery, kernel snapshot compilation and caching, isolate lifecycle management, JSON-RPC client wrappers, and adapters bridging extension services into native Flutter Tool subsystems (doctor, config, templates, devices, build system, artifacts, and clean). + +Both `package:flutter_tools_core` and `package:flutter_tools_extension` are published to `pub.dev` and resolved via standard semantic versioning rather than being pinned to the exact version shipped inside the user's Flutter SDK. Because extensions run in separate isolates and communicate with the Flutter Tool over a backwards-compatible JSON-RPC 2.0 wire protocol (rather than sharing in-memory Dart class hierarchies with `package:flutter_tools`), an extension compiled against an older minor version of `package:flutter_tools_extension` remains compatible with newer Flutter SDKs even when newer SDKs add methods, fields, or parameters to the service interfaces. + +--- + +### 2. Project-Scoped Discovery & Two-Tier Kernel Compilation + +#### 2.1 Discovery via `package:extension_discovery` + +Following the precedent established by DevTools extensions, extensions are shipped as standard Dart packages and declared in a project's `pubspec.yaml` (typically under `dev_dependencies`). When the Flutter Tool runs inside a project directory, it uses `package:extension_discovery` to inspect `.dart_tool/package_config.json` and locate packages containing `extension/flutter_tools/config.yaml`. This enables teams to lock extension versions in version control and execute reproducible builds across developer machines and CI environments with a standard `flutter pub get`. + +To prevent unvetted transitive dependencies or incompatible host packages from executing code automatically, the tool enforces several safeguards during discovery: + +* **Direct Dependency Filtering**: The tool inspects the active project's `pubspec.yaml` (and the Pub workspace root `pubspec.yaml`, if the project participates in a workspace) and activates only extensions listed in direct `dependencies` or `dev_dependencies`. +* **Host Platform Filtering**: Before compiling or spawning an extension isolate, the tool filters out any extension whose `host_platforms` list in `config.yaml` is non-empty and does not match the current host operating system or `HostPlatform.cliName`. +* **Safe Mode Bypass**: To ensure users can recover from broken or misbehaving extensions, passing `--no-extensions` (`--[no-]extensions`) on the command line or setting `FLUTTER_NO_EXTENSIONS=1` (`true` or `yes`) in the environment skips extension discovery and isolate spawning for the invocation. +* **`pub get` Lifecycle Ordering**: Commands that register dynamic CLI flags or subcommands from extensions ensure `.dart_tool/package_config.json` is up to date before querying available extensions, and refresh discovery if `pub get` resolves new packages during command startup. + +#### 2.2 Static Manifest (`extension/flutter_tools/config.yaml`) + +Every extension package includes an `extension/flutter_tools/config.yaml` manifest at its package root that declares its package name, isolate entrypoint script, supported host platforms, implemented services, and contributed build targets and target platforms. Static declarations of `services`, `build_targets`, and `target_platforms` allow the Flutter Tool to register subcommands and flags and lazily spawn only the extensions required by the active command, rather than booting every installed extension on every CLI invocation. These declarations are verified against runtime service registration during the extension isolate handshake (see [Section 3.1](#31-json-rpc-20-transport-handshake--exit-monitoring)): + +| Field | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| `name` | `String` | Yes | The Dart package name of the extension. Validated against `packageName` from `package_config.json`. | +| `entrypoint` | `String` | No | Relative POSIX path within the package to the `.dart` isolate entrypoint script (defaults to `bin/.dart`; must end with `.dart`). | +| `host_platforms` | `List` | No | Lowercase host operating systems (`linux`, `macos`, `windows`) or `HostPlatform.cliName` identifiers (`linux-x64`, `linux-arm64`, `darwin-x64`, `darwin-arm64`, `windows-x64`, `windows-arm64`) supported by the extension. If omitted or empty, all host platforms are considered supported. | +| `services` | `List` | No | Service namespaces implemented by the extension (`diagnostics`, `config`, `template`, `device`, `build`, `artifact`, `clean`). Validated against runtime registration at handshake. | +| `build_targets` | `List` | No | Lowercase kebab-case subcommand target names contributed to `flutter build `. | +| `target_platforms` | `List` | No | Lowercase snake_case custom platform identifiers surfaced in `flutter create --platforms=`. | + +#### 2.3 Two-Tier Kernel (`.dill`) Compilation Cache + +Because `Isolate.spawnUri` requires either a Dart source URI with a resolved package config or a precompiled Dart kernel (`.dill`) snapshot, the Flutter Tool compiles extension entrypoints with `dart compile kernel` into a two-tier cache to keep startup fast: + +1. **Tier 1: Shared Cache for Hosted (`$PUB_CACHE`) Extensions**: + * **Location**: Stored under `$DART_DATA_HOME/flutter_tool_extensions/cache` (resolved using `package:dart_data_home` to follow standard per-OS user state directories). + * **Dependency Graph Cache Keying**: Because `dart compile kernel` embeds the consuming project's resolved transitive dependencies (`flutter_tools_extension`, `flutter_tools_core`, `json_rpc_2`, etc.) into the compiled `.dill`, two projects on the same machine may depend on the same hosted extension version while resolving different transitive package versions in their respective `.dart_tool/package_config.json` files. The tool keys Tier 1 snapshots by `--.dill`, hashing the Dart SDK version together with the exact resolved versions of all direct and transitive package dependencies of the extension from `.dart_tool/package_graph.json` and `.dart_tool/package_config.json`. + * **Concurrency**: Protected across concurrent `flutter` invocations using cross-process file locking and atomic temporary-file renames. +2. **Tier 2: Project-Local Cache for Mutable (`path:` / Workspace) Extensions**: + * **Location**: `/.dart_tool/flutter_tools/extensions//extension.dill` alongside `extension.dill.d`. + * **Invalidation**: Before spawning the isolate, the tool inspects the `.dill.d` depfile emitted by `dart compile kernel --depfile` and recompiles automatically if any transitive source file or `.dart_tool/package_config.json` has a newer modification timestamp than `extension.dill`. + +--- + +### 3. Isolate Transport, Lifecycle & Isolation Boundaries + +#### 3.1 JSON-RPC 2.0 Transport, Handshake & Exit Monitoring + +```mermaid +sequenceDiagram + participant Host as Flutter Tool (Host Isolate) + participant Iso as Extension Isolate (ToolExtensionEntryPoint) + participant Svc as Registered ToolExtensionServices + + Host->>Iso: Isolate.spawnUri(dillUri, [], handshakeSendPort, onError, onExit) + Iso->>Svc: await service.initialize() (returns RPC handler maps) + Note over Iso: Connect IsolateChannel only after fallible setup succeeds + Iso->>Host: IsolateChannel.connectSend(handshakeSendPort) + Iso->>Svc: Wire notificationSender & requestSender to Peer + Note over Host,Iso: Bidirectional IsolateChannel + Peer.withoutJson + Host->>Iso: RPC "extension.getCapabilities" + Iso-->>Host: ToolExtensionCapabilities (services) + Host->>Host: Validate registered services match config.yaml + Note over Host,Iso: Active Command RPCs & Notifications + Host->>Iso: RPC "extension.shutdown" + Iso->>Svc: await Future.wait(services.map((s) => s.shutdown())) + Iso-->>Host: { "acknowledged": true } + Host->>Iso: peer.close() & isolate.kill(priority: Isolate.immediate) +``` + +Because both endpoints execute inside the same Dart VM process, the Flutter Tool and `ToolExtensionEntryPoint` communicate over a `Peer.withoutJson` backed by an `IsolateChannel`. Structured `Map` payloads cross the isolate port directly without intermediate UTF-8 string encoding or decoding while preserving standard JSON-RPC 2.0 request, response, and notification framing. + +When an extension isolate boots, `ToolExtensionEntryPoint.run` invokes `await service.initialize()` on every registered service **before** calling `IsolateChannel.connectSend(sendPort)`. Because `connectSend` opens a `ReceivePort` that keeps the isolate alive, deferring channel connection until after all fallible initialization completes ensures that an unhandled exception during `initialize()` allows the isolate to exit immediately. On the host side, the tool attaches listeners to `Isolate.spawnUri`'s `onError` and `onExit` ports so unexpected isolate termination immediately fails any pending handshake or in-flight RPCs. + +**Wire Payload Examples**: + +1. **Capability Handshake Request**: Sent by the Flutter Tool to the extension isolate immediately after the `IsolateChannel` connects to query which services the extension registers: + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "method": "extension.getCapabilities" + } + ``` + +2. **Capability Handshake Response**: Returned by the extension isolate listing the active service namespaces: + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "services": ["build", "device", "template"] + } + } + ``` + +3. **Structured Log Notification**: Sent asynchronously by the extension isolate to stream log output to the Flutter Tool's logger: + + ```json + { + "jsonrpc": "2.0", + "method": "extension.log", + "params": { + "level": "info", + "message": "Launching /path/to/bundle on embedder-01..." + } + } + ``` + +#### 3.2 Graceful Shutdown & Subprocess Cleanup + +Extensions frequently spawn OS subprocesses (such as `ssh -L` port-forwarding tunnels, device log readers, or emulators) whose lifecycles must not outlive the `flutter` process: + +* The tool registers extension teardown with its shutdown hooks on CLI exit. +* When shutting down an active extension, the tool sends an `extension.shutdown` RPC to the isolate and awaits completion with a short bounded grace period before closing the JSON-RPC `Peer` and calling `isolate.kill(priority: Isolate.immediate)`, ensuring a wedged extension cannot hang CLI exit. +* `ToolExtensionEntryPoint` handles `extension.shutdown` by invoking `await Future.wait(services.map((s) => s.shutdown()))`, allowing `DeviceService.shutdown()` and other services to terminate active tunnels and child processes cleanly. +* Because OS child processes belong to the host VM process rather than the child isolate, we also propose introducing `ExtensionProcessManager` as an injectable abstraction (allowing fake implementations to be injected in unit tests via `ExtensionDependencies`; extensions currently invoke `dart:io` `Process` or `package:process` directly) that tracks active background process PIDs and reports them to the host tool, allowing the tool to terminate any remaining child processes via `Process.killPid` if an extension isolate exits unexpectedly. + +#### 3.3 Isolate Isolation Boundaries & Process-Wide Constraints + +While Dart isolates provide separate heaps and prevent uncaught Dart exceptions in an extension from crashing the main tool isolate's event loop, **isolates share the same OS process** as the Flutter Tool. Extension authors and the host runtime must account for four process-wide boundaries: + +1. **`stdout` / `stderr` / `stdin` Stream Integrity**: Commands such as `flutter daemon` and `flutter devices --machine` emit structured JSON protocol frames over `stdout`. Unintercepted writes to `io.stdout` or `print()` calls inside an extension isolate would corrupt the tool's machine output, and reads from `io.stdin` would contend with the host CLI for terminal input. + * **Mitigation**: `ToolExtensionService.log` and `ExtensionLogger` emit structured `extension.log` JSON-RPC notifications that the host forwards to its logger. In addition, `ToolExtensionEntryPoint.run` executes inside both a custom `ZoneSpecification` (intercepting `print()` calls) and `IOOverrides.runZoned` (overriding `stdout`, `stderr`, and `stdin`). Any direct writes to `io.stdout` or `io.stderr` inside the extension isolate are captured by the overridden `stdout` and `stderr` streams and forwarded as `extension.log` JSON-RPC notifications (`level: .info` and `level: .error`), while `io.stdin` is overridden with an empty stream so extensions cannot block waiting on raw terminal input (interactive CLI prompts are instead mediated by the host tool over the bidirectional JSON-RPC `Peer`; see [Section 4.3](#43-configuration-flutter-config)). +2. **Process Exit & Working Directory**: Calling `io.exit()` inside an extension terminates the entire `flutter` process immediately, and mutating `io.Directory.current` alters the process-wide current working directory for the main isolate and sibling extensions. + * **Mitigation**: The `IOOverrides.runZoned` environment installed by `ToolExtensionEntryPoint.run` also overrides `exit` (`exit: (int code) => throw UnsupportedError('Tool extensions must not call exit().')`, using the `IOOverrides.exit` hook added in Dart 3.10) and guards `getCurrentDirectory` / `setCurrentDirectory` against working-directory mutations. Instead of calling `exit()` or mutating `Directory.current`, extensions must throw `ToolExtensionException` on failure, pass explicit `workingDirectory` arguments when spawning processes, and resolve paths against project URIs. +3. **Native Crashes (FFI) and Out-of-Memory**: A segmentation fault in native FFI code loaded by an extension or process-level memory exhaustion will terminate the `flutter` process. +4. **Snapshot Format Compatibility (`Isolate.spawnUri`)**: The snapshot format passed to `Isolate.spawnUri` must match the execution mode of the host Dart runtime. Because the Flutter Tool currently executes on the Dart JIT VM, the tool compiles extensions into kernel (`.dill`) snapshots (`dart compile kernel`). If the Flutter Tool is compiled into a standalone AOT executable (`dart compile exe`), `Isolate.spawnUri` cannot load kernel `.dill` files, but it *can* spawn precompiled AOT snapshots (`dart compile aot-snapshot`) compiled with the matching Dart runtime. (See [Alternatives Considered](#alternatives-considered) for `stdio` subprocess trade-offs.) + +--- + +### 4. Base Service Contracts & Host Integration + +#### 4.1 Core Service, Entrypoint & Error Abstractions + +`package:flutter_tools_extension` provides a set of base classes for the services that can be implemented by an extension. All seven extension services (`DiagnosticsService`, `ConfigService`, `TemplateService`, `DeviceService`, `BuildService`, `ArtifactService`, and `CleanService`), as well as `BuildTarget` and `AssembleTarget`, are declared as `abstract base class`. Restricting `implements` ensures that `package:flutter_tools_extension` can add new defaulted methods in minor releases without breaking existing extensions. + +To replace the current ad-hoc error handling (where extensions throw generic exceptions or `RpcException` directly), this RFC proposes a single structured error model in `package:flutter_tools_core`: operational failures throw `ToolExtensionException` (with default error code `-32001`), optional capabilities that are not supported throw `UnsupportedError` (mapped to JSON-RPC error code `-32002`), and unexpected uncaught exceptions map to `json_rpc_2`'s standard `-32000` (`SERVER_ERROR`), allowing the tool to distinguish actionable domain errors from unhandled extension bugs. + +```dart +// package:flutter_tools_core/src/exception.dart +/// An exception thrown by a tool extension to report an intentional, +/// user-actionable operational failure over JSON-RPC 2.0. +@immutable +class ToolExtensionException implements Exception { + /// Creates a domain exception with a user-facing [message], optional JSON-RPC + /// error [code], and optional structured diagnostic [data]. + const ToolExtensionException( + this.message, { + this.code = domainErrorCode, + this.data, + }); + + /// JSON-RPC error code for intentional, user-actionable domain errors. + /// Distinct from -32000 (`SERVER_ERROR`), which indicates an unhandled + /// exception in the extension isolate. + static const domainErrorCode = -32001; + + /// JSON-RPC error code returned when an optional capability is not supported + /// by the extension. + static const unsupportedCapabilityErrorCode = -32002; + + /// The JSON-RPC 2.0 error code reported to the Flutter Tool. + final int code; + + /// Optional structured error details serialized into the JSON-RPC error data. + final Map? data; + + /// Human-readable error message surfaced by the Flutter Tool. + final String message; +} +``` + +```dart +// package:flutter_tools_extension/src/protocol_base/service.dart +/// Signature for a JSON-RPC 2.0 request handler registered by a +/// [ToolExtensionService]. +typedef ExtensionRpcHandler = FutureOr Function( + Map params, +); + +/// Callback that dispatches a fire-and-forget JSON-RPC 2.0 notification from a +/// [ToolExtensionService] to the host tool. +typedef ExtensionNotificationSender = void Function( + String method, + Map params, +); + +/// Callback that dispatches a JSON-RPC 2.0 request from a +/// [ToolExtensionService] to the host tool over the bidirectional peer. +typedef ExtensionRequestSender = Future Function( + String method, + Map params, +); + +/// Base class for all services exposed by a Flutter Tool extension isolate. +abstract base class ToolExtensionService { + /// JSON-RPC notification method used to stream structured log messages to the + /// host tool. + static const logNotificationMethod = 'extension.log'; + + /// JSON-RPC notification method used to report progress for long-running + /// operations. + static const progressNotificationMethod = 'extension.progress'; + + /// JSON-RPC request method used to prompt the user to select an option from + /// an interactive terminal menu on the host tool. + static const promptForOptionRequestMethod = 'terminal.promptForOption'; + + /// Callback wired by [ToolExtensionEntryPoint.run] when the JSON-RPC peer + /// channel connects. + @internal + ExtensionNotificationSender? notificationSender; + + /// Callback wired by [ToolExtensionEntryPoint.run] when the JSON-RPC peer + /// channel connects to send requests to the host tool. + @internal + ExtensionRequestSender? requestSender; + + /// The JSON-RPC method namespace prefix owned by this service (for example, + /// `'device'` or `'build'`). + String get namespace; + + /// Initializes state needed by the service and returns a map of RPC method + /// handlers (without the [namespace] prefix). + Future> initialize(); + + /// Sends a fire-and-forget JSON-RPC 2.0 notification to the host tool. + /// Throws a [StateError] if called before the peer channel is connected. + void sendNotification(String method, Map params) { + final ExtensionNotificationSender? sender = notificationSender; + if (sender == null) { + throw StateError( + 'Cannot send notification "$method": service "$namespace" is not ' + 'attached to an active peer.', + ); + } + sender(method, params); + } + + /// Sends a structured `extension.log` notification to the host tool. + void log( + String message, { + ExtensionLogLevel level = .info, + DateTime? timestamp, + }) { + sendNotification(logNotificationMethod, { + 'level': level.name, + 'message': message, + 'timestamp': (timestamp ?? DateTime.now()).toUtc().toIso8601String(), + }); + } + + /// Emits an `extension.progress` notification to report progress for + /// long-running operations to the host tool. + void reportProgress(String message) { + sendNotification(progressNotificationMethod, { + 'message': message, + }); + } + + /// Prompts the user to select an option from [options] via the host tool's + /// interactive terminal over the bidirectional JSON-RPC peer. + /// + /// Returns the selected 0-based index in [options], or `null` if the user + /// cancels the prompt. Throws a [ToolExtensionException] if the host tool is + /// running without an interactive terminal or with `--machine`. + Future promptForOption({ + required String message, + required List options, + int? defaultChoiceIndex, + }) async { + final ExtensionRequestSender? sender = requestSender; + if (sender == null) { + throw StateError( + 'Cannot send request "$promptForOptionRequestMethod": service ' + '"$namespace" is not attached to an active peer.', + ); + } + final Object? response = await sender( + promptForOptionRequestMethod, + { + 'message': message, + 'options': options, + 'defaultChoiceIndex': ?defaultChoiceIndex, + }, + ); + if (response case {'selectedIndex': final int? selectedIndex}) { + return selectedIndex; + } + return null; + } + + /// Cleans up resources held by the service when the extension is shut down. + Future shutdown() async {} +} + +/// Bootstraps a tool extension isolate and binds registered +/// [ToolExtensionService]s to a JSON-RPC 2.0 peer over an `IsolateChannel`. +abstract final class ToolExtensionEntryPoint { + /// Initializes [services], connects the isolate handshake port from + /// [message], and listens for JSON-RPC 2.0 requests inside an `IOOverrides` + /// and print-capturing `Zone`. + static Future run( + Object? message, + List services, + ) async { + // Validates that `message` is a SendPort, initializes all services, + // connects the IsolateChannel, and listens inside IOOverrides.runZoned + // and a print-capturing Zone. + } +} +``` + +#### 4.2 Diagnostics (`flutter doctor`) + +To provide environment diagnostics via `flutter doctor`, extensions can register a `DiagnosticsService` that runs one or more validation checks for the target platform's SDK and host toolchain. Each extension providing a `DiagnosticsService` appears as a category in `flutter doctor`, and when `runDiagnostics()` returns multiple `ValidationResult` entries, the tool merges them by severity (`crash > missing > partial > notAvailable > success`). + +```dart +/// The service responsible for executing custom diagnostic checks that +/// can be reported via `flutter doctor`. +abstract base class DiagnosticsService extends ToolExtensionService { + /// The JSON-RPC namespace for diagnostics RPCs (`'diagnostics'`). + static const String serviceNamespace = 'diagnostics'; + + /// The JSON-RPC method namespace owned by this service. + @override + String get namespace => serviceNamespace; + + /// This is displayed in the CLI as the header for the check (for example, + /// `'Tizen toolchain'`). + String get title; + + /// An RPC handler the main tool invokes to retrieve and run all custom + /// diagnostic checks provided by this extension. + Future> runDiagnostics(); +} +``` + +When `flutter doctor` runs in a project with custom embedder extensions installed, each extension's diagnostic result (including passing `[✓]`, partial `[!]`, or failing `[✗]` status and any `ValidationMessage` error or hint lines) is displayed alongside the built-in categories: + +```text +$ flutter doctor +Doctor summary (to see all details, run flutter doctor -v): +[✓] Flutter (Channel main, 3.43.0-1.0.pre, on Linux, locale en_US.UTF-8) +[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0) +[✓] Chrome - develop for the web +[✓] Linux toolchain - develop for Linux desktop +[✗] Tizen toolchain - develop for Tizen devices + ✗ Tizen Studio is not installed; download from https://developer.tizen.org/development/tizen-studio/download + ! Set the Tizen Studio installation path with "flutter config --tizen-sdk=" +[!] Embedded Linux toolchain - develop for eLinux devices (eLinux SDK 1.4.0) + ! clang++ cross-compiler for aarch64-linux-gnu not found on PATH +[✓] Connected device (2 available) +[✓] Network resources +``` + +#### 4.3 Configuration (`flutter config`) + +To support custom configuration options, extensions can provide an implementation of `ConfigService`, which allows extensions to add boolean feature flags (`--[no-]enable-`), key-value configuration options (`--