Complete reference for #[miniextendr] on every Rust item type.

🔗Dispatch Overview

#[miniextendr] adapts its behavior based on the item it’s applied to:

ItemDefault BehaviorExample
fnGenerate C wrapper + R wrapper#[miniextendr] pub fn add(x: i32, y: i32) -> i32
impl (inherent)Env-class methods#[miniextendr] impl Counter { ... }
impl Trait for TypeTrait ABI shims#[miniextendr(s3)] impl Display for Counter { ... }
traitVtable + ABI generation#[miniextendr] pub trait Counter { ... }
struct (any field count)ExternalPtr#[miniextendr] struct Point { x: f64, y: f64 }
fieldless enumRFactor#[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, see docs/WEBR.md) handling SEXP conversion
  • R wrapper (greet <- function(name) { .Call(C_mypkg_greet, name) })
  • pub functions get @export; non-pub get @noRd
  • Each function documents its own page, man/greet.Rd, as in roxygen2; a block without a @title is titled with the function name. Grouping is opt-in: @rdname topic puts the function on topic’s page (give the functions of zero_copy_tests.rs @rdname zero_copy_tests to share zero_copy_tests.Rd), and @describeIn / @name work as in roxygen2 (#1289).
  • Each parameter the doc comment does not document gets a generated @param line ((no documentation available), or the choice list of a choices / match_arg parameter), except in a block with @describeIn, @inheritParams, or an @rdname naming 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 @tag line is leading prose. It becomes @description unless 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 sets roxygen_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 @tag line 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, @concept and @aliases fold 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().

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).

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, self or Self;
  • 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()).
Writtenroxygen2 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.

Writtenroxygen2 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

AttributeEffect
internalAdd @keywords internal, suppress @export
noexportSuppress @export only
invisibleWrap R return in invisible() (same as an Invisible<T> return type)
visibleForce visible return (same as a Visible<T> return type)
call = wrapper | caller | noneWhich 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_argA 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:

SelectionR expression
R6T$new(.ptr = .val)
S7T(.ptr = .val)
S4methods::new("T", ptr = .val)
S3 / Envstructure(.val, class = "T")
VctrsPrepend 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)callernone
.call =sys.call().mx_call, the caller’s call, or the frame / call passed as .callFALSE: no call
Marker parametercall: Callcall: CallerCallnone
Attributecall = wrappercall = callercall = none
Cargo.toml defaultcall_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):

RankKeepsDropsWhere
1n: Checked<T>n: Unchecked<T>a parameter of a function or inherent-impl method
1#[miniextendr(preconditions)] n: T#[miniextendr(no_preconditions)] n: Ta function parameter
1preconditions(n, m)no_preconditions(n)a method attribute (inherent and trait impls)
2preconditions (or = true)no_preconditions (or = true)the function attribute; bare on a method
3preconditionsno_preconditionsthe impl-block attribute (trait impls too)
4preconditions = truepreconditions = false[package.metadata.miniextendr] in Cargo.toml
5the no-preconditions-default cargo featurethe build (FEATURE_DEFAULTS.md)
6the framework default
[package.metadata.miniextendr]
preconditions = false        # every wrapper of the crate drops its type checks
use 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

AttributeEffect
workerRun on worker thread (default if worker-default feature). Deprecated (#2117): warns at compile time
no_workerRun 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

AttributeEffect
coerceAuto-coerce R types (e.g., double → int)
no_coerceReject type mismatches
wrap = "r6"Explicitly wrap the return in R6 (also s7, s4, s3, env, vctrs); see explicit cross-class return wrapping above
serializeReturn the complete value through serde, equivalent to AsSerialize<T>; requires the serde feature (see serde return transport)
strictPanic on lossy conversions (i64/u64 overflow)
no_strictAllow 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:

AttributeEffect
coerceCoerce 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_argValidate 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_okWith match_arg / choices: accept several values (Either<Vec<T>, R>: several values or a value of another kind; see ENUMS_AND_FACTORS.md)
no_defaultWith 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_naR 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_preconditionsKeep 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_, …). ..., ..1 are refused: the dots parameter is always ... and takes no formal.
  • 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’s self and private).
  • It must differ from the other formals, declared or not.
  • formal is 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

AttributeEffect
(default)Result::Err, panics, and Option::None ride the tagged-condition transport: R wrapper raises a structured rust_* condition
unwrap_in_rReturn 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

AttributeEffect
check_interruptInsert R_CheckUserInterrupt() before call
rngManage 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 = [..]) / dataWhat ... 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.

AttributeEffect
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

SyntaxSystemR Pattern
#[miniextendr]Env (default)obj$method() environment dispatch
#[miniextendr(r6)]R6R6Class with $new()
#[miniextendr(s3)]S3generic.Class(x, ...)
#[miniextendr(s4)]S4setClass/setMethod formal OOP
#[miniextendr(s7)]S7new_class/new_generic modern OOP
#[miniextendr(vctrs)]vctrsvctrs-compatible S3 vector class

🔗Impl-Level Attributes

AttributeApplies ToEffect
class = "..."All systemsCustom R class name
strict / no_strictAll systemsStrict type conversion for all methods
internalAll systems@keywords internal on class
noexportAll systemsSuppress @export on class
preconditions / no_preconditionsAll systems, trait impls tooKeep or drop the type-derived R checks of every method whose parameters and own attribute say nothing (see R-side preconditions)
blanketTrait implsSkip 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)

AttributeEffect
ignoreDon’t generate R wrapper for this method
constructorMark 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_dotsS3/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_workerThread override
check_interruptInsert interrupt check
coerce / no_coerceType coercion override
wrap = "r6"Explicitly wrap the return in R6 (also s7, s4, s3, env, vctrs); see explicit cross-class return wrapping above
serializeConvert the complete return value through AsSerialize<T>
rngRNG state management; a &RngGuard parameter receives the guard. rng(seed = ..) is for standalone functions only (a compile error here)
unwrap_in_rReturn 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_preconditionsKeep 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 constant
  • CounterVTable: function pointer table
  • Method shims: extern "C" trampolines with with_r_unwind_protect
  • CounterView: runtime dispatch wrapper

No attributes accepted on the #[miniextendr] for traits (the attr parameter is unused).

Constraints:

  • Methods must take &self or &mut self (not self by 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.

SyntaxResult
#[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:

DeriveDirectionRole
IntoListRust → Rstruct → R list (one element per field)
TryFromListR → RustR list → struct (one field per element)
PreferListRust → Rwrites 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 kindR 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 onA 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 parameterR formal
an impl method or associated itemC wrapper, registration, R method or docs
a trait methodvtable slot or R method
a trait impl methodwrapper (an alternative under another gate takes its place)
an enum variantfactor level or match_arg choice
a struct fieldcolumn, 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:

DeriveWhat It Generates
ExternalPtrType-safe external pointer wrapper
AltrepFull ALTREP class (IntoR + TryFromSexp + registration)
AltrepInteger / AltrepReal / …ALTREP for specific vector types
IntoListStruct → R list
TryFromListR list → Struct
DataFrameRowStruct → columnar data.frame + companion type
RFactorEnum ↔ R factor
MatchArgEnum ↔ R character with match.arg()
RNativeTypeNewtype for native R types
PreferListIntoR through list
PreferExternalPtrIntoR through ExternalPtr, for a hand-written TypedExternal
PreferRNativeTypeIntoR through native SEXP
Vctrsvctrs-compatible S3 class (feature-gated)
PreferVctrsIntoR 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