Skip to content
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

Generate Firefox Desktop front-end bindings with WebIDL #255

Merged
merged 34 commits into from
Sep 30, 2020
Merged

Conversation

linabutler
Copy link
Member

This PR is a first cut at a UniFFI backend for Firefox! It's a work in progress, but can generate working bindings now, so I wanted to get it up for review. I'd love your comments! 💜

The backend works a little differently than the others. Firefox expects one header file per interface (and a separate one for the namespace that holds top-level functions), so UniFFI emits multiple .h and .cpp files. There's also complexity around handling different types. Some are reflected differently depending on whether they're arguments, type parameters, or local variables; some are returned directly and others are passed as out parameters; and the WebIDL implementation signatures have extra leading and trailing arguments. All this makes macros.cpp pretty gnarly 🙀

SharedHeaderTemplate.h contains the RustBuffer helpers and serialization logic. The ideas are the same as the Swift, Kotlin, and Python backends, but the implementation is a pile of C++ code. I'll add some more comments throughout it in the coming days, so that it's a bit easier to digest. This part can also be reused wholesale for our pure-C++ Nimbus interface, there's not much that's WebIDL specific here.

Getting this building

The setup for this is a little janky for now, but here's how to get this running! You'll need a full (not artifact) build of Firefox, since this touches lots of C++ and Rust code.

  1. git checkout gecko to switch to the Gecko branch.
  2. Make a new folder for the bindings: mkdir rondpoint-gecko
  3. Generate the Gecko bindings for Rondpoint: cd uniffi_bindgen; and cargo run -- generate ../examples/rondpoint/src/rondpoint.idl -l gecko -o ../rondpoint-gecko
  4. Symlink the generated WebIDL file into dom/chrome-webidl in m-c: ln -s /uniffi-rs/rondpoint-gecko/Rondpoint.webidl /gecko/dom/chrome-webidl/Rondpoint.webidl
  5. Add Rondpoint.webidl to dom/chrome-webidl/moz.build, in the WEBIDL_FILES section.
  6. Symlink the rondpoint-gecko folder from step (2) into an existing (or new!) directory in m-c that has a moz.build file. Any one will do; I picked toolkit/components/extensions/storage for no other reason than it's a pretty small directory and makes it easy to do directory-specific builds.
  7. Symlink uniffi-rs into m-c.
  8. Add uniffi-example-rondpoint as a dependency to toolkit/library/rust/shared/Cargo.toml, as a path dependency inside the symlink from step (7).
  9. Add extern crate uniffi_rondpoint to toolkit/library/rust/shared/lib.rs.
  10. Run mach vendor rust.
  11. Add the generated sources and xpcshell test manifest to the moz.build in the folder from step (6), like this:
EXPORTS.mozilla.dom += [
    'rondpoint-gecko/Retourneur.h',
    'rondpoint-gecko/Rondpoint.h',
    'rondpoint-gecko/RondpointShared.h',
    'rondpoint-gecko/Stringifier.h',
]

UNIFIED_SOURCES += [
    'rondpoint-gecko/Retourneur.cpp',
    'rondpoint-gecko/Rondpoint.cpp',
    'rondpoint-gecko/Stringifier.cpp',
]

XPCSHELL_TESTS_MANIFESTS += [
    'uniffi-rs/examples/rondpoint/tests/gecko/xpcshell.ini'
]

And then run mach build, and you should be good to go! You can run mach xpcshell-test toolkit/components/extensions/storage/uniffi-rs/examples/rondpoint/tests/gecko/xpcshell.ini after the build is done! 😊

Hacks

I had to make two changes to the Cargo.tomls in UniFFI to get this building in Firefox, which we'll want to fix before merging:

  1. Remove the top-level Cargo.toml, so that I could symlink uniffi-rs into my m-c tree and add uniffi-example-rondpoint as a path dependency. I'm not sure this is necessary; if we can avoid symlinking uniffi-rs into m-c, we can do without.
  2. Remove the cargo_metadata dependency. When I tried building Firefox with the version of cargo_metadata from Crates.io, I got a panic in one of its derive macros. Looks lik that was fixed in bump serde dependency to 1.0.107 oli-obk/cargo_metadata#123 by bumping the Serde version, but that hasn't been published yet, so mach vendor rust doesn't know to pull the new version of Serde in. But changing cargo_metadata to a Git dependency confused Cargo: mach vendor rust didn't vendor any of UniFFI's dependencies, and then mach build complained that it couldn't resolve the versions properly. I'm not sure what's going on there, and didn't dig too much more into it.

I also had to change the crate-type to ["cdylib", "lib"], so that we could link the crate into gkrust. If it's just cdylib, extern crate won't find it.

/// Returns a dummy empty value for the given type. This is used to
/// implement the `cpp::bail` macro to return early if a WebIDL function or
/// method throws an exception.
pub fn default_return_value_cpp(type_: &Type) -> Option<String> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s unfortunate that this has to exist. Basically, if you want to throw an error from inside a WebIDL method with a signature like int32_t Foo(ErrorResult& aRv), you can call aRv.Throw(...) in the method, but you still need to return some value—even though it’ll get ignored by the bindings code. Methods that return results via out params don’t need to do this; they can just do return; and not set the param.

uniffi_bindgen/src/bindings/gecko/gen_gecko.rs Outdated Show resolved Hide resolved
uniffi_bindgen/src/bindings/gecko/gen_gecko.rs Outdated Show resolved Hide resolved
uniffi_bindgen/src/bindings/gecko/gen_gecko.rs Outdated Show resolved Hide resolved
uniffi_bindgen/src/bindings/gecko/gen_gecko.rs Outdated Show resolved Hide resolved
uniffi_bindgen/src/bindings/gecko/templates/macros.cpp Outdated Show resolved Hide resolved
uniffi_bindgen/src/bindings/gecko/templates/macros.cpp Outdated Show resolved Hide resolved
uniffi_bindgen/src/bindings/gecko/templates/macros.cpp Outdated Show resolved Hide resolved
@@ -0,0 +1,141 @@
add_task(async function test_rondpoint() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't believe this is real 🎉

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is add_task? I can see it used in this file, but I can't see it anywhere else.

Copy link
Member

@mhammond mhammond Sep 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine the idea is to run these tests using mach xpcshell-test somehow - no idea what process lina has in mind for having mach find them though

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this file is an xpcshell test!

no idea what process lina has in mind for having mach find them though

Haha, my workflow so far has been “symlink rondpoint into m-c and add it to moz.build with all the other stuff”, but that’s not going to fly for anything more than a demo. Still, if you want to take it for a spin locally, there are only 11 easy steps! 😆

It compiles in Gecko, but haven't tried running it or anything. There
are probably memory safety bugs galore. But now it's time to relax,
I'll fix these up on Friday.
* Remove XPIDL generation and wire up WebIDL.
* Generate serializers for enums and dictionaries.
* Generate proper C++ WebIDL implementations.
When building in Gecko, `cargo_metadata` panics in a macro. This is
fixed in oli-obk/cargo_metadata#123 (and in Serde upstream), but
pulling `uniffi` in as a path dependency in Gecko, and depending on
`cargo_metadata` as a Git dependency, confuses Cargo. It doesn't
update the vendored versions of Serde or other dependencies, then
fails to build complaining it can't satisfy the newer versions that
`uniffi` requests.

In the interest of getting a demo working, I've just commented out
the dependencies for now, and downgraded the other versions to match
what's already vendored in m-c.
...But doesn't link, since I haven't figured out how to get
`uniffi` vendored correctly. Progress, though, we're correctly
generating WebIDL bindings to where they compile!
We can now generate (and compile!) the Rondpoint binding.
...And pass only the fields from the `ComponentInterface` that each
template needs, not the full interface.
@linabutler linabutler changed the title Generate Firefox bindings Generate Firefox Desktop front-end bindings with WebIDL Sep 10, 2020
Copy link
Contributor

@nnethercote nnethercote left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. A lot of boilerplate-y stuff, but that's unavoidable, and it's easy to read and well-commented. I have a few nits, mostly about improving comments.

struct Serializable {
/// Returns the size of the serialized value, in bytes. This is used to
/// calculate the allocation size for the Rust byte buffer.
static CheckedInt<size_t> Size(const T& aValue) = delete;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the return type CheckedInt?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was using CheckedInt throughout to avoid accidental overflows when serializing, but with the RustBuffer changes on main now, Size isn't necessary anymore. I think we can remove CheckedInt from the other uses, too, since RustBuffers can only hold an int32_t worth of bytes.

@linabutler linabutler marked this pull request as ready for review September 26, 2020 07:29
This makes it hard to symlink UniFFI into the Firefox tree, but once
this PR is merged, we shouldn't need that.
Copy link
Collaborator

@rfk rfk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really good.

I mean, I kinda glazed over a bit at the intricacies of the C++ code because I trust that more experienced folks are looked at it, but I'm confident I could use the guideposts here to dig in more to the details if needed!

I note that the generated bindings occupy a kind of global namespace, e.g. we get a class mozilla::dom::Optionnuer in a header file conditioned on #ifndef mozilla_dom_Optionneur. Are we able to add the uniffi package name as a level here, to give e.g. mozilla::dom::Rondpointer::Optionnuer and similar? I'm concerned about accidental name conflicts in future, but I'm also cautious about potential limitations of the way Firefox generates the bindings.

Similarly, if we had multiple uniffi libraries being build together in Firefox, would they run into trouble by both declaring RustBuffer structs etc? I think maybe it's OK because the RondpointShared.h file is only included by its .cpp files, not by any other header files. But I'm no expert, so kind of just looking for explicit re-assurance here :-)

A couple of other broad nits I noticed:

  • When running cargo test in this branch, I get quite a few warnings about unused symbols which it's probably worth cleaning up.
  • Running uniffi-bindgen via the cargo run -- method you describe in the PR works, but if I try to build and run the executable via ./target/debug/uniffi-bindgen using the same arguments, it produces an error 'Missing $CARGO_MANIFEST_DIR, cannot load config file: NotPresent'. We should expect that some consumers will use an external uniffi-bindgen rather than building and running it from within this repo.

examples/rondpoint/tests/gecko/test_rondpoint.js Outdated Show resolved Hide resolved
examples/arithmetic/Cargo.toml Show resolved Hide resolved
uniffi/src/testing.rs Outdated Show resolved Hide resolved
uniffi/src/testing.rs Outdated Show resolved Hide resolved
@linabutler
Copy link
Member Author

I mean, I kinda glazed over a bit at the intricacies of the C++ code because I trust that more experienced folks are looked at it, but I'm confident I could use the guideposts here to dig in more to the details if needed!

Thank you so much for digging into this, Ryan, that's mainly what I wanted from your review! 🚀

I note that the generated bindings occupy a kind of global namespace, e.g. we get a class mozilla::dom::Optionnuer in a header file conditioned on #ifndef mozilla_dom_Optionneur. Are we able to add the uniffi package name as a level here, to give e.g. mozilla::dom::Rondpointer::Optionnuer and similar?

I don't think so 😞 I covered this in the Theory of Operation—which would be nice to convert into Markdown and check in to the repo, adding that to my list for this week ☑️ —but the binding declarations generated by Firefox's Codegen.py expect headers to be in mozilla/dom/InterfaceName.h, anda emit forward declarations that expect to find the interfaces and namespaces directly under mozilla::dom. (I think that also means type names will have to be globally unique, across UniFFI interfaces and all the Web APIs currently in Gecko).

Similarly, if we had multiple uniffi libraries being build together in Firefox, would they run into trouble by both declaring RustBuffer structs etc?

Yes, they would, because they're declared in the extern "C" block—thanks for noting this! I'll get that fixed up before landing, by prefixing RustBuffer, ForeignBytes, and RustError with the ffi_namespace(), like you do for the functions. It does mean we'll end up with duplication if we vendor multiple UniFFI components, but that's already the case today, and I suspect making cross-component helper deduping work is going to be more involved.

@rfk
Copy link
Collaborator

rfk commented Sep 28, 2020

prefixing RustBuffer, ForeignBytes, and RustError with the ffi_namespace(), like you do for the functions.
It does mean we'll end up with duplication

I have perhaps too much faith in Sufficiently Smart Compilers, but hopefully these duplicated struct definitions would not result in duplicate code in the compiled artifacts, would they?

@rfk
Copy link
Collaborator

rfk commented Sep 28, 2020

(I think that also means type names will have to be globally unique, across UniFFI interfaces and all the Web APIs currently in Gecko).

Hrm. I can see this being a problem (and worse, one that catches us by surprise). If you think there's scope for adding some sort of unique-ifying prefix to these names, it's probably worth putting the infra for that in place sooner rather than later.

@linabutler
Copy link
Member Author

I have perhaps too much faith in Sufficiently Smart Compilers, but hopefully these duplicated struct definitions would not result in duplicate code in the compiled artifacts, would they?

Good question—my knowledge of how C++ compilers inline templates, functions, and types is nonexistent 😭 Just like you suggested profiling the size impact of vendoring Nimbus and the bindings, it would be worth measuring the impact of vendoring multiple UniFFI components.

The one thing I'm worried about that might make inlining difficult is that the serialization helpers call into component-specific allocation functions. In libxul or an App Services-style megazord, ffi_rondpoint_abc123_rustbuffer_alloc does the exact same thing as ffi_arithmetic_789xyz_rustbuffer_alloc—but they're exported as two different symbols, and we end up generating two copies of the helpers wrapping them—rondpoint_detail::{Reader, Writer, Serializable, ViaFfi} and arithmetic_detail::{Reader, Writer, Serializable, ViaFfi}, plus all the specializations. Is Clang smart enough to recognize that, and optimize some (or all) those calls out? I don't know 😞

@linabutler
Copy link
Member Author

Hrm. I can see this being a problem (and worse, one that catches us by surprise). If you think there's scope for adding some sort of unique-ifying prefix to these names, it's probably worth putting the infra for that in place sooner rather than later.

Good idea. Let's do that now, so that this doesn't sneak up on us later.

dmose and others added 9 commits September 28, 2020 17:23
Fix #301 cargo tests by updating and reenabling cargo_metadata
There are two pieces of information that we want to pass along to filters:

* The `definition_prefix` config option, used to prefix the generated
  namespace, interface, enum, and dictionary names.
* The `ffi_namespace`, used to generate the names for the `RustBuffer`,
  `RustError`, and `ForeignBytes` structs.

Threading these arguments through Askama is tricky. For example, if the
`type_ffi` filter needs to know the `ffi_namespace`, that infects any
filters that call it (like `lift_cpp` and `lower_cpp`), all the way up
the call chain. The same happens for the `definition_prefix`, which
changes the names of the types declared in the WebIDL...so now all type
declaration filters need to know about the prefix.

Making this work in the generated code is difficult, so we take the
least-worst possible option of passing around a `Context` argument
(other, less-generic names welcome!) with all the names we need.
Copy link
Contributor

@nnethercote nnethercote left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@linabutler
Copy link
Member Author

Woohoo, thanks so much everyone! 😅 The latest push has the definition_prefix config option, prefixed names for the C structs (so we should be able to compile multiple UniFFIed components in Firefox), and a last-minute fix for nullable strings uncovered by the Rondpoint tests 👍🏻 (that example is paying for itself over and over!)

I'll go ahead and mash the "merge" button now :shipit: and we can file follow-ups for anything we forgot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants