Struct
Basic usage
You can use #[savvy]
macro on a struct
to convert it to an R object. More
precisely, this macro adds implementations of TryFrom
between Sexp
and the
struct so you can specify the type as the function input and output.
/// @export
#[savvy]
struct Person {
pub name: String,
}
The most handy form is to implement methods and associated functions for the
type. You can add #[savvy]
before the impl
block to make it available on R
sessions.
#[savvy]
impl Person {
fn new() -> Self {
Self {
name: "".to_string(),
}
}
fn set_name(&mut self, name: &str) -> savvy::Result<()> {
self.name = name.to_string();
Ok(())
}
fn name(&self) -> savvy::Result<savvy::Sexp> {
let mut out = OwnedStringSexp::new(1)?;
out.set_elt(0, &self.name)?;
out.into()
}
fn say_hello() -> savvy::Result<savvy::Sexp> {
"Hello!".try_into()
}
}
If we focus on the arguments, there are two types of functions here:
- method: the first argument is
self
1 (set_name()
andname()
) - associated function: no
self
argument (new()
andsay_hello()
)
You should almost always use &self
or &mut self
, not self
, except
when you are an expert and your intention is really to comsume it. Let's
discuss later.
On an R session, associated functions are available as the element of the same
name of R object as the Rust type (in this case, Person
).
p <- Person$new()
Person$say_hello()
#> [1] "Hello"
Among these two associated functions, new()
is a constructor which returns
Self
. This creates an instance of the struct.
The instance has the methods. You can call them like below.
# create an instance
p <- Person$new()
# call methods
p$set_name("たかし")
p$name()
#> [1] "たかし"
The instance has the same name of S3 class as the Rust type, so you can implement
S3 methods such as print.<your struct>()
if necessary.
class(p)
#> [1] "Person"
# register print() S3 method for Person
print.Person <- function(x, ...) print(x$name())
registerS3method("print", "Person", print.Person)
p
#> たかし
Struct output
The above example uses -> Self
as the return type of the associated function,
but it's not the only specification. You can wrap it with savvy::Result<Self>
.
#[savvy]
impl Person {
fn new_fallible() -> savvy::Result<Self> {
let x = Self {
name: "".to_string(),
};
Ok(x)
}
}
More generally, you can specify an arbitrary struct marked with #[savvy]
as
the return type. For example, you can create an instance of the struct outside
of impl
,
/// @export
#[savvy]
fn create_person() -> savvy::Result<Person> {
let x = Self {
name: "".to_string(),
};
Ok(x)
}
and you can generate another type of instance from an instance.
/// @export
#[savvy]
struct UpperPerson {
pub name: String,
}
#[savvy]
impl Person {
fn reborn_as_upper_person(&self) -> savvy::Result<UpperPerson> {
let x = UpperPerson {
name: self.name.to_uppercase(),
};
Ok(x)
}
}
Struct input
You can also use the struct as the argument of a #[savvy]
-ed function. Note
that, in most of the cases, you should specify &T
or &mut T
, not T
.
/// @export
#[savvy]
fn get_name_external(x: &Person) -> savvy::Result<savvy::Sexp> {
x.name()
}
get_name_external(x)
#> [1] "たかし"
&T
vs T
If you are familiar with Rust, you should know the difference. T
moves the
ownership while &T
is just borrowing. But, what does this matter savvy? What
happens in actual when you specify T
in a #[savvy]
function?
Say, you mistyped &Person
above as Person
like this:
/// @export
#[savvy]
fn get_name_external2(x: Person) -> savvy::Result<savvy::Sexp> {
x.name()
}
This function works the same as the previous one. The result of the first call is the same. Yay!
get_name_external2(p)
#> [1] "たかし"
Then, what's wrong? You'll find it when you call the function on the same object second time; it doesn't work anymore.
get_name_external2(p)
#> Error: This external pointer is already consumed or deleted
This is because the Person
object is already moved. The R variable p
doesn't
hold the ownership anymore. So, you should almost always specify &T
(or &mut T
),
not T
.
The same is true for a method. Use &self
and &mut self
instead of self
unless you want such a method like this!
#[savvy]
impl Person {
fn invalidate(self) -> savvy::Result<()> {
r_println!("This instance is invalidated!");
Ok(())
}
}
When is T
useful?
You might wonder why savvy allows this specification at all. Are there any cases when this is useful?
The answer is yes. The advantage of moving the ownership is that you can avoid
copying. For example, consider there's a type HeavyData
, which contains huge
size of data, and HeavyDataBundle
which bundles two HeavyData
s.
/// @export
#[savvy]
#[derive(Clone)]
struct HeavyData(Vec<i32>);
/// @export
#[savvy]
struct HeavyDataBundle {
data1: HeavyData,
data2: HeavyData,
}
#[savvy]
impl HeavyData {
// ...snip...
}
HeavyDataBundle
requires the ownership of the DataBundle
s. So, if the input
is &
, you need to clone()
the data, which can be costly.
/// @export
#[savvy]
impl HeavyDataBundle {
fn new(
data1: &HeavyData,
data2: &HeavyData,
) -> Self {
Self {
data1: data1.clone(),
data2: data2.clone(),
}
}
}
In this case, you can move the ownership to avoid copying.
/// @export
#[savvy]
impl HeavyDataBundle {
fn new(
data1: HeavyData,
data2: HeavyData,
) -> Self {
Self { data1, data2 }
}
}
Of course, this is an expert-only usage and is rarely needed. Again, you should
almost always use &T
or &mut T
instead of T
. If you are really sure it
doesn't work well, you can use T
.
Lifetime
#[savvy]
macro doesn't support a struct with lifetimes. This is because
crossing the boundary of FFI means losing the track of the lifetimes.
For example, the struct below contains a reference to a variable of usize
.
However, once an instance of Foo
is passed to R's side, Rust cannot know
whether the variable is still alive when Foo
is passed back to Rust's side.
struct Foo<'a>(&'a usize)
Then, what should we do to deal with such structs? I'm yet to find the best practices, but you might be able to
- use
'static
lifetime (i.e.struct Foo(&'static usize)
) probably by referencing a global variable - instead of passing the struct itself to R, store the struct in a global
OnceCell<HashMap>
and pass the key
External pointer?
Under the hood, the Person
struct is stored in EXTPTRSXP
. But, you don't
need to care about how to deal with EXTPTRSXP
. This is because it's stored in
a closure environment on creation and never exposed to the user. As it's
guaranteed on R's side that self
is always a EXTPTRSXP
of Person
, Rust
code just restore a Person
instance from the EXTPTRSXP
without any checks.
.savvy_wrap_Person <- function(ptr) {
e <- new.env(parent = emptyenv())
e$.ptr <- ptr
e$set_name <- Person_set_name(ptr)
e$name <- Person_name(ptr)
class(e) <- "Person"
e
}
Person <- new.env(parent = emptyenv())
Person$new <- function() {
.savvy_wrap_Person(.Call(Person_new__impl))
}
Person$say_hello <- function() {
.Call(Person_say_hello__impl)
}
Person_set_name <- function(self) {
function(name) {
invisible(.Call(Person_set_name__impl, self, name))
}
}
Person_name <- function(self) {
function() {
.Call(Person_name__impl, self)
}
}
It's important to mention that savvy only wraps the EXTPTRSXP
in a closure
environment when the type is used directly as the returning type of the function.
If the user wants to return Person
inside a List
, for example, the external
pointer will be directly exposed to the user and it will be the user's responsibility
to deal with it.
#[savvy]
struct Person {}
// This case savvy handles nicely.
/// @export
#[savvy]
impl Person {
fn new() -> savvy::Result<Person> {
Ok(Person {})
}
}
// In this case, the user is handled an external pointer.
/// @export
#[savvy]
fn create_list() -> savvy::Result<Sexp> {
let mut list = OwnedListSexp::new(1, false)?;
let person = Person {};
list.set_value(0, Sexp::try_from(person)?)?;
list.into()
}
in R:
> person = Person$new()
> print(person)
<environment: 0x0000027cf9d46a20>
attr(,"class")
[1] "Person"
> l = create_list()
> print(l)
[[1]]
<pointer: 0x0000000000000001>
Traps about protection
This is a bit advanced topic. It's okay to have a struct to contain arbitrary
things, however, if you want to pass an SEXP
from an R session, it's your
responsibility to take care of the protection on it.
The SEXP
passed from outside doesn't need an additional protection at the time
of the function call because it belongs to some environment on R session, which
means it's not GC-ed accidentally. However, after the function call, it's
possible the SEXP
loses its link to any other R objects. To prevent the
tragedy (i.e., R session crash), you should create a owned version and copy the
values into it because savvy takes care of the protection on it. So, in short,
you should never define such a struct like this:
struct Foo {
a: IntegerSexp
}
Instead, you should write
struct Foo {
a: OwnedIntegerSexp
}