uniffi-rs icon indicating copy to clipboard operation
uniffi-rs copied to clipboard

Allow Rust traits to be exposed as an interface

Open mhammond opened this issue 3 years ago • 5 comments

We've a use-case where we'd like to have a .udl describe an interface that's actually implemented on the Rust side of the world as a trait rather than a struct. Hopefully the motivation will be clear when looking at the new fixture.

This actually turned out reasonably well, but there is still quite a bit of cleanup to do, and also other edge-cases to explore. I'd also welcome alternative strategies, and something tells me that there's some synergy now between traits and callback interfaces, but I'm yet to explore that. In the meantime, I thought I'd get this up for early feedback.

mhammond avatar Aug 12 '22 07:08 mhammond

Uhh... This already works without [Trait] though?

https://github.com/matrix-org/matrix-rust-sdk/blob/ae18b01c25a99ec9fe7c11f6524332af67e256ee/bindings/matrix-sdk-ffi/src/messages.rs#L238-L254

https://github.com/matrix-org/matrix-rust-sdk/blob/ae18b01c25a99ec9fe7c11f6524332af67e256ee/bindings/matrix-sdk-ffi/src/api.udl#L163-L165

jplatte avatar Aug 12 '22 08:08 jplatte

Uhh... This already works without [Trait] though?

The goal would be to support a Trait that's implemented by multiple types and have allow the foreign code to have something that works like Arc<dyn Trait>.

bendk avatar Aug 12 '22 19:08 bendk

In the spirit of early feedback, here's a patch that makes the FfiConverter code add the box so user's don't have to: https://github.com/bendk/uniffi-rs/pull/new/traits-unboxed.

I also wonder about avoiding the box altogether. AFAICT, the reason we need it is because Arc<dyn Trait> is 2 pointers (AKA a fat pointer), but the foreign code is only expecting to handle 1 pointer. If the foreign code stored both pointers we could avoid the heap allocation. That said, this might add enough complexity that it's worth it to keep the box.

The idea of combining this new thing with CallbackInterface is very appealing to me, then you could have Rust code that operated on a set objects where some were implemented in Rust and others were implemented in the foreign language. It opens up some wild possibilities, like using SyncManager on Desktop where some of the SyncEngines are implemented in JS:

  • SyncManager would get a method like register_engine(Arc<dyn SyncEngine>)
  • Our Rust components would have a create_sync_engine() -> Arc<dyn SyncEngine> function
  • But you could also implement the SyncEngine interface in JS and pass that to register_engine()

How could that last part work? We already have code that inputs a callback interface handle from the foreign side and creates a Box<dyn CallbackInterfaceTrait> from it. We could refactor it to create an Arc<> instead, expose that as a scaffolding function, and add some code in the binding templates to glue it all together. Hand-wavey I know, but I think it's doable.

This makes me think that maybe the Type hierarchy should change in a different way. Instead of grouping this together with Type::Object, maybe it should be grouped together with Type::CallbackInterface which we then rename to Type::Interface.

bendk avatar Aug 12 '22 21:08 bendk

How could that last part work? We already have code that inputs a callback interface handle from the foreign side and creates a Box<dyn CallbackInterfaceTrait> from it.

Yeah - I think there is real scope here - ultimately, I think the only real difference will be whether the code expects the trait to already exist, or whether uniffi needs to generate it.

Thanks for that tweak - I'll play with it more later next week.

mhammond avatar Aug 13 '22 02:08 mhammond

How could that last part work? We already have code that inputs a callback interface handle from the foreign side and creates a Box<dyn CallbackInterfaceTrait> from it.

I tweaked the callbacks example to try and move towards this - there's now a "SimCard" trait (and I think that probably means my new fixture can die). My next step is to work out how to have the foreign code also implement this new trait, which IIUC, is what you described above.

This makes me think that maybe the Type hierarchy should change in a different way. Instead of grouping this together with Type::Object, maybe it should be grouped together with Type::CallbackInterface which we then rename to Type::Interface.

I suspect it might play out slightly different to that and we might even be able to just remove CallbackInterface entirely - but that should become clearer once we flesh out some examples.

mhammond avatar Aug 14 '22 05:08 mhammond

This is now in #1457

mhammond avatar Jan 23 '23 02:01 mhammond