knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
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 = 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.
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:
#' @export add <- function(x, y) .Call(wrap__add, x, y)
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.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.