Using Rust code in R Markdown documents
Claus O. Wilke
2024-11-21
Source:vignettes/rmarkdown.Rmd
rmarkdown.Rmd
The rextendr package enables two new chunk types for knitr,
extendr
and extendrsrc
. The former can be used
to evaluate individual Rust statements while the latter can be used to
define Rust functions and export them to R. To use either chunk type,
you need to first load the rextendr package in a regular R chunk. You
would normally do this in a hidden setup chunk at the top of your R
Markdown document.
Evaluating Rust statements
Chunks of type extendr
evaluate a block of Rust
statements. The value of the last statement can be returned to R and
will be printed as chunk output. For example:
This chunk will look in the knitted document as follows:
## Hello from Rust!
## [1] 35
In the background, the extendr
knit engine casts the
Rust integer variable into type Result<Robj>
and
returns its value to R. Notice the lack of a semicolon (;
)
at the end of this line, to indicate that you want to return a value.
You can also write code that doesn’t return any result to R. In this
case, the last line would end in a semicolon, as in this example:
## 5*7 = 35
Accessing R values from Rust
It is possible to access variables defined in R from Rust. Let’s define two R variables, a numeric and a string:
x <- 5.3
y <- "hello"
We can read these variables from Rust using the R!()
macro. Unfortunately, it takes a bit of error checking and
type-converting to get these into native Rust values. This is the case
because things could go wrong at each step. We’re handling the errors
with ?
. For example, R!()
calls the R
interpreter and it could generate an error. And even if it returns a
value, we’re not guaranteed that the value can be coerced into a real
value or a string. However, as_real()
and
as_str()
don’t return errors, they return options, so we
need to turn the None
option into an error via
ok_or()
.
let x:f64 = R!(x)?
.as_real()
.ok_or("expected a real")?;
let y:String = R!(y)?
.as_str()
.ok_or("expected a string reference")?
.to_string();
rprintln!("The value of x: {}\nThe value of y: {}", x, y);
## The value of x: 5.3
## The value of y: hello
Chaining chunks together
We may sometimes want to break a block of Rust code into separate
Markdown chunks but treat them as one compile unit. We can do so by
including the code from one chunk into the compile unit of another, by
specifying a preamble
chunk option. This option takes a
character vector of chunk names, which get included into the compile
unit in the order provided.
Consider this example Markdown code:
Define variable `x`:
```{extendr chunk_x, eval = FALSE}
let x = 1;
```
Define variable `y`:
```{extendr chunk_y, eval = FALSE}
let y = 2;
```
Print:
```{extendr out, preamble = c("chunk_x", "chunk_y")}
rprintln!("x = {}, y = {}", x, y);
```
It produces the following output.
Define variable x
:
Define variable y
:
Print:
## x = 1, y = 2
Exporting Rust functions or objects to R
The chunk type extendrsrc
compiles Rust functions and
objects and registers them with R to be called later. For example,
consider this code chunk. It creates R functions hello()
and foo()
, as well as a class called
Counter
.
```{extendrsrc}
#[extendr]
fn hello() -> &'static str {
"Hello from Rust!"
}
#[extendr]
fn foo(a: &str, b: i64) {
rprintln!("Data sent to Rust: {}, {}", a, b);
}
struct Counter {
n: i32,
}
#[extendr]
impl Counter {
fn new() -> Self {
Self { n: 0 }
}
fn increment(&mut self) {
self.n += 1;
}
fn get(&self) -> i32 {
self.n
}
}
```
The only output the chunk itself creates is the Rust source code:
#[extendr]
fn hello() -> &'static str {
"Hello from Rust!"
}
#[extendr]
fn foo(a: &str, b: i64) {
rprintln!("Data sent to Rust: {}, {}", a, b);
}
struct Counter {
n: i32,
}
#[extendr]
impl Counter {
fn new() -> Self {
Self { n: 0 }
}
fn increment(&mut self) {
self.n += 1;
}
fn get(&self) -> i32 {
self.n
}
}
However, now we can call the functions hello()
and
foo()
from R and use the Counter
object:
out <- hello()
out
## [1] "Hello from Rust!"
foo(out, nchar(out))
## Data sent to Rust: Hello from Rust!, 16
x <- Counter$new()
x$get()
## [1] 0
x$increment()
x$increment()
x$get()
## [1] 2
If your code requires external crates, you can set dependencies via
the engine.opts
chunk option, like so:
```{extendrsrc engine.opts = list(dependencies = list(`pulldown-cmark` = "0.8"))}
use pulldown_cmark::{Parser, Options, html};
#[extendr]
fn md_to_html(input: &str) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
let parser = Parser::new_ext(input, options);
let mut output = String::new();
html::push_html(&mut output, parser);
output
}
```
As before, the Rust code chunk just outputs the source:
use pulldown_cmark::{Parser, Options, html};
#[extendr]
fn md_to_html(input: &str) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
let parser = Parser::new_ext(input, options);
let mut output = String::new();
html::push_html(&mut output, parser);
output
}
And the generated functions can be used from R:
md_text <- "# The story of the fox
The quick brown fox **jumps over** the lazy dog.
The quick *brown fox* jumps over the lazy dog."
md_to_html(md_text)
## [1] "<h1>The story of the fox</h1>\n<p>The quick brown fox <strong>jumps over</strong> the lazy dog.\nThe quick <em>brown fox</em> jumps over the lazy dog.</p>\n"