`#[miniextendr]` Attribute Reference
Complete reference for #[miniextendr] on every Rust item type.
Complete reference for #[miniextendr] on every Rust item type.
🔗Dispatch Overview
#[miniextendr] adapts its behavior based on the item it’s applied to:
| Item | Default Behavior | Example |
|---|---|---|
fn | Generate C wrapper + R wrapper | #[miniextendr] pub fn add(x: i32, y: i32) -> i32 |
impl (inherent) | Env-class methods | #[miniextendr] impl Counter { ... } |
impl Trait for Type | Trait ABI shims | #[miniextendr(s3)] impl Display for Counter { ... } |
trait | Vtable + ABI generation | #[miniextendr] pub trait Counter { ... } |
struct (any field count) | ExternalPtr | #[miniextendr] struct Point { x: f64, y: f64 } |
fieldless enum | RFactor | #[miniextendr] #[derive(Copy, Clone)] enum Color { Red, Green, Blue } |
🔗Functions
#[miniextendr]
pub fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
Generates:
- C wrapper (
C_mypkg_greet— prefixed with your crate’s name for webR cross-package uniqueness, seedocs/WEBR.md) handling SEXP conversion - R wrapper (
greet <- function(name) { .Call(C_mypkg_greet, name) }) pubfunctions get@export; non-pub get@noRd- Each function documents its own page,
man/greet.Rd, as in roxygen2; a block without a@titleis titled with the function name. Grouping is opt-in:@rdname topicputs the function ontopic’s page (give the functions ofzero_copy_tests.rs@rdname zero_copy_teststo sharezero_copy_tests.Rd), and@describeIn/@namework as in roxygen2 (#1289). - Each parameter the doc comment does not document gets a generated
@paramline ((no documentation available), or the choice list of achoices/match_argparameter), except in a block with@describeIn,@inheritParams, or an@rdnamenaming another page than its own or its file-stem page: there the page it joins, or the topic it inherits from, documents the arguments. On a file-stem page (@rdname <file stem>) the line is kept only when no function on the page documents that argument. A block that joins another topic also gets@order NaN, so that topic’s own block names and titles the page whatever the R file order. See Documentation pages for standalone methods and Parameters on shared pages.
🔗Doc comments to roxygen
The /// comment is read line by line into the wrapper’s #' block:
- Text before the first
@tagline is leading prose. It becomes@descriptionunless the comment has one, with its lines, blank lines and indentation, so a markdown list or a fenced block renders as one. Rustdoc intra-doc links ([`Foo`]) lose their brackets, outside code, and so does a roxygen2 link such as[other_fn()], unless the crate setsroxygen_prose_links = "keep"(below). A link that only rustdoc can resolve, such as[crate::Foo], loses them under either setting and in tag text too (rustdoc-only links). - A tag runs from its
@tagline to the next one. - A multi-line tag (
@description,@details,@param,@return,@examples,@examplesIf,@section, …) keeps its blank lines, which start a new roxygen2 paragraph (or leave a blank line in an example). It also keeps the indentation of its continuation lines, less the one space after///. Trailing blank lines are dropped. - roxygen2 markdown reads a continuation indented 4 or more spaces after a blank line as a code block, as rustdoc does.
@title,@keywords,@conceptand@aliasesfold a wrapped line onto one line.- A single-line tag (
@export,@noRd,@rdname topic, …) ends at its line. The lines after it, up to the next tag, stay in rustdoc only. - rustdoc keeps the leading prose and those rustdoc-only lines, and drops every tag with its text.
/// @description First paragraph.
///
/// Second paragraph.
/// @examples
/// x <- c(1, 2) |>
/// sum()
gives #' @description First paragraph., #', #' Second paragraph.,
#' @examples, #' x <- c(1, 2) |>, #' sum().
🔗Links in leading prose
rustdoc and roxygen2 (markdown on) share the [...] link syntax:
[other_fn()], [pkg::fn()] and [Topic] are intra-doc links to Rust items
for cargo doc and \link{}s to R help topics for roxygen2. rustdoc resolves
[other_fn()] to an in-scope Rust fn and warns
rustdoc::broken_intra_doc_links when there is none, so the text cannot tell
which reader it was written for. By default leading prose is taken as written
for rustdoc: its links lose their brackets ([other_fn()] becomes
other_fn(), [`Foo`][crate::Foo] becomes `Foo`), so roxygen2 never
tries to resolve a Rust item as an R topic. Markdown links [text](url), code
spans and fenced blocks are left alone, except rustdoc’s inline form
[text](crate::x) (see rustdoc-only links). The text
of an explicit tag (@description, @details, @param, …) keeps its
links, except the rustdoc-only ones.
A crate whose doc comments are written for R first sets the default once:
[package.metadata.miniextendr]
roxygen_prose_links = "keep" # or "strip", the default
Leading prose then keeps its links, like an explicit @description:
/// `summary()`: 50 unless [set_threshold()] set another; see [stats::median()].
/// @param x An object.
#[miniextendr(s3(generic = "summary", class = "thing"))]
fn summary_thing(x: List) -> List { /* ... */ }
renders \code{\link[=set_threshold]{set_threshold()}} and
\code{\link[stats:median]{stats::median()}} in the Rd. Every link in the
crate’s leading prose but the rustdoc-only ones is then
roxygen2’s to resolve, so a rustdoc link written in one of roxygen2’s own
forms ([`RustType`], [`Type::method`]) is a roxygen2 warning there
(“could not resolve link”, “refers to un-installed package”). Give it a
rustdoc-only target or move it to a rustdoc-only line (both below). To keep
the links of one block without the crate setting, write its prose under an
explicit @description. The key must be "strip" or "keep", set once;
anything else is a compile error (see MACRO_ERRORS.md).
A block that renders no help page (noexport without internal, or an
explicit @noRd; on an impl block, its methods too) keeps no links whatever
the setting: every link in its leading prose and in its non-code tag text loses
its brackets, since none could render and roxygen2 would still try to resolve
each one (rustdoc keeps them).
🔗Rustdoc-only links
roxygen2 reads a link target pkg::topic as a link into the R package pkg,
everything before the last ::. When pkg is a Rust path root or cannot be
an R package name, no R package can resolve the link, so it was written for
rustdoc. Such a link loses its brackets wherever it is: in leading prose under
either roxygen_prose_links setting, and in the text of every explicit tag
except the code ones (@examples, @examplesIf, @usage, @eval,
@evalRd, @evalNamespace, @rawRd, @rawNamespace). The target, bracketed
(with or without backticks), after [text], or in rustdoc’s inline form
[text](target), is rustdoc-only when its pkg part is:
crate,selforSelf;- not a valid R package name (ASCII letters, digits and
., at least two characters, starting with a letter and not ending in.): a path with two or more::(a::b::c), or a module with a_(my_mod::f()).
| Written | roxygen2 gets |
|---|---|
[`crate::Foo`] | `crate::Foo` |
[Self::new()] | Self::new() |
[the registry][crate::registry] | the registry |
[`Sources::prepare`][crate::Sources::prepare] | `Sources::prepare` |
[`a::b::c`], [my_mod::f()] | `a::b::c`, my_mod::f() |
[text](crate::x), [`Foo`](crate::Foo), [new](Self::new()) | text, `Foo`, new |
Everything else stays as written outside the default "strip" prose, because
roxygen2 may read it as its own link: [fn()], [pkg::fn()], [topic],
[`topic`], [`pkg::topic`], [text][topic]. That includes
[`Type::method`] and [Type::method], since R package names can be
capitalised (R6, S7, Matrix), and [super::x], since super is a CRAN
package. Code spans and fenced blocks are never rewritten.
An inline link [text](target) loses its link when the target is a Rust path:
identifiers joined by at least one ::, with an optional () or ! suffix
and an optional rustdoc disambiguator prefix (fn@, struct@, enum@,
trait@, macro@, mod@, type@, const@, static@, method@, field@,
variant@, union@, value@, prim@, …). Unlike the bracketed forms, the
root does not matter: roxygen2 never reads an inline destination as an R
topic, only as the web link \href{crate::x}{text} (dead, and without a
warning). Every other markdown link stays: any URL ([text](https://a.b/c::d),
[text](./x.html), since a /, ., #, ? or lone : is never part of a
Rust path), and a bare [text](Foo), which may be a relative URL.
| Written | roxygen2 gets |
|---|---|
[`Serialize`](serde::Serialize), [text](fn@crate::x) | `Serialize`, text |
[text](Foo) | [text](Foo) |
A bracketed [`Type::method`] or [Type::method] in tag text (or in leading
prose under roxygen_prose_links = "keep") is the case the macro cannot
classify: it is a roxygen2 link into the R package Type when that package
exists. The lint rule MXL204 reports each pkg:: link (outside a block that
renders no page, whose links never reach roxygen2) whose pkg is not the
package itself, not declared in DESCRIPTION (Depends, Imports, Suggests,
Enhances, LinkingTo) and not a base or recommended R package, and names the
[text][crate::path] rewrite below. It reads DESCRIPTION from
<crate dir>/../../DESCRIPTION and skips when there is none.
So to link a Rust item from text that reaches R without the link reaching
roxygen2, root its target at the crate: [`Sources::prepare`][crate::Sources::prepare]
is a link in rustdoc and `Sources::prepare` on the R page. Inside an
impl, [`Self::prepare`] does the same.
To keep a whole line out of R help, put it after a single-line tag
(@export, @noRd, @rdname topic, …): the lines after such a tag, up
to the next tag, stay in rustdoc only.
/// Prepares the sources.
///
/// @export
///
/// Rust callers: see [`Sources::prepare`] and [`Prepared`].
#[miniextendr]
pub fn prepare_sources() { /* ... */ }
The R page gets “Prepares the sources.” and rustdoc gets both paragraphs.
🔗Function Attributes
🔗Visibility & Export
| Attribute | Effect |
|---|---|
internal | Add @keywords internal, suppress @export |
noexport | Suppress @export only |
invisible | Wrap R return in invisible() (same as an Invisible<T> return type) |
visible | Force visible return (same as a Visible<T> return type) |
call = wrapper | caller | none | Which call conditions are attributed to (wrapper and caller are the same as a Call / CallerCall parameter); caller needs noexport / internal; none drops the call from every condition, argument errors included. An S3 method’s wrapper call is the generic’s. See below |
call_arg | A trailing .call = NULL formal, exported functions included: NULL is the wrapper’s own call, and a function composing it passes .call = environment(). Not with call = caller or s3(...). See below |
doc = "..." | Custom roxygen block (replaces auto-generated) |
#[miniextendr(internal)]
pub fn helper() -> i32 { 42 }
#[miniextendr(invisible)]
pub fn set_option(key: String, value: i32) -> i32 { /* ... */ }🔗Return visibility: markers and defaults
Nothing is invisible unless it says so, with one carve-out: a bare function
whose R value is NULL (no return type, -> (), Option<()>,
Result<(), E> on success) returns invisible(NULL), because a side-effect
call should not print NULL. Everything else is visible, including the
receiver a method hands back for chaining (&mut self -> (),
&mut self -> &mut Self, self -> Self): obj$mutate() prints the object
at the console, like an R function returning self would; obj$a()$b() and
obj |> a() |> b() keep working either way.
Two spellings change the decision and do the identical codegen: the return
type markers Invisible<T> / Visible<T> (miniextendr_api::{Invisible, Visible}, transparent newtypes around the real value, IntoR forwards to
T) and the attribute invisible / visible (on a function, or on a method
as #[miniextendr(invisible)] / #[miniextendr(r6(invisible))] & co.). A
marker and an attribute that disagree are a compile error; markers are
return-position only and cannot be nested.
use miniextendr_api::{Invisible, Visible, miniextendr};
#[miniextendr]
pub fn quiet_value() -> Invisible<i32> { Invisible(42) } // withVisible()$visible == FALSE
#[miniextendr]
pub fn loud_null() -> Visible<()> { Visible(()) } // prints NULL
#[miniextendr(r6)]
impl Counter {
pub fn tick(&mut self) {} // chainable, prints the counter
pub fn tick_quietly(&mut self) -> Invisible<()> { Invisible(()) } // chainable, silent
pub fn add(&mut self, k: i32) -> Invisible<&mut Self> { self.n += k; Invisible(self) }
pub fn copy(&self) -> Invisible<Self> { Invisible(self.clone()) } // invisible classed copy
}
The marker is peeled before any other analysis, so Invisible<Option<T>>
converts None as Option<T> does, Invisible<Result<T, E>> still raises
on Err, and Invisible<Self> still wraps the handle in the class. Trait
methods take the marker in the impl’s signature (which must match the trait’s
declaration): fn poke(&mut self) -> Invisible<()>.
Generated standalone sidecar setters and R6 active-binding setters return the
receiver invisibly by default. On a public sidecar field, use
#[r_data(setter = "visible")] or #[r_data(setter = "invisible")] to choose
explicitly; this can share the attribute with prop_doc = "...". It controls
both the standalone setter and the class-integrated setter. S7 property setters
keep their existing visible default when the field option is absent.
For an inherent R6 setter method, the existing Visible<T> / Invisible<T>
return marker or #[miniextendr(visible)] / #[miniextendr(invisible)] option
also controls the generated active-binding setter branch. A direct call via
activeBindingFunction("field", obj)(value) returns obj with that visibility.
An unmarked binding setter stays invisible even though an ordinary unmarked
Rust method call is visible. R assignment (obj$field <- value) is always
invisible, regardless of the setter’s choice.
use miniextendr_api::{ExternalPtr, miniextendr};
use miniextendr_api::externalptr::RSidecar;
#[derive(ExternalPtr)]
#[externalptr(r6)]
pub struct Settings {
#[r_data]
sidecar: RSidecar,
#[r_data(setter = "visible")]
pub label: String,
}
#[miniextendr(r6(r_data_accessors))]
impl Settings {
pub fn new(label: String) -> Self { Self { sidecar: RSidecar, label } }
}obj <- Settings$new("before")
set_label <- activeBindingFunction("label", obj)
withVisible(set_label("after"))$visible # TRUE; returns obj
withVisible(obj$label <- "assigned")$visible # FALSE: R assignment
Two syntactic limits, shared with Dots and Missing<T>: a type alias or a
use Invisible as Quiet rename defeats the last-segment detection. The miss
is fail-safe (the value converts through the marker’s IntoR; only the
visibility falls back to the default).
Migrating from the old default. Before #1213, R6 and Env chainable
tails and every trait-impl void method were invisible(self) /
invisible(x) implicitly. A package that relied on the silence adds
#[miniextendr(invisible)] or returns Invisible<()> /
Invisible<&mut Self> on those methods; pipes and $-chains need no change.
🔗Explicit cross-class return wrapping
Use WrapAsR6<T>, WrapAsS7<T>, WrapAsS4<T>, WrapAsS3<T>,
WrapAsEnv<T>, or WrapAsVctrs<T> when a return needs a specific R class
system. The equivalent attribute is #[miniextendr(wrap = "r6")], with
"s7", "s4", "s3", "env", and "vctrs" selecting the other systems.
Both spellings work on free functions, inherent methods, and trait impl
methods. Nested method syntax such as #[miniextendr(env(wrap = "r6"))]
is also accepted.
For example, an Env factory can return a usable R6 board:
use miniextendr_api::{WrapAsR6, miniextendr};
#[miniextendr(env)]
impl Factory {
pub fn build(&self, w: i32, h: i32) -> WrapAsR6<Board> {
WrapAsR6(Board::new(w, h))
}
#[miniextendr(wrap = "r6")]
pub fn build_attr(&self, w: i32, h: i32) -> Board {
Board::new(w, h)
}
}
Both R methods finish with Board$new(.ptr = .val), so callers can use
factory$build(3L, 4L)$dimensions(). The explicit system takes precedence
over automatic return-class detection. A trait factory can similarly return
WrapAsR6<Self> (in both the trait declaration and implementation), or put
#[miniextendr(wrap = "r6")] on an implementation returning bare Self.
The macro emits the chosen wrapping expression directly:
| Selection | R expression |
|---|---|
| R6 | T$new(.ptr = .val) |
| S7 | T(.ptr = .val) |
| S4 | methods::new("T", ptr = .val) |
| S3 / Env | structure(.val, class = "T") |
| Vctrs | Prepend T to the existing class vector, retaining vctrs parent classes |
The marker forwards ordinary IntoR conversion to its payload; it does not
turn an arbitrary external pointer into vctrs vector data. A vctrs payload
must already provide a valid vctrs representation, for example through
#[derive(Vctrs, PreferVctrs)].
Put containers outside the marker: Option<WrapAsR6<Board>>,
Result<WrapAsR6<Board>, Error>, Vec<WrapAsR6<Board>>,
Option<Vec<WrapAsR6<Board>>>, or Result<Vec<WrapAsR6<Board>>, Error>.
The attribute accepts the same shapes with bare Board payloads. Err
retains normal error transport; vectors wrap each element. An optional class
return gives NULL for None, from a function or a method, as an unwrapped
Option<Vec<T>> does: the wrap passes NULL through instead of constructing
from it. Result<T, ()> is not supported by explicit wrapping.
Put visibility outside the complete return, e.g.
Invisible<Result<WrapAsR6<Board>, Error>>.
The payload must be an owned named type. The final Rust path segment names
the R class; Self uses the implementing class name. No registry lookup or
cross-check is added: the selected class system and name must match the R
class definition, and a mismatch fails when R constructs or uses the object.
As with visibility markers, aliases and renamed imports do not select the
marker behavior. Different marker/attribute systems, nested wrapping markers,
argument-position markers, and combinations with serialize or unwrap_in_r
are compile errors. Raw extern "C-unwind" functions cannot use wrap.
🔗S7 conversion return markers
ConvertTo<T> and ConvertFrom<T> add the type spellings of
s7(convert_to = "Target") and s7(convert_from = "Source"). They are
restricted to inherent S7 methods because these attributes register methods
with S7::convert() as well as wrapping the returned class.
use miniextendr_api::{ConvertFrom, ConvertTo, miniextendr};
#[miniextendr(s7)]
impl Fahrenheit {
pub fn from_celsius(source: &Celsius) -> ConvertFrom<Self> {
ConvertFrom(Self { value: source.value * 9.0 / 5.0 + 32.0 })
}
pub fn to_celsius(&self) -> ConvertTo<Celsius> {
ConvertTo(Celsius { value: (self.value - 32.0) * 5.0 / 9.0 })
}
}
ConvertTo<T> selects T as the target of an instance method with no
additional parameters. ConvertFrom<T> keeps T as the returned payload
(Self or the enclosing class type); its static method’s sole source
parameter supplies the source class name. That parameter may be a named
class, a reference to it (&Source), a view (ExternalPtrRef<'_, Source>)
or ExternalPtr<Source>. The equivalent attributes
keep the plain return types and state the target/source name explicitly.
Both spellings feed the existing S7 class-reference resolver, so registered
R class renames work for conversion methods. Ordinary method calls and
S7::convert(from, to) use the same return wrapping. This also distinguishes
conversion markers from WrapAsS7<T>, which explicitly wraps a return without
registering an S7 conversion.
Result<ConvertTo<T>, E> / Result<ConvertFrom<Self>, E> preserve error
transport. Option<ConvertTo<T>> and Option<ConvertFrom<Self>> give NULL
for None, like any optional class return. Put Invisible or Visible
outside the complete return. Visibility applies to both ordinary method
calls and S7::convert(). Conversion returns represent one class instance,
so vectors of class instances are rejected. Conflicting marker/attribute
names, incompatible class systems, wrong source/receiver arity, and
serialize/unwrap_in_r combinations are compile errors. As with the other
return markers, renamed imports and type aliases do not select the syntax.
🔗Condition call: markers and defaults
Every generated wrapper passes a call object to its C entry point
(.Call(C_pkg_f, .call = <call>, ...)), and conditions raised from Rust are
attributed to it, as written. Which call it is has three attributions and up
to three equivalent spellings (#1566, #1851), most specific first:
wrapper (default) | caller | none | |
|---|---|---|---|
.call = | sys.call() | .mx_call, the caller’s call, or the frame / call passed as .call | FALSE: no call |
| Marker parameter | call: Call | call: CallerCall | none |
| Attribute | call = wrapper | call = caller | call = none |
Cargo.toml default | call_attribution = "wrapper" | call_attribution = "caller" | call_attribution = "none" |
An S3 method (s3(generic = ..., class = ...), or a method of a
#[miniextendr(s3)] impl block) under wrapper reports the generic’s call,
summary(x) where its own frame’s call is summary.mx_rec(x), and a
replacement method x$f <- value, a compact call that never holds the new
value; a direct call of the method by name is reported as is. The wrapper
passes .call = environment() and the frame is resolved only when a condition
is raised (S3 methods: the generic call).
none drops the call from every condition the function raises, the R-side
argument checks and failed conversions included; it has no marker, and
call_arg or a marker next to it is a compile error
(No call).
use miniextendr_api::{Call, CallerCall, miniextendr};
#[miniextendr]
pub fn scale(x: f64, call: Call) -> f64 { // R wrapper: scale <- function(x)
let _ = call.sexp(); // the wrapper's sys.call()
x * 2.0
}
#[miniextendr(noexport)] // behind a hand-written scale2()
pub fn scale2_impl(x: f64, _call: CallerCall) -> f64 { x * 2.0 }
The marker (miniextendr_api::{Call, CallerCall}, repr(transparent) over
SEXP) is not an R formal: the C wrapper binds it from its hidden call slot,
so the body sees exactly the call the wrapper attributed to. A function taking
one runs on the main thread. caller, in either spelling, requires
noexport / internal; a crate default of "caller" applies to those
functions only and leaves exported ones at wrapper, while "none" applies
to every free function. Two spellings on one
function must agree, a function takes at most one marker, per-parameter
options do not apply to it, and class / trait methods accept none of the three
(they keep the wrapper’s own call). A caller standalone wrapper (S3 methods
aside) ends its formals with .call = NULL, so a hand-written helper in
between can pass on its caller’s frame (call = parent.frame()) or a call
(A helper in between).
call_arg gives a wrapper standalone function, exported or not, the same
formal, whose NULL is the wrapper’s own call: an R function composing
exported functions (an update() method applying verbs through do.call())
passes .call = environment(), and every condition names its call; a Call
parameter receives that call
(An exported function).
The attribution is independent of no_preconditions, which on an impl block
applies to trait impls too. Details and the fixtures:
CALL_ATTRIBUTION.md.
🔗Unevaluated arguments: Quoted / Quosure
A Quoted parameter stays an R formal that the wrapper passes unevaluated, as
list(substitute(x), parent.frame()); a Quosure parameter passes
rlang::enquo(x) (the package then needs rlang in Imports:). Rust reads the
expression and its environment and evaluates it in the caller’s R context,
against a data frame’s columns if it likes. Both work under Missing<..>, on
standalone functions and standalone S3 methods only, without per-parameter
options except a default under Missing<..> (it writes the formal and is
never evaluated), and keep the function on the main thread. See
QUOTED_ARGUMENTS.md.
🔗Argument count: NArgs
A parameter of type miniextendr_api::NArgs receives the number of arguments
in the generated function’s own call, as R’s nargs() gives it: x[1:3] is
2 and x[1:3, ] is 3, the one difference between the two forms a [ method
sees (#1860). It is no R formal: the wrapper passes nargs() at the
parameter’s position in the .Call(), and a function without one keeps its
wrapper unchanged.
#[miniextendr(s3(generic = "[", class = "mx_vec1"))]
pub fn mx_vec1_subset(x: Vec<f64>, i: Missing<Vec<i32>>, _j: Missing<SEXP>,
drop: Missing<SEXP>, nargs: NArgs) -> Vec<f64> { /* ... */ }
// `[.mx_vec1` <- function(x, i, j, drop) { ... .Call(..., x, i, j, drop, nargs()) }
It is matched by the last path segment (NArgs, miniextendr_api::NArgs);
&NArgs and Option<NArgs> are not it. Standalone functions, s3(...)
methods, impl-block methods and trait methods accept it; for an R6 or
environment-class inherent method the count leaves out the object. A function
takes at most one, per-parameter options (default, coerce, match_arg,
no_na, … and a method’s defaults(..)) do not apply to it, and an
extern "C-unwind" function cannot take it (no generated wrapper passes the
count). It holds a plain number and leaves the function’s thread as it was.
Unlike the call markers it has no attribute or crate-default spelling: it
hands the body a value, and an attribute would compute a count that no
parameter receives. The subscript forms, the count table and the fixtures:
S3_METHODS.md.
🔗Dots left unforced: LazyDots
A parameter of type miniextendr_api::LazyDots is R’s ... at its position,
as &Dots is, but the wrapper passes its own frame, environment(), where
&Dots passes list(...) (#1892). Nothing is evaluated on the way in: the
body reads the count, the names and the expressions, sees the empty elements
(f(a = ), x[1, , , ]) with is_missing_arg(i), and forces the elements
it wants with force(i) / try_force(i), each in its own environment.
#[miniextendr(s3(generic = "update", class = "mx_lazy"))]
pub fn mx_lazy_update(object: SEXP, rest: LazyDots) -> Result<SEXP, LazyUnsupportedArgs> { /* ... */ }
// update.mx_lazy <- function(object, ...) { ... .Call(..., object, environment()) }
It is matched by the last path segment (LazyDots, LazyDots<'_>,
miniextendr_api::LazyDots) and taken by value; &LazyDots,
Option<LazyDots> and Missing<LazyDots> are compile errors. Standalone
functions, s3(...) functions and the impl-block methods of every class
system (env, R6, S3, S4, S7, vctrs) accept it; trait methods and
extern "C-unwind" functions do not. A function takes at most one ...
(&Dots or LazyDots), per-parameter options and dots = typed_list!(..)
do not apply to it, and it keeps the function on the main thread. A function
without one keeps its wrapper unchanged. The methods, missing and forwarded
elements, conditions and the fixtures:
DOTS_TYPED_LIST.md.
🔗R-side preconditions: markers and defaults
A generated wrapper checks each argument’s R type before the .Call(), one
guard per check (if (!isTRUE(is.integer(n))) .miniextendr_arg_error("n", "must be integer")), so a wrong argument fails in R with a message naming it.
These are the type-derived checks. Without them the Rust conversion still
refuses the argument with the same condition (kind = "conversion",
e$param, plus e$rust_type); only the message is the conversion’s ('n' must be a single integer: got character). Each parameter keeps or drops them
as the most specific spelling says (#1566):
| Rank | Keeps | Drops | Where |
|---|---|---|---|
| 1 | n: Checked<T> | n: Unchecked<T> | a parameter of a function or inherent-impl method |
| 1 | #[miniextendr(preconditions)] n: T | #[miniextendr(no_preconditions)] n: T | a function parameter |
| 1 | preconditions(n, m) | no_preconditions(n) | a method attribute (inherent and trait impls) |
| 2 | preconditions (or = true) | no_preconditions (or = true) | the function attribute; bare on a method |
| 3 | preconditions | no_preconditions | the impl-block attribute (trait impls too) |
| 4 | preconditions = true | preconditions = false | [package.metadata.miniextendr] in Cargo.toml |
| 5 | the no-preconditions-default cargo feature | the build (FEATURE_DEFAULTS.md) | |
| 6 | the framework default |
[package.metadata.miniextendr]
preconditions = false # every wrapper of the crate drops its type checksuse miniextendr_api::{Checked, List, Unchecked, miniextendr};
#[miniextendr(noexport)]
pub fn fit_impl(data: List, n_iter: Checked<i32>, tol: f64) -> f64 {
let n = *n_iter; // or n_iter.into_inner()
/* ... */
}
#[miniextendr]
pub fn scale_by(#[miniextendr(no_na)] factor: f64, xs: Unchecked<Vec<f64>>) -> Vec<f64> {
xs.into_inner().into_iter().map(|x| x * factor).collect()
}
Under that crate default fit_impl keeps n_iter’s two guards and none for
data or tol; scale_by (in any crate) drops is.double(xs) and keeps
factor’s guards and its no_na. The per-parameter spelling
#[miniextendr(no_preconditions)] xs: Vec<f64> generates the same R. On a
class, the method-level forms do the same:
#[miniextendr(r6, no_preconditions)]
impl Sampler {
#[miniextendr(preconditions(n))] // n keeps its guards, scale does not
pub fn draw(&self, n: i32, scale: f64) -> Vec<f64> { /* ... */ }
pub fn reseed(&mut self, seed: Checked<i32>) { /* ... */ }
}
The switch covers the type-derived checks only: inherits, not_inherits, no_na, the
match_arg / choices validation, the .call validation and the Rust
conversion stay whatever the spelling. The markers
(miniextendr_api::{Checked, Unchecked}, repr(transparent), Deref to
T) are peeled before any R-side analysis: the formals, @param lines and
the conversion see T (e$rust_type says i32), and the C wrapper wraps the
converted value before the call. They implement neither TryFromSexp nor
IntoR, so a type alias of a marker does not compile (the guards are R text
written from the syntax, so an alias could only silently follow the default).
Two keywords follow the pair rule, the last one written wins (preconditions, no_preconditions at one level, a list naming one parameter twice). A marker
and a keyword on one parameter that disagree are a compile error, as are two
markers on a parameter, a marker not outermost (Option<Checked<T>>; write
Checked<Option<T>>), a rank-1 spelling on a parameter with no type-derived
check (SEXP, Missing<T>, ExternalPtr<T>, &Dots, a type the check table
does not know) or on a match_arg / choices parameter, a marker in a
#[miniextendr] trait’s method signature (spell it on the impl), a marker
on an extern "C-unwind" function, a marker as a return type, and the
function attribute’s preconditions = true | false on a parameter (write it
bare there). Checked<u16> with coerce keeps the
widened guard; Checked<&str> and Checked<&[f64]> work on both thread
paths. Checked means the R-side guard is kept: it is not an
overflow-checked conversion, nor the thread-checked FFI variants.
🔗Threading
| Attribute | Effect |
|---|---|
worker | Run on worker thread (default if worker-default feature). Deprecated (#2117): warns at compile time |
no_worker | Run on main R thread |
Functions taking or returning !Send framework types automatically run on the
main thread — the generated wrapper can only execute there. No attribute is
needed. This covers SEXP (parameters and returns), the R-backed views
(AltrepSexp, RDVector, RDMatrix, RndVec, RndMat, ProtectedStrVec,
and the borrowed StrVec / RCow parameters),
and the GC-rooted owned handles (BuiltDataFrame, DataFrameShape) anywhere
in the return type, including nested inside Result, Option, or containers.
Arbitrary user !Send types can’t be detected syntactically — mark those
functions no_worker explicitly.
#[miniextendr(no_worker)]
pub fn fast_add(x: i32, y: i32) -> i32 { x + y }
#[miniextendr]
pub fn inspect_sexp(x: miniextendr_api::sys::SEXP) -> i32 { /* ... */ }🔗Type Conversion
| Attribute | Effect |
|---|---|
coerce | Auto-coerce R types (e.g., double → int) |
no_coerce | Reject type mismatches |
wrap = "r6" | Explicitly wrap the return in R6 (also s7, s4, s3, env, vctrs); see explicit cross-class return wrapping above |
serialize | Return the complete value through serde, equivalent to AsSerialize<T>; requires the serde feature (see serde return transport) |
strict | Panic on lossy conversions (i64/u64 overflow) |
no_strict | Allow lossy conversions |
prefer = "..." | Return type preference: "list", "externalptr", "vector", "native", "auto" |
#[miniextendr(strict)]
pub fn exact_value(x: i64) -> i64 { x }
#[miniextendr(prefer = "list")]
pub fn get_record() -> MyStruct { /* ... */ }🔗Parameter Attributes
Written on a single parameter of a standalone function:
| Attribute | Effect |
|---|---|
coerce | Coerce this argument only (see COERCE.md) |
default = "..." | R formal default (an R expression). On a Missing<T> parameter it only writes the formal (#2161): an omitted argument is still Absent and the default is never evaluated, while a supplied value is Present, even one identical() to the default, as R’s missing() says. The generated @param line reads “Omitting the argument means the function’s own handling.” Not on a Missing<..> match_arg / choices parameter (see ENUMS_AND_FACTORS.md) |
match_arg | Validate against the parameter type’s MatchArg choices (Option<T>: NULL is no choice; Missing<..>: the formal keeps the choices (none under no_default) and an omitted argument is Absent, as is the formal’s own vector passed whole unless several_ok; Either<T, R> / Either<Vec<T>, R> (several_ok): non-character input goes to R, so R must read more than character or factor input; see ENUMS_AND_FACTORS.md). The type’s aliases (#[match_arg(alias = "grey")] on a variant) are accepted when typed in full, and the errors name them after the choices unless listed = false (ENUMS_AND_FACTORS.md) |
choices("a", "b") | Validate a string against a literal choice list (the formal is c("a", "b"); the same type layers and no_default as match_arg, no aliases) |
several_ok | With match_arg / choices: accept several values (Either<Vec<T>, R>: several values or a value of another kind; see ENUMS_AND_FACTORS.md) |
no_default | With match_arg / choices: the R formal has no default (the bare name instead of the choice vector or NULL), and the generated @param line drops its omission note. An omitted Missing<..> argument is still Absent; any other type gets R’s missing-argument error. Not with default (see ENUMS_AND_FACTORS.md) |
inherits = "cls" / inherits("a", "b") | R check inherits(x, c(...)): the argument must inherit from one of the classes |
inherits(class = "cls", message = "...") / inherits("a", "b", message = "...") | The same check, failing with your message |
inherits(class = "cls", when(class = "data.frame", message = "...")) | The same check; a refused value of a when class gets that message instead (see Hints for common wrong classes) |
not_inherits = "cls" / not_inherits("a", "b") | R check !inherits(x, c(...)): the argument must inherit from none of the classes (see Refusing classes) |
not_inherits(class = "cls", message = "...") / not_inherits("a", "b", message = "...") | The same check, failing with your message |
no_na | R check !anyNA(x): the argument must not be (or contain) NA; NaN is refused too. On a type that reads more values as missing than anyNA() sees (AsNumeric*, AsCharacter*, and aliases or derived newtypes of them), the converted value is checked too. On an Either written out in the signature (not behind an alias), the check runs only after the conversion, for the arm taken (see below) |
no_na(message = "...") | The same check, failing with your message |
preconditions / no_preconditions | Keep or drop this argument’s type-derived R checks, whatever the function, impl or crate says (the Checked<T> / Unchecked<T> spelling; see R-side preconditions) |
formal = "row.names" | The parameter’s R formal, for an R name that is no Rust identifier (see Declared R formals) |
#[miniextendr]
pub fn obj_summary(#[miniextendr(inherits = "pkg_obj")] x: List) -> String { /* ... */ }
#[miniextendr]
pub fn scale_by(#[miniextendr(no_na)] factor: f64, #[miniextendr(no_na)] xs: Vec<f64>) -> Vec<f64> { /* ... */ }if (!isTRUE(inherits(x, "pkg_obj"))) .miniextendr_arg_error("x", "must inherit from 'pkg_obj'")
if (!isTRUE(is.list(x))) .miniextendr_arg_error("x", "must be a list")
inherits and then not_inherits run before the parameter’s type checks and
no_na after them, one guard per check (under call = caller, the same
guards raising with the caller’s call).
A failure raises the same condition as a failed Rust conversion: the crate’s
conversion_error_class, rust_error, kind = "conversion" and e$param,
with the message 'x' must inherit from 'pkg_obj'
(ERROR_HANDLING.md). The
match_arg / choices validation raises it too
('mode' should be one of "fast", "slow"). An Option<T> parameter passes
NULL and a Missing<T>
parameter an omitted argument. Unlike the type checks, they stay under
Unchecked / no_preconditions / the crate’s preconditions = false: the
Rust conversion does not repeat them. A
plain f64 accepts NA_real_ (it is a valid double; Option<f64> is the
NA-carrying form), so no_na is the way to refuse it before Rust sees it.
The no_na message says 'x' must not contain NA for an argument that holds
several values (Vec<T>, slices, arrays, maps and lists, and the vector
markers such as AsNumericVec or AsFromStrVec<T>) and 'x' must not be NA
for a scalar. It looks through Option<T>, Missing<T> and Result<T, _> to
T, and an Either<L, R> holds several values when either arm does.
The reading markers read some inputs as missing that anyNA() passes, so on
them no_na also checks the converted value, in the C wrapper right after the
conversion. AsNumeric / AsNumericVec refuse the text "NA" and blank
strings (and factor labels among those), and NaN read from the text "NaN",
as the R guard refuses a numeric NaN. AsCharacter / AsCharacterVec
refuse a factor NA level (factor(x, exclude = NULL)) and an NA returned
by a class’s as.character() method; "NA" and "" stay values there. The
check follows the type, not its name, so a type alias (type Dose = AsNumeric) or a #[derive(TryFromSexp)] newtype of a marker is checked too.
The condition is the R guard’s (same classes, kind, e$param, message,
your message when given, no e$rust_type, the same call: the caller’s
under call = caller). It stays under no_preconditions. On
Option<AsNumeric>, NULL is still “not given” and passes as None.
On a list or map, no_na checks the top-level elements only: R’s anyNA()
on the list, and for a map of reading markers (HashMap<String, AsNumeric>)
each value as the marker reads it. list(a = c(1, NA)) passes.
On an Either<L, R> (also under Missing or Option), no_na emits no R guard. The
check runs in the C wrapper after the conversion, for the arm the value
converted to. That arm makes anyNA()’s check on the input in Rust, then its
marker check, so every arm refuses what the guard refused. The exception is a
DataFrame arm, which refuses nothing: a data frame whose cells are NA
reaches it. A plain DataFrame parameter keeps the R guard, which refuses
any NA cell. With no guard in front, an input that neither arm converts gets
the conversion’s refusal ('x' must be raw or a single number: got length 2)
before any NA check. A match_arg / choices parameter typed Either keeps
the R guard, because its conversion has no Rust check.
#[miniextendr]
pub fn value_or_table(#[miniextendr(no_na)] x: Either<AsNumeric, DataFrame>) -> String { /* ... */ }value_or_table(NA) # Error: 'x' must not be NA
value_or_table(data.frame(id = 1:2, v = c(1, NA))) # reaches the DataFrame arm
An arm can be optional. For an optional number or a table, write
#[miniextendr(no_na, default = "NULL")] x: Either<Option<AsNumeric>, DataFrame>.
The left arm is tried first, so NULL (given or by default) converts to
Left(None) and passes as not given; it never reaches the DataFrame arm.
NA and "NA" are still refused, with the same condition as on
Either<AsNumeric, DataFrame>: the check reads the input, so an NA that the
arm converts to None (as Option<f64> does) is refused too.
The whole Either can be optional instead: Option<Either<AsNumericVec, DataFrame>> reads NULL as None before either arm is tried, and None
passes. A given value is checked by the arm it converted to, as above, so a
data frame with NA cells still reaches the DataFrame arm. An
Option<DataFrame> parameter is not an Either: it keeps the R guard
(is.null(x) || !anyNA(x)), which refuses any NA cell.
The macro finds an Either by the type as written. An Either behind a type
alias (type NumberOrTable = Either<AsNumeric, DataFrame>), a newtype or
Result<Either<..>, ()> keeps the R guard, so a data frame with NA cells is
refused again: write the outer Either out in the signature. The arms
themselves can be wrapped. A #[derive(TryFromSexp)] newtype arm or a
Result<T, ()> arm is checked as the T it wraps: with
struct Table(DataFrame), Either<AsNumeric, Table> lets a data frame with
NA cells through, and a Result<AsNumeric, ()> arm still refuses NA.
The generated message states the rule ('model' must inherit from 'pkg_model'). To say where the object comes from instead, give the check a
message:
#[miniextendr]
pub fn fit_summary(
#[miniextendr(inherits(
class = "pkg_model",
message = "`model` must be a `pkg_model` object; create one with `pkg_model()`."
))]
model: List,
#[miniextendr(no_na(message = "`weight` must be a number, not NA"))] weight: f64,
) -> String { /* ... */ }if (!isTRUE(inherits(model, "pkg_model"))) .miniextendr_arg_error("model", message = "`model` must be a `pkg_model` object; create one with `pkg_model()`.")
if (!isTRUE(is.list(model))) .miniextendr_arg_error("model", message = "`model` must be a `pkg_model` object; create one with `pkg_model()`.")
if (!isTRUE(is.double(weight))) .miniextendr_arg_error("weight", "must be double")
if (!isTRUE(length(weight) == 1L)) .miniextendr_arg_error("weight", "must have length 1")
if (!isTRUE(!anyNA(weight))) .miniextendr_arg_error("weight", message = "`weight` must be a number, not NA")
The inherits message also replaces the messages of the parameter’s type
checks. Any value that is not a list of that class gets it: a number, NULL,
or a classed double. A parameter without a message keeps the generated type
messages. Under no_preconditions, and on a type without an R type check
(Missing<T>, DataFrame, custom newtypes), no type check carries the
message: a value of the right class that the Rust conversion refuses gets the
conversion’s message. no_na keeps its own message.
The message becomes the condition message as written, without a 'model'
prefix. Everything else stays the same: the classes, kind = "conversion",
e$param and the call (under call = caller, the caller’s). Inside
inherits(...) and not_inherits(...), each string literal and each
class = "..." names one class, and one message covers all of them. Each
check takes at most one message, and it must not be empty. The macro escapes
it for the R string literal: quotes, backslashes, newlines and other control
characters, and non-ASCII text as \u{..} (R code in a package must be
ASCII).
Impl and trait methods cannot carry parameter attributes, so the same options
are method-level and name the parameter: match_arg(p),
match_arg_several_ok(p), choices(p = "a, b"), choices_several_ok(p = "a, b"),
no_default(p, q), inherits(p = "cls_a, cls_b"), not_inherits(p = "cls_a, cls_b"),
no_na(p, q). A message goes in parentheses after the parameter:
inherits(p(class = "cls_a, cls_b", message = "...")),
not_inherits(p(class = "cls_a, cls_b", message = "...")),
no_na(p(message = "..."), q). At method level the classes are one
comma-separated string, as in choices(p = "a, b"), because a nested option
cannot hold a list of literals. At parameter level no class name is split, so
inherits(class = "a, b") names one class, a, b.
🔗Hints for common wrong classes
Some wrong values are common enough to deserve their own advice: a data frame
passed where a model object is expected should be told which function takes a
data frame, but NULL or a number should not. A when(...) inside
inherits(...) gives a refused value of the listed classes its own message:
#[miniextendr]
pub fn set_dose(
#[miniextendr(inherits(
class = "pkg_model",
message = "`model` must be a `pkg_model` object; start from `pkg_model()`.",
when(
class = "data.frame",
message = "`model` must be a `pkg_model` object; start from `pkg_model()`. To name the dose column of a data frame, use `col_dose()`."
),
when("pkg_fit", "pkg_results", message = "`model` is a fit; pass its `$model`."),
))]
model: List,
) -> List { /* ... */ }if (!isTRUE(inherits(model, "pkg_model") || !inherits(model, "data.frame"))) .miniextendr_arg_error("model", message = "`model` must be a `pkg_model` object; start from `pkg_model()`. To name the dose column of a data frame, use `col_dose()`.")
if (!isTRUE(inherits(model, "pkg_model") || !inherits(model, c("pkg_fit", "pkg_results")))) .miniextendr_arg_error("model", message = "`model` is a fit; pass its `$model`.")
if (!isTRUE(inherits(model, "pkg_model"))) .miniextendr_arg_error("model", message = "`model` must be a `pkg_model` object; start from `pkg_model()`.")
if (!isTRUE(is.list(model))) .miniextendr_arg_error("model", message = "`model` must be a `pkg_model` object; start from `pkg_model()`.")
Each hint is one more guard ahead of the inherits guard, so the hints are
tried in the order written and the first one naming a class of the value is
raised. A value that passes inherits passes every hint: an object whose
class is c("pkg_model", "data.frame") is accepted. A hint changes the
message of a refusal, never what the function accepts. A value of no hinted
class gets the check’s own message, or the generated
'model' must inherit from 'pkg_model' without one.
A hint raises the condition the check raises (the classes,
kind = "conversion", e$param, the call, under call = caller the
caller’s), its message is escaped like any other, and an Option<T> /
Missing<T> parameter still passes NULL / an omitted argument. Inside
when(...) the classes are spelled as in inherits(...) (each string
literal and each class = "..." names one class), and message is
required. At method level the classes are one comma-separated string:
inherits(model(class = "pkg_model", when(class = "pkg_fit, pkg_results", message = "..."))).
The hints of several inherits attributes on one parameter run in the order
written, and each one accepts every required class. A when class that
inherits requires is a compile error, since a value of that class passes
the check and never sees the hint. when(...) belongs to inherits only:
not_inherits has one message for every class it refuses. A class may be
both a hint class and a not_inherits class: a value of that class alone gets
the hint, and one that also inherits from a required class gets the
not_inherits refusal.
🔗Refusing classes
not_inherits is the negative of inherits: the argument must inherit from
none of the listed classes. It takes the same spellings
(not_inherits = "cls", not_inherits("a", "b", message = "..."), method
level not_inherits(p = "a, b") / not_inherits(p(class = "a, b", message = "..."))),
raises the same condition, lets NULL through on an Option<T> and an
omitted argument on a Missing<T>, and stays under no_preconditions.
Use it to keep a marker’s checks while refusing the classes whose value the
marker would read wrongly. AsNumeric reads a difftime as its bare number,
which drops the unit, and a Date or date-time as days or seconds since 1970:
#[miniextendr]
pub fn set_interval(
#[miniextendr(
no_na,
default = "NULL",
not_inherits(
"difftime",
"Date",
"POSIXt",
message = "`tau` must be a plain number in the time unit of the data"
)
)]
tau: Option<AsNumeric>,
) { /* ... */ }if (!isTRUE(is.null(tau) || !inherits(tau, c("difftime", "Date", "POSIXt")))) .miniextendr_arg_error("tau", message = "`tau` must be a plain number in the time unit of the data")
if (!isTRUE(is.null(tau) || is.numeric(tau) || is.logical(tau) || is.character(tau) || is.factor(tau))) .miniextendr_arg_error("tau", "must be NULL or numeric, logical, character, or factor")
if (!isTRUE(is.null(tau) || length(tau) == 1L)) .miniextendr_arg_error("tau", "must be NULL or have length 1")
if (!isTRUE(is.null(tau) || !anyNA(tau))) .miniextendr_arg_error("tau", "must not be NA")
The refusal runs before the type checks (after inherits, when both are
given). Here that order matters: is.numeric() is FALSE for a difftime, a
Date and a POSIXt, so the type check would refuse them first, with its own
message. A value of any other class passes the refusal and meets the type
checks as before: list(1) still gets 'tau' must be NULL or numeric, logical, character, or factor. So, unlike the inherits message, a
not_inherits message does not replace the type checks’ messages. Without a
message, a failure reads 'tau' must not inherit from 'difftime', 'Date' or 'POSIXt'.
The check is plain R on the argument, so it needs no type check: it also runs
on an Either parameter, which has none, and under no_preconditions.
inherits and not_inherits can share a parameter
(inherits = "pkg_model", not_inherits = "pkg_model_v1"); naming the same
class in both is a compile error.
not_inherits("NULL") refuses an explicit NULL, whose implicit class is
"NULL". On a match_arg / choices parameter it runs before the choice is
matched, so NULL gets your message instead of becoming the first choice; see
Refusing an Explicit NULL.
🔗Declared R formals (formal)
A parameter’s R formal is its Rust name without leading underscores, so it
can’t be an R name with a . (row.names, na.rm, conf.level, .data).
formal = "..." gives the parameter that name. It combines with the other
parameter attributes, and an impl method spells it formal(p = "..."):
/// @param x A `fit`.
/// @param row.names `NULL`, or the name of the one row.
/// @param optional Ignored.
/// @param ... Ignored.
/// @export
#[miniextendr(s3(generic = "as.data.frame", class = "fit"))]
pub fn as_data_frame_fit(
x: List,
#[miniextendr(formal = "row.names", default = "NULL")] row_names: Option<String>,
#[miniextendr(default = "FALSE")] optional: bool,
_dots: ...
) -> BuiltDataFrame { /* ... */ }
#[miniextendr(r6)]
impl Tally {
/// @param by.value The amount.
/// @param na.rm Whether an `NA` amount is skipped.
#[miniextendr(formal(by = "by.value", na_rm = "na.rm"), defaults(na_rm = "FALSE"))]
pub fn add(&mut self, by: f64, na_rm: bool) -> f64 { /* ... */ }
}as.data.frame.fit <- function(x, row.names = NULL, optional = FALSE, ...) {
# ...
if (!isTRUE(is.null(row.names) || is.character(row.names))) .miniextendr_arg_error("row.names", "must be NULL or character", environment())
# ...
}
R matches the argument by its formal, by name, partial name or position:
as.data.frame(fit, NULL, TRUE) passes row.names and optional. Everything
R-facing uses the formal: the usage line, the .Call() arguments, a
Missing<T> argument’s forwarding, the match_arg / choices check, the
argument checks and their errors (e$param is "row.names", also for a
conversion that fails in Rust), and the generated @param lines. Your own
@param line names the formal (@param row.names). The Rust name stays the
key of the method-level attributes (formal(row_names = "row.names"),
defaults(row_names = "NULL")).
The formal is checked when the function compiles; each error points at the string:
- It must be a syntactic R name: letters, digits,
.and_, starting with a letter or with a.not followed by a digit, and not a reserved word (if,TRUE,NA_real_, …)....,..1are refused: the dots parameter is always...and takes noformal. - It must not be a name the generated wrapper uses itself:
.call,.val,.ptr, the S3 dispatch variables (.Generic,.Class,.Method,.Group,.GenericCallEnv,.GenericDefEnv), and names starting.mx_,.miniextendr_,.rdata_or.__MX_; nor a method’s receiver (x, or R6’sselfandprivate). - It must differ from the other formals, declared or not.
formalis given once per parameter, as a string.
An NArgs parameter (no formal) and the parameters of trait methods take no
formal: a trait method’s R formals are its declaration’s parameter names,
shared by every implementing type. Nor does an S7 property getter, setter or
validator, whose arguments S7 fixes. An R6 setter’s set_* method takes the
declared formal; its active binding’s formal stays value.
🔗Parameters named like a base function
A parameter may share its name with a base function that generated wrappers
call, such as length, c, list, missing, attr or inherits, or,
declared with formal, is.null, sys.call or .Call. The
wrapper then writes each call to that function as base::length(...), so
omitting the argument, or passing a function there, affects only that argument:
#[miniextendr]
pub fn resample(overwrite: bool, length: f64) -> f64 { /* ... */ }if (!isTRUE(base::length(overwrite) == 1L)) .miniextendr_arg_error("overwrite", "must have length 1")
if (!isTRUE(is.double(length))) .miniextendr_arg_error("length", "must be double")
if (!isTRUE(base::length(length) == 1L)) .miniextendr_arg_error("length", "must have length 1")
Your own R code in the wrapper (r_entry, r_on_exit, r_post_checks, a
default = "...") gets the same treatment, and so does the usage line: a
choice formal next to a parameter named c reads mode = base::c("a", "b").
In an impl block, one method’s parameter qualifies the calls of the whole
class. Only these calls are qualified: base:: costs time on every call, so
wrappers without such a parameter keep the unqualified form.
🔗Error Handling
| Attribute | Effect |
|---|---|
| (default) | Result::Err, panics, and Option::None ride the tagged-condition transport: R wrapper raises a structured rust_* condition |
unwrap_in_r | Return Result<T, E> to R as a list with an $error slot — Err is delivered as a value, not as an R error |
// Default: Err becomes a `rust_error` R condition
#[miniextendr]
pub fn parse_data(s: String) -> Result<i32, String> {
s.parse::<i32>().map_err(|e| e.to_string())
}
// `unwrap_in_r`: Err is delivered as `list(value = NULL, error = "...")`
#[miniextendr(unwrap_in_r)]
pub fn parse_data_unwrap(s: String) -> Result<i32, String> {
s.parse::<i32>().map_err(|e| e.to_string())
}🔗Miscellaneous
| Attribute | Effect |
|---|---|
check_interrupt | Insert R_CheckUserInterrupt() before call |
rng | Manage R’s RNG state (GetRNGstate/PutRNGstate); a &RngGuard parameter receives the guard (no R argument; see RNG.md) |
rng(seed = <param>) | rng, plus the seed contract of stats::simulate() in the R wrapper: set.seed() with the parameter’s value (by Rust name), the session’s .Random.seed put back on exit, and a seed attribute on the result. Standalone functions and s3(...) methods only (see Seeded Simulation) |
c_symbol = "..." | Custom C function name |
lifecycle = "..." | Mark as deprecated/experimental/superseded |
dots = typed_list!(...) | Validate ... arguments (see DOTS_TYPED_LIST.md) |
dots = empty / empty(allow = [..]) / forward(allow = [..]) / data | What ... takes; the wrapper refuses the other arguments before evaluating any, with a “did you mean” suggestion (see DOTS_TYPED_LIST.md) |
#[miniextendr(check_interrupt, rng)]
pub fn long_simulation(n: i32, rng: &RngGuard) -> Vec<f64> { /* ... */ }
// `simulate(fit, nsim, seed = 42)` draws from `set.seed(42)`, then the
// session's stream is put back.
#[miniextendr(rng(seed = _seed), s3(generic = "stats::simulate", class = "my_fit"))]
pub fn simulate_my_fit(
_object: SEXP,
#[miniextendr(default = "1")] nsim: usize,
#[miniextendr(default = "NULL")] _seed: Option<i64>,
_dots: &Dots,
rng: &RngGuard,
) -> Vec<f64> { /* ... */ }
#[miniextendr(lifecycle = "deprecated")]
pub fn old_api() -> i32 { 0 }🔗Rust keywords as R names
Rust keywords are ordinary R names (type, where, match, mod, ref, …).
Spell them as raw identifiers on the Rust side; the r# prefix is Rust syntax
only and is dropped from every generated name — R function and method names,
R formals, @param names, list element and data-frame column names, C symbols:
/// @param where Filter expression.
/// @param type Output type.
#[miniextendr]
pub fn r#match(r#where: &str, r#type: i32) -> String { /* ... */ }
// R: match(where, type)
#[derive(DataFrameRow)]
pub struct Row { pub r#type: String, pub r#where: i32 }
// R: data.frame(type = ..., where = ...)
This works in every name position: #[miniextendr] fn and method names,
parameters, named dots (r#dyn: ...), trait methods, struct fields in the
IntoList / TryFromList / DataFrameRow / ExternalPtr derives, and the
typed_list! / typed_dataframe! DSLs. Names that R itself reserves (if,
for, in, while, break) are still not valid R formals — use r_name or
a different name for those.
🔗R Wrapper Customization
These attributes inject custom R code into the generated wrapper function, giving fine-grained control over the R-side behavior without touching the Rust logic.
| Attribute | Effect |
|---|---|
r_name = "..." | Override R function name (e.g., "is.widget") |
r_entry = "..." | Inject R code at function entry (before all checks) |
r_post_checks = "..." | Inject R code after checks (before .Call()) |
r_on_exit = "..." | Register on.exit() cleanup (short form, add = TRUE) |
r_on_exit(expr = "...", add = bool, after = bool) | Long form with full on.exit() control |
Generated wrapper layout:
fn_name <- function(formals) {
# r_entry code
on.exit(...) # r_on_exit
# missing defaults, lifecycle, preconditions, match.arg
# r_post_checks code
# the seed contract of rng(seed = ..)
.Call(C_mypkg_fn_name, ...)
}// Rename R function (C symbol still derived from Rust name)
#[miniextendr(r_name = "is.widget")]
pub fn is_widget(x: i32) -> bool { x > 0 }
// Coerce input before Rust sees it
#[miniextendr(r_entry = "x <- as.integer(x)")]
pub fn process(x: i32) -> i32 { x * 2 }
// Validate after built-in checks
#[miniextendr(r_post_checks = "stopifnot(x > 0L)")]
pub fn positive_only(x: i32) -> i32 { x }
// Register cleanup code
#[miniextendr(r_on_exit = "message(\"done\")")]
pub fn with_cleanup(x: i32) -> i32 { x + 1 }
// Full on.exit control (LIFO order)
#[miniextendr(r_on_exit(expr = "close(con)", after = false))]
pub fn with_connection(x: i32) -> i32 { x }
// Combine all four
#[miniextendr(
r_name = "widget.create",
r_entry = "n <- as.integer(n)",
r_on_exit = "message(\"cleanup\")",
r_post_checks = "stopifnot(n > 0L)",
)]
pub fn create_widget(n: i32) -> i32 { n * 10 }
r_on_exit defaults: add = TRUE, after = TRUE (composable, FIFO, following standard R convention).
When add = FALSE: omits both add and after (R ignores after when add = FALSE).
🔗S3 Standalone Functions
Functions can be standalone S3 methods without an impl block:
#[miniextendr(s3(generic = "format", class = "percent"))]
pub fn format_percent(x: SEXP, _dots: ...) -> Vec<String> { /* ... */ }
🔗Impl Blocks (Inherent)
#[derive(ExternalPtr)]
pub struct Counter { value: i32 }
#[miniextendr] // default: env-class
impl Counter {
pub fn new(initial: i32) -> Self { Counter { value: initial } }
pub fn value(&self) -> i32 { self.value }
pub fn increment(&mut self) { self.value += 1; }
}🔗Class System Selection
| Syntax | System | R Pattern |
|---|---|---|
#[miniextendr] | Env (default) | obj$method() environment dispatch |
#[miniextendr(r6)] | R6 | R6Class with $new() |
#[miniextendr(s3)] | S3 | generic.Class(x, ...) |
#[miniextendr(s4)] | S4 | setClass/setMethod formal OOP |
#[miniextendr(s7)] | S7 | new_class/new_generic modern OOP |
#[miniextendr(vctrs)] | vctrs | vctrs-compatible S3 vector class |
🔗Impl-Level Attributes
| Attribute | Applies To | Effect |
|---|---|---|
class = "..." | All systems | Custom R class name |
strict / no_strict | All systems | Strict type conversion for all methods |
internal | All systems | @keywords internal on class |
noexport | All systems | Suppress @export on class |
preconditions / no_preconditions | All systems, trait impls too | Keep or drop the type-derived R checks of every method whose parameters and own attribute say nothing (see R-side preconditions) |
blanket | Trait impls | Skip trait ABI (for blanket impls) |
// A type's methods can be split over several impl blocks
#[miniextendr(s4)]
impl Counter { /* constructor + getters */ }
#[miniextendr(s4)]
impl Counter { /* mutating methods */ }
The wrapper writer merges the blocks of a type into one class (#1776). One
block defines the class (the constructor and S7 properties go there), every
block’s doc comment documents the class (one @title, page tags on the
defining block), every block spells the same class settings (the class system
with its options, class, internal / noexport, @noRd /
@keywords internal), and no two blocks bind one member; see
Multiple Impl Blocks.
🔗R6-Specific Options
#[miniextendr(r6(
inherit = "BaseClass",
portable = true,
cloneable = true,
lock_objects = false,
lock_class = false,
r_data_accessors,
))]
impl MyClass { /* ... */ }🔗S7-Specific Options
#[miniextendr(s7(
parent = "ParentClass",
abstract = true,
r_data_accessors,
))]
impl MyClass { /* ... */ }🔗S3, S4 and env Options
#[miniextendr(s3(r_data_accessors))] // or s4(...), env(...)
impl MyClass { /* ... */ }
#[miniextendr(s3(r_data_accessors = "get"))] // reading methods only
impl MyClass { /* ... */ }
r_data_accessors gives the class $ / [[ methods that read the pub
#[r_data] fields of the type and $<- / [[<- methods that write them in
place (S4: $ and $<- only). On an S3 bare pointer [[, as.list() and
[[<- refuse an unused argument, as dots = empty does; [[ takes exact,
as an env class’s [[ does.
r_data_accessors = "get" generates the
reading methods only, for a class that keeps its own $<-. The type needs an
#[r_data] _r: RSidecar selector and at least one pub #[r_data] field.
The impl block is refused when one of its own methods is a generated method
(s3(generic = "$<-") next to the setters, for example). The R behaviour
is in CLASS_SYSTEMS.md, “Field syntax on S3, S4 and env classes”.
🔗vctrs-Specific Options
#[miniextendr(vctrs(
kind = "vctr", // vctr | rcrd | list_of
base = "double", // underlying R type
inherit_base_type = true,
ptype = "double(0)", // prototype R expression
abbr = "pct", // vec_ptype_abbr
))]
impl Percent { /* ... */ }🔗Method-Level Attributes
Methods inside impl blocks can have per-method attributes nested under the class system keyword:
#[miniextendr(s3)]
impl Person {
// Override the S3 generic name
#[miniextendr(s3(generic = "print"))]
pub fn show(&mut self) { println!("{}", self.name); }
// Override the class suffix for double-dispatch
#[miniextendr(s3(generic = "vec_ptype2", class = "my_vctr.my_vctr"))]
pub fn ptype2_self(&self) -> SEXP { /* ... */ }
// Generate as.data.frame.Person
#[miniextendr(as = "data.frame")]
pub fn as_df(&self) -> List { /* ... */ }
// Skip this method
#[miniextendr(ignore)]
pub fn internal_helper(&self) { /* ... */ }
// Parameter defaults
#[miniextendr(defaults(n = "1"))]
pub fn increment_by(&mut self, n: i32) { self.value += n; }
}🔗Shared Method Attributes (all class systems)
| Attribute | Effect |
|---|---|
ignore | Don’t generate R wrapper for this method |
constructor | Mark as the constructor, spelled <system>(constructor) (r6(constructor)). Without a mark, a receiver-less new returning Self is the constructor, else the type’s one receiver-less method returning Self. A constructor not named new is also a static method under its own name (see Constructor Patterns) |
generic = "..." | Override S3/S4/S7 generic name |
receiver = "..." | S3/S4 with generic: the receiver formal, for a generic whose first argument is not x (summary(object, ...)); see Methods on an existing generic |
no_dots | S3/S4 with generic: no ... after the parameters (show(object), length(x)); S7: see below |
class = "..." | Override S3 class suffix |
as = "..." | Generate as.<target>() coercion method |
defaults(p = "val") | R-side parameter defaults (on Missing<T>, as default = "..." above) |
worker / no_worker | Thread override |
check_interrupt | Insert interrupt check |
coerce / no_coerce | Type coercion override |
wrap = "r6" | Explicitly wrap the return in R6 (also s7, s4, s3, env, vctrs); see explicit cross-class return wrapping above |
serialize | Convert the complete return value through AsSerialize<T> |
rng | RNG state management; a &RngGuard parameter receives the guard. rng(seed = ..) is for standalone functions only (a compile error here) |
unwrap_in_r | Return Result<T, E> as a list with $value/$error instead of raising on Err |
r_name = "..." | Override R method name |
r_entry = "..." | Inject R code at method entry |
r_post_checks = "..." | Inject R code after checks |
r_on_exit = "..." | Register on.exit() cleanup |
match_arg(p) / choices(p = "a, b") | Validate p with match.arg() (see Parameter Attributes) |
no_default(p, q) | The R formals of the choice parameters p and q have no default (Parameter Attributes) |
inherits(p = "cls_a, cls_b") | R check inherits(p, c(...)) |
inherits(p(class = "cls_a, cls_b", message = "...")) | The same check, failing with your message |
inherits(p(class = "cls", when(class = "df_a, df_b", message = "..."))) | The same check; a refused value of a when class gets that message instead (Hints for common wrong classes) |
not_inherits(p = "cls_a, cls_b") | R check !inherits(p, c(...)): p must inherit from none of the classes |
not_inherits(p(class = "cls_a, cls_b", message = "...")) | The same check, failing with your message |
no_na(p, q) | R check !anyNA(p); on a type that reads more values as missing than anyNA() sees (AsNumeric*, AsCharacter*, and aliases or derived newtypes of them), the converted value is checked too; on an Either, only after the conversion, for the arm taken (Parameter Attributes) |
no_na(p(message = "...")) | The same check, failing with your message |
preconditions / no_preconditions | Keep or drop the type-derived R checks of this method’s parameters, over the impl block’s (see R-side preconditions) |
preconditions(p, q) / no_preconditions(p) | The same for the named parameters only, over the method’s own |
formal(p = "p.name") | The R formal of p (Declared R formals); not on trait methods or S7 property accessors |
An S4 instance method uses s4_<R method name> as its default generic:
r#type becomes s4_type, r_name = "kind" gives s4_kind, and
postfix = "_code" gives s4_type_code. An explicit s4(generic = "category")
uses exactly category, without the prefix (it can’t be combined with
r_name), and defines it only when no generic or function of that name is
visible, so s4(generic = "show", receiver = "object", no_dots) adds a method
to methods::show. Two instance methods of one impl block on the same generic
are a compile error, since the second setMethod() would replace the first.
Valid as = "..." targets: data.frame, list, character, numeric, double,
integer, logical, matrix, vector, factor, Date, POSIXct, complex,
raw, environment, function.
🔗R6-Specific Method Attributes
#[miniextendr(r6)]
impl MyClass {
// Private method (R6 convention: prefixed with .)
#[miniextendr(private)]
pub fn internal_calc(&self) -> f64 { /* ... */ }
// Active binding getter
#[miniextendr(active, prop = "count")]
pub fn get_count(&self) -> i32 { self.count }
// Active binding setter
#[miniextendr(setter, prop = "count")]
pub fn set_count(&mut self, value: i32) { self.count = value; }
// Destructor
#[miniextendr(finalize)]
pub fn cleanup(&mut self) { /* ... */ }
}🔗S7-Specific Method Attributes
#[miniextendr(s7)]
impl MyClass {
// Computed property getter
#[miniextendr(s7(getter, prop = "area"))]
pub fn get_area(&self) -> f64 { self.width * self.height }
// Property setter
#[miniextendr(s7(setter, prop = "area"))]
pub fn set_area(&mut self, value: f64) { /* ... */ }
// Property validator
#[miniextendr(s7(validate, prop = "width"))]
pub fn validate_width(value: f64) -> Result<(), String> {
if value < 0.0 { Err("width must be non-negative".into()) } else { Ok(()) }
}
// Property with defaults + constraints
#[miniextendr(s7(getter, required))] // no default, must be provided
pub fn name(&self) -> String { self.name.clone() }
#[miniextendr(s7(getter, frozen))] // immutable after creation
pub fn id(&self) -> i32 { self.id }
#[miniextendr(s7(getter, default = "0"))] // R expression for default
pub fn score(&self) -> f64 { self.score }
// Remove ... from generic signature
#[miniextendr(s7(no_dots))]
pub fn length(&self) -> i32 { self.len }
// Multiple dispatch: the receiver `x`, then the leading parameter `other`
#[miniextendr(s7(dispatch = "x, other"))]
pub fn combine(&self, other: &Self) -> Self { /* ... */ }
// Ops operator: dispatches on (e1, e2); the operand is named `e2`
#[miniextendr(s7(generic = "+"))]
pub fn add(&self, e2: &Self) -> Self { /* ... */ }
// Another package's generic, attached when that package loads
#[miniextendr(s7(generic = "generics::tidy"))]
pub fn tidy(&self) -> BuiltDataFrame { /* ... */ }
// Type conversion methods
#[miniextendr(s7(convert_from = "OtherClass"))]
pub fn from_other(other: OtherClass) -> Self { /* ... */ }
#[miniextendr(s7(convert_to = "OtherClass"))]
pub fn to_other(&self) -> OtherClass { /* ... */ }
}
🔗Trait Definitions
#[miniextendr]
pub trait Counter {
fn value(&self) -> i32;
fn increment(&mut self);
fn default_initial() -> i32 { 0 } // static method
}
Generates cross-package ABI:
TAG_Counter: type tag constantCounterVTable: function pointer table- Method shims:
extern "C"trampolines withwith_r_unwind_protect CounterView: runtime dispatch wrapper
No attributes accepted on the #[miniextendr] for traits (the attr parameter is unused).
Constraints:
- Methods must take
&selfor&mut self(notselfby value) - No async methods
- No generic method parameters (trait-level generics are OK)
- Static methods work (resolved at compile time, not in vtable)
🔗Trait Impl Blocks
#[miniextendr(s3)]
impl Counter for MyCounter {
fn value(&self) -> i32 { self.val }
fn increment(&mut self) { self.val += 1; }
}
Use blanket to skip ABI emission for blanket impls:
#[miniextendr(blanket)]
impl<T: AsRef<str>> Display for T { /* ... */ }
🔗Structs
🔗ALTREP via #[miniextendr] Is Removed
#[miniextendr] no longer generates ALTREP classes. Applying the old ALTREP
attributes (class, base) to a struct of any shape is a compile error with
migration guidance. Use the per-family ALTREP derives instead:
use miniextendr_api::prelude::*;
#[derive(AltrepInteger)]
#[altrep(len = "len", elt = "value", class = "MyInts")]
pub struct LazyInts {
value: i32,
len: usize,
}
See ALTREP.md for the full derive surface.
🔗Struct → ExternalPtr (default)
#[miniextendr]
pub struct Point { x: f64, y: f64 }
Generates ExternalPtr + TypedExternal derives. The struct lives as an opaque
R external pointer. This is the default for any field count — a bare 1-field
struct is ExternalPtr too, not ALTREP.
Override with an explicit mode:
#[miniextendr(externalptr)]
pub struct Wrapper(Vec<i32>); // ExternalPtr (same as the default)
#[miniextendr(list)]
pub struct Wrapper(Vec<i32>); // List conversion instead of ExternalPtr🔗Struct Mode Overrides
When no explicit mode attribute (list / dataframe / externalptr) is also
present, prefer = "list" and prefer = "dataframe" resolve to the same full
mode as their explicit counterparts. Only prefer = "native" has no
explicit-mode counterpart: it generates ExternalPtr plus the
PreferRNativeType derive, whose IntoR needs the struct to implement
RNativeType (a #[repr(transparent)] newtype with
#[derive(Clone, Copy, RNativeType)]). See expand_struct in
miniextendr-macros/src/struct_enum_dispatch.rs for the resolution logic.
| Syntax | Result |
|---|---|
#[miniextendr] (no mode attr) | ExternalPtr |
#[miniextendr(list)] | IntoList + TryFromList + PreferList |
#[miniextendr(dataframe)] | DataFrameRow + companion type |
#[miniextendr(externalptr)] | ExternalPtr + TypedExternal |
#[miniextendr(prefer = "list")] | Same as #[miniextendr(list)] — full list mode (IntoList + TryFromList + PreferList) |
#[miniextendr(prefer = "dataframe")] | Same as #[miniextendr(dataframe)] — full dataframe mode (DataFrameRow + companion type) |
#[miniextendr(prefer = "externalptr")] | ExternalPtr (explicit; identical to the no-mode-attr default) |
#[miniextendr(prefer = "native")] | ExternalPtr + PreferRNativeType (needs RNativeType) |
To keep the PreferRNativeType derive’s concrete IntoR (which routes through
AsRNative) from colliding with the blanket impl<T: IntoExternalPtr> IntoR
(E0119, #1283), a struct-level prefer = "native" type deliberately does not
implement IntoExternalPtr. As a result the AsExternalPtr call-site wrapper
does not apply to such a type — its default IntoR is the native representation.
🔗List Mode
#[miniextendr(list)] makes a struct round-trip with an R list. It expands
to three derives:
| Derive | Direction | Role |
|---|---|---|
IntoList | Rust → R | struct → R list (one element per field) |
TryFromList | R → Rust | R list → struct (one field per element) |
PreferList | Rust → R | writes the IntoR impl so the struct can be returned directly from a #[miniextendr] fn — it routes through IntoList |
#[miniextendr(list)]
pub struct Record {
pub name: String,
pub value: i32,
}
#[miniextendr]
pub fn make_record() -> Record {
Record { name: "test".into(), value: 42 }
}
// R: make_record() -> list(name = "test", value = 42L)
Field shape determines list shape:
| Struct kind | R list produced |
|---|---|
Named (struct R { a, b }) | named list — list(a = …, b = …) |
Tuple (struct R(A, B)) | unnamed list — list(…, …) (positional) |
Unit (struct R) | empty list — list() |
Field type requirements. Each non-ignored field’s type must implement
IntoR (for IntoList) and TryFromSexp (for TryFromList). Heterogeneous
fields are fine — the list holds each element at its natural R type. This
includes scalars (i32, f64, String, bool), vectors (Vec<f64>,
Vec<i32>, Vec<String>), and Option<T> (None → NA for a scalar T,
NULL for a vector):
#[miniextendr(list)]
pub struct FitResult {
pub estimate: Vec<f64>, // numeric vector element
pub sigma: f64, // length-1 numeric element
}
Skipping fields. #[into_list(ignore)] drops a field from the list. On the
way back (TryFromList) an ignored field is filled with Default::default(),
so its type must implement Default. The helper works with both
#[miniextendr(list)] and the equivalent three derives:
use miniextendr_api::miniextendr;
use std::cell::RefCell;
#[miniextendr(list)]
pub struct Cfg {
pub label: String,
#[into_list(ignore)]
cache: RefCell<Option<Vec<u8>>>, // not in the R list; Default on the way back
}
Return-only by default. List mode generates PreferList (return path) but
not TryFromSexp on the struct, so a list-mode struct is not usable as a
#[miniextendr] function argument directly. To reconstruct one from R, take a
List parameter and call try_from_list yourself:
use miniextendr_api::list::{List, TryFromList};
#[miniextendr]
pub fn use_record(x: List) -> i32 {
let r = Record::try_from_list(x).expect("record list");
r.value
}
TryFromList reports a MissingField error when a named element is absent, a
DuplicateName error when a field’s name appears more than once in the list
(the struct could keep only one of the values; other repeated names are
ignored, as unknown names are), and the field’s own conversion error (e.g. a
type mismatch) when an element is present but the wrong type.
🔗DataFrame Mode
#[miniextendr(dataframe)]
pub struct Obs {
pub id: i32,
pub score: f64,
}
#[miniextendr]
pub fn make_obs() -> ObsDataFrame { // companion type
Obs::to_dataframe(vec![
Obs { id: 1, score: 0.5 },
Obs { id: 2, score: 0.8 },
])
}
// R: data.frame(id = c(1L, 2L), score = c(0.5, 0.8))
Dataframe mode derives only DataFrameRow; the companion already implements
IntoR. Add #[derive(IntoList)] explicitly if individual rows also need list
conversion.
#[dataframe(...)] options work on the struct and its fields with either
#[miniextendr(dataframe)] or #[derive(DataFrameRow)]. For example,
#[dataframe(name = "ObsFrame")] changes the companion type’s name, and
#[dataframe(rename = "observation_id")] changes a field’s column name. See
DATAFRAME.md for the available options.
🔗Enums (Fieldless)
🔗Default → RFactor
#[miniextendr]
#[derive(Copy, Clone)]
pub enum Season {
Spring,
Summer,
Autumn,
Winter,
}
Generates MatchArg + RFactor + IntoR + TryFromSexp. The enum maps to
an R factor with levels matching variant names.
s <- get_season() # factor("Summer", levels = c("Spring", "Summer", "Autumn", "Winter"))🔗MatchArg Mode
#[miniextendr(match_arg)]
#[derive(Copy, Clone)]
pub enum Verbosity {
Quiet,
Normal,
Verbose,
}
Like RFactor but uses R’s match.arg() semantics with partial matching. The enum
maps to a character scalar in R, validated against the allowed choices.
🔗Explicit Factor
#[miniextendr(factor)] // same as default, but explicit
#[derive(Copy, Clone)]
pub enum Color { Red, Green, Blue }
🔗Conditional Compilation
#[cfg(...)] and #[cfg_attr(...)] work wherever Rust accepts them in a
#[miniextendr] item, and the generated code follows the compiled item:
| Gate on | A gated-off member has no |
|---|---|
the function, impl block or trait itself (before or after #[miniextendr]) | C wrapper, registration, R function or class |
| a function parameter | R formal |
| an impl method or associated item | C wrapper, registration, R method or docs |
| a trait method | vtable slot or R method |
| a trait impl method | wrapper (an alternative under another gate takes its place) |
| an enum variant | factor level or match_arg choice |
| a struct field | column, list element or sidecar accessor |
#[miniextendr]
pub fn read(path: &str, #[cfg(feature = "json")] json: bool) -> String { todo!() }
#[miniextendr(r6)]
impl Reader {
#[cfg(feature = "json")]
pub fn read_json(&self, text: &str) -> String { todo!() }
#[cfg_attr(feature = "short-names", miniextendr(r_name = "read"))]
pub fn read_text(&self, text: &str) -> String { todo!() }
}
#[miniextendr]
pub enum Format {
Csv,
#[cfg(feature = "json")]
Json,
}
The macro resolves the gates before it reads the item, so item-wide decisions
use only the enabled members: constructor inference, S7 property pairing, name
collisions, factor levels and R formals. A cfg_attr can add miniextendr
options (r_name, skip, worker, …) as well as other gates. Keep a
conditional S7 property’s getter and setter under compatible gates so the
enabled methods still form a property. A trait’s ABI tag covers its enabled
methods, so a trait method gated differently in two packages gives them
different tags: objects of one package then don’t implement the other’s trait.
Build a producer and its consumers with the same features (#2129).
🔗Automatic Registration
All #[miniextendr] items are automatically registered via linkme distributed slices. No manual module declarations are needed – simply annotate your items with #[miniextendr] and they will be available to R at load time.
🔗Derive Macros
These are separate from #[miniextendr] but complementary:
| Derive | What It Generates |
|---|---|
ExternalPtr | Type-safe external pointer wrapper |
Altrep | Full ALTREP class (IntoR + TryFromSexp + registration) |
AltrepInteger / AltrepReal / … | ALTREP for specific vector types |
IntoList | Struct → R list |
TryFromList | R list → Struct |
DataFrameRow | Struct → columnar data.frame + companion type |
RFactor | Enum ↔ R factor |
MatchArg | Enum ↔ R character with match.arg() |
RNativeType | Newtype for native R types |
PreferList | IntoR through list |
PreferExternalPtr | IntoR through ExternalPtr, for a hand-written TypedExternal |
PreferRNativeType | IntoR through native SEXP |
Vctrs | vctrs-compatible S3 class (feature-gated) |
PreferVctrs | IntoR through Vctrs (feature-gated) |
The #[miniextendr] attribute on structs/enums is a convenience that calls the
appropriate derives internally. List and dataframe modes expand to normal
#[derive(...)] invocations, so #[into_list(...)] in list mode and
#[dataframe(...)] in dataframe mode work alongside other derives on the struct.
🔗See Also
- CLASS_SYSTEMS.md: all 6 class systems with examples
- S3_METHODS.md: detailed S3 print/format guide
- DOTS_TYPED_LIST.md: dots and typed_list validation
- ALTREP.md: ALTREP deep dive
- DATAFRAME.md: DataFrame conversion (derive + serde + columnar)
- SERDE_R.md: serde integration for direct Rust-R serialization
- ERROR_HANDLING.md: tagged-condition transport,
unwrap_in_r, panic handling - LIFECYCLE.md: deprecation/experimental lifecycle attributes
- TRAIT_ABI.md: cross-package trait dispatch