Using Rust types in R

This tutorial demonstrates some of the basics of passing data types back and forth between Rust and R. This includes all of the following:

We’ll start with examples showing how to pass R types as explicit Rust types. This is useful for demonstration purposes, but it does ignore one very very big issue, and that’s missing values. Rust data types do not allow for missing values, so they have to be handled carefully. Fortunately, extendr offers its own data types built on top of the Rust types to do that for you. For this reason, it is strongly recommended that you work with the extendr types wherever possible. However, when first getting comfortable with extendr, and possible even Rust, it may feel more comfortable to work with Rust native types.

Scalar Type Mapping with Rust Types

In R, there is no such thing as a scalar value. Everything is a vector. When using a scalar value in R, that is really a length one vector. In Rust, however, scalar values are the building blocks of everything.

Below is a mapping of scalar values between R, extendr, and Rust.

R type extendr type Rust type
integer(1) Rint i32
double(1) Rfloat f64
logical(1) Rbool bool
complex(1) Rcplx Complex<f64>
character(1) Rstr String

To see how these scalars get passed back and forth between Rust and R, we’ll first explore Rust’s f64 value which is a 64-bit float. This is equivalent to R’s double(1). We’ll write a very simple Rust function that prints the value of the input and does not return anything.

#[extendr]
fn scalar_double(x: f64) { 
    rprintln!("The value of x is {x}"); 
}
Note

Note the use of rprintln!() instead of the println!() macro. Using println!() will not always be captured by the R console. Using rprintln!() will ensure that it is.

If you are not working inside of an extendr R package, you can create this function locally using rextendr::rust_function().

rextendr::rust_function("
fn scalar_double(x: f64) { 
    rprintln!("The value of x is {x}"); 
}
")

Try calling this function on a single double value.

scalar_double(4.2)
The value of x is 4.2

A couple of things to note with this example. First, x: f64 tells Rust that the type of x being passed to the function is a single double vector or “float” value. Second, rprintln!("{}", x); is an extendr macro (the give-away for this is the !) that makes it easier to print information from Rust to the console in R. R users will perhaps notice that the syntax is vaguely {glue}-like in that the value of x is inserted into the curly brackets.

Now, what if, rather than printing the value of x to the R console, we wanted instead to return that value to R? To do that, we just need to let Rust know what type is being returned by our function. This is done with the -> type notation. The extendr crate knows how to handle the scalar f64 type and pass it to R as double.

fn scalar_double(x: f64) -> f64 { 
    x 
}
x <- scalar_double(4.2)
The value of x is 4.2
typeof(x)
[1] "NULL"
x + 1
numeric(0)

Additional examples

We can extend this example to i32, bool and String values in Rust.

#[extendr]
fn scalar_integer(x: i32) -> i32 { x }

#[extendr]
fn scalar_logical(x: bool) -> bool { x }

#[extendr]
fn scalar_character(x: String) -> String { x }
scalar_integer(4L)
[1] 4
scalar_logical(TRUE)
[1] TRUE
scalar_character("Hello world!")
[1] "Hello world!"

Vector Type Mapping with Rust Types

What happens if we try to pass more than one value to scalar_double()?

scalar_double(c(4.2, 1.3, 2.5))
Error in scalar_double(c(4.2, 1.3, 2.5)): Input must be of length 1. Vector of length >1 given.

It errors because the function expects a scalar of the f64 type, not a vector of f64.

In this section, we show you how to pass Rust vectors between R and Rust.

Important

While using a Rust vector is possible in some cases, it is strongly not recommended. Instead, extendr types should be used as they provide access directly to R objectes. Whereas using Rust vectors requires additional allocations.

The syntax is basically the same as with scalars, with just some minor changes. We’ll use doubles again to demonstrate this.

For reference, below are the type of Rust vectors that can be utilized with extendr.

R type extendr type Rust type
integer() Integers Vec<i32>
double() Doubles Vec<f64>
complex() Complexes Vec<Complex<f64>>
character() Strings Vec<String>
raw() Raw &[u8]
logical() Logicals
list() List
Note

You might have anticipated Vec<bool> to be a supported Rust vector type. This is not possible because in R, logical vectors do not contain only true and false like Rust’s bool type. They also can be an NA value which has no corresponding representation in Rust.

Below defines Rust function which takes in a vector of f64 values and prints them out.

#[extendr]
fn vector_double(x: Vec<f64>) {
    rprintln!("The values of x are {x:?}");
}

That function can be called from R which prints the Debug format of the vector.

Tip

Rust’s vector do not implement the Display trait so the debug format (:?) is used.

vector_double(c(4.2, 1.3, 2.5))
The values of x are [4.2, 1.3, 2.5]

Returning values using Rust follows the same rules as R. You do not need to explicitly return a value as long as the last item in an expression is not followed by a ;.

#[extendr]
fn vector_double(x: Vec<f64>) -> Vec<f64> { 
    x 
}

Calling the function returns the input as a double vector

x <- vector_double(c(4.2, 1.3, 2.5))
typeof(x)
[1] "double"
x + 1
[1] 5.2 2.3 3.5

Additional examples

These same principles can be extended to other supported vector types such as Vec<i32> and Vec<String>.

#[extendr]
fn vector_integer(x: Vec<i32>) -> Vec<i32> { 
    x
}

#[extendr]
fn vector_character(x: Vec<String>) -> Vec<String> {
    x 
}
vector_integer(c(4L, 6L, 8L))
[1] 4 6 8
vector_character(c("Hello world!", "Hello extendr!", "Hello R!"))
[1] "Hello world!"   "Hello extendr!" "Hello R!"      

Missing values

In Rust, missing values do not exist this in part why using Rust types alone is insufficient. Below a simple function which adds 1 to the input is defined.

#[extendr]
fn plus_one(x: f64) -> f64 { 
    x + 1.0 
}

Running this using a missing value results in an error.

plus_one(NA_real_)
Error in plus_one(NA_real_): Input must not be NA.

These extendr types, however, can be utilized much like a normal f64 that is NA aware. You will see that we have replaced the Rust type f64 with the extendr type Rfloat. Since Rfloat maps to a scalar value and not vector, the conversion needs to be handled more delicately. The macro was invoked with the use_try_from = true argument. This will eventually become the default behavior of extendr.

#[extendr(use_try_from = true)]
fn plus_one(x: Rfloat) -> Rfloat { 
    x + 1.0 
}
plus_one(NA_real_)
[1] NA
plus_one(4.2)
[1] 5.2

The combination of these two changes allows us to pass missing values to our plus_one() function and return missing values without raising an error.