Implementing an XPCOM interface in Rust

In this example, we’ll introduce the following interface:

#include "nsISupports.idl"

interface nsIStreamListener;

[scriptable, rust_sync, uuid(5c135256-c199-4d26-9d11-f5e8c8002869)]
interface nsIHelloWorld : nsISupports
{
    ACString generateHello(in ACString name);
};

Implementing the interface in a struct

A classic, pure Rust implementation of this interface could look something like this:

struct HelloWorld {}

impl HelloWorld {
    fn generate_hello(&self, name: String) -> String {
        format!("hello {name}")
    }
}

The first thing we need to do is to decorate the struct declaration to indicate it implements the nsIHelloWorld interface:

#[xpcom::xpcom(implement(nsIHelloWorld), atomic)]
struct HelloWorld {}
Note

This decorator is provided by the xpcom crate, which also requires the nserror and nsstring crates to be listed as dependencies alongside it. All three crates are located in xpcom/rust.

More information on how to use the #[xpcom::xpcom] decorator can be found in the Cargo documentation of the xpcom_macros crate (see this section for instructions on building Cargo documentation for internal crates).

This decorator defines that the struct implements the nsIHelloWorld interface. It also defines an initializer struct called InitHelloWorld (following the naming convention Init{ImplementationClassName}), alongside a HelloWorld::allocate method that can be used to initialize the method. This means we can now define a new method to HelloWorld:

impl HelloWorld {
    pub fn new() -> xpcom::RefPtr<HelloWorld> {
        HelloWorld::allocate(InitHelloWorld {})
    }

    ...
}
Note

InitHelloWorld can be used to define the fields of HelloWorld, if it had any. For example, if HelloWorld was defined as:

struct HelloWorld {
    name: &str,
}

Then a call to HelloWorld::allocate could look like this:

HelloWorld::allocate(InitHelloWorld {
    name: "Sarah",
})

HelloWorld::allocate also handles all the necessary operations to ensure that the new instance of HelloWorld is correctly ref-counted (hence why it wraps it into a RefPtr).

Using XPCOM-compatible types

Next, we want to fix up HelloWorld::generate_hello to use XPCOM-compatible types. In this case, both the input and the output are defined as ACString in the XPIDL definition of our interface, which maps to nsstring::nsACString.

This means generate_hello will take *const nsACString as an argument (note that all arguments which are purely described as input (in in XPCOM terms) are passed as *const).

For the method’s return value, we might not want to use nsACString straight away since this type can be a bit difficult to work with for what we want to do; instead we can use its subtype nsCString (see this page on the Firefox source docs, as well as the Cargo doc for nsstring, for more information about internal string types):

impl HelloWorld {
    ...

    fn generate_hello(&self, name: *const nsACString) -> nsCString {
        let mut hello = nsCString::from("hello ");
        hello.append(unsafe{ &*name });

        hello
    }
}
Note

Note the unsafe block inside the call to hello.append(). This is because we need to dereference the raw pointer of type *const nsACString to access the data with the nsACString type, which is an unsafe operation.

Additionally, XPCOM requires the method to implement some kind of error handling by returning a Result, which error type is either nserror::nsresult or any type that implements Into<nserror::nsresult>. This gives us this final form of HelloWorld::generate_hello:

impl HelloWorld {
    ...

    fn generate_hello(&self, name: *const nsACString) -> Result<nsCString, nsresult> {
        let mut hello = nsCString::from("hello ");
        hello.append(unsafe{ &*name });

        Ok(hello)
    }
}

Exposing the method as XPCOM

Now, all we need to do is to associate HelloWorld::generate_hello() with the generateHello() method from the XPIDL interface. Note that, just like in C++, XPIDL method names are translated to Pascal case in Rust (i.e., GenerateHello()).

We do this association using the xpcom_method! macro from the xpcom crate:

xpcom_method!(generate_hello => GenerateHello(name: *const nsACString) -> nsACString);

We use nsACString here as the return value in the xpcom_method! macro, to fit with the XPIDL definition. We can do this (and not have to stick strictly to generate_hello()’s return value) because the macro implements a new GenerateHello() method onto the HelloWorld struct, which has the following signature:

unsafe fn GenerateHello(&self, name: *const nsACString, retval: *mut nsACString) -> nsresult

This new method calls generate_hello(), and either assigns its return value to retval or returns the error value included in the Result (if no error is returned, GenerateHello() returns with nserror::NS_OK).

You can refer to the Cargo documentation for xpcom::xpcom_method for more information.

It’s also possible to skip the xpcom_method! entirely, by refactoring the generate_hello() method into a GenerateHello() one, which implements the signature described above.

By this point, and with a bit of cleanup, this is what HelloWorld should look like:

use nsstring::{nsACString, nsCString};
use xpcom::{xpcom_method, RefPtr};
use nserror::NS_OK;
use nserror::nsresult;

#[xpcom::xpcom(implement(nsIHelloWorld), atomic)]
struct HelloWorld {}

impl HelloWorld {
    pub fn new() -> RefPtr<HelloWorld> {
        HelloWorld::allocate(InitHelloWorld {})
    }

    xpcom_method!(generate_hello => GenerateHello(name: *const nsACString) -> nsACString);

    fn generate_hello(&self, name: *const nsACString) -> Result<nsCString, nsresult> {
        let mut hello = nsCString::from("hello ");
        hello.append(unsafe { &*name });

        Ok(hello)
    }
}
Note

All XPCOM objects are reference-counted, and as such exclusive access to them can’t be guaranteed. As a result, it’s not possible to use XPCOM to expose a method that takes a mutable reference to self (i.e. &mut self) to directly modify fields on the struct.

To mutate a struct field, it is necessary to provide interior mutability, such as by wrapping the field in a data structure from std::cell or similar.

Writing a constructor for the struct

We can now write a constructor that can be used by XPCOM’s createInstance() to instantiate the HelloWorld struct:

use xpcom::nsIID;

#[no_mangle]
pub unsafe extern "C" fn nsHelloWorldConstructor(
    iid: &nsIID,
    result: *mut *mut c_void,
) -> nsresult {
    let instance = HelloWorld::new();
    instance.QueryInterface(iid, result)
}
Note

The QueryInterface method is automatically implemented for HelloWorld by the #[xpcom::xpcom] decorator.

Registering the XPCOM component

We now have all the Rust code necessary to expose HelloWorld to the rest of Thunderbird – now all that’s left to do is to tell it our component exists.

A current limitation of XPCOM’s compatibility with Rust is that the constructor needs to be a symbol which exists within C++ land. To do this, we need to create a dummy C++ header for it. Let’s call it nsHelloWorld.h and locate it at the root of our crate:

#ifndef ThunderbirdRustHelloWorld_h
#define ThunderbirdRustHelloWorld_h

#include "nsID.h"

extern "C" {
// Implemented in Rust.
MOZ_EXPORT nsresult nsHelloWorldConstructor(REFNSIID aIID, void** aResult);
}  // extern "C"

#endif  // defined ThunderbirdRustHelloWorld_h

Then we can create a components.conf file (let’s locate it at the root of our crate as well):

Classes = [
    {
        'cid': '{5c135256-c199-4d26-9d11-f5e8c8002869}',
        'contract_ids': ['@mozilla.org/comm/rust/hello-world;1'],
        'headers': ['/comm/rust/hello_world/nsHelloWorld.h'],
        'legacy_constructor': 'nsHelloWorldConstructor',
    },
]

There are a few noteworthy aspects to this file:

  • the cid must be identical to the uuid in the XPIDL interface’s header

  • we have decided to call our crate hello_world, and we have located it at comm/rust/hello_world as per Adding a Rust crate to Thunderbird

Now we need to tell the build system to include the registration when building Thunderbird, which we can do by editing comm/rust/hello_world/moz.build (or creating it if it does not exist) and adding the following line to it:

XPCOM_MANIFESTS += ["components.conf"]

And finally, if this has not already been done previously, we can add the relative path to the crate to comm/rust/moz.build to make our crate’s moz.build discoverable:

DIRS += [
    ...
    "hello_world",
    ...
]

And that’s it! You should now be able to create a new instance of HelloWorld from JavaScript or C++.