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 TypeRust Type
integeri32
numericf64
characterString, &str
logicalbool
integer vectorVec<i32>, &[i32]
numeric vectorVec<f64>, &[f64]
listVarious (see below)
NULL()
NAOption<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 Drop when R garbage collects
  • Provides type-safe access across function calls

πŸ”—Development Workflow

πŸ”—Iteration Cycle

  1. Edit Rust code in src/rust/lib.rs
  2. Run minirextendr::miniextendr_build()
  3. Test in R

miniextendr_build() is the only supported rebuild path: it runs autoconf

  • ./configure, compiles the Rust code, regenerates the R wrappers, updates NAMESPACE + man/ with roxygen2, and installs – all in one step. See β€œBuild and Install” above for why devtools::document() / devtools::install() / R CMD INSTALL . are unsafe to use directly.

πŸ”—Debugging Tips

  • Rust panics: Set MINIEXTENDR_BACKTRACE=1 for full backtraces
  • Compilation errors: Check src/rust/Cargo.toml dependencies
  • R errors: Check that functions have #[miniextendr] and are pub

πŸ”—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


πŸ”—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:

  1. Marked pub
  2. 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