Visibility and Export Control
#[miniextendr] functions exist at three scopes: the Rust binary (C symbol), the package namespace (callable inside the package), and the public API (importable by other packages). Rust pub and the #[miniextendr] export attributes control which scope each function occupies. This document explains the mapping.
#[miniextendr] functions exist at three scopes: the Rust binary (C symbol), the package
namespace (callable inside the package), and the public API (importable by other packages).
Rust pub and the #[miniextendr] export attributes control which scope each function
occupies. This document explains the mapping.
πThree-Tier Model
Every #[miniextendr] function always gets a C symbol registered in R_CallMethodDef.
What varies is whether the generated R wrapper carries @export, @keywords internal,
or neither.
| Rust visibility | #[miniextendr] option | @export | @keywords internal | NAMESPACE |
|---|---|---|---|---|
pub fn | (none) | yes | no | exported |
pub fn | noexport | no | no | not in NAMESPACE |
pub fn | internal | no | yes | not in NAMESPACE |
non-pub fn | (none) | no | no | not in NAMESPACE |
non-pub fn | export | yes | no | exported |
Non-pub functions behave identically to pub + noexport: an R wrapper is generated and
the C symbol is registered, but no @export is emitted.
πThe C Symbol Is Always Registered
Every #[miniextendr] function β regardless of Rust visibility or export flags β
produces a R_CallMethodDef entry and an R wrapper. This means
.Call(C_<crate>_fn_name, ...) works from any R code inside the package, even for
non-exported functions. (C symbols are prefixed with the crateβs name β mypkg
in the examples below β for webR cross-package uniqueness, see docs/WEBR.md.)
NAMESPACE controls importability from outside the package. It has no effect on
.Call() visibility within the package itself.
// C_mypkg_internal_helper is callable via .Call() from R code in the same package,
// but does not appear in NAMESPACE and cannot be imported by downstream packages.
fn internal_helper(x: i32) -> i32 {
x + 1
}
πFunction Attribute Reference
| Attribute | Effect on R wrapper |
|---|---|
noexport | Omit @export; wrapper exists and C symbol is registered |
internal | Omit @export; add @keywords internal; appears in ?help only when searched directly |
export | Force @export on a non-pub function |
r_name = "..." | Rename the R wrapper (e.g. r_name = "is.widget"); does not affect NAMESPACE membership |
postfix = "..." | Append a suffix to the Rust name for the R wrapper (postfix = "_impl" on fn f gives f_impl); states the βhand-written f() delegates to generated f_impl()β convention once. Exclusive with r_name and s3(...). A crate-wide default for noexport / internal functions lives in Cargo.toml (see below) |
call = wrapper | caller | Which call conditions are attributed to, as written. caller names the wrapperβs caller (the hand-written R function delegating to this internal entry point) instead of the wrapperβs own call: Rust-side errors and the wrapperβs own R-side checks (stopifnot preconditions, match_arg / choices) alike; requires noexport or internal. A caller wrapper ends its formals with .call = NULL, through which a hand-written helper in between passes on the frame or call to report. wrapper is the default (sys.call()). A Call / CallerCall parameter spells wrapper / caller at the type level, and Cargo.toml can set a crate default (see below) |
c_symbol = "..." | Rename the C symbol used in .Call() and R_CallMethodDef. The value is used verbatim β no crate prefix is added, so you own its cross-package uniqueness on webR (see docs/WEBR.md) |
πWhen to use each option
| Goal | Use |
|---|---|
| Public API function | pub fn (default) |
pub for Rust trait bounds, but package-internal | #[miniextendr(noexport)] |
Internal helper that should appear in ?help search | #[miniextendr(internal)] |
Non-pub function that must be exported | #[miniextendr(export)] (rare; prefer making the fn pub) |
| Rename the R-facing name | r_name = "my.function" |
| Internal entry point behind a hand-written R function | #[miniextendr(noexport, postfix = "_impl")] |
| Internal entry point whose errors should name the public R function | #[miniextendr(noexport, call = caller)] |
| Exported function an R function composes, whose errors should name that function | #[miniextendr(call_arg)], called with .call = environment() |
| Rename the C symbol (e.g. to avoid collision) | c_symbol = "pkg_my_fn" |
πMutually exclusive combinations
The following combinations are compile errors:
internal + noexportβinternalis a strict superset; dropnoexportexport + noexportβ contradictoryexport + internalβ contradictorypostfix + r_nameβ both set the R wrapper name; use onepostfix + s3(...)β S3 method names are alwaysgeneric.classcall = caller(or aCallerCallparameter) withoutnoexport/internalβ caller attribution is for internal entry points- a
Call/CallerCallparameter that disagrees with thecall = ...attribute β two spellings of one decision must agree - two
Call/CallerCallparameters on one function β the call slot is a single value call_argwithcall = calleror aCallerCallparameter β acallerwrapper already takes.callcall_argwiths3(...)β an S3 method canβt take a formal its generic lacks
πExamples
πDefault: exported public function
#[miniextendr]
pub fn add(x: i32, y: i32) -> i32 {
x + y
}# Generated:
#' @export
add <- function(x, y) .Call(C_mypkg_add, x, y)
add appears in NAMESPACE and is importable by downstream packages.
πnoexport: suppress NAMESPACE entry
// pub needed so this type's method can call it via a trait bound,
// but we don't want it in the public API.
#[miniextendr(noexport)]
pub fn validate_internal(x: i32) -> bool {
x > 0
}
No @export is emitted. The R wrapper exists and .Call(C_mypkg_validate_internal, ...) works
inside the package, but validate_internal is not in NAMESPACE.
πinternal: hide from normal help search
#[miniextendr(internal)]
pub fn debug_repr(x: i32) -> String {
format!("debug: {}", x)
}# Generated:
#' @keywords internal
debug_repr <- function(x) .Call(C_mypkg_debug_repr, x)
@keywords internal hides the function from help.search() results by default. It remains
accessible to R users who know the name (?debug_repr still works) but does not clutter
discovery.
πexport: force-export a non-pub function
// Rare β prefer making the function pub instead.
#[miniextendr(export)]
fn legacy_compat() -> i32 {
42
}
@export is emitted despite the function not being pub in Rust.
πr_name: rename the R wrapper
#[miniextendr(r_name = "is.widget")]
pub fn is_widget(x: i32) -> bool {
x == 1
}
The R wrapper is named is.widget (valid R identifier style). The C symbol remains
derived from the Rust identifier (C_mypkg_is_widget). NAMESPACE gets export(is.widget).
πpostfix: state the internal-entry-point convention once
A common layout keeps the documented R function hand-written (argument checking,
defaults, match.arg()) and has it delegate to the generated wrapper. With
r_name every such entry point spells its own name again in a string; postfix
derives it from the Rust name:
#[miniextendr(noexport, postfix = "_impl")]
pub fn summarise_widget(x: i32) -> i32 {
x * 2
}# Generated (not exported):
summarise_widget_impl <- function(x) .Call(C_mypkg_summarise_widget, x)
# Hand-written, in R/:
#' @export
summarise_widget <- function(x) {
stopifnot(is.numeric(x), length(x) == 1L)
summarise_widget_impl(as.integer(x))
}
The value is appended verbatim to the Rust identifier and must be a valid R
identifier fragment (letters, digits, _, .). The C symbol is unchanged.
Inherent impl methods accept it too (obj$bump_impl() on R6/Env,
bump_impl.Widget on S3, the generic name on S4/S7); trait impls take their R
names from the trait declaration and do not.
πCrate-level default from the manifest
A package whose hand-written R layer delegates to many internal entry points
repeats postfix = "_impl" on every one of them. The default belongs in the
crateβs Cargo.toml, the one file every #[miniextendr] expansion can see
(proc-macro invocations share no state, so miniextendr_init! cannot carry
it):
[package.metadata.miniextendr]
noexport_postfix = "_impl"#[miniextendr(noexport)] // R wrapper: summarise_widget_impl
pub fn summarise_widget(x: i32) -> i32 { x * 2 }
#[miniextendr(noexport, postfix = "_rs")] // per-item postfix wins: parse_widget_rs
pub fn parse_widget(x: i32) -> i32 { x }
#[miniextendr] // exported: keeps its Rust name
pub fn widget_version() -> i32 { 1 }
The default applies to noexport and internal free functions only.
Exported functions keep their Rust names, S3 methods are always
generic.class, and impl methods are named per impl block. Precedence for an
internal free function is r_name > postfix > crate default > Rust name.
The same table carries source_tags = true, which puts a
@source Generated by miniextendr from β¦ line on every generated roxygen
block (free functions, class methods, trait shortcuts, sidecar accessors).
It is off by default (#1552): roxygen2 renders each @source as its own
paragraph, so a shared Rd page collected one per function, and the
# Generated from Rust fn β¦ (file:line:col) comment above each wrapper is
the pointer back to the Rust source. A @noRd block, including every
noexport function, also gets no injected @title and keeps no [...]
links, since it renders no page. roxygen_prose_links = "keep" lets the
leading prose of every other doc comment keep its [other_fn()] links on
the way to @description, for a
crate whose doc comments are written for roxygen2 (default "strip"; see
MINIEXTENDR_ATTRIBUTE.md).
The macro reads the manifest through CARGO_MANIFEST_DIR at expansion time;
Cargo refingerprints the package when Cargo.toml changes, so an edit takes
effect on the next build. Write the key as a table header (or a dotted key
under [package] / [package.metadata]); inline tables
(miniextendr = { ... }) are rejected with a compile error, as is a value
that is not a valid R identifier fragment. The manifest is the crateβs own:
workspace roots are not consulted.
πcall = caller: attribute conditions to the hand-written caller
#[miniextendr(noexport, call = caller)]
pub fn summarise_impl(x: i32) -> Result<i32, String> {
if x <= 0 { return Err(format!("x must be positive, got {x}")); }
Ok(x)
}# Hand-written, in R/:
#' @export
summarise <- function(value) summarise_impl(as.integer(value))
summarise(-1)
#> Error in summarise(-1) : x must be positive, got -1
Without the option the error would read Error in summarise_impl(as.integer(value)),
leaking the bridge that noexport exists to hide. The wrapperβs own R-side
checks follow the same rule: summarise("a") reports
Error in summarise("a") : 'x' must be integer, not the wrapperβs
own frame. See
CALL_ATTRIBUTION.md for
how the frame is chosen, the top-level fallback and the shape of those checks.
The generated wrapper is summarise_impl <- function(x, .call = NULL). A
helper between summarise() and the entry point passes its callerβs frame
there, so the error still names summarise():
.check_summary <- function(value, call = parent.frame()) {
summarise_impl(as.integer(value), .call = call)
}
summarise <- function(value) .check_summary(value)
#> Error in summarise(-1) : x must be positive, got -1
Pass a frame, not a call object: do.call() evaluates a call in its argument
list (details).
The same decision has two more spellings (#1566). A CallerCall parameter
(miniextendr_api::CallerCall) selects caller attribution at the type level
and hands the body the call the wrapper attributed to; it is not an R formal,
so the wrapper is summarise_impl <- function(x, .call = NULL) either way.
Its sibling Call spells the default, call = wrapper. A package whose hand-written layer delegates to many internal entry points sets
the default once, next to noexport_postfix:
[package.metadata.miniextendr]
noexport_postfix = "_impl"
call_attribution = "caller" # every noexport / internal entry point#[miniextendr(noexport)] // R wrapper summarise_impl, reports summarise()
pub fn summarise(x: i32) -> Result<i32, String> { /* ... */ }
#[miniextendr(noexport)]
pub fn scale_impl(x: f64, _call: CallerCall) -> f64 { x } // same, spelled on the signature
#[miniextendr(noexport, call = wrapper)] // per-item spelling wins: reports itself
pub fn probe_impl(x: i32) -> i32 { x }
#[miniextendr] // exported: a "caller" default never applies
pub fn version() -> i32 { 1 }
Precedence is marker > attribute > crate default > wrapper; a marker and an
attribute that disagree are a compile error. The
full table and the marker semantics are in
CALL_ATTRIBUTION.md.
πImpl and Class-Level Export Control
The noexport and internal flags can be applied to an entire impl block, suppressing
or marking all methods at once.
// All methods in this impl are internal
#[miniextendr(internal)]
impl DebugType {
pub fn dump(&self) -> String { ... }
pub fn inspect(&self) -> i32 { ... }
}// Suppress @export on the whole class
#[miniextendr(noexport)]
impl InternalHelper {
pub fn run(&self) -> i32 { ... }
}
Individual methods can override the impl-level flag:
#[miniextendr(noexport)]
impl MyType {
pub fn internal_method(&self) -> i32 { ... }
// Override: this one is exported despite the block-level noexport
#[miniextendr(export)]
pub fn public_method(&self) -> String { ... }
}
πLint Rules
πMXL106: non-pub function with an @export tag
[MXL106] src/lib.rs:12: function `my_helper` has `/// @export` but is not `pub`. ...
This fires when a non-pub #[miniextendr] function carries a hand-written
/// @export tag. The tag still exports the R wrapper, so R exports a function
that Rust keeps private. Fix: make it pub fn to export it, or drop the tag to
keep the R wrapper internal. To export a non-pub fn on purpose, use
#[miniextendr(export)] instead of the tag.
πMXL203: internal + noexport redundancy
warning[MXL203]: `internal` and `noexport` are redundant together
internal already suppresses @export and adds @keywords internal. Drop noexport.
// Bad
#[miniextendr(internal, noexport)]
pub fn helper() -> i32 { 42 }
// Good
#[miniextendr(internal)]
pub fn helper() -> i32 { 42 }
πQuick Decision Guide
Is the function part of the package's public API?
βββ Yes β pub fn (default)
βββ No β should it appear in help search?
β βββ Yes, but not exported β pub fn + #[miniextendr(internal)]
β βββ No β non-pub fn (or pub + #[miniextendr(noexport)])
βββ Need pub for Rust trait bounds only? β pub fn + #[miniextendr(noexport)]
πSee Also
- MINIEXTENDR_ATTRIBUTE.md β complete
#[miniextendr]option reference - CLASS_SYSTEMS.md β class-level export control details
- MACRO_ERRORS.md β MXL106, MXL203, and other lint details