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:
usethis::create_package() and
rextendr::use_extendr().devtools::document().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.
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.
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
R/extendr-wrappers.R: This file
contains auto-generated R functions from Rust code. We don’t modify this
file by hand.src/Makevars,
src/Makevars.win: These files ensure that
cargo build --lib and cargo run --bin document
get called during the installation of the R package, ensuring that the
crate library is compiled and the R wrappers generated. In most cases,
we don’t edit these by hand.src/entrypoint.c: This file is needed
to avoid the linker removing the static library. In 99.9% of cases, we
don’t edit this (except for changing the crate name).src/rust/: Rust code of a crate using
extendr-api. This is where we mainly write code.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 = 1The 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
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.
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)
...
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.src/myextendr.so (the extension
depends on the OS): This is the shared object that is actually called
from R.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.man/,
NAMESPACE: These are generated from
roxygen comments.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:
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().
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.