New subcommand generate-api to generate all the expected Rust boilerplate from UDL
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!()
}
}
}
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.udlresults in a new filefixtures/coverall/src/coverall_uniffi.rswith 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).
Not sure if this is quite a
backendbut 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
scaffoldingsubcommand, 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.
/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.
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.
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.
Updated with a new subcommand generate-api because fitting it in as generate api was too complex (or not possible in Clap?)