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

Use the minirextendr helper package to scaffold a new project:

# Install minirextendr (once)
install.packages("minirextendr")  # or: devtools::install_github("...")

# Create a new package
library(minirextendr)
create_miniextendr_package("mypackage")

This creates a package structure with:

mypackage/
β”œβ”€β”€ DESCRIPTION
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R/
β”‚   └── mypackage_wrappers.R    # Auto-generated R wrappers
β”œβ”€β”€ src/
β”‚   └── rust/
β”‚       β”œβ”€β”€ Cargo.toml
β”‚       β”œβ”€β”€ lib.rs              # Your Rust code goes here
β”‚       └── vendor/             # Vendored miniextendr crates
β”œβ”€β”€ configure                   # Build configuration script
└── configure.ac                # Autoconf source

πŸ”—Step 2: Write Your First Function

Edit src/rust/lib.rs:

use miniextendr_api::miniextendr;

/// Add two integers.
/// @param a First number
/// @param b Second number
/// @return The sum of a and b
#[miniextendr]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Greet someone by name.
/// @param name The name to greet
/// @return A greeting string
#[miniextendr]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}
// Registration is automatic via #[miniextendr].

πŸ”—Step 3: Build and Test

# Recommended: devtools handles everything in one step
# (compiles Rust, generates R wrappers, runs roxygen2)
devtools::document("mypackage")
devtools::install("mypackage")

Or manually without devtools:

cd mypackage
./configure            # Generate build files
R CMD INSTALL .        # Compile Rust and install

πŸ”—Step 4: Use from R

library(mypackage)

add(1L, 2L)
# [1] 3

greet("World")
# [1] "Hello, World!"

πŸ”—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 devtools::document() β€” compiles Rust, generates R wrappers, runs roxygen2
  3. Run devtools::install() β€” install the package
  4. Test in R

devtools::document() handles ./configure, compilation, and wrapper generation automatically via bootstrap.R and the Makevars dependency chain. No manual ./configure or two-pass install needed.

πŸ”—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(_dots: &Dots, ...) -> i32 {
    _dots.len() as i32
}
count_args(1, 2, 3, "a", "b")  # 5

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

Run autoconf first:

cd mypackage && autoconf && ./configure

πŸ”—β€œcould not find function” in R

Ensure the function is:

  1. Marked pub
  2. Has #[miniextendr] attribute

Then rebuild: ./configure && R CMD INSTALL .

πŸ”—Compilation Errors

Check src/rust/Cargo.toml for dependency issues. Run:

cd src/rust && cargo check

πŸ”—Next Steps