-
Notifications
You must be signed in to change notification settings - Fork 258
Add a doc comparing UniFFI with diplomat #1146
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
Changes from all commits
7597def
631dea0
f024f03
a86e949
eb044e2
2d22d03
ee6ee35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
# Comparing UniFFI with Diplomat | ||
|
||
[Diplomat](https://github.com/rust-diplomat/diplomat/) and [UniFFI](https://github.com/mozilla/uniffi-rs/) | ||
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](https://github.com/mozilla/uniffi-rs/pull/1146), | ||
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](https://github.com/unicode-org/icu4x/blob/7d9f89fcd7df4567e17ddd8c46810b0db287436a/ffi/diplomat/src/pluralrules.rs#L50-L51) | ||
uses types from a [different ffi module](https://github.com/unicode-org/icu4x/blob/7d9f89fcd7df4567e17ddd8c46810b0db287436a/ffi/diplomat/src/locale.rs#L19). | ||
|
||
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](https://github.com/rust-diplomat/diplomat/blob/main/docs/design_doc.md#requirements) 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](https://github.com/rust-diplomat/diplomat/blob/main/core/src/ast/structs.rs), | ||
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](https://github.com/mozilla/uniffi-rs/pull/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: | ||
|
||
```rust | ||
#[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: | ||
|
||
```Rust | ||
pub struct MyFFIType { | ||
mhammond marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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](https://github.com/mozilla/uniffi-rs/pull/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: | ||
|
||
```Rust | ||
#[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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, this explains a lot the design decisions :) |
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I think this helps answer my question about about API stability in UniFFI vs Diplomat. I still feel like the They won't change to FFI surface by stealth, because you'll have to update the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I think we agree that UniFFI's use of the .udl file and Diplomat's decision to restrict where types are exposed do serve the same purpose in that regard. The broader point I'm trying to make though is that application-services would prefer to not have those guards in place - ie, it would probably prefer a world where the UDL file didn't exist and nor did any limitation about where these types could be defined. |
||
|
||
## How the type universe is constructed for the macro approach. | ||
|
||
In both diplomat and [#416](https://github.com/mozilla/uniffi-rs/pull/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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this work in Diplomat, does the FFI allow you to pass it around as a pointer but not construct one for yourself on the foreign-language side? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No - diplomat doesn't have that problem because the struct definition must appear inside the module, so the foreign bindings, which parse that module, do know the struct elements. This is the same as we discovered in #416 - that forcing all type definitions into the single ffi module would technically work, but the impact that would have on how our code is organized made it less appealing than the status quo. I added a few words here to try and make that clearer. |
||
|
||
This is the problem which caused us to decide to stop working on | ||
[#416](https://github.com/mozilla/uniffi-rs/pull/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](https://github.com/mozilla/uniffi-rs/pull/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: | ||
|
||
```Rust | ||
#[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](https://github.com/rust-diplomat/diplomat/issues/34) | ||
|
||
## 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: | ||
```rust | ||
// 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, I think the toy example is the worst-case for highlighting the duplication here because the two duplicate declarations are separated by just 7 lines of text. In a real-world crate I would expect the duplication to not feel quite so bad because the source struct and its redeclaration would be further apart. (That doesn't help with some of the other contra points raised here though) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is a good point, but from my POV, that can almost make the duplication feel worse - eg, you get a rust compiler error, and it can be hard to work out whether the UDL needs to change or the rust duplicate of that UDL needs to change - eg, making something optional means adding a |
||
We probably also haven't helped with documentation, because the natural location for | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could imagine explicitly telling the macro about the path to the redeclared struct, like:
Then if it wanted to, the code processing this declaration could go find the corresponding I think this would probably be more trouble than it's worth (we don't want to re-implement vast swathes of Rust's name lookup machinery, for example) but it's interesting to think about. |
||
the documentation of `MyFFIType` is probably at the *actual* implementation. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This section is some serious food for thought. For one, as @rfk points out, we're currently duplicating the struct/function definitions in the UDL. The way I see it: both projects have made a similar design decision. @Manishearth describes it as "we do not want changes far away to change the FFI API". I would maybe reword that to "the FFI should be fully defined in one place". For UniFFI, that place is the UDL. For diplomat, it's the ffi module. (Multiple ffi modules complicates this picture a bit, but doesn't fundamentally change things). However, there is one difference: in some cases you only need code inside the ffi module. This has the potential to reduce duplication. For example, it would be very natural to move our One potential issue with this is that it couples the library with the FFI code. But this doesn't seem to be a problem for our components. As @MarkH points out, our Rust APIs exists purely to service the FFIs. It really makes me wonder about switching from a UDL-based approach to a macro based approach. At the start, maybe each consumer would simply refactor their current UDL file to a macro. This shouldn't be much work, maybe we could even automate it. After that, we have the ability to do to small refactors that eliminate the unneeded duplication. BTW, this also could solve some documentation issues since a) we can actually see docstrings when using syn and b) if there's only 1 place where a type is defined then it's clear where to put the docstrings. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a brief note in a86e949 which doesn't capture all of this comment, but does briefly say why we should consider this more. |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. diplomat just treats documentation as "yet another backend", which works reasonably well, since the architecture of a diplomat backend is just "here's the type structure, you know what to expect on the FFI layer, do what you want". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By contrast, the only reason that we don't already have documentation as yet-another-backend in UniFFI, is that the off-the-shelf parser that we use for the IDL throws away comments by default :-( |
||
documentation might go a long way. |
Uh oh!
There was an error while loading. Please reload this page.