Skip to content

Instantly share code, notes, and snippets.

@zbraniecki
Last active April 7, 2024 04:42
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zbraniecki/b251714d77ffebbc73c03447f2b2c69f to your computer and use it in GitHub Desktop.
Save zbraniecki/b251714d77ffebbc73c03447f2b2c69f to your computer and use it in GitHub Desktop.
Rust <--> C/C++ FFI for newbies

As Gecko is moving toward more Rust code, the cases where Rust and C code interoperate will become more common.

This document is an attempt to ease the learning curve for engineers facing it for the first time. It assumes no prior experience with cross-language C interfaces (called FFI).

It also assumes that Rust code is already built into Gecko. If you need help with that, read Introducing Rust code in Firefox.

What can you transfer across the fence

Generally speaking, the more complicated is the data you want to transfer, the harder it'll be.

The ideal case are:

  • boolean
  • unsigned/signed integers
  • pointer

Those can be send back and forth without much trouble.

Lists are handled by ThinVec (Rust) /nsTArray (C).

For strings, you can either use raw ptr+len:

fn foo(len: *mut u32) -> *const u8 {
    *len = lang.len() as u32;
    string.as_bytes().as_ptr()
}

and

uint32_t len;
const uint8_t* chars = foo(&len);
return nsDependentCSubstring(reinterpret_cast<const char*>(chars), len);

or use nsstring on both Rust and C side.

use nsstring::nsCString;

fn foo(ret_val: &mut ncString) {
    ret_val.assign("foo");
}

and

nsCString result;
foo(&result);

If you need a map, you'll likely want to decompose it into two lists - keys/values - and send them separately.

Calling C from Rust

If you need to call C code from Rust, you'll design a signature of a function, place the header definition in Rust, and the implementation in C. It may look like this:

extern "C" {
    pub fn UniqueNameOfMyFunction(input: &nsCString, ret_val: &mut nsCString) -> bool;
}
extern "C" {

bool UniqueNameOfMyFunction(const nsCString* aInput, nsCString* aRetVal) {
  return true;
}

}

That's it. Assuming both Rust and C are compiled into Gecko, your Rust code should now be able to call the UniqueNameOfMyFunction and the C code will be executed with the return value CString and bool coming back to Rust.

Calling Rust from C

There are multiple ways to achieve that, but here I'm going to use cbindgen, which is a utility that helps us generate C header files.

  1. Add ffi definitions to your Rust
#[no_mangle]
pub unsafe extern "C" fn unic_langid_canonicalize(
    langid: &nsCString,
    ret_val: &mut nsCString
) -> bool {
    ret_val.assign("new value");
    true
}
  1. cbinden.toml

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++"
namespaces = ["mozilla", "intl", "ffi"]

[export.rename]
"ThinVec" = "nsTArray"

The only thing we specified here is that the FFI calls will end up in mozilla::intl::ffi namespace and that all references to ThinVec will be exported as Mozilla's C nsTArray. More options are available in the documentation.

  1. moz.build changes

Next, we'll want to extend our module's moz.build file to add a cbindgen call against our crate to generate the header. It may look like this:

if CONFIG['COMPILE_ENVIRONMENT']:
    GENERATED_FILES += [
        'unic_langid_ffi_generated.h',
    ]

    EXPORTS.mozilla.intl += [
        '!unic_langid_ffi_generated.h',
    ]

    ffi_generated = GENERATED_FILES['unic_langid_ffi_generated.h']
    ffi_generated.script = '/layout/style/RunCbindgen.py:generate'
    ffi_generated.inputs = [
        '/intl/locale/rust/unic-langid-ffi',
    ]

Here, we're telling our build system to call RunCBindgen.py:generate against intl/locale/rust/unic-langid-ffi, generating unic_langid_ffi_generated.h in result. The file will be generated in $objdir/dist/include/mozilla/intl/ directory.

  1. include the header

With those two steps completed, we can now include the generated header into our .h/.cpp file:

#include "mozilla/intl/unic_langid_ffi_generated.h"
  1. call the function

And then we can call our function

using namespace mozilla::intl::ffi;

void Locale::MyFunction(nsCString& aInput) const {
  nsCString result;
  unic_langid_canonicalize(aInput, &result);
}

This should be it.

Enums

You can expose a Rust enum to C++.

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

extern "C" {
    pub fn FluentBuiltInGetPlatform() -> FluentPlatform;
}
ffi::FluentPlatform FluentBuiltInGetPlatform() {
  return ffi::FluentPlatform::Linux;
}

Instances

If you need to create an instance of a struct on the Rust side, keep it allocated on in the reflection of that instance on the C side, and call its methods from C, the following example may work for you:

  1. Define constructor/destructor and a method FFI functions
#[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());
}
  1. In your header define destructor
#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
  1. Define your class header
class Locale {
 public:
   explicit Locale(const nsACString& aLocale);
 private:
   UniquePtr<ffi::LanguageIdentifier> mRaw;
}
  1. Implement your class
Locale::Locale(): mRaw(unic_langid_new()) {}

const nsCString Locale::AsString() const {
  nsCString tag;
  unic_langid_as_string(mRaw.get(), &tag);
  return tag;
}

This should make it possible on the CPP side to instantiate a Locale class and call its AsString method.

@badboy
Copy link

badboy commented Feb 4, 2020

Implement your class

On first view I missed where unic_langid_new() is ever called. Mentioning this would help.

@badboy
Copy link

badboy commented Feb 4, 2020

explicit Locale(const nsACString& aLocale);

The constructor shown in "Implement your class" is different. To avoid confusion they should be the same.

@bynect
Copy link

bynect commented Jan 22, 2021

  1. cbinden.toml

In line 101 there's a little typo, cbinden.toml instead of cbindgen.toml.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment