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 {}
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 {})
}
...
}
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 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)
}
}
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)
}
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
cidmust be identical to theuuidin the XPIDL interface’s headerwe have decided to call our crate
hello_world, and we have located it atcomm/rust/hello_worldas 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++.