Using Rust code in R packages

The rextendr package supports R package development by building the scaffolding necessary to use extendr. This is done by calling rextendr::use_extendr(), which should feel familiar to anyone who has worked with usethis::use_cpp11(). As with devtools, it is not required to add rextendr to the Depends or Imports of the package DESCRIPTION.

The development workflow is designed to be as consistent with any R extension workflow using devtools. The whole process can be summarized this way:

  1. Initialize a package with usethis::create_package() and rextendr::use_extendr().
  2. Compile a package with devtools::document().
  3. Load a package with devtools::load_all().

The following vignette walks through this workflow in more detail, highlighting important features of the r/extendr scaffolding and build process along the way.

Initialize a package

As an example workflow, let’s pick myextendr as the name of our extendr-powered R package. The first step in development is to create an empty R package directory with usethis::create_package("path/to/myextendr"). Then, we simply execute rextendr::use_extendr() inside that package directory to generate the required extendr scaffolding.

rextendr::use_extendr()
#> ✔ Writing 'src/entrypoint.c'
#> ✔ Writing 'src/Makevars.in'
#> ✔ Writing 'src/Makevars.win.in'
#> ✔ Writing 'cleanup'
#> ✔ Writing 'cleanup.win'
#> ✔ Writing 'src/.gitignore'
#> ✔ Writing 'src/rust/Cargo.toml'
#> ✔ Writing 'src/rust/src/lib.rs'
#> ✔ Writing 'src/testpkg-win.def'
#> ✔ Writing 'src/rust/document.rs'
#> ✔ File 'R/extendr-wrappers.R' already exists. Skip writing the file.
#> ✔ Writing 'tools/msrv.R'
#> ✔ Writing 'tools/config.R'
#> ✔ Writing 'configure'
#> ✔ Writing 'configure.win'
#> ✔ Finished configuring extendr for package testpkg.
#> * Please run `devtools::document()` for changes to take effect.
#> i Call `use_extendr_badge()` to add an extendr badge to your 'README'

For developers who use RStudio, we also provide a project template that will call usethis::create_package() and rextendr::use_extendr() for you. This is done using RStudio’s Create Project command, which you can find on the global toolbar or in the File menu. Choose “New Directory” then select “R package with extendr.” You can then fill out the details to match your preferences.

Once you have the project directory setup, we strongly encourage you to run rextendr::rust_sitrep() in the console. This will provide a detailed report of the current state of your Rust infrastructure, along with some helpful advice about how to address any issues that may arise.

Assuming we have a proper installation of Rust, we are just one step away from calling Rust functions from R. As the message above says, we need to run devtools::document(). Before doing that, however, let’s look at the scaffolding files that were added to our package directory.

Package structure

Calling rextendr::use_extendr() generates the following scaffolding:

.
├── R
│   └── extendr-wrappers.R
...
└── src
    ├── Makevars
    ├── Makevars.win
    ├── entrypoint.c
    └── rust
        ├── Cargo.toml
        ├── document.rs
        └── src
            └── lib.rs

Two files in src/rust deserve some further consideration:

src/rust/Cargo.toml

[package]
name = 'myextendr'
publish = false
version = '0.1.0'
edition = '2021'
rust-version = '1.65'

[lib]
crate-type = [ 'rlib', 'staticlib' ]
name = 'myextendr'

[[bin]]
name = 'document'
path = 'document.rs'
bench = false

[dependencies]
extendr-api = '*'

[profile.release]
lto = true
codegen-units = 1

The crate name is the same name as the R package’s name by default. You can change this, but it might be a bit cumbersome to tweak other files accordingly, so we recommend leaving it as is. You will also probably want to specify a concrete extendr version, for example extendr-api = '0.2'. To try the development version of the extendr, you can modify the dependency to read

extendr-api = { git = 'https://github.com/extendr/extendr' }

Now let us look at the main Rust library script.

src/rust/src/lib.rs

use extendr_api::prelude::*;

/// Return string `"Hello world!"` to R.
/// @export
#[extendr]
fn hello_world() -> &'static str {
    "Hello world!"
}

// Macro to generate exports.
// This ensures exported functions are registered with R.
// See corresponding C code in `entrypoint.c`.
extendr_module! {
    mod myextendr;
    fn hello_world;
}

There are a few things to note about this file. First, the use statement brings commonly used extendr API functions into the current scope. Second, the three forward slashes /// indicate a Rust document comment, which is used to generate a crate’s documentation. In extendr, these lines are copied to the auto-generated R code as roxygen comments. This is analogous to Rcpp/cpp11’s //'. Finally, the #[extendr] and extendr_module! macros ensure that corresponding R functions are generated automatically, similar to how Rcpp’s [[Rcpp::export]] and cpp11’s [[cpp11::register]] work. Note that it is never sufficient to add the #[extendr] macro above a function definition. Those function names must also be collected in extendr_module! to generate the necessary wrappers.

Compile a package

Compiling Rust code into R functions is as easy as calling devtools::document(), just as we would do if writing a package around C or C++. The documentation process first compiles the Rust library, then generates R wrappers along with their documentation if provided, and updates the NAMESPACE. The whole process thus leads to several files being either updated or generated from scratch:

.
...
├── NAMESPACE                 ----------(4)
├── R
│   └── extendr-wrappers.R    ----------(3)
├── man
│   └── hello_world.Rd        ----------(4)
└── src
    ├── myextendr.so          ----------(2)
    └── rust
        └── target
            └── release
                ├── libmyextendr.a   ---(1)
                ...
  1. src/rust/target/release/libmyextendr.a (the extension depends on the OS): This is the static library built from Rust code. This will be then used for compiling the shared object myextendr.so.
  2. src/myextendr.so (the extension depends on the OS): This is the shared object that is actually called from R.
  3. R/extendr-wrappers.R: The auto-generated R functions, including roxygen comments, go into this file. The roxygen comments are accordingly converted into Rd files and NAMESPACE.
  4. man/, NAMESPACE: These are generated from roxygen comments.

Generated R code

While we never edit the R code in R/extendr-wrappers.R by hand, it might be good to know what that file looks like. For our default hello-world library, it is this:

# Generated by extendr: Do not edit by hand
#
#' @usage NULL
#' @useDynLib testPackage, .registration = TRUE
NULL

#' Return string `"Hello world!"` to R.
#' @export
hello_world <- function() .Call(wrap__hello_world)

The Roxygen directive @useDynLib testPackage, .registration = TRUE ensures that useDynLib(myextendr, .registration = TRUE) is added to the NAMESPACE, which allows for calling the compiled Rust code in R. We also see that the roxygen comments from the Rust script are copied here.

To help clarify this compilation process, let’s implement a new Rust function. First, we add the function with @export, so it will get exported from the generated R package. This is followed by the #[extendr] macro above the function definition, with the function name then added to extendr_module!.

/// @export
#[extendr]
fn add(x: i32, y: i32) -> i32 {
    x + y
}

extendr_module! {
    mod myextendr;
    fn hello_world;
    fn add;
}

After we re-build the package with devtools::document(), you should see the new add function in R/extendr-wrappers.R:

#' @export
add <- function(x, y) .Call(wrap__add, x, y)

Load a package

Currently, our R package has two Rust-powered functions, hello_world() and add(). In a development workflow, we would access these functions in the current R session by simply loading the package with devtools::load_all().

devtools::load_all(".")

hello_world()
#> [1] "Hello world!"

add(1L, 2L)
#> [1] 3

Alternatively, we could install the package with devtools::install(), then attach it with library(). In either case, we are now free to test our functions interactively.

mirror server hosted at Truenetwork, Russian Federation.