--- title: "Using Rust code in R packages" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Using Rust code in R packages} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} 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: 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. ``` r 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 ``` * **`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` ``` 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 ``` toml extendr-api = { git = 'https://github.com/extendr/extendr' } ``` Now let us look at the main Rust library script. `src/rust/src/lib.rs` ``` 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](https://doc.rust-lang.org/reference/comments.html#doc-comments), 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: ``` r # 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!`. ``` rs /// @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`: ```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()`. ``` r 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.