fzyzcjy / flutter_rust_bridge

Flutter/Dart <-> Rust binding generator, feature-rich, but seamless and simple.
https://fzyzcjy.github.io/flutter_rust_bridge/
MIT License
4.12k stars 281 forks source link

Sending closures and trait objects from Dart to Rust #141

Closed Michael-F-Bryan closed 2 years ago

Michael-F-Bryan commented 2 years ago

Is your feature request related to a problem? Please describe.

My library requires the caller to pass in a trait object to implement certain behaviour, and I am unsure how to best implement this with flutter_rust_bridge.

Describe the solution you'd like

If this were being done in C, I would create a vtable struct containing a bunch of function pointers, some void * user state, and a destructor for that user state, then I would implement my trait for that vtable.

Manual vtable and a trait implementation that defers to the vtable's functions ```rust #[repr(C)] struct VTable { user_data: *mut c_void, some_method: unsafe extern "C" fn(*mut c_void, *const c_char, c_int) -> c_int, ... free: unsafe extern "C" fn(*mut c_void), } impl Drop for VTable { fn drop(&mut self) { unsafe { (self.free)(self.user_data); } } } impl MyTrait for VTable { fn some_method(&mut self, input: &str) -> Result<(), Error> { unsafe { match (self.some_method)(self.user_data, input.as_ptr().cast(), input.len() as c_int) { 0 => Ok(()), error_code => Err(error_from_error_code(error_code)), } } } } ```

If this were normal Dart, I'm guessing the user would want to pass in a reference an object which inherits from an autogenerated abstract class.

Translating a trait definition into a Dart class could be a rather complex feature to add to flutter_rust_bridge though (how would it interact with GC?), so there might be a way to pass Dart closures across the foreign function interface.

Describe alternatives you've considered

Instead of passing objects directly and invoking synchronous methods, we could generate some sort of proxy object which passes messages back and forth between Rust and Dart when a method needs to be called. I'm not experienced enough in Dart to know whether this would be viable, though.

Michael-F-Bryan commented 2 years ago

Sorry for all the duplicate issues (#136, #137, #138, #139, #140). It seems like GitHub created a new ticket every time I hit <space> while I was typing that last sentence.

fzyzcjy commented 2 years ago

how would it interact with GC?

Possibly related: https://github.com/fzyzcjy/flutter_rust_bridge/issues/68 (quick answer: yes, quite hard to have dart GC + satisfy Rust's safety + make users of the lib safe)

there might be a way to pass Dart closures across the foreign function interface

Good question. You may find this link and this link related.

By the way, may I know why your lib wants to do this? Maybe it is a X-Y problem and we can find another way.

Feel free to make a PR and I am willing to merge it!

Michael-F-Bryan commented 2 years ago

By the way, may I know why your lib wants to do this? Maybe it is a X-Y problem and we can find another way.

I have an API which looks roughly like this:

struct Runtime { ... }

impl Runtime {
  pub fn load(wasm: &[u8], dependencies: impl Image) -> Result<Self, Error> { ... }
  pub fn execute(&mut self) -> Result<(), Error> { ... }
}

trait Image {
  fn load_input(&mut self, input_type: &str, args: &HashMap<String, serde_json::Value>) -> Result<Box<Input>, Error>;
  fn load_model(&mut self, model_type: &str, model_bytes: &[u8]) -> Result<Box<dyn Model>, Error>;
}

trait Input {
  fn generate_input(&mut self) -> Result<Tensor, Error>;
}

trait Model {
  fn infer(&mut self, inputs: &[Tensor]) -> Result<Vec<Tensor>, Error>;
}

The caller can use the Image type to provide support for different ML frameworks at runtime, or the WebAssembly code being run might request different inputs from your device (microphone, GPS, etc.) with arguments that aren't known until you call the load() method.

fzyzcjy commented 2 years ago

Interesting. So I guess the closure/trait implementation should be written in Rust, not Dart?

Michael-F-Bryan commented 2 years ago

I'm guessing you would need to generate some Rust type which implements that trait and whenever you call a method it "somehow" invokes the Dart equivalent provided by the caller.

When I've done this sort of thing for languages like C#, C, and Python I'll typically accept a function pointer and a void * pointer to some state, and the provided function pointer will have some glue code that casts the state pointer back to its original type and invokes the desired method.

I don't know if that approach will work with Dart though because as far as I am aware there is no way to get a pointer to a Dart object.

Michael-F-Bryan commented 2 years ago

I just re-read the replies on that Google groups thread and they mention one tactic which might work:

The most straightforward solution I thought of so far is to have a global map in Dart which indexes from an integer to a closure, then pass that ID down to C++ and back up in the static function callback.

From what I can tell, we would need to:

Does that sound like a viable approach to you? If so, I'd be happy to create a PR that implements it.

fzyzcjy commented 2 years ago

Does that sound like a viable approach to you? If so, I'd be happy to create a PR that implements it.

Oh I forget to reply this thread after reading the email notification... This sounds great, and I am looking forward to your PR!

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

fzyzcjy commented 2 years ago

Does that sound like a viable approach to you? If so, I'd be happy to create a PR that implements it.

Looking forward to your PR ;)

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] commented 2 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new issue.