#[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_fn_name, ...) works from any R code inside the package, even for non-exported functions.

NAMESPACE controls importability from outside the package. It has no effect on .Call() visibility within the package itself.

// C_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
c_symbol = "..."Rename the C symbol used in .Call() and R_CallMethodDef

🔗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"
Rename the C symbol (e.g. to avoid collision)c_symbol = "pkg_my_fn"

🔗Mutually exclusive combinations

The following combinations are compile errors:

  • internal + noexportinternal is a strict superset; drop noexport
  • export + noexport — contradictory
  • export + internal — contradictory

🔗Examples

🔗Default: exported public function

#[miniextendr]
pub fn add(x: i32, y: i32) -> i32 {
    x + y
}
# Generated:
#' @export
add <- function(x, y) .Call(C_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_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_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 C_is_widget. NAMESPACE gets export(is.widget).


🔗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 not exported

warning[MXL106]: registered function `my_helper` is not `pub`

This fires when a #[miniextendr] function is not pub. The C symbol is registered and the R wrapper exists, but users cannot call it from outside the package. Fix: add pub, or add #[miniextendr(export)] if you intentionally want to export a non-pub fn.

🔗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