Skip to main content

Dart Opaque

Design

For completeness, the goal is repeated from the last page:

goal
  • The unsafe code should be carefully encapsulated, as minimal as possible, and have a clearly defined and easily checkable semantics. Each component should be focused on one clear thing only ("single-responsibility rule").
  • No matter how users use or mis-use it, it should be safe (e.g. no undefined behavior). Safety and soundness is critical.

Safety concern

Questions similar to last section can be asked and answered, so I do not repeat it here, since the semantics of each component is mentioned below.

Details of the components

(Generalized)AutoDropDartPersistentHandle

struct AutoDropDartPersistentHandle(Dart_PersistentHandle);
impl Drop for AutoDropDartPersistentHandle { ... }

A Dart_PersistentHandle that delete the handle when Dropped.

GuardedBox, ThreadBox, DartIsolateBox

Take ThreadBox as an example. It is equivalent to:

struct ThreadBox<T> { inner: T, thread_id: ThreadId }
impl<T> ThreadBox<T> {
fn any_method_that_uses_inner_value(&self) {
if !self.is_on_creation_thread() { panic!(); }
...
}
}
unsafe impl<T> Send for ThreadBox<T> {} // and Sync

Only allows manipulation at the thread which it is created. It is a "black box" that nobody can open it when it is on another thread.

The inner value can never be (1) used or (2) dropped on any thread except for the creation thread.

Therefore, even though it is Send/Sync among threads, it is just a blackbox on all other threads, so we are safe.

DartOpaqueNonClone

struct DartOpaqueNonClone {
persistent_handle: ThreadBox<GeneralizedAutoDropDartPersistentHandle>,
drop_port: SendableMessagePortHandle,
}

Only safe functions. Semantics about Send/Sync are already encapsulated in the ThreadBox.

DartOpaque

struct DartOpaque {
arc: Arc<DartOpaqueNonClone>,
}

Add Arc to make it cloneable.

V1 documentations

info

This section was written for V1, so it may be slightly outdated for V2.

Ownership and GC

From the moment the opaque type is passed, Rust will own a persistent representation of the dart object (Dart_PersistentHandle or JsValue). This means that while Rust owns DartOpaque the object will not be cleared by GC. Also flutter_rust_bridge provides a thread-safe drop for DartOpaque: Rust delegates the drop to the Dart side using the Dart port.

Example

Case 1: loopBack.

Rust api.rs:

pub fn loop_back(opaque: DartOpaque) -> DartOpaque {
opaque
}

Dart:


String f() => 'Test_String';

var fn = await api.loopBack(opaque: f) as String Function();

expect(fn(), 'Test_String');

Case 2: drop.

Rust api.rs:

pub fn sync_accept_dart_opaque(opaque: DartOpaque) -> SyncReturn<String> {
drop(opaque);
SyncReturn("test".to_owned())
}

pub fn async_accept_dart_opaque(opaque: DartOpaque) {
drop(opaque);
}

Dart:

// the closure is safely removed on the Rust side (on another thread)
await api.asyncAcceptDartOpaque(opaque: () => 'Test_String');
// the closure is safely removed on the Rust side (on current thread)
api.syncAcceptDartOpaque(opaque: () => 'Test_String');

Case 3: Unwrap.

Rust api.rs:

/// [DartWrapObject] can be safely retrieved on a dart thread.
pub fn unwrap_dart_opaque(opaque: DartOpaque) -> SyncReturn<String> {
let handle = opaque.try_unwrap().unwrap();
SyncReturn("Test".to_owned())
}

/// [DartWrapObject] cannot be obtained
/// on a thread other than the thread it was created on.
pub fn panic_unwrap_dart_opaque(opaque: DartOpaque) {
let handle = opaque.try_unwrap().unwrap();
}

Dart:

// Rust gets (drop safely) wrap Dart_PersistentHandler (or JsValue).
api.unwrapDartOpaque(opaque: () => 'Test_String');

// We get an error because DartOpaque was passed to another thread.
await expectLater(() => api.panicUnwrapDartOpaque(opaque: () => 'Test_String'), throwsA(isA<FfiException>()));

Dispose

If there is an attempt to delegate the drop to the Dart side after the drop port (RustApi.dispose()) has been closed, flutter_rust_bridge will issue a warning in the logs, the memory behind the object will leak.

However, this should not happen, because RustApi itself usually live for the whole application lifetime, and there is no need to dispose it.