diff options
Diffstat (limited to '')
-rw-r--r-- | docs/writing-rust-code/cpp-interop.md | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/docs/writing-rust-code/cpp-interop.md b/docs/writing-rust-code/cpp-interop.md new file mode 100644 index 0000000000..9e9c516683 --- /dev/null +++ b/docs/writing-rust-code/cpp-interop.md @@ -0,0 +1,250 @@ +# Rust/C++ interop + +This document describes how to use FFI in Firefox to get Rust code and C++ code to interoperate. + +## Transferable types + +Generally speaking, the more complicated is the data you want to transfer, the +harder it'll be to transfer across the FFI boundary. + +Booleans, integers, and pointers cause little trouble. +- C++ `bool` matches Rust `bool` +- C++ `uint8_t` matches Rust `u8`, `int32_t` matches Rust `i32`, etc. +- C++ `const T*` matches Rust `*const T`, `T*` matches Rust `*mut T`. + +Lists are handled by C++ `nsTArray` and Rust `ThinVec`. + +For strings, it is best to use the `nsstring` helper crate. Using a raw pointer +plus length is also possible for strings, but more error-prone. + +If you need a hashmap, you'll likely want to decompose it into two lists (keys +and values) and transfer them separately. + +Other types can be handled with tools that generate bindings, as the following +sections describe. + +## Heap allocation + +C++ and Rust code in Firefox all use the same heap allocator, so C++ +`malloc`, `free`, and friends interoperate with Rust's `std::alloc` +functions: raw memory allocated on the heap by Rust can be freed by +C++ and vice versa. For example, memory allocated by a Rust `Vec` +could be freed in C++ by passing it to `std::free`. This is arranged +via a global variable with the Rust `#[global_allocator]` attribute in +the `mozglue-static` crate. + +## Accessing C++ code and data from Rust + +To call a C++ function from Rust requires adding a function declaration to Rust. +For example, for this C++ function: + +``` +extern "C" { +bool UniquelyNamedFunction(const nsCString* aInput, nsCString* aRetVal) { + return true; +} +} +``` +add this declaration to the Rust code: +```rust +extern "C" { + pub fn UniquelyNamedFunction(input: &nsCString, ret_val: &mut nsCString) -> bool; +} +``` + +Rust code can now call `UniquelyNamedFunction()` within an `unsafe` block. Note +that if the declarations do not match (e.g. because the C++ function signature +changes without the Rust declaration being updated) crashes are likely. (Hence +the `unsafe` block.) + +Because of this unsafety, for non-trivial interfaces (in particular when C++ +structs and classes must be accessed from Rust code) it's common to use +[rust-bindgen](https://github.com/rust-lang/rust-bindgen), which generates Rust +bindings. The documentation is +[here](https://rust-lang.github.io/rust-bindgen/). + +## Accessing Rust code and data from C++ + +A common option for accessing Rust code and data from C++ is to use +[cbindgen](https://github.com/mozilla/cbindgen), which generates C++ header +files. for Rust crates that expose a public C API. cbindgen is a very powerful +tool, and this section only covers some basic uses of it. + +### Basics + +First, add suitable definitions to your Rust. `#[no_mangle]` and `extern "C"` +are required. + +```rust +#[no_mangle] +pub unsafe extern "C" fn unic_langid_canonicalize( + langid: &nsCString, + ret_val: &mut nsCString +) -> bool { + ret_val.assign("new value"); + true +} +``` + +Then, add a `cbindgen.toml` file in the root of your crate. It may look like this: + +```toml +header = """/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */ +#ifndef mozilla_intl_locale_MozLocaleBindings_h +#error "Don't include this file directly, instead include MozLocaleBindings.h" +#endif +""" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +# Put FFI calls in the `mozilla::intl::ffi` namespace. +namespaces = ["mozilla", "intl", "ffi"] + +# Export `ThinVec` references as `nsTArray`. +[export.rename] +"ThinVec" = "nsTArray" +``` + +Next, extend the relevant `moz.build` file to invoke cbindgen. + +```python +if CONFIG['COMPILE_ENVIRONMENT']: + CbindgenHeader('unic_langid_ffi_generated.h', + inputs=['/intl/locale/rust/unic-langid-ffi']) + + EXPORTS.mozilla.intl += [ + '!unic_langid_ffi_generated.h', + ] +``` + +This tells the build system to run cbindgen on +`intl/locale/rust/unic-langid-ffi` to generate `unic_langid_ffi_generated.h`, +which will be placed in `$OBJDIR/dist/include/mozilla/intl/`. + +Finally, include the generated header into a C++ file and call the function. + +```cpp +#include "mozilla/intl/unic_langid_ffi_generated.h" + +using namespace mozilla::intl::ffi; + +void Locale::MyFunction(nsCString& aInput) const { + nsCString result; + unic_langid_canonicalize(aInput, &result); +} +``` + +### Complex types + +Many complex Rust types can be exposed to C++, and cbindgen will generate +appropriate bindings for all `pub` types. For example: + +```rust +#[repr(C)] +pub enum FluentPlatform { + Linux, + Windows, + Macos, + Android, + Other, +} + +extern "C" { + pub fn FluentBuiltInGetPlatform() -> FluentPlatform; +} +``` + +```cpp +ffi::FluentPlatform FluentBuiltInGetPlatform() { + return ffi::FluentPlatform::Linux; +} +``` + +For an example using cbindgen to expose much more complex Rust types to C++, +see [this blog post]. + +[this blog post]: https://crisal.io/words/2020/02/28/C++-rust-ffi-patterns-1-complex-data-structures.html + +### Instances + +If you need to create and destroy a Rust struct from C++ code, the following +example may be helpful. + +First, define constructor, destructor and getter functions in Rust. (C++ +declarations for these will be generated by cbindgen.) + +```rust +#[no_mangle] +pub unsafe extern "C" fn unic_langid_new() -> *mut LanguageIdentifier { + let langid = LanguageIdentifier::default(); + Box::into_raw(Box::new(langid)) +} + +#[no_mangle] +pub unsafe extern "C" fn unic_langid_destroy(langid: *mut LanguageIdentifier) { + drop(Box::from_raw(langid)); +} + +#[no_mangle] +pub unsafe extern "C" fn unic_langid_as_string( + langid: &mut LanguageIdentifier, + ret_val: &mut nsACString, +) { + ret_val.assign(&langid.to_string()); +} +``` + +Next, in a C++ header define a destructor via `DefaultDelete`. + +```cpp +#include "mozilla/intl/unic_langid_ffi_generated.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +template <> +class DefaultDelete<intl::ffi::LanguageIdentifier> { + public: + void operator()(intl::ffi::LanguageIdentifier* aPtr) const { + unic_langid_destroy(aPtr); + } +}; + +} // namespace mozilla +``` + +(This definition must be visible any place where +`UniquePtr<intl::ffi::LanguageIdentifier>` is used, otherwise C++ will try to +free the code, which might lead to strange behaviour!) + +Finally, implement the class. + +```cpp +class Locale { +public: + explicit Locale(const nsACString& aLocale) + : mRaw(unic_langid_new()) {} + + const nsCString Locale::AsString() const { + nsCString tag; + unic_langid_as_string(mRaw.get(), &tag); + return tag; + } + +private: + UniquePtr<ffi::LanguageIdentifier> mRaw; +} +``` + +This makes it possible to instantiate a `Locale` object and call `AsString()`, +all from C++ code. + +## Other examples + +For a detailed explanation of an interface in Firefox that doesn't use cbindgen +or rust-bindgen, see [this blog post](https://hsivonen.fi/modern-cpp-in-rust/). |