9 releases (breaking)
0.7.0 | May 10, 2024 |
---|---|
0.6.0 | Feb 8, 2024 |
0.5.0 | Dec 26, 2023 |
0.4.0-dev2 | Nov 29, 2023 |
0.1.0 | Nov 15, 2022 |
#1 in #background-thread
4,804 downloads per month
225KB
5.5K
SLoC
irondash_message_channel
Rust-dart bridge similar to Flutter's platform channel.
This package allows calling Rust code from Dart and vice versa using pattern similar to Flutter's platform channel.
- Easy to use convenient API (Dart side mimics platform channel API).
- High performance
- Zero copy for binary data when calling Dart from Rust
- Exactly one copy of binary data when calling Rust from Dart
- Rust macros for automatic serialization and deserialization (similar to Serde but optimized for zero copy)
- No code generation
- Thread affinity - Rust channel counterpart is bound to thread on which the channel was created. You can have channels on platform thread or on any background thread as long as it's running a RunLoop.
- Finalize handlers - Rust side can get notified when Dart object is garbage collected.
- Async support
Usage
Initial setup
Because Rust code needs access to Dart FFI api some setup is required.
/// initialize context for Native library.
MessageChannelContext _initNativeContext() {
final dylib = defaultTargetPlatform == TargetPlatform.android
? DynamicLibrary.open("libmyexample.so")
: (defaultTargetPlatform == TargetPlatform.windows
? DynamicLibrary.open("myexample.dll")
: DynamicLibrary.process());
// This function will be called by MessageChannel with opaque FFI
// initialization data. From it you should call
// `irondash_init_message_channel_context` and do any other initialization,
// i.e. register rust method channel handlers.
final function =
dylib.lookup<NativeFunction<MessageChannelContextInitFunction>>(
"my_example_init_message_channel_context");
return MessageChannelContext.forInitFunction(function);
}
final nativeContext = _initNativeContext();
// Now you can create method channels
final _channel =
NativeMethodChannel('my_method_channel', context: nativeContext);
_channel.setMethodCallHandler(...);
Rust side:
use irondash_message_channel::*;
#[no_mangle]
pub extern "C" fn my_example_init_message_channel_context(data: *mut c_void) -> FunctionResult {
irondash_init_message_channel_context(data)
}
Simple usage
After the setup, you can use the Dart NativeMethodChannel
similar to Flutter's PlatformChannel
:
final _channel = NativeMethodChannel('my_method_channel', context: nativeContext);
_channel.setMessageHandler((call) async {
if (call.method == 'myMethod') {
return 'myResult';
}
return null;
});
final res = await _channel.invokeMethod('someMethod', 'someArg');
On Rust side, you can implement the MethodHandler
trait for non-async version, or AsyncMethodHandler
if you want to use async/await:
use irondash_message_channel::*;
struct MyHandler {}
impl MethodHandler for MyHandler {
fn on_method_call(&self, call: MethodCall, reply: MethodCallReply) {
match call.method.as_str() {
"getMeaningOfUniverse" => {
reply.send_ok(42);
}
_ => reply.send_error(
"invalid_method".into(),
Some(format!("Unknown Method: {}", call.method)),
Value::Null,
),
}
}
}
fn init() {
let handler = MyHandler {}.register("my_method_channel");
// make sure handler is not dropped, otherwise it can't handle method calls.
}
Or async version:
use irondash_message_channel::*;
struct MyHandler {}
#[async_trait(?Send)]
impl AsyncMethodHandler for MyHandler {
async fn on_method_call(&self, call: MethodCall) -> PlatformResult {
match call.method.as_str() {
"getMeaningOfUniverse" => {
Ok(42.into())
}
_ => Err(PlatformError {
code: "invalid_method".into(),
message: Some(format!("Unknown Method: {}", call.method)),
detail: Value::Null,
})),
}
}
}
fn init() {
let handler = MyHandler {}.register("my_method_channel");
// make sure handler is not dropped, otherwise it can't handle method calls.
}
Calling Dart from Rust
use irondash_message_channel::*;
struct MyHandler {
invoker: Late<AsyncMethodInvoker>,
}
#[async_trait(?Send)]
impl AsyncMethodHandler for MyHandler {
// This will be called right after method channel registration.
// You can use invoker to call Dart methods handlers.
fn assign_invoker(&self, invoker: AsyncMethodInvoker) {
self.invoker.set(invoker);
}
// ...
}
Note that to use Invoker
you need to know target isolateId
. You can get it from
MethodCall
structure while handling method calls in Rust. You can also get notified
when isolate is destroyed:
impl MethodHandler for MyHandler {
/// Called when isolate is about to be destroyed.
fn on_isolate_destroyed(&self, _isolate: IsolateId) {}
// ...
To see message channel in action look at the example project.
Threading consideration
MethodHandler
and AsyncMethodHandler
are bound to thread on which they were created. The thread must be running a RunLoop. This is implicitely true for platform thread. To use channels on background threads, you need to create a RunLoop
and run it yourself.
MethodInvoker
is Send
. It can be passed between threads and the response to method call will be received on same thread as the request was sent. Again, the thread must have a RunLoop
running.
Converting to and from Value
Value
is represents all types that can be sent between Rust and Dart. To simplify serialization and deserialization on Rust side, irondash_message_channel
provides IntoValue
and TryFromValue
proc macros, that generate TryInto<YourStruct>
and From<YourStruct>
traits for Value
. This is an optional feature:
[dependencies]
irondash_message_channel = { version = "0.6.0", features = ["derive"] }
#[derive(TryFromValue, IntoValue)]
struct AdditionRequest {
a: f64,
b: f64,
}
#[derive(IntoValue)]
struct AdditionResponse {
result: f64,
request: AdditionRequest,
}
let value: Value = get_value_from_somewhere();
let request: AdditionRequest = value.try_into()?;
let response: Value = AdditionResponse {
result: request.a + request.b,
request,
}.into();
More advanced mapping options are also supported, for example:
#[derive(IntoValue, TryFromValue)]
#[irondash(tag = "t", content = "c")]
#[irondash(rename_all = "UPPERCASE")]
enum Enum3CustomTagContent {
Abc,
#[irondash(rename = "_Def")]
Def,
SingleValue(i64),
#[irondash(rename = "_DoubleValue")]
DoubleValue(f64, f64),
Xyz {
x: i64,
s: String,
z1: Option<i64>,
#[irondash(skip_if_empty)]
z2: Option<i64>,
z3: Option<f64>,
},
}
Unlike serde, .into()
and try_into()
consume the original value, making it possible for zero-copy serialization and deserializaton.
Dependencies
~1–15MB
~144K SLoC