XPCOM components in Rust

XPCOM components can be written in Rust.

A tiny example

The following example shows a new type that implements nsIObserver.

First, create a new empty crate (e.g. with cargo init --lib), and add the following dependencies in its Cargo.toml file.

[dependencies]
libc = "0.2"
nserror = { path = "../../../xpcom/rust/nserror" }
nsstring = { path = "../../../xpcom/rust/nsstring" }
xpcom = { path = "../../../xpcom/rust/xpcom" }

(The number of ../ occurrences will depend on the depth of the crate in the file hierarchy.)

Next hook it into the build system according to the build documentation.

The Rust code will need to import some basic types. xpcom::interfaces contains all the usual nsI interfaces.

use libc::c_char;
use nserror::nsresult;
use std::sync::atomic::{AtomicBool, Ordering};
use xpcom::{interfaces::nsISupports, RefPtr};

The next part declares the implementation.

#[derive(xpcom)]
#[xpimplements(nsIObserver)]
#[refcnt = "atomic"]
struct InitMyObserver {
    ran: AtomicBool,
}

It defines an initializer struct, prefixed with Init, with three attributes.

  • Some derive magic.

  • An xpimplements declaration naming the interface(s) being implemented.

  • The reference count type.

Next, all interface methods are declared in the impl block as unsafe methods.

impl MyObserver {
    #[allow(non_snake_case)]
    unsafe fn Observe(
        &self,
        _subject: *const nsISupports,
        _topic: *const c_char,
        _data: *const i16,
    ) -> nsresult {
        self.ran.store(true, Ordering::SeqCst);
        nserror::NS_OK
    }
}

These methods always take &self, not &mut self, so we need to use interior mutability: AtomicBool, RefCell, Cell, etc.

XPCOM methods are unsafe by default, but the xpcom_method! macro can be used to clean this up. It also takes care of null-checking and hiding pointers behind references, lets you return a Result instead of an nsresult, and so on.

To use this type within Rust code, do something like the following.

let observer = MyObserver::allocate(InitMyObserver {
  ran: AtomicBool::new(false),
});
let rv = unsafe {
  observer.Observe(x.coerce(),
                   cstr!("some-topic").as_ptr(),
                   ptr::null())
};
assert!(rv.succeeded());

The implementation has an (auto-generated) allocate method that takes in an initialization struct, and returns a RefPtr to the instance.

coerce casts any XPCOM object to one of its base interfaces; in this case, the base interface is nsISupports. In C++, this would be handled automatically through inheritance, but Rust doesn’t have inheritance, so the conversion must be explicit.

Bigger examples

The following XPCOM components are written in Rust.

  • kvstore, which exposes the LMDB key-value store (via the Rkv library) The API is asynchronous, using moz_task to schedule all I/O on a background thread, and supports getting, setting, and iterating over keys.

  • cert_storage, which stores lists of revoked intermediate certificates.

  • bookmark_sync, which merges bookmarks from Firefox Sync with bookmarks in the Places database.

  • webext_storage_bridge, which powers the WebExtension storage.sync API. It’s a self-contained example that pulls in a crate from application-services for the heavy lifting, wraps that up in a Rust XPCOM component, and then wraps the component in a JS interface. There’s also some boilerplate there around adding a components.conf file, and a dummy C++ header that declares the component constructor.

  • firefox-accounts-bridge, which wraps the Rust Firefox Accounts client with which we eventually want to replace our creaky JS implementation. It has a lot of the same patterns as webext_storage_bridge.