跪拜 Guibai
← All articles
Frontend · Flutter · Android

Dart's Shared-Memory Multithreading Lands with IsolateGroupBound

By 恋猫de小郭 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

This removes a fundamental interop tax for Flutter apps that embed native audio engines, UI frameworks, or other C/C++ libraries. Native threads can now call Dart synchronously without the overhead of copying data or the complexity of routing through a specific isolate's event loop.

Summary

Dart's long-running effort to add shared memory to isolates has reached a concrete milestone. The `NativeCallable.isolateGroupBound` API, now in Dart 3.12.2, allows native code on any thread to synchronously execute a Dart callback within an isolate group, without binding to a particular isolate. This callback can only access static or top-level state explicitly marked as shared via `@pragma('vm:shared')`.

Shared state is currently limited to deeply immutable types, `TypedData`, `Struct`, and closures meeting strict capture constraints. Ordinary mutable Dart objects like `List` and `Map` remain isolate-local. The new `dart:concurrent` library provides `Mutex` and `ConditionVariable` to manage concurrent access to shared mutable binary data, since the VM does not guarantee sequential consistency for those writes.

The design solves two persistent pain points: the high cost of copying large object graphs between isolates, and the friction of native threads needing to understand Dart's isolate model. A future goal is for Dart AOT code to behave like a standard C library, where any native thread can call exported Dart symbols directly. The current implementation uses a hybrid of compile-time and runtime checks, and `dart:async` APIs like `Future` and `Stream` are unavailable in this context, making it best suited for short, synchronous FFI callbacks.

Takeaways
`NativeCallable.isolateGroupBound` lets any native thread synchronously execute a Dart callback within the creator's isolate group, but outside any specific isolate.
Code running in this context can only access static or top-level state marked with `@pragma('vm:shared')`; accessing ordinary non-shared static state throws an `AccessError`.
Shareable types include strings, numbers, compile-time constants, `TypedData`, `Struct`, static method tear-offs, and closures with trivially shareable captures. Ordinary `List` and `Map` objects cannot be shared.
The `dart:concurrent` library now includes `Mutex` and `ConditionVariable` to prevent data races on shared mutable binary data like `Uint8List`.
`dart:async` APIs (`Future`, `Stream`, `Timer`, `Zone`) are unavailable in isolate-group-bound callbacks, limiting them to synchronous, short-lived FFI work.
Third-party packages that use ordinary static variables for caching or singletons will break inside these callbacks, and the resulting `AccessError` can be hard to trace.
A long-term goal is to let Dart AOT code be linked into native apps like a C library, with any thread calling `@ffi.Export()` symbols directly.
Conclusions

By restricting shared state to a narrow set of trivially shareable types instead of opening up arbitrary Dart objects, the Dart team sidesteps the hardest concurrency problems while still solving the most expensive interop bottlenecks.

The hybrid checking approach—compile-time for field declarations, creation-time for callback captures, and runtime for deep call chains—is a pragmatic compromise that avoids a massive type-system overhaul but leaves a class of bugs that only surface in production.

Third-party ecosystem compatibility is the biggest hidden risk. Libraries written under the old isolate model freely use static state, and none of them were designed for an execution context where that state is off-limits.

Making `dart:io` eventually implementable in pure Dart is a signal that the team sees this shared-memory model as foundational enough to rewrite core platform libraries on top of it.

Concepts & terms
Isolate Group
A set of Dart isolates that share code, type information, and potentially the same managed heap, but still enforce object access boundaries so isolates cannot directly read each other's ordinary mutable objects.
NativeCallable.isolateGroupBound
A Dart FFI API that creates a callable object allowing native code on any thread to synchronously invoke a Dart function within the isolate group of its creator, without entering a specific isolate.
@pragma('vm:shared')
An annotation marking a static field or top-level variable as shared across an isolate group. The field must be final and its value must be deeply immutable or trivially shareable.
Trivially Shareable
A constraint on Dart objects that can be safely shared between isolates without copying. Includes strings, numbers, compile-time constants, TypedData, Struct, and closures that only capture shareable values.
From the discussion

The discussion splits between appreciation for the author's consistency and skepticism toward shared-memory concurrency. One view holds that Flutter's current model is sufficient and shouldn't chase Swift's complexity. A competing perspective argues that shared-memory concurrency imposes too much cognitive load on developers and is fading in favor of message-passing approaches like Go's channels.

Flutter's existing concurrency model works well and does not need to adopt the complexity Swift 6 introduced to solve similar problems.
Shared-memory concurrency places a high mental burden on developers, especially those from frontend backgrounds.
Message-passing via channels (as in Go) is becoming the dominant concurrency paradigm, with shared memory used only sparingly and through locking, making full shared-memory approaches feel outdated.
Featured comments
Suica

Swift 6 created a bunch of problems trying to solve this. Flutter is fine as it is, no need to follow suit.

蜗牛98

As someone who mainly works on the frontend, I feel there's no need to keep shared-memory concurrency. After all, this kind of concurrency demands more mental overhead from developers. Even Go primarily uses channels for communication, supplemented by some shared memory for performance, but that's done through code-level locking. This kind of shared-memory concurrency will become less and less common in the future.

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗