Skip to content
This repository has been archived by the owner on Jun 30, 2024. It is now read-only.

Commit

Permalink
Merge pull request #15 from extendr/extendr-macro
Browse files Browse the repository at this point in the history
Section on using the #[extendr] macro
  • Loading branch information
JosiahParry authored Apr 1, 2024
2 parents 12f9e29 + dcf11c2 commit f9f881a
Show file tree
Hide file tree
Showing 3 changed files with 392 additions and 0 deletions.
15 changes: 15 additions & 0 deletions _freeze/extendr-macro/execute-results/html.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hash": "c8aaedd9ac73832eed986467d1586751",
"result": {
"engine": "knitr",
"markdown": "---\ntitle: \"Making Rust items available to R\"\n---\n\n\nThe power of extendr is in its ability to use utilize 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. \n\n[`#[extendr]`](https://extendr.github.io/extendr/extendr_api/attr.extendr.html) is what is referred to as an [attribute macro](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros) (which itself is a type of [procedural macro](https://doc.rust-lang.org/reference/procedural-macros.html)). An attribute macro is attached to an [item](https://doc.rust-lang.org/reference/items.html) such as a function, `struct`, `enum`, or `impl`. \n\nThe `#[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. \n\n\n\n\n\n## Exporting functions \n\nIn 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()`\n\n::: {.callout-note collapse=\"true\"}\nIn the Hitchhiker's Guide to the Galaxy, the number 42 is the answer to the universe. See this fun [article from Scientific American](https://www.scientificamerican.com/article/for-math-fans-a-hitchhikers-guide-to-the-number-42/)\n:::\n\n```rust\n#[extendr]\nfn answer_to_life() -> i32 {\n 42\n}\n```\n\nBy 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`.\n\n```rust\nextendr_module! {\n mod hellorust;\n fn answer_to_life;\n}\n```\n\n::: callout-tip\nEverything 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!](./hello-world.qmd).\n:::\n\nWhat 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. \n\n```rust\n#[derive(Debug)]\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\n#[extendr]\nfn make_shape(shape: &str) -> Shape {\n match shape {\n \"triangle\" => Shape::Triangle,\n \"rectangle\" => Shape::Rectangle,\n \"pentagon\" => Shape::Pentagon,\n \"hexagon\" => Shape::Hexagon,\n &_ => unimplemented!()\n }\n}\n```\n\nWhen 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! \n\n```\n#[extendr]\n | ^^^^^^^^^^ the trait `ToVectorValue` is not implemented for `Shape`, which is required by `extendr_api::Robj: From<Shape>`\n |\n = help: the following other types implement trait `ToVectorValue`:\n bool\n i8\n i16\n i32\n i64\n usize\n u8\n u16\n and 45 others\n = note: required for `extendr_api::Robj` to implement `From<Shape>`\n = note: this error originates in the attribute macro `extendr` \n```\n\nIt tells you that `Shape` does not implement the `ToVectorValue` trait. The `ToVectorValue` trait is what enables items from Rust to be returned to R.\n\n## `ToVectorValue` trait\n\nIn 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. \n\n::: callout-note\nFor those familiar with PyO3, the `Robj` struct is similar in concept to the [`PyAny`](https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html) struct.\n::: \n\nThe `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](https://extendr.github.io/extendr/extendr_api/robj/into_robj/trait.ToVectorValue.html#foreign-impls)) which enables these functions to be returned from a Rust function marked with `#[extendr]`. \n\n::: callout-note\nIn 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`.\n:::\n\nThis 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. \n\n\n## Exporting `impl` blocks\n\nThe other supported item that can be made available to R is an [`impl`](https://doc.rust-lang.org/std/keyword.impl.html) block. \n`impl` is a keyword that allows you to _implement_ a trait or an inherent implementation. The `#[extendr]` macro works with inherent implementations. These are `impl`s on a type such as an `enum` or a `struct`. extendr _does not_ support using `#[extendr]` on trait impls. \n\n::: callout-note\nYou 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](https://github.com/Ixrec/rust-orphan-rules?tab=readme-ov-file#what-are-the-orphan-rules).\n:::\n\nContinuing with the `Shape` example, this enum alone cannot be returned to R. For example, the following code will result in a compilation error\n\n```rust\n#[derive(Debug)]\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\n#[extendr]\nfn make_shape(shape: &str) -> Shape {\n match shape {\n \"triangle\" => Shape::Triangle,\n \"rectangle\" => Shape::Rectangle,\n \"pentagon\" => Shape::Pentagon,\n \"hexagon\" => Shape::Hexagon,\n &_ => unimplemented!()\n }\n}\n```\n```\nerror[E0277]: the trait bound `Shape: ToVectorValue` is not satisfied\n --> src/lib.rs:19:1\n |\n19 | #[extendr]\n | ^^^^^^^^^^ the trait `ToVectorValue` is not implemented for `Shape`, which is required by `extendr_api::Robj: From<Shape>`\n |\n```\n\nHowever, if an impl block is added to the `Shape` enum, it can be returned to R. \n\n\n::: {.cell}\n\n```{.rust .cell-code}\n#[derive(Debug)]\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\n#[extendr]\nimpl Shape {\n fn new(x: &str) -> Self {\n match x {\n \"triangle\" => Self::Triangle,\n \"rectangle\" => Self::Rectangle,\n \"pentagon\" => Self::Pentagon,\n \"hexagon\" => Self::Hexagon,\n &_ => unimplemented!(),\n }\n }\n\n fn n_coords(&self) -> usize {\n match &self {\n Shape::Triangle => 3,\n Shape::Rectangle => 4,\n Shape::Pentagon => 4,\n Shape::Hexagon => 5,\n }\n }\n}\n```\n:::\n\n\nIn 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]` atrtibute macro, it can be exported to R by inclusion in the `extendr_module! {}` macro.\n\n```rust\nextendr_module! {\n mod hellorust;\n impl Shape;\n}\n```\n\nDoing so creates an environment in your package called `Shape`. The environment contains the all of the methods that are available to you. \n\n::: callout-tip\nThere 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. \n::: \n\nIf 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](https://r6.r-lib.org/articles/Introduction.html) and you'd be right because an R6 object essentially is an environment! \n\n\n::: {.cell}\n\n```{.r .cell-code}\nas.list(Shape)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n$n_coords\nfunction () \n.Call(\"wrap__Shape__n_coords\", self, PACKAGE = \"librextendr1.dylib\")\n\n$new\nfunction (x) \n.Call(\"wrap__Shape__new\", x, PACKAGE = \"librextendr1.dylib\")\n```\n\n\n:::\n:::\n\n\nCalling the `new()` method instantiates a new enum variant. \n\n\n::: {.cell}\n\n```{.r .cell-code}\ntri <- Shape$new(\"triangle\")\ntri\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n<pointer: 0x14198d960>\nattr(,\"class\")\n[1] \"Shape\"\n```\n\n\n:::\n:::\n\n\nThe newly made `tri` object is an [external pointer](https://cran.r-project.org/doc/manuals/R-exts.html#External-pointers-and-weak-references) 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.\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntri$n_coords()\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] 3\n```\n\n\n:::\n:::\n\n\n::: callout-tip\nTo 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 a an impl a core part of a package.\n\n\n::: {.cell}\n\n```{.r .cell-code}\n.DollarNames.Shape = function(env, pattern = \"\") {\n ls(Shape, pattern = pattern)\n}\n```\n:::\n\n\n:::\n\n### `impl` ownership\n\nAdding 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. \n\n## Accessing exported `impl`s from Rust\n\nInvariably, 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. \n\nDue 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.\n\n```rust\n#[extendr]\nfn subtract_coord(x: Shape, n: i32) -> i32 {\n (x.n_coords() as i32) - n\n}\n```\n```\nthe trait bound `Shape: extendr_api::FromRobj<'_>` is not satisfied\n --> src/lib.rs:53:22\n |\n | fn subtract_coord(x: Shape, n: i32) -> i32 {\n | ^^^^^ the trait `extendr_api::FromRobj<'_>` is not implemented for `Shape`\n |\nhelp: consider borrowing here\n |\n | fn subtract_coord(x: &Shape, n: i32) -> i32 {\n | +\n | fn subtract_coord(x: &mut Shape, n: i32) -> i32 {\n | ++++\n```\n\nAs most often, the compiler's suggestion is a good one. Use `&Shape` to use a reference is ideal. \n\n## `ExternalPtr`: returning arbitrary Rust types \n\nIn the event that you need to return a Rust type to R that doesnt 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. \n\nHere, for example, an `ExternalPtr<Shape>` is returned from the `shape_ptr()` function.\n\n::: callout-tip\nAnything that is wrapped in `ExternalPtr<T>` must implement the `Debug` trait.\n:::\n\n\n::: {.cell}\n\n```{.rust .cell-code}\n#[derive(Debug)]\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\n#[extendr]\nfn shape_ptr(shape: &str) -> ExternalPtr<Shape> {\n let variant = match shape {\n \"triangle\" => Shape::Triangle,\n \"rectangle\" => Shape::Rectangle,\n \"pentagon\" => Shape::Pentagon,\n \"hexagon\" => Shape::Hexagon,\n &_ => unimplemented!(),\n };\n\n ExternalPtr::new(variant)\n}\n```\n:::\n\n\nUsing an external pointer, however, is far more limiting than the `impl` block. For example, you cannot access and of its methods.\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntri_ptr <- shape_ptr(\"triangle\")\ntri_ptr$n_coords()\n```\n\n::: {.cell-output .cell-output-error}\n\n```\nError in tri_ptr$n_coords: object of type 'externalptr' is not subsettable\n```\n\n\n:::\n:::\n\n\nTo use an `ExternalPtr<T>`, you have to go through a bit of extra work for it. \n\n\n\n\n\n```rust\n#[extendr]\nfn n_coords_ptr(x: Robj) -> i32 {\n let shape = TryInto::<ExternalPtr<Shape>>::try_into(x); \n \n match shape {\n Ok(shp) => shp.n_coords() as i32,\n Err(_) => 0\n }\n}\n```\n\nThis 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.\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntri_ptr <- shape_ptr(\"triangle\")\n\nn_coords_ptr(tri_ptr)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] 3\n```\n\n\n:::\n\n```{.r .cell-code}\nn_coords_ptr(list())\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] 0\n```\n\n\n:::\n:::\n\n\nFor a good example of using `ExternalPtr<T>` within an R package, refer to the [`b64` R package](https://github.com/extendr/b64). \n",
"supporting": [],
"filters": [
"rmarkdown/pagebreak.lua"
],
"includes": {},
"engineDependencies": {},
"preserve": {},
"postProcess": true
}
}
1 change: 1 addition & 0 deletions _quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ website:
- getting-started.qmd
- hello-world.qmd
- project-structure.qmd
- extendr-macro.qmd
- conversion.qmd
- data-types.qmd
style: "docked"
Expand Down
Loading

0 comments on commit f9f881a

Please sign in to comment.