mozilla / uniffi-rs

a multi-language bindings generator for rust
https://mozilla.github.io/uniffi-rs/
Mozilla Public License 2.0
2.7k stars 224 forks source link

How to implement builder pattern using uniffi? #2208

Open fw6 opened 1 month ago

fw6 commented 1 month ago

I’m new to Rust and I’m currently trying to use the uniffi library to develop a cross-platform component. However, I’m having trouble finding a suitable way to use the builder pattern to create a struct(crates such as bonderive_builder).

I would appreciate any guidance or examples that could help me better understand how to use the builder pattern in Rust with uniffi.

image

Blow is my code👇, so many errors occur👆. user.udl:

namespace user {
    UserBuilder? get_user_builder();
};

interface User {
    [Name=default]
    constructor();
    string say_hi();
};

interface UserBuilder {
    [Name=default]
    constructor();
    UserBuilder with_name(string name);
    User build();
};
use std::sync::Arc;

#[derive(Debug, Default, Clone)]
struct User {
    name: String,
}

impl User {
    pub fn say_hi(&self) -> String {
        format!("Hi! {}", self.name)
    }
}

#[derive(Debug, Clone, Default)]
struct UserBuilder {
    name: String,
}

impl UserBuilder {
    fn with_name(&mut self, name: String) -> &mut Self {
        self.name = name;

        self
    }

    fn build(&self) -> Arc<User> {
        Arc::new(User {
            name: self.name.clone(),
        })
    }
}

fn get_user_builder() -> Option<Arc<UserBuilder>> {
    Some(Arc::new(UserBuilder::default()))
}

uniffi::include_scaffolding!("user");

Related issue

mhammond commented 1 month ago

Hi, thanks for checking out UniFFI!

UniFFI imposes many restrictions on your interface which means many Rust patterns do not really work. In particular, returning &mut Self isn't supported - objects like this always need to be wrapped in an Arc<> so they can be correctly shared with foreign bindings. &mut self as an argument isn't supported either as the interface needs to be "Send + Sync". You typically get mutable references by using mutexes, but this tends to mean the builder pattern isn't going to work in the same way.

This is described in the manual - in the first instance you are probably best working with our examples and test fixtures to see what is and isn't supported.

jplatte commented 1 month ago

I've been wanting to support [Self=ByArcUnwrapOrClone]¹ that adds a call to Arc::unwrap_or_clone to the scaffolding to enable self consuming methods, which would help a ton for by-value builders. I guess the same idea could enable &mut methods via some [Self=ByMakeMut] attribute, adding a Arc::make_mut call to the scaffolding code. I didn't end up putting time into this after switching jobs into a position where I no longer use UniFFI professionally.

I think the builder pattern is the perfect motivation for this, so maybe it makes sense to re-open this issue? Or maybe better to open a new one for this proposal. cc @bendk

¹ using UDL syntax here, but a proc-macro attribute would be just as interesting if not more

mhammond commented 1 month ago

so maybe it makes sense to re-open this issue?

Sure, let's do that!

bendk commented 1 month ago

Interesting idea. I think if you used unwrap_or_clone we would always use the clone side of it, since there's no equivalent on the foreign side to a self-consuming method and we have to assume that the foreign code might have another reference to the object. However, that could be fine. From Rust's POV it will look like a self-consuming method and nowadays you should be able to return a Self value and have UniFFI automatically wrap it in an arc. From the foreign code's POV, it's a method that returns a new object. The fact that we needed to make a clone and a new arc is slightly inefficient, but most of the time that's probably fine. I think that's how I ended up writing builders anyways in Python.

For similar reasons, I think make_mut will result in a clone which is probably not what was intended. One thing I've wondered for awhile is if we could have a way for users to opt-out of Rust's safety guarantees, allow &mut self methods using unsafe code, and make it the caller's responsibility to ensure that those methods aren't called at the same time from different threads. For many languages that's the normal state of affairs and for Python you often write single-threaded apps anyways. It seems reasonable, but I'm also a bit afraid to go down this path.

jplatte commented 1 month ago

we have to assume that the foreign code might have another reference to the object

But it has to be freed at some point, is this not solved with the regular Arc refcount? I'm a bit confused here ^^

make it the caller's responsibility to ensure that those methods aren't called at the same time from different threads

I think this is super risky. Getting unsoundness with unsafely created mutable references does not need to involve multiple threads.

bendk commented 1 month ago

we have to assume that the foreign code might have another reference to the object

But it has to be freed at some point, is this not solved with the regular Arc refcount? I'm a bit confused here ^^l

Yes, so unwrap_or_clone should work fine. I just think that it's always end up cloning the underlying object.