gecko-dev/docs/writing-rust-code/cpp-interop.md

7.0 KiB

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:

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, which generates Rust bindings. The documentation is here.

Accessing Rust code and data from C++

A common option for accessing Rust code and data from C++ is to use 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.

#[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:

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.

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.

#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:

#[repr(C)]
pub enum FluentPlatform {
    Linux,
    Windows,
    Macos,
    Android,
    Other,
}

extern "C" {
    pub fn FluentBuiltInGetPlatform() -> FluentPlatform;
}
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.

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.)

#[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.

#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.

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.