#[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 internalNAMESPACE
pub fn(none)yesnoexported
pub fnnoexportnononot in NAMESPACE
pub fninternalnoyesnot in NAMESPACE
non-pub fn(none)nononot in NAMESPACE
non-pub fnexportyesnoexported

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

AttributeEffect on R wrapper
noexportOmit @export; wrapper exists and C symbol is registered
internalOmit @export; add @keywords internal; appears in ?help only when searched directly
exportForce @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 | callerWhich 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

GoalUse
Public API functionpub 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 namer_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 β€” internal is a strict superset; drop noexport
  • export + noexport β€” contradictory
  • export + internal β€” contradictory
  • postfix + r_name β€” both set the R wrapper name; use one
  • postfix + s3(...) β€” S3 method names are always generic.class
  • call = caller (or a CallerCall parameter) without noexport / internal β€” caller attribution is for internal entry points
  • a Call / CallerCall parameter that disagrees with the call = ... attribute β€” two spellings of one decision must agree
  • two Call / CallerCall parameters on one function β€” the call slot is a single value
  • call_arg with call = caller or a CallerCall parameter β€” a caller wrapper already takes .call
  • call_arg with s3(...) β€” 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.

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