Getting Started with miniextendr
This guide walks you through creating your first R package with a Rust backend using miniextendr.
This guide walks you through creating your first R package with a Rust backend using miniextendr.
πPrerequisites
- Rust (1.85+): Install from rustup.rs
- R (4.0+): Install from CRAN
- R development tools:
install.packages("devtools")
Verify your setup:
rustc --version # Should be 1.85+
R --version # Should be 4.0+πQuick Start
πStep 1: Create a New Package
minirextendr is not yet on CRAN β install it from GitHub, then scaffold a
new project:
# Install minirextendr (once)
devtools::install_github("A2-ai/miniextendr", subdir = "minirextendr")
# or: pak::pak("A2-ai/miniextendr/minirextendr")
# Create a new package
library(minirextendr)
create_miniextendr_package("mypackage")
create_miniextendr_package() does not initialize git. Do that next β itβs
what the installed git hooks and other usethis::use_git()-dependent
tooling expect, and it keeps a stray cargo-revendor on PATH from
silently flipping a from-scratch build into offline vendor mode (see
CRAN_COMPATIBILITY.md):
cd mypackage && git init && git add -A && git commit -m "Initial scaffold"
This creates a package structure with:
mypackage/
βββ DESCRIPTION
βββ NAMESPACE
βββ R/
β βββ mypackage-package.R
βββ src/
β βββ stub.c
β βββ rust/
β βββ Cargo.toml
β βββ lib.rs # Your Rust code goes here
βββ configure # Generated by autoconf
βββ configure.ac # Autoconf source
There is no R/mypackage-wrappers.R yet β that file is generated by your
first build (Step 3). Thereβs also an empty, gitignored top-level vendor/
directory; it only gets populated by minirextendr::miniextendr_vendor()
before a CRAN release and isnβt something you touch day to day.
πStep 2: Write Your First Function
The scaffold already ships two working example functions in
src/rust/lib.rs:
use miniextendr_api::miniextendr;
miniextendr_api::miniextendr_init!();
/// A simple function that adds two numbers
/// @param a First number
/// @param b Second number
/// @return Sum of a and b
#[miniextendr]
pub fn add(a: f64, b: f64) -> f64 {
a + b
}
/// Say hello to someone
/// @param name Name to greet
/// @return Greeting string
#[miniextendr]
pub fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}
Add your own the same way β pub fn + #[miniextendr], anywhere reachable
from lib.rs. Registration is automatic via linkme distributed slices, no
module declarations needed:
/// Multiply two numbers.
/// @param a First number
/// @param b Second number
/// @return The product of a and b
#[miniextendr]
pub fn multiply(a: f64, b: f64) -> f64 {
a * b
}πStep 3: Build and Install
minirextendr::miniextendr_build()
This runs autoconf + ./configure, compiles the Rust code, generates the
R wrappers (R/mypackage-wrappers.R) via linkme, updates NAMESPACE +
man/ with roxygen2, and installs the package β all in one step.
Do not install with a bare devtools::install(), R CMD INSTALL ., or
devtools::document(): on a fresh package, R CMD buildβs bootstrap.R
vendors dependencies into inst/vendor.tar.xz, which flips ./configure
into offline βtarballβ mode. Tarball mode ships pre-generated wrappers and
skips wrapper generation, so on a first build those paths install a
package whose namespace exposes no functions. miniextendr_build() avoids
this trap by bootstrapping a fresh packageβs wrappers through an in-place
install first.
πStep 4: Use from R
library(mypackage)
add(1, 2)
# [1] 3
hello("World")
# [1] "Hello, World!"
multiply(2, 3)
# [1] 6
πCore Concepts
πThe #[miniextendr] Attribute
Mark functions for export to R:
#[miniextendr]
pub fn my_function(x: i32) -> i32 {
x * 2
}
The macro:
- Generates a C wrapper callable from R
- Handles type conversion (R β Rust)
- Manages error handling and panics
- Extracts documentation from Rust doc comments
πAutomatic Registration
Items annotated with #[miniextendr] are automatically registered via linkme distributed slices β no manual module declarations needed.
πType Conversions
miniextendr automatically converts between R and Rust types:
| R Type | Rust Type |
|---|---|
| integer | i32 |
| numeric | f64 |
| character | String, &str |
| logical | bool |
| integer vector | Vec<i32>, &[i32] |
| numeric vector | Vec<f64>, &[f64] |
| list | Various (see below) |
| NULL | () |
| NA | Option<T> (None = NA) |
πCreating Classes
miniextendr supports multiple R class systems. Hereβs a quick comparison:
πEnvironment Style (Default)
Simple method dispatch via $:
#[derive(miniextendr_api::ExternalPtr)]
pub struct Counter { value: i32 }
#[miniextendr] // Default: env style
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;
}
}c <- Counter$new(0L)
c$value() # 0
c$increment()
c$value() # 1πR6 Style
Full R6 class with encapsulation:
#[miniextendr(r6)]
impl Counter {
// ... same methods
// Active binding (property-like access)
#[miniextendr(r6(active))]
pub fn current(&self) -> i32 {
self.value
}
}c <- Counter$new(0L)
c$value() # Method call
c$current # Active binding (no parens)πS3, S4, S7
#[miniextendr(s3)] // S3 generic functions
#[miniextendr(s4)] // S4 setClass/setMethod
#[miniextendr(s7)] // S7 new_class
impl Counter { ... }
See CLASS_SYSTEMS.md for detailed comparison.
πError Handling
πPanics
Rust panics are converted to R errors:
#[miniextendr]
pub fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
panic!("Division by zero");
}
a / b
}divide(1, 0)
# Error: Division by zeroπResult Types
Return Result<T, E> for structured error handling:
#[miniextendr]
pub fn parse_int(s: &str) -> Result<i32, String> {
s.parse().map_err(|e| format!("Parse error: {}", e))
}
By default, Err values cause R errors. Use #[miniextendr(unwrap_in_r)] to return errors as R values:
#[miniextendr(unwrap_in_r)]
pub fn try_parse(s: &str) -> Result<i32, String> {
s.parse().map_err(|e| e.to_string())
}try_parse("42") # 42
try_parse("abc") # list(error = "invalid digit...")
πWorking with Vectors
πSlices (Zero-Copy)
For read-only access, use slices:
#[miniextendr]
pub fn sum_slice(x: &[f64]) -> f64 {
x.iter().sum()
}
This provides zero-copy access to Rβs vector data.
πOwned Vectors
For modification, use Vec<T>:
#[miniextendr]
pub fn double_values(x: Vec<i32>) -> Vec<i32> {
x.into_iter().map(|v| v * 2).collect()
}πNA Handling
Use Option<T> to handle NA values:
#[miniextendr]
pub fn replace_na(x: Vec<Option<f64>>, replacement: f64) -> Vec<f64> {
x.into_iter()
.map(|v| v.unwrap_or(replacement))
.collect()
}
πOpaque Pointers (ExternalPtr)
For complex Rust types that donβt map to R types:
#[derive(miniextendr_api::ExternalPtr)]
pub struct Database {
connection: Connection,
}
#[miniextendr]
impl Database {
pub fn new(path: &str) -> Self {
Database { connection: Connection::open(path).unwrap() }
}
pub fn query(&self, sql: &str) -> Vec<String> {
// ...
}
}
The ExternalPtr derive:
- Wraps the Rust struct in Rβs external pointer type
- Automatically runs
Dropwhen R garbage collects - Provides type-safe access across function calls
πDevelopment Workflow
πIteration Cycle
- Edit Rust code in
src/rust/lib.rs - Run
minirextendr::miniextendr_build() - Test in R
miniextendr_build() is the only supported rebuild path: it runs autoconf
./configure, compiles the Rust code, regenerates the R wrappers, updatesNAMESPACE+man/with roxygen2, and installs β all in one step. See βBuild and Installβ above for whydevtools::document()/devtools::install()/R CMD INSTALL .are unsafe to use directly.
πDebugging Tips
- Rust panics: Set
MINIEXTENDR_BACKTRACE=1for full backtraces - Compilation errors: Check
src/rust/Cargo.tomldependencies - R errors: Check that functions have
#[miniextendr]and arepub
πCommon Patterns
πDefault Parameters
/// @param amount Amount to add (default: 1)
#[miniextendr]
pub fn increment(value: i32, #[miniextendr(default = "1")] amount: i32) -> i32 {
value + amount
}increment(5) # 6 (uses default)
increment(5, 3) # 8πVariadic Arguments (Dots)
use miniextendr_api::dots::Dots;
#[miniextendr]
pub fn count_args(...) -> i32 {
// `...` automatically creates a `_dots: &Dots` parameter.
_dots.len() as i32
}count_args(1, 2, 3, "a", "b") # 5
See DOTS_TYPED_LIST.md for giving dots a custom name
and validating their contents with typed_list!.
πFactors (Enums)
use miniextendr_api::RFactor;
#[derive(RFactor)]
pub enum Color { Red, Green, Blue }
#[miniextendr]
pub fn describe_color(color: Color) -> &'static str {
match color {
Color::Red => "warm",
Color::Green => "cool",
Color::Blue => "cool",
}
}describe_color(factor("Red", levels = c("Red", "Green", "Blue")))
# [1] "warm"
πNext Steps
- CLASS_SYSTEMS.md - Detailed class system comparison
- ALTREP.md - Lazy/compact vectors
- THREADS.md - Threading and parallelism
- SAFETY.md - Memory safety guarantees
πTroubleshooting
πβconfigure: command not foundβ
minirextendr::miniextendr_build() runs autoconf for you, so this only
comes up if youβre generating build files by hand:
cd mypackage && autoconf && ./configure
Then continue with minirextendr::miniextendr_build().
πβcould not find functionβ in R
Ensure the function is:
- Marked
pub - Has
#[miniextendr]attribute
Then rebuild: minirextendr::miniextendr_build()
πCompilation Errors
Check src/rust/Cargo.toml for dependency issues. Run:
cd src/rust && cargo checkπNext Steps
- Documentation Index β Browse all available documentation
- Known Gaps & Limitations β Important context on whatβs missing or limited
- Troubleshooting β Common issues and solutions
- Architecture Overview β How miniextendr works under the hood