Skip to content

Latest commit

 

History

History
275 lines (213 loc) · 11.9 KB

diplomat-and-macros.md

File metadata and controls

275 lines (213 loc) · 11.9 KB

Comparing UniFFI with Diplomat

Diplomat and UniFFI are both tools which expose a rust implemented API over an FFI. At face value, these tools are solving the exact same problem, but their approach is significantly different.

This document attempts to describe these different approaches and discuss the pros and cons of each. It's not going to try and declare one better than the other, but instead just note how they differ. If you are reading this hoping to find an answer to "what one should I use?", then that's easy - each tool currently supports a unique set of foreign language bindings, so the tool you should use is the one that supports the languages you care about!

Disclaimer: This document was written by one of the UniFFI developers, who has never used diplomat in anger. Please feel free to open PRs if anything here misrepresents diplomat.

See also: This document was discussed in this PR, which has some very interesting discussion - indeed, some of the content in this document has been copy-pasted from that discussion, but there's further detail there which might be of interest.

The type systems

The key difference between these 2 tools is the "type system". While both are exposing Rust code (which obviously comes with its own type system), the foreign bindings need to know lots of details about all the types expressed by the tool.

For the sake of this document, we will use the term "type universe" to define the set of all types known by each of the tools. Both of these tools build their own "type universe" then use that to generate both Rust code and foreign bindings.

UniFFI's type universe

UniFFI's model is to parse an external ffi description from a .udl file which describes the entire "type universe". This type universe is then used to generate both the Rust scaffolding (on disk as a .rs file) and the foreign bindings.

What's good about this is that the entire type system is known when generating both the rust code and the foreign binding, and is known without parsing any Rust code. This is important because things like field names and types in structs must be known on both sides of the FFI.

What's bad about this is that the external UDL is very ugly and redundant in terms of the implemented rust API.

Diplomat's type universe

Diplomat defines its "type universe" (ie, the external ffi) using macros.

What's good about this is that an "ffi module" (and there may be many) defines the canonical API and it is defined in terms of Rust types - the redundant UDL is removed. The Rust scaffolding can also be generated by the macros, meaning there are no generated .rs files involved. Types can be shared among any of the ffi modules defined in the project - for example, this diplomat ffi module uses types from a different ffi module.

Restricting the definition of the FFI to a single module instead of allowing that definition to appear in any Rust code in the crate also offers better control over the stability of the API, because where the FFI is defined is constrained. This is an explicit design decision of diplomat.

While the process for defining the type universe is different, the actual in-memory representation of that type universe isn't radically different from UniFFI - for example, here's the definition of a Rust struct, and while it is built from a syn struct, the final representation is independent of syn and its ast representation.

UniFFI's experience with the macro approach.

Ryan tried this same macro approach for UniFFI in #416 - but we struck a limitation in this approach for UniFFI's use-cases - the context in which the macro runs doesn't know about types defined outside of that macro, which are what we need to expose.

Example of this limitation

Let's look at diplomat's simple example:

#[diplomat::bridge]
mod ffi {
    pub struct MyFFIType {
        pub a: i32,
        pub b: bool,
    }

    impl MyFFIType {
        pub fn create() -> MyFFIType { ... }
        ...
    }
}

This works fine, but starts to come unstuck if you want the types defined somewhere else. In this trivial example, something like:

pub struct MyFFIType {
    pub a: i32,
    pub b: bool,
}

#[diplomat::bridge]
mod ffi {
    impl MyFFIType {
        pub fn create() -> MyFFIType { ... }
        ...
    }
}

fails - diplomat can't handle this scenario - in the same way and for the same reasons that Ryan's #416 can't - the contents of the struct aren't known.

From the Rust side of the world, this is probably solvable by sprinkling more macros around - eg, something like:

#[uniffi::magic]
pub struct MyFFIType {
    pub a: i32,
    pub b: bool,
}

might be enough for the generation of the Rust scaffolding - in UniFFI's case, all we really need is an implementation of uniffi::RustBufferViaFfi which is easy to derive, and UniFFI can generate code which assumes that exists much like it does now. However, the problems are in the foreign bindings, because those foreign bindings do not know the names and types of the struct elements without re-parsing every bit of Rust code with those annotations. As discussed below, re-parsing this code might be an option if we help Uniffi to find it, but asking UniFFI to parse this and all dependent crates to auto-discover them probably is not going to be viable.

Why is this considered a limition for UniFFI but not diplomat?

As mentioned above, diplomat considers the limitation described above as an intentional design feature. By limiting where FFI types can be described, there's no risk of changes made "far away" from the FFI to change the FFI. This was born of experience in tools like cbindgen.

For Uniffi, all use-cases needed by Mozilla don't share this design goal, primarily because the FFI is the primary consumer of the crate. The Rust API exists purely to service the FFI. It's not really possible to accidentally change the API, because every API change made will be in service of exposing that change over the FFI. The test suites written in the foreign languages are considered canonical.

How the type universe is constructed for the macro approach.

In both diplomat and #416, the approach taken is very similar - it takes a path to a the Rust source file/tree, and uses syn to locate the special modules (ie, ones annotated with #[diplomat:bridge] in the case of diplomat.)

While some details differ, this is just a matter of implementation - #416 isn't quite as agressive about consuming the entire crate to find multiple FFI modules (and even then, diplomat doesn't actually process the entire crate, just modules tagged as a bridge), but could easily be extended to do so.

But in both cases, for our problematic example above, this process never sees the layout of the MyFFIType struct because it's not inside the processed module, so that layout can't be communicated to the foreign bindings. As noted above, this is considered a feature for diplomat, but a limitation for UniFFI.

This is the problem which caused us to decide to stop working on #416 - the current world where the type universe is described externally doesn't have this limitation - only the UDL file needs to be parsed when generating the foreign bindings. The application-services team has concluded that none of our non-trival use-cases for UniFFI could be described using macros, so supporting both mechanisms is pain for no gain.

As noted in #416, wasm-bindgen has a similarly shaped problem, and solves it by having the Rust macro arrange for the resulting library to have an extra data section with the serialized "type universe" - foreign binding generation would then read this information from the already built binary. This sounds more complex than the UniFFI team has appetite for at the current time.

Looking forward

Adapting the diplomat/#416 model to process the entire crate?

We noted that diplomat intentionally restricts where the ffi is generated, whereas UniFFI considers that a limitation - but what if we can teach UniFFI to process more of the Rust crate?

It might be reasonable for the foreign bindings to know that Rust "paths" to modules which should be processed, and inside those modules find structs "marked up" as being used by the FFI.

In other words, borrowing the example above:

#[uniffi::magic]
pub struct MyFFIType {
    pub a: i32,
    pub b: bool,
}

maybe can be made to work, so long as we are happy to help UniFFI discover where such annotations may exist.

A complication here is that currently UniFFI allows types defined in external crates, but that might still be workable - eg, diplomat has an issue open to support exactly this

Duplicating structs inside Rust

When reviewing the draft of this document, @rfk noted that we are already duplicating Rust structs in UDL and in Rust. So instead of having:

// In a UDL file:
dictionary MyFFIType {
    i32 a;
    bool b;
};
// Then in Rust:
pub struct MyFFIType {
    pub a: i32,
    pub b: bool,
}

we could have:

// In the Rust implementation, in some other module.
pub struct MyFFIType {
    pub a: i32,
    pub b: bool,
}

// And to expose it over the FFI:
#[ffi::something]
mod ffi {

    #[ffi::magic_external_type_declaration]
    pub struct MyFFIType {
        pub a: i32,
        pub b: bool,
    }

    impl MyFFIType {
        pub fn create() -> MyFFIType { ... }
        ...
    }
}

So while we haven't exactly reduced the duplication, we have removed the UDL. We probably also haven't helped with documentation, because the natural location for the documentation of MyFFIType is probably at the actual implementation.

While it might not solve all our problems, it is worthy of serious consideration - fewer problems is still a worthwhile goal, and needing a UDL file and parser seems like one worth removing.

Try and share some definitions with diplomat

We note above that the type universe described by diplomat is somewhat "leaner" than that described by UniFFI, but in general they are very similar. Thus, there might be a future where merging or otherwise creating some interoperability between these type universes might make sense.

It seems likely that this would start to add unwelcome constraints - eg, diplomat would not want its ability to refactor type representations limited by what UniFFI needs.

However, what you could see happening in the future is UniFFI becoming a kind of higher-level wrapper around Diplomat. You can imagine a Diplomat backend for UniFFI that converts a .udl file into a bridge module and then uses the Diplomat toolchain to generate bindings from it, keeping some of the additional affordances/conveniences UniFFI built for its specific use-cases.

Next steps for UniFFI

As much as some of the UniFFI team dislike the external UDL file, there's no clear path to moving away from it. We could experiment with some of the options above and see if they are both viable and worth the investment for the UniFFI use-cases. That sounds like a long-term goal.

In the short term, the best we can probably do is to enumerate the perceived problems with the UDL file and try to make them more ergonomic - for example, avoiding repetition of [Throws=SomeError] would remove alot of noise, and some strategy for generating documentation might go a long way.