Making Rust items available to R

The power of extendr is in its ability to use Rust from R. The #[extendr] macro is what determines what is exported to R from Rust. This section covers the basic usage of the #[extendr] macro.

#[extendr] is what is referred to as an attribute macro (which itself is a type of procedural macro). An attribute macro is attached to an item such as a function, struct, enum, or impl.

The #[extendr] attribute macro indicates that an item should be made available to R. However, it can only be used with a function or an impl block.

Exporting functions

In order to make a function available to R, two things must happen. First, the #[extendr] macro must be attached to the function. For example, you can create a function answer_to_life()

In the Hitchhiker’s Guide to the Galaxy, the number 42 is the answer to the universe. See this fun article from Scientific American

#[extendr]
fn answer_to_life() -> i32 {
    42
}

By adding the #[extendr] attribute macro to the answer_to_life() function, we are indicating that this function has to be compatible with R. This alone, however, does not make the function available to R. It must be made available via the extendr_module! {} macro in lib.rs.

extendr_module! {
    mod hellorust;
    fn answer_to_life;
}
Tip

Everything that is made available in the extendr_module! {} macro in lib.rs must be compatible with R as indicated by the #[extendr] macro. Note that the module name mod hellorust must be the name of the R package that this is part of. If you have created your package with rextendr::use_extendr() this should be set automatically. See Hello, world!.

What happens if you try and return something that cannot be represented by R? Take this example, an enum Shape is defined and a function takes a string &str. Based on the value of the arugment, an enum variant is returned.

#[derive(Debug)]
enum Shape {
    Triangle,
    Rectangle,
    Pentagon,
    Hexagon,
}

#[extendr]
fn make_shape(shape: &str) -> Shape {
    match shape {
        "triangle" => Shape::Triangle,
        "rectangle" => Shape::Rectangle,
        "pentagon" => Shape::Pentagon,
        "hexagon" => Shape::Hexagon,
        &_ => unimplemented!()
    }
}

When this is compiled, an error occurs because extendr does not know how to convert the Shape enum into something that R can use. The error is fairly informative!

#[extendr]
   | ^^^^^^^^^^ the trait `ToVectorValue` is not implemented for `Shape`, which is required by `extendr_api::Robj: From<Shape>`
   |
   = help: the following other types implement trait `ToVectorValue`:
             bool
             i8
             i16
             i32
             i64
             usize
             u8
             u16
           and 45 others
   = note: required for `extendr_api::Robj` to implement `From<Shape>`
   = note: this error originates in the attribute macro `extendr` 

It tells you that Shape does not implement the ToVectorValue trait. The ToVectorValue trait is what enables items from Rust to be returned to R.

ToVectorValue trait

In order for an item to be returned from a function marked with the #[extendr] attribute macro, it must be able to be turned into an R object. In extendr, the struct Robj is a catch all for any type of R object.

Note

For those familiar with PyO3, the Robj struct is similar in concept to the PyAny struct.

The ToVectorValue trait is what is used to convert Rust items into R objects. The trait is implemented on a number of standard Rust types such as i32, f64, usize, String and more (see all foreign implementations here) which enables these functions to be returned from a Rust function marked with #[extendr].

Note

In essence, all items that are returned from a function must be able to be turned into an Robj. Other extendr types such as List, for example, have a From<T> for Robj implementation that defines how it is converted into an Robj.

This means that with a little extra work, the Shape enum can be returned to R. To do so, the #[extendr] macro needs to be added to an impl block.

Exporting impl blocks

The other supported item that can be made available to R is an impl block. impl is a keyword that allows you to implement a trait or an inherent implementation. The #[extendr] macro works with inherent implementations. These are impls on a type such as an enum or a struct. extendr does not support using #[extendr] on trait impls.

Note

You can only add an inherent implementation on a type that you have own and not provided by a third party crate. This would violate the orphan rules.

Continuing with the Shape example, this enum alone cannot be returned to R. For example, the following code will result in a compilation error

#[derive(Debug)]
enum Shape {
    Triangle,
    Rectangle,
    Pentagon,
    Hexagon,
}

#[extendr]
fn make_shape(shape: &str) -> Shape {
    match shape {
        "triangle" => Shape::Triangle,
        "rectangle" => Shape::Rectangle,
        "pentagon" => Shape::Pentagon,
        "hexagon" => Shape::Hexagon,
        &_ => unimplemented!()
    }
}
error[E0277]: the trait bound `Shape: ToVectorValue` is not satisfied
  --> src/lib.rs:19:1
   |
19 | #[extendr]
   | ^^^^^^^^^^ the trait `ToVectorValue` is not implemented for `Shape`, which is required by `extendr_api::Robj: From<Shape>`
   |

However, if an impl block is added to the Shape enum, it can be returned to R.

#[derive(Debug)]
enum Shape {
    Triangle,
    Rectangle,
    Pentagon,
    Hexagon,
}

#[extendr]
impl Shape {
    fn new(x: &str) -> Self {
        match x {
            "triangle" => Self::Triangle,
            "rectangle" => Self::Rectangle,
            "pentagon" => Self::Pentagon,
            "hexagon" => Self::Hexagon,
            &_ => unimplemented!(),
        }
    }

    fn n_coords(&self) -> usize {
        match &self {
            Shape::Triangle => 3,
            Shape::Rectangle => 4,
            Shape::Pentagon => 4,
            Shape::Hexagon => 5,
        }
    }
}

In this example two new methods are added to the Shape enum. The first new() is like the make_shape() function that was shown earlier: it takes a &str and returns an enum variant. Now that the enum has an impl block with #[extendr] attribute macro, it can be exported to R by inclusion in the extendr_module! {} macro.

extendr_module! {
    mod hellorust;
    impl Shape;
}

Doing so creates an environment in your package called Shape. The environment contains all of the methods that are available to you.

Tip

There are use cases where you may not want to expose any methods but do want to make it possible to return a struct or an enum to the R. You can do this by adding an empty impl block with the #[extendr] attribute macro.

If you run as.list(Shape) you will see that there are two functions in the environment which enable you to call the methods defined in the impl block. You might think that this feel like an R6 object and you’d be right because an R6 object essentially is an environment!

as.list(Shape)
$n_coords
function () 
.Call("wrap__Shape__n_coords", self, PACKAGE = "librextendr1.dylib")

$new
function (x) 
.Call("wrap__Shape__new", x, PACKAGE = "librextendr1.dylib")

Calling the new() method instantiates a new enum variant.

tri <- Shape$new("triangle")
tri
<pointer: 0x13373d320>
attr(,"class")
[1] "Shape"

The newly made tri object is an external pointer to the Shape enum in Rust. This pointer has the same methods as the Shape environment—though they cannot be seen in the same way. For example you can run the n_coords() method on the newly created object.

tri$n_coords()
[1] 3
Tip

To make the methods visible to the Shape class you can define a .DollarNames method which will allow you to preview the methods and attributes when using the $ syntax. This is very handy to define when making an impl a core part of your package.

.DollarNames.Shape = function(env, pattern = "") {
  ls(Shape, pattern = pattern)
}

impl ownership

Adding the #[extendr] macro to an impl allows the struct or enum to be made available to R as an external pointer. Once you create an external pointer, that is then owned by R. So you can only get references to it or mutable references. If you need an owned version of the type, then you will need to clone it.

Accessing exported impls from Rust

Invariably, if you have made an impl available to R via the #[extendr] macro, you may want to define functions that take the impl as a function argument.

Due to R owning the impl’s external pointer, these functions cannot take an owned version of the impl as an input. For example trying to define a function that subtracts an integer from the n_coords() output like below returns a compiler error.

#[extendr]
fn subtract_coord(x: Shape, n: i32) -> i32 {
    (x.n_coords() as i32) - n
}
the trait bound `Shape: extendr_api::FromRobj<'_>` is not satisfied
  --> src/lib.rs:53:22
   |
   | fn subtract_coord(x: Shape, n: i32) -> i32 {
   |                      ^^^^^ the trait `extendr_api::FromRobj<'_>` is not implemented for `Shape`
   |
help: consider borrowing here
   |
   | fn subtract_coord(x: &Shape, n: i32) -> i32 {
   |                      +
   | fn subtract_coord(x: &mut Shape, n: i32) -> i32 {
   |                      ++++

As most often, the compiler’s suggestion is a good one. Use &Shape to use a reference.

ExternalPtr: returning arbitrary Rust types

In the event that you need to return a Rust type to R that doesn’t have a compatible impl or is a type that you don’t own, you can use ExternalPtr<T>. The ExternalPtr struct allows any item to be captured as a pointer and returned to R.

Here, for example, an ExternalPtr<Shape> is returned from the shape_ptr() function.

Tip

Anything that is wrapped in ExternalPtr<T> must implement the Debug trait.

#[derive(Debug)]
enum Shape {
    Triangle,
    Rectangle,
    Pentagon,
    Hexagon,
}

#[extendr]
fn shape_ptr(shape: &str) -> ExternalPtr<Shape> {
    let variant = match shape {
        "triangle" => Shape::Triangle,
        "rectangle" => Shape::Rectangle,
        "pentagon" => Shape::Pentagon,
        "hexagon" => Shape::Hexagon,
        &_ => unimplemented!(),
    };

    ExternalPtr::new(variant)
}

Using an external pointer, however, is far more limiting than the impl block. For example, you cannot access and of its methods.

tri_ptr <- shape_ptr("triangle")
tri_ptr$n_coords()
Error in tri_ptr$n_coords: object of type 'externalptr' is not subsettable

To use an ExternalPtr<T>, you have to go through a bit of extra work for it.

#[extendr]
fn n_coords_ptr(x: Robj) -> i32 {
    let shape = TryInto::<ExternalPtr<Shape>>::try_into(x); 
    
    match shape {
        Ok(shp) => shp.n_coords() as i32,
        Err(_) => 0
    }
}

This function definition takes an Robj and from it, tries to create an ExternalPtr<Shape>. Then, if the conversion did not error, it returns the number of coordinates as an i32 (R’s version of an integer) and if there was an error converting, it returns 0.

tri_ptr <- shape_ptr("triangle")

n_coords_ptr(tri_ptr)
[1] 3
n_coords_ptr(list())
[1] 0

For a good example of using ExternalPtr<T> within an R package, refer to the b64 R package.