Getting Started with miniextendr
miniextendr lets you write R packages with Rust backends. From an empty R package to a callable Rust function is five steps. This skill walks a developer who knows R and basic Rust — but has never written R-Rust FFI — through those steps.
When to use this skill
- "I'm new to miniextendr, where do I start?"
- "Can I add Rust to an existing R package?"
- "What does the dev loop look like?"
- "Where do I put my Rust code?"
- "How do I scaffold a new package?"
- "What is the difference between miniextendr and minirextendr?"
- "What does
#[miniextendr]actually do?" - "How do I call my Rust function from R?"
Key concepts
miniextendr vs minirextendr
These are two separate packages with different audiences:
miniextendr (this framework) is the runtime and macro system your Rust code links against. It lives in
miniextendr-api/andminiextendr-macros/in this repo. End users add it as a Cargo dependency in their package'sCargo.toml.minirextendr is a pure-R scaffolding helper. It generates the directory structure,
configure.ac,Makevars.in, and boilerplate that wires an R package to its Rust backend. End users runminirextendr::use_miniextendr()once to set up a new package and never touch it again during normal development.
In short: minirextendr sets up the scaffolding; miniextendr is the framework the scaffolded Rust code uses at build and runtime.
Source mode vs tarball mode
During local development, the package builds in source mode: cargo resolves
miniextendr from git or a local path, and the configure script writes
appropriate [patch] or [source] entries in .cargo/config.toml.
When shipping to CRAN, you run just vendor to bundle all cargo dependencies
into rpkg/inst/vendor.tar.xz. The presence of that tarball is the single
signal that flips configure into offline (CRAN-safe) tarball mode.
For a full explanation of the latch and why it works this way, see the
miniextendr-architecture skill. For now: if you are just developing locally,
you never need to worry about this unless minirextendr_doctor() reports a
stale tarball.
What #[miniextendr] does
Placing #[miniextendr] on a pub fn does three things automatically:
- Generates a
extern "C-unwind"C wrapper that receives SEXPs, converts them to Rust types viaTryFromSexp, calls your function, and converts the return value back to an SEXP viaIntoR. - Emits an entry into the
MX_CALL_DEFSdistributed_slice so the C wrapper is registered with R at package load time viaR_init_<pkg>. - Emits an entry into the
MX_R_WRAPPERSdistributed_slice so that a corresponding.Call(C_my_fn, ...)R wrapper is generated intoR/miniextendr-wrappers.Rat build time.
You write plain Rust. The macro handles all the FFI plumbing.
How it works: the five-step walkthrough
Step 1: Create the package skeleton
If you are starting from scratch, use the scaffolding helper:
install.packages("minirextendr") # or devtools::install_github("...")
library(minirextendr)
use_miniextendr()
Run this from your new package's root directory (which should already have a
DESCRIPTION file — create it first with usethis::create_package("mypkg")
if needed).
use_miniextendr() writes the following into your package:
src/Makevars.in— build configuration template.configurewritessrc/rust/.cargo/config.tomlinline per install mode (no.intemplate for that file).configure.ac— the autoconf source for install-mode detection.configure— the generated configure script (viaautoconf).src/rust/Cargo.toml— a Rust crate depending onminiextendr-apiandminiextendr-macros.src/rust/src/lib.rs— a minimal Rust library withminiextendr_init!.src/stub.c— the minimal C file that R's build system requires.
The key entry point is minirextendr/R/create.R. If you want to see what the
scaffolded package looks like before running the function, the canonical live
example is rpkg/ in this repository.
Step 2: Add a Rust function
Open src/rust/src/lib.rs (inside your R package's source tree) and add:
use miniextendr_api::miniextendr;
/// Add two integers together.
///
/// @param x First integer.
/// @param y Second integer.
/// @return Integer sum.
/// @export
#[miniextendr]
pub fn add_integers(x: i32, y: i32) -> i32 {
x + y
}
A few rules:
- The function must be
pub. - The module containing the function must be reachable via
modfromlib.rs. - Argument and return types must implement
TryFromSexpandIntoRrespectively. Common types (integers, floats, strings, logical values, vectors) all work out of the box. - The roxygen comment above the function is real: the macros pick it up and include it in the generated R wrapper.
For the simplest live example, look at the small helper functions in
rpkg/src/rust/lib.rs around the small_vec_copy and large_vec_altrep
functions.
Step 3: Build and install
For local development:
bash ./configure # generates Makevars and .cargo/config.toml
R CMD INSTALL . # compiles Rust, generates R wrappers, installs
Or, if you are working inside the miniextendr monorepo:
just configure # wraps bash ./configure with workspace settings
just rcmdinstall # R CMD INSTALL with correct flags
just force-document # run roxygen2 to regenerate NAMESPACE and man/
The just recipes are for maintainers of this repo. If you are scaffolding
your own separate R package, use plain bash ./configure && R CMD INSTALL ..
Important: always use bash ./configure, not ./configure. The script uses
#!/bin/sh as its shebang, and AC_CONFIG_COMMANDS passthrough produces
spurious errors under that shell. Invoking with explicit bash avoids the
problem on every platform.
Step 4: Call from R and iterate
After installing, load the package and call your function:
library(mypkg)
add_integers(3L, 4L)
# [1] 7
The iteration loop for Rust changes is:
- Edit
.rssource files. bash ./configure && R CMD INSTALL .(orjust configure && just rcmdinstall).- If you added or removed
#[miniextendr]functions, also regenerate wrappers: in the monorepo runjust force-document; in a standalone package runRscript -e 'devtools::document()'after install. - Restart R (or reload the package) and test.
Generated files (R/miniextendr-wrappers.R, NAMESPACE, man/*.Rd) must be
committed in the same PR as the Rust changes that produced them. The pre-commit
hook in this repo enforces this for wrappers.R.
Step 5: Build a tarball for CRAN
just vendor # bundles all cargo dependencies into inst/vendor.tar.xz
R CMD build . # produces the tarball
R CMD check mypkg_*.tar.gz --as-cran
just vendor is the maintainer-only step. It calls cargo-revendor to produce
a vendor/ tree, compresses it, and places it at inst/vendor.tar.xz. Once
that file is present, configure automatically switches to offline tarball mode
for any subsequent R CMD INSTALL or check.
The tarball recipes in the justfile trap-clean inst/vendor.tar.xz on exit so
it does not linger after a build. If it does linger (for example, after a
failed build), run just clean-vendor-leak to remove it.
For a detailed account of the CRAN compatibility requirements, see
docs/CRAN_COMPATIBILITY.md.
Decision trees
I want to create a new package vs add Rust to an existing one
Starting from scratch:
usethis::create_package("mypkg")(creates the R package skeleton).minirextendr::use_miniextendr()from the package root (adds Rust scaffolding).- Write Rust functions with
#[miniextendr]. bash ./configure && R CMD INSTALL ..
Adding Rust to an existing R package:
- From the package root, run
minirextendr::use_miniextendr(). It detects an existingDESCRIPTIONand adds scaffolding without overwriting your existing R code. - The scaffolded
Makevars.inandconfigure.accoexist with any existingsrc/files. - Same build steps as above.
I am shipping to CRAN vs internal use only
For internal use:
- No vendoring needed. Source mode (no
inst/vendor.tar.xz) is the default. - Users install via
remotes::install_github()or similar. Cargo fetches dependencies from crates.io or git at install time.
For CRAN submission:
- Run
just vendorbeforeR CMD build. This bundles all dependencies. - Check the built tarball:
R CMD check mypkg_*.tar.gz --as-cran. Always check the tarball, not the source directory — the source directory skipsAuthors@RtoAuthor/Maintainerconversion and misses real CRAN failures. - See
docs/CRAN_COMPATIBILITY.mdfor the full set of requirements.
Key files
minirextendr/R/create.R—use_miniextendr()andcreate_miniextendr_package()entry points.rpkg/src/rust/lib.rs— the canonical live example of a miniextendr Rust crate. The simplest exported functions near thesmall_vec_copyregion show the minimal#[miniextendr] pub fnpattern.rpkg/configure.ac— the per-install entrypoint that detects source vs tarball mode and writes the cargo config.rpkg/src/Makevars.in— the Makevars template that drives the cdylib to staticlib double-link build.docs/CRAN_COMPATIBILITY.md— vendoring requirements, offline build verification, and CRAN submission checklist.
Common pitfalls
bash ./configure, not./configure: the script uses#!/bin/sh, andAC_CONFIG_COMMANDSpassthrough produces spurious errors under that shell. Always usebash ./configureor run thejust configurerecipe.justis maintainer-only: thejustrecipes are for working inside this repository. End-user packages must build viaconfigure.ac,tools/*.R, and standard R mechanisms (R CMD INSTALL,devtools::install()). Never requirejustin scaffolded package instructions.Generated wrappers must be committed:
R/miniextendr-wrappers.Ris generated duringR CMD INSTALL. If you add or remove#[miniextendr]functions, regenerate and commit this file in the same PR. The pre-commit hook in this repo blocks commits where*-wrappers.Ris staged without a matching updatedNAMESPACE.Install-mode latch leak: if
inst/vendor.tar.xzis present during local development (left over from a previousR CMD buildthat did not clean up), configure writes vendored mode and your edits to miniextendr workspace crates have no effect. Runjust clean-vendor-leakor delete the file manually. Useminirextendr_doctor()to detect the condition automatically.Modules must be reachable from
lib.rs: if you add a new.rsfile with#[miniextendr]functions, you must addmod my_module;tolib.rs. Functions in unreachable modules are silently excluded from registration.configure.acmust not be called viaminirextendr::*: configure-time helpers belong intools/*.Rand are invoked viaRscript tools/foo.R. Do not call minirextendr functions from configure — configure runs in a minimal environment without the R library available.
Related skills
miniextendr-architecture— how the cdylib to staticlib double-link works, the distributed_slice registration system, and the install-mode latch in depth.miniextendr-build— configure.ac, Makevars.in, vendor pipeline, and the justfile recipes.miniextendr-scaffolding— minirextendr templates, template sync workflow,minirextendr_doctor(), and upgrade mechanics.miniextendr-conversions—TryFromSexp,IntoR,Coerce, NA handling, and the full type conversion matrix.miniextendr-macros— deep dive into what#[miniextendr]generates and how to use advanced attributes.
Source: A2-ai/miniextendr — distributed by TomeVault.