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

New subcommand generate-api to generate all the expected Rust boilerplate from UDL

Open badboy opened this issue 4 years ago • 6 comments

The conversion from the UDL definition to the actual Rust signature is not always obvious, especially if just getting started. Optional values, throwable exceptions, Arc vs. &self, ...

Given we already have the UDL and we can parse that and we do know what code we expect ... why don't we "just" generate it?

That's what this code adds: a new Rust backend for the code generator. It generates all structs, (error) enums, functions, and methods (including the constructor ones) in Rust code. All implementation is stubbed out with todo!(). But because this is not really a backend it gets its own subcommand. Its code however lives on as a binding because that's where it neatly fits in.

Someone wanting to use UniFFI can start with definining the API in the UDL file. Then run uniffi-bindgen generate-api path/to/src/namespace.udl, take the code from the generated namespace_uniffi.rs and start implementing functionality.

Note: this is not intended as a full-blown backend. It's not capable of updating user code. Once generated the expectation is that the user copies over the function signatures to their own code.


This is an idea I brought up earlier, and regardless if we land this as a supported backend, this might help me get started with the Glean->UniFFI migration eventually.

Running cargo run -p uniffi_bindgen -- generate --language rust fixtures/coverall/src/coverall.udl results in a new file fixtures/coverall/src/coverall_uniffi.rs with the following content:

/*
This file was autogenerated by some hot garbage in the `uniffi` crate.
Trust me, you don't want to mess with it!
*/

mod coverall {
    // Error definitions

    enum CoverallError {
        TooManyHoles,
    }
    enum ComplexError {
        OsError { code: i16, extended_code: i16 },
        PermissionDenied { reason: String },
    }

    // Public interface members begin here.

    enum MaybeSimpleDict {
        Yeah { d: SimpleDict },
        Nah,
    }

    enum Color {
        Red = 1,
        Blue = 2,
        Green = 3,
    }

    // Record type SimpleDict
    #[derive(Clone, Eq, PartialEq)]
    struct SimpleDict {
        text: String,
        maybe_text: Option<String>,
        a_bool: bool,
        maybe_a_bool: Option<bool>,
        unsigned8: u8,
        maybe_unsigned8: Option<u8>,
        unsigned16: u16,
        maybe_unsigned16: Option<u16>,
        unsigned64: u64,
        maybe_unsigned64: Option<u64>,
        signed8: i8,
        maybe_signed8: Option<i8>,
        signed64: i64,
        maybe_signed64: Option<i64>,
        float32: f32,
        maybe_float32: Option<f32>,
        float64: f64,
        maybe_float64: Option<f64>,
        coveralls: Option<Coveralls>,
    }

    impl SimpleDict {
        pub fn new(
            text: String,
            maybe_text: Option<String>,
            a_bool: bool,
            maybe_a_bool: Option<bool>,
            unsigned8: u8,
            maybe_unsigned8: Option<u8>,
            unsigned16: u16,
            maybe_unsigned16: Option<u16>,
            unsigned64: u64,
            maybe_unsigned64: Option<u64>,
            signed8: i8,
            maybe_signed8: Option<i8>,
            signed64: i64,
            maybe_signed64: Option<i64>,
            float32: f32,
            maybe_float32: Option<f32>,
            float64: f64,
            maybe_float64: Option<f64>,
            coveralls: Option<Coveralls>,
        ) -> Self {
            Self {
                text,
                maybe_text,
                a_bool,
                maybe_a_bool,
                unsigned8,
                maybe_unsigned8,
                unsigned16,
                maybe_unsigned16,
                unsigned64,
                maybe_unsigned64,
                signed8,
                maybe_signed8,
                signed64,
                maybe_signed64,
                float32,
                maybe_float32,
                float64,
                maybe_float64,
                coveralls,
            }
        }
    }

    fn create_some_dict() -> SimpleDict {
        todo!()
    }

    fn create_none_dict() -> SimpleDict {
        todo!()
    }

    fn get_num_alive() -> u64 {
        todo!()
    }

    struct Coveralls;

    impl Coveralls {
        fn new(name: String) -> Self {
            todo!()
        }

        fn fallible_new(name: String, should_fail: bool) -> Result<Self, CoverallError> {
            todo!()
        }
        fn panicing_new(message: String) -> Self {
            todo!()
        }

        fn get_name(&self) -> String {
            todo!()
        }

        fn maybe_throw(&self, should_throw: bool) -> Result<bool, CoverallError> {
            todo!()
        }

        fn maybe_throw_complex(&self, input: i8) -> Result<bool, ComplexError> {
            todo!()
        }

        fn panic(&self, message: String) -> () {
            todo!()
        }

        fn fallible_panic(&self, message: String) -> Result<(), CoverallError> {
            todo!()
        }

        fn strong_count(self: Arc<Self>) -> u64 {
            todo!()
        }

        fn take_other(&self, other: Option<Coveralls>) -> () {
            todo!()
        }

        fn get_other(&self) -> Option<Coveralls> {
            todo!()
        }

        fn take_other_fallible(self: Arc<Self>) -> Result<(), CoverallError> {
            todo!()
        }

        fn take_other_panic(self: Arc<Self>, message: String) -> () {
            todo!()
        }

        fn clone_me(&self) -> Coveralls {
            todo!()
        }
    }

    struct Patch;

    impl Patch {
        fn new(color: Color) -> Self {
            todo!()
        }

        fn get_color(&self) -> Color {
            todo!()
        }
    }

    struct ThreadsafeCounter;

    impl ThreadsafeCounter {
        fn new() -> Self {
            todo!()
        }

        fn busy_wait(&self, ms: i32) -> () {
            todo!()
        }

        fn increment_if_busy(&self) -> i32 {
            todo!()
        }
    }
}

badboy avatar Aug 05 '21 13:08 badboy

There's a lot to like here and I haven't dug into the details, but I wanted to share one quick piece of very high-level feedback:

Running cargo run -p uniffi_bindgen -- generate --language rust fixtures/coverall/src/coverall.udl results in a new file fixtures/coverall/src/coverall_uniffi.rs with the following content

As a consumer, I feel like I would expect uniffi-bindgen generate -l rust to generate Rust bindings from the given UDL file. Using Glean as a concrete example, I'd expect this to generate Rust code that knows how to load libglean.so and invoke the functions therein.

This might make more sense of a separate subcommand, or perhaps a feature of the scaffolding subcommand, rather than being treated like a backend (even if it re-uses much of the backend-related machinery under the hood).

rfk avatar Aug 05 '21 23:08 rfk

Not sure if this is quite a backend but it feels like the best place to put this logic now, and as we separate the backends (ref: #997) we can figure out where this goes.

This might make more sense of a separate subcommand, or perhaps a feature of the scaffolding subcommand, rather than being treated like a backend (even if it re-uses much of the backend-related machinery under the hood).

I agree here. Mostly did this as a "backend" because it was easy to replicate. For the sake of UX it should be called differently though.

We might also want to keep #416 in mind, I'm not opposed to doing this work, then IF we end up doing macros, discarding it (or figuring out how some form of this can help library authors avoid mistakes)

As discussed in that PR I think the macro approach is currently not as straight-forward yet, so this would be a good intermediate step to get started with UniFFI.

badboy avatar Aug 06 '21 07:08 badboy

/cc @skhamis to make sure you saw this, as I think it's got potential for intersecting nicely with the work you've been doing on expanding the command-line interface.

rfk avatar Aug 09 '21 04:08 rfk

This is definitely awesome! I totally agree that this makes more sense in the scaffolding subcommand or even just generate api <path_to_udl. Also since this is generating the actual rust code I don't think this will be affected by the split of backends? Since this seems like a core uniffi command.

skhamis avatar Aug 09 '21 19:08 skhamis

This is definitely awesome! I totally agree that this makes more sense in the scaffolding subcommand or even just generate api <path_to_udl. Also since this is generating the actual rust code I don't think this will be affected by the split of backends? Since this seems like a core uniffi command.

generate api sounds good to me. I won't get to update this PR this week, so I just resync next week and see where the other work is heading.

badboy avatar Aug 10 '21 07:08 badboy

Updated with a new subcommand generate-api because fitting it in as generate api was too complex (or not possible in Clap?)

badboy avatar Aug 18 '21 10:08 badboy