with one click
miniextendr-macros
Use when the user asks how
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Use when the user asks how
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | miniextendr-macros |
| description | Use when the user asks how |
The #[miniextendr] attribute macro is the primary developer-facing API of
miniextendr. When placed on a Rust function or impl block, it generates all
the FFI plumbing needed to make that code callable from R: a C wrapper
function, registration entries, and an R wrapper function. This skill covers
the full codegen pipeline and all attribute options.
#[miniextendr] work?"strict do?"For every #[miniextendr] pub fn foo(x: i32) -> String, the macro generates:
extern "C-unwind" #[no_mangle] C wrapper that receives SEXPs, converts
them with TryFromSexp, calls the Rust function, and converts the return
value with IntoR. Panics are caught by with_r_unwind_protect and
returned as a tagged SEXP value; the R wrapper raises a structured condition
past the Rust boundary so destructors unwind cleanly.#[distributed_slice(MX_CALL_DEFS)] entry containing the C function pointer
and name string, registered with R at package load via R_init_*.#[distributed_slice(MX_R_WRAPPERS)] entry containing an R wrapper function
string that calls .Call(C_foo, x, .call = match.call()). This string is
written to R/miniextendr-wrappers.R during the cdylib build phase by
miniextendr_write_wrappers in miniextendr-api/src/registry.rs.The macro never writes files. It emits Rust code at compile time; the file-writing happens at runtime during the cdylib phase (see the miniextendr-architecture skill).
Standalone functions and impl block methods use separate codegen paths with different C wrapper signatures. This distinction matters when reading generated code or extending the macro.
Standalone functions (miniextendr-macros/src/miniextendr_fn.rs):
The #[miniextendr] attribute on a bare pub fn is processed by
miniextendr_fn.rs. Return type analysis runs through
miniextendr-macros/src/return_type_analysis.rs (analyze_return_type), which
determines: whether SEXP is returned (forces main-thread execution), whether the
R wrapper should call invisible(), and how to handle Option<T> / Result<T,E>
unwrapping.
Impl block methods (miniextendr-macros/src/miniextendr_impl.rs):
The #[miniextendr(r6)] or similar attribute on an impl block is processed by
miniextendr_impl.rs. Each method in the block generates a C wrapper via
CWrapperContext in miniextendr-macros/src/c_wrapper_builder.rs.
Critical difference: CWrapperContext prepends __miniextendr_call: SEXP as
the first parameter on every impl-method C wrapper. This slot carries the R
match.call() result for error attribution. The corresponding R wrapper uses
.Call(C_Type__method, .call = match.call(), ...).
Sidecar accessors (*_get_field / *_set_field) generated by
miniextendr-macros/src/externalptr_derive.rs are an exception: they hand-roll
their C wrappers with signatures (x: SEXP) or (x, value: SEXP) and
numArgs = 1 / 2 — no call slot. Adding .call = match.call() to an R-side
sidecar wrapper causes "Incorrect number of arguments" at runtime. Do not
conflate sidecar accessors with regular method wrappers.
The C wrapper runs on R's main thread by default (ThreadStrategy::MainThread,
wrapping in with_r_unwind_protect). A function opts into the worker thread with
#[miniextendr(worker)] (ThreadStrategy::WorkerThread), which moves the Rust
call into run_on_worker while argument conversion and SEXP conversion remain on
the main thread. Functions are forced to the main thread when: the function takes
or returns SEXP, the receiver &self/&mut self is not Send, it takes Dots,
or #[miniextendr(check_interrupt)] is used.
Every #[miniextendr] function and method routes errors through the
tagged-condition transport:
with_r_unwind_protect.RCondition value) is packed into a tagged SEXP via
make_rust_condition_value in miniextendr-api/src/error_value.rs.condition_check_lines() (from
miniextendr-macros/src/method_return_builder.rs): if the value inherits
"rust_condition_value" and carries the __rust_condition__ attribute, it
calls .miniextendr_raise_condition(.val, sys.call())..miniextendr_raise_condition (emitted once at the top of
R/miniextendr-wrappers.R) dispatches on .val$kind and raises the condition
with the layered class vector c(user_class, "rust_error", "simpleError", "error", "condition").This is the only path for user functions. Direct Rf_error / Rf_errorcall
is reserved for trait-ABI vtable shims (miniextendr_trait.rs:808) and ALTREP
RUnwind guards (ffi_guard.rs). MXL300 enforces this.
Opt-out: #[miniextendr(unwrap_in_r)] passes Result<T, E> to R as a list with
an error field rather than treating Err as a Rust-origin failure. This is
orthogonal to the transport (unwrap_in_r returns the Result intact; tagged
conditions only fire for panics and unwrapped errors).
RCondition macrosThe error!(), warning!(), message!(), condition!() macros in
miniextendr-api/src/condition.rs produce RCondition enum payloads that ride
the same tagged-SEXP transport. The RCondition variant is recognised by
with_r_unwind_protect before the generic panic-to-string path.
Name collision caution: pub mod error and pub mod condition exist at the
miniextendr_api crate root, shadowing the error! and condition! macros.
Always invoke these via the fully-qualified path miniextendr_api::error!(...).
warning! and message! have no conflicting modules.
miniextendr_write_wrappers in miniextendr-api/src/registry.rs (around L1104)
is the cdylib entry point called during the build. It calls
write_r_wrappers_to_file, which:
.miniextendr_raise_condition helper (hard-coded preamble).collect_r_wrappers(): reads MX_R_WRAPPERS, deduplicates, sorts by
RWrapperPriority, and topologically sorts S7 classes parent-before-child..__MX_MATCH_ARG_CHOICES_*__ placeholders
with actual enum choices, .__MX_CLASS_REF_*__ with resolved R class names,
and .__MX_S7_SIDECAR_PROP_DOCS_*__ with @prop roxygen lines.DotCallBuilder in miniextendr-macros/src/r_wrapper_builder.rs (around L390)
is the canonical site for emitting .Call(C_..., .call = match.call(), ...) in R
wrapper bodies. For lambda contexts (R6 finalizer/deep_clone, S7 property
getter/setter/validator), use .null_call_attribution() to emit .call = NULL
instead — match.call() in those contexts captures an internal dispatch frame.
Rust /// doc comments on #[miniextendr] functions are forwarded to the
generated R wrapper as roxygen2 tags. Extraction and tag manipulation live in
miniextendr-macros/src/roxygen.rs. Class-level documentation (class title,
method docs, active bindings) uses builders in miniextendr-macros/src/r_class_formatter.rs
(ClassDocBuilder, MethodDocBuilder).
For class systems, the macro emits @importFrom R6 R6Class, @importFrom S7 new_class, etc. at the class level. ClassDocBuilder::with_export_control(internal, noexport) handles the @export / @noRd combination consistently across all six
generators.
For multi-line tag support (@examples, @description, @return, @param,
@prop), the DESCRIPTION file must include Roxygen: list(markdown = TRUE).
Free functions default to @rdname <source-file-stem>. During wrapper
collection (collect_r_wrappers in miniextendr-api/src/registry.rs), any
RWrapperPriority::Function entry without an explicit @rdname or @noRd
gets #' @rdname <file-stem> injected (rdname_from_source_file; lib.rs /
mod.rs stems are exempt, and a @title is injected if missing). Consequence:
all exported free functions in one .rs file silently share a single .Rd
page — the extras become aliases with concatenated usage/params, with no
diagnostic. Impl-block/class entries group by class name instead. For one man
page per function, put an explicit #' @rdname <fn_name> on each free
function. Making per-function pages the default (grouping opt-in) is proposed
in A2-ai/miniextendr#1289.
#[miniextendr] applied to a trait definition (not an impl block) is processed
by miniextendr-macros/src/miniextendr_trait.rs. It generates:
TAG_<TraitName> — 128-bit identifier for runtime type
checking).<TraitName>VTable) with one mx_meth entry per method.<TraitName>View) combining data pointer + vtable pointer.unsafe extern "C" shim functions per method that check arity, cast the raw
pointer, and call the Rust method.__<trait>_build_vtable::<T>() for use by impl blocks.Trait-ABI shims use with_r_unwind_protect_shim, not the user-facing
with_r_unwind_protect. They return a tagged error SEXP that propagates to the
consumer's outer with_r_unwind_protect guard, which applies rust_* class
layering (issue #345). The TAG_ / vtable / view machinery is the bridge to the
miniextendr-externalptr and miniextendr-ffi skills.
#[miniextendr] pub fn foo(x: i32) -> Stringminiextendr-macros/src/lib.rs parses
MiniextendrFnAttrs from the attribute arguments and routes to
miniextendr_fn.rs.miniextendr_fn.rs normalizes the function signature, runs CoercionMapping
to detect types needing coercion (e.g., bool from i32), and calls
analyze_return_type from return_type_analysis.rs.extern "C-unwind" #[no_mangle] fn C_foo(...) with
with_r_unwind_protect wrapping the body.#[distributed_slice(MX_CALL_DEFS)] entry registers the wrapper under the
name "C_foo" with numArgs = 1.#[distributed_slice(MX_R_WRAPPERS)] entry registers the R wrapper string
at RWrapperPriority::Function.miniextendr_write_wrappers (cdylib phase) writes the R wrapper
to R/miniextendr-wrappers.R.| Attribute | Effect |
|---|---|
strict | Lossy integer types (i64, u64, isize, usize) use checked conversions that panic on overflow instead of silent truncation. Also controls -> Result<T, E> wrapping. |
internal | Adds @keywords internal to generated roxygen; class is still exported but not user-visible in docs. |
noexport | Omits @export from generated roxygen; function is not exported from the package namespace. |
unwrap_in_r | Result<T, E> is passed to R as a list with an $error field instead of treating Err as a Rust-origin failure. Orthogonal to the tagged-condition transport (which only fires for panics and unwrapped errors). |
worker | Executes the Rust function on the worker thread (see miniextendr-worker skill). |
check_interrupt | Forces main-thread execution; inserts R_CheckUserInterrupt around the call. |
rng | Wraps the call in GetRNGstate/PutRNGstate. |
dots = typed_list!(...) | Validates ... arguments; generates dots_typed parameter. See miniextendr-dots skill. |
coerce_all | Applies coercion mappings to all eligible types. |
Attributes for impl block methods are the same, with class-system–specific
additions (e.g., r6(prop = "name"), r6(finalize), r6(private),
constructor, label).
A separate family of per-parameter attributes (choices, match_arg,
several_ok, default) goes on individual parameters, not the function —
see the next section.
choices, match_arg, defaultStandalone functions accept #[miniextendr(...)] attributes on individual
parameters that generate R argument defaults and match.arg() validation,
single-sourced from the Rust signature — no hand-written R:
#[miniextendr]
pub fn interp(
#[miniextendr(choices("linear", "cubic", "nearest"))] method: &str,
#[miniextendr(default = "NULL")] grid_kind: Option<&str>,
#[miniextendr(default = "TRUE")] check_bounds: bool,
) -> Vec<f64> { ... }
generates the R formals method = c("linear", "cubic", "nearest"), grid_kind = NULL, check_bounds = TRUE plus method <- match.arg(method) in
the wrapper body. Per match.arg semantics, the first choice is the
effective default. Details:
default = "<expr>" splices the string verbatim as the R formal default:
"NULL", "TRUE", "0L", "\"World\"" (escaped quotes for string
defaults).choices("a", "b", "c") is for string parameters (&str / String). Add
several_ok (with a Vec<String> parameter) to get
match.arg(..., several.ok = TRUE).#[derive(MatchArg)] on a fieldless enum plus
#[miniextendr(match_arg)] on the parameter. Choices come from
MatchArg::CHOICES (miniextendr-api/src/match_arg.rs) at wrapper-write
time via the MX_MATCH_ARG_CHOICES distributed slice — the placeholder in
the R formal default is substituted in write_r_wrappers_to_file. Adding
default = "\"Variant\"" alongside match_arg rotates that choice to
position 0 so it becomes the match.arg default.validate_per_param_attr_conflicts
in miniextendr-macros/src/miniextendr_fn.rs): choices + default
(choices derives its default from the first choice), coerce + choices,
default on a &Dots or Missing<T> parameter (use Option<T> +
default instead), several_ok without choices/match_arg.match_arg/choices parameter has no user-written @param doc, a
"One of …" / "One or more of …" description is generated
(MX_MATCH_ARG_PARAM_DOCS).Impl methods use a different, method-level grammar. Rust rejects attribute
macros on method parameters inside an impl block ("expected non-macro
attribute"), so the surface moves onto the method attribute with
param = "comma, separated" values:
#[miniextendr(match_arg(mode))]
#[miniextendr(choices(level = "low, medium, high"))]
#[miniextendr(match_arg_several_ok(modes))]
(parsed in miniextendr-macros/src/miniextendr_impl.rs; annotated parameter
names are validated against the signature, so typos fail at compile time).
Do not mix the grammars: choices(...) placed on a standalone function
(rather than on its parameter) is not a recognized fn-level option and errors
with "choices does not accept parenthesized arguments".
Live fixtures: rpkg/src/rust/match_arg_tests.rs,
rpkg/src/rust/default_tests.rs, rpkg/src/rust/match_arg_impl_tests.rs.
#[miniextendr] function but R can't find itpub? Functions without pub are silently excluded.mod from lib.rs? A #[cfg(feature = "foo")]
gate on the mod declaration is sufficient if the feature is enabled.just configure && just rcmdinstall && just force-document? The R
wrapper is generated during rcmdinstall, not before. If you skipped
force-document, roxygen2 may not have regenerated NAMESPACE.inst/vendor.tar.xz is present, edits to
workspace crates are silently ignored. Run just clean-vendor-leak.Add #[miniextendr(strict)]. This applies checked TryFrom conversions for
i64, u64, isize, usize and similar types that would otherwise silently
truncate. Strict mode is also configurable globally via the strict-default
Cargo feature.
miniextendr-macros/src/miniextendr_fn.rs
and miniextendr-macros/src/r_wrapper_builder.rs (the DotCallBuilder).miniextendr-macros/src/miniextendr_impl/<system>_class.rs.miniextendr-macros/src/method_return_builder.rs
(condition_check_lines, standalone_body).miniextendr-api/src/registry.rs
write_r_wrappers_to_file.Add #[miniextendr(worker)]. The function must not take or return SEXP (SEXP
is not Send), must not take Dots, must not have a self receiver (unless the
type is Send), and must not use check_interrupt. If any of these constraints
are violated, the macro falls back silently to main-thread execution. See the
miniextendr-worker skill for the full story.
miniextendr-macros/src/miniextendr_fn.rs — #[miniextendr] on standalone
functions: signature parsing, MiniextendrFnAttrs, CoercionMapping.miniextendr-macros/src/miniextendr_impl.rs — #[miniextendr] on impl blocks:
ImplAttrs, ParsedImpl, ParsedMethod, class-system dispatch.miniextendr-macros/src/miniextendr_trait.rs — #[miniextendr] on trait
definitions: vtable, view, shims.miniextendr-macros/src/c_wrapper_builder.rs — CWrapperContext for impl
method C wrappers; prepends __miniextendr_call: SEXP.miniextendr-macros/src/r_wrapper_builder.rs — DotCallBuilder at ~L390 for
.Call(...) R wrapper emission; RArgumentBuilder, RoxygenBuilder.miniextendr-macros/src/return_type_analysis.rs — analyze_return_type;
ReturnTypeAnalysis struct; output_is_result.miniextendr-macros/src/method_return_builder.rs — condition_check_lines,
standalone_body, condition_check_inline_block; ReturnStrategy enum.miniextendr-macros/src/roxygen.rs — doc-comment extraction; multiline tag
list; has_roxygen_tag.miniextendr-macros/src/r_class_formatter.rs — ClassDocBuilder,
MethodDocBuilder, shared MethodContext for all six class generators.miniextendr-macros/src/externalptr_derive.rs — sidecar accessor C wrappers
(no call slot); numArgs = 1/2.miniextendr-api/src/registry.rs — MX_CALL_DEFS, MX_R_WRAPPERS,
RWrapperPriority, collect_r_wrappers, miniextendr_write_wrappers,
write_r_wrappers_to_file.miniextendr-api/src/error_value.rs — make_rust_condition_value;
tagged SEXP format.miniextendr-api/src/condition.rs — RCondition enum, condition macros.miniextendr-api/src/unwind_protect.rs — with_r_unwind_protect,
with_r_unwind_protect_shim.Sidecar accessors have no call slot: The R-side accessor wrapper for
#[r_data] fields (generated by externalptr_derive.rs) does not pass
.call = match.call(). Adding it causes "Incorrect number of arguments"
at runtime. This is by design — sidecar C wrappers have numArgs = 1/2,
not numArgs + 1 like regular wrappers.
#[macro_export] name collision for error! / condition!: The macros
error! and condition! are shadowed by pub mod error and pub mod condition
at the miniextendr_api crate root. use miniextendr_api::{error, condition}
resolves to the modules. Always invoke via miniextendr_api::error!(...).
Type/const generic params are rejected; lifetimes are allowed: type and
const parameters require monomorphization (one symbol per instantiation),
which is incompatible with the single extern "C-unwind" #[no_mangle]
symbol — the proc-macro rejects them (validation block in
miniextendr-macros/src/lib.rs). Lifetimes are erased at codegen, so
&[T] / &str arguments and explicit <'a> params are fine. (The former
MXL112 lifetime lint is retired.) To wrap an upstream API that is generic
over const N: usize, keep the exported fn non-generic over a runtime value
and monomorphize inside it — a match over the supported N values, or
the upstream crate's own dispatch macro if it exposes one (e.g. interpn's
dispatch_ndims!(ndims, "err msg", [1, 2, ..., 8], |N| { ... })).
#[miniextendr] on 1-field structs is removed: The shorthand for wrapping
a newtype struct is gone. Use ALTREP derives instead.
UI test .stderr snapshots must be updated when error message wording
changes: TRYBUILD=overwrite cargo test -p miniextendr-macros, then review the
diff before committing.
with_r_unwind_protect leaks ~8 bytes on R longjmp (the
RErrorMarker + Box header). Regular panics do not leak. This is why MXL300
warns against direct Rf_error calls — see miniextendr-api/CLAUDE.md.
miniextendr-architecture — how the cdylib-to-staticlib double-link enables
wrapper generation, and the distributed_slice registration system.miniextendr-class-systems — R6/S3/S4/S7/Env/Vctrs codegen, method dispatch
mechanics, constructor error-checking, and the six class generators.miniextendr-ffi — #[r_ffi_checked], _unchecked variants, MXL300/MXL301.miniextendr-externalptr — Box<Box<dyn Any>> storage, pointer provenance,
TypedExternal display tags.miniextendr-worker — run_on_worker, Sendable<T>, ALTREP-callbacks-on-main.miniextendr-dots — Dots, typed_list!, name @ ... syntax.miniextendr-lint — MXL rule catalogue and how to add new rules.Use when the user asks how to expose a Rust struct as an R class, which R class system to use (R6 vs S3 vs S4 vs S7 vs Env vs Vctrs), how method dispatch works, how constructors are generated, how trait methods map to R, or when working in miniextendr-macros/src/miniextendr_impl/*.rs or miniextendr-macros/src/r_class_formatter.rs.
Use when the user asks about converting between R and Rust types, how TryFromSexp or IntoR work, what NA handling looks like, how strict mode differs from normal coercion, why Vec<i32> from an empty R vector panics, or how bool, Option<T>, or large integer types behave across the R-Rust boundary.
Use when the user asks about ExternalPtr, how Rust structs are stored as R objects, TypedExternal, Box<Box<dyn Any>> storage, pointer provenance for cached_ptr, the release_any finalizer, sidecar field accessors, the TYPE_NAME_CSTR vs TYPE_ID_CSTR distinction, or how to pass an ExternalPtr across crate or package boundaries.
Use when a new user asks how to start a miniextendr-backed R package from zero, how to scaffold via minirextendr::use_miniextendr(), what their first Rust function should look like, what the dev loop is, or "can I add Rust to an existing R package". Also use when someone is confused about which package is which (miniextendr vs minirextendr).
Use when debugging configure.ac failures, Makevars issues, Cargo.lock mismatches, vendor tarball problems, build mode confusion, or the cdylib-to-staticlib double-link pipeline. Also use when changing a Makevars value, diagnosing a latch-leak, understanding the bash-vs-sh configure requirement, or working with m4 quoting in configure.ac.
Use when working in an R package with a miniextendr Rust backend and you need orientation — how the build pipeline works (configure → Makevars → cargo → wrapper generation), which files are generated vs hand-written, what the dev loop is, how to add a function or a Cargo feature, or which of the other miniextendr skills to load for a specific task.