Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Rust traits to be exposed as an interface #1308

Closed
wants to merge 4 commits into from

Conversation

mhammond
Copy link
Member

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.

@bendk
Copy link
Contributor

bendk commented Aug 12, 2022

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
Copy link
Contributor

bendk commented Aug 12, 2022

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.

@mhammond
Copy link
Member Author

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
Copy link
Member Author

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 mhammond force-pushed the traits branch 2 times, most recently from ded3a02 to 7fa707d Compare August 14, 2022 12:16
mhammond and others added 4 commits September 20, 2022 12:28
Slight tweak on Mark's original code to remove the requirement that
users return `Arc<Box<dyn Trait>>` for UniFFI functions that return
trait objects.  Now they only need to return `Arc<dyn Trait>`, which
corresponds to how regular objects work.

We still add a box to avoid the fat pointer issue, but now it's added by
the FfiConverter code instead.  Also, the Box is now wrapping the Arc
instead of the other way around, I'm not sure if that makes any
difference.

I think this basic approach should work, but I'm not completely
confident in how the pointers are being handled.  I also think there's a
a way to cleanup the way we handle pointer types slightly that would
consolidate some redundant code between regular objects and trait
objects.
/// the `lower()` or `write()` method of this impl.
fn try_lift(v: Self::FfiType) -> Result<Self::RustType> {
// Use Box::leak to avoid dropping the `Arc<T>` that is owned by the foreign-language code.
let foreign_arc = Box::leak(unsafe { Box::from_raw(v as *mut std::sync::Arc<T>) });
Copy link
Contributor

@bendk bendk Oct 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is where the leak comes from? This really makes me think Arc<Box<T>> is the way to go and maybe it's more precise to call this Arc<Box<dyn Trait>>. I think I can even wrap my head around it because it's closer the the way we both intuitively thought that things worked. Box<dyn Trait> is a fat pointer, where one points to the underlying T and the other points to the vtable. Then Arc<Box<dyn trait>> let's you share that between the foreign and Rust code, with the nice side benefit it's just a regular, skinny, pointer.

In fact, could you then just use the existing Arc<T> impl?

I think whatever way we choose to implement this, we need good documentation about how everything is layed out. I know I'm not going to remember in a couple months.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still a bit confused by this TBH - I think I need to better understand the leak.

My only real problem with Arc<Box<dyn T>> is the ergonomics, and I'm not even sure how much of a problem I think it is :) But having an argument be foo: Arc<Box<impl Foo>> seems like it's going to wind up with lots of &**foo and other gymnastics which makes the code look messy and just surfaces the limitations of UniFFI rather than looking "natural".

So I'm still trying to think about ways we can avoid this and use the slightly more natural Arc<impl Foo> in the signatures of functions being exposed. Although we discussed how ugly and hacky it would be, and I expressed hostility to the idea, I am starting to wonder how bad it would be to try and pull some of the tricks mentioned in this discussion about std::raw::TraitObject? Or whether we can make the current model not leak (which seems like it should be possible, although might end up being very heavy on many small, short-lived allocations). Or something :) WDYT?

I think whatever way we choose to implement this, we need good documentation about how everything is layed out.

Agreed - mermaid FTW I guess :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I need to better understand the leak.

I can't actually reproduce a leak in that lower/lift implementation - there is a leak in my branch, but it doesn't look like it's there - so let me get more info before we start exploring the more radical options.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only real problem with Arc<Box<dyn T>> is the ergonomics, and I'm not even sure how much of a problem I think it is :) But having an argument be foo: Arc<Box<impl Foo>> seems like it's going to wind up with lots of &**foo and other gymnastics which makes the code look messy and just surfaces the limitations of UniFFI rather than looking "natural".

That's a good point. I wonder if we could write our own wrapper type that uses Arc<Box<dyn T>> internally and has Deref impl that makes it easy to use. Or maybe the type should wrap Box<dyn T> in a way that makes it easy to use when wrapped in an Arc.

@mhammond
Copy link
Member Author

This is now in #1457

@mhammond mhammond closed this Jan 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants