Full Project Tutorial
Walk through the pigworld model project from package skeleton to Rust runtime, configure glue, and generated R wrappers.
This guide uses tests/model_project/ as a concrete end-to-end example. It is a more instructive reference than a minimal hello-world because it shows a full package with configure logic, a nested Rust crate, generated R wrappers, and a nontrivial Rust type exported to R.
🔗Why this project is useful
The model project is a scaffolded package called pigworld. It includes:
- package metadata in
DESCRIPTION,NAMESPACE, andR/ - a nested Rust crate in
src/rust/ - configure/bootstrap/build glue for the R package build
- generated R wrappers and roxygen output
- a real exported Rust-backed
Worldtype instead of a single toy function
It shows the whole shape of a package, not just one boundary crossing.
🔗Step 1: the package shell
Start with the R package files:
tests/model_project/DESCRIPTIONtests/model_project/NAMESPACEtests/model_project/R/pigworld-package.R
These establish the installed package name (pigworld), register the dynamic library, and provide the minimal R-side package shell that the generated wrappers plug into.
This layer should look familiar to an R package author. miniextendr does not replace the package shell; it fills in the Rust side and the wrapper generation workflow.
If the extra DESCRIPTION entries look unfamiliar, read DESCRIPTION Fields for miniextendr Packages. That page explains what the Config/build/*, SystemRequirements, roxygen, and testthat lines are doing there.
🔗Step 2: the Rust crate inside the package
The Rust code lives in:
tests/model_project/src/rust/Cargo.tomltests/model_project/src/rust/lib.rs
Cargo.toml shows the standard shape of a nested package-local Rust crate:
crate-type = ["staticlib"]for linking into the R package- a package-local standalone
[workspace] - dependency on vendored
miniextendr-api - optional features forwarded into the runtime crate
lib.rs shows a more realistic exported type than a trivial function. The project defines a World struct, derives ExternalPtr, and exports methods through #[miniextendr]. The file starts with the package entry point, then the struct and impl:
use miniextendr_api::miniextendr;
// Required: generates the R_init_pigworld entry point that R calls on package load.
miniextendr_api::miniextendr_init!(pigworld);
#[derive(miniextendr_api::ExternalPtr)]
pub struct World {
pigs: Vec<Pig>,
tick: u32,
next_id: u64,
rng: rand::rngs::StdRng,
food_per_tick: f64,
move_cost: f64,
reproduction_threshold: f64,
max_age: u32,
interaction_radius: f64,
}
#[miniextendr]
impl World {
pub fn new(
n_initial: i32,
food_per_tick: f64,
move_cost: f64,
reproduction_threshold: f64,
max_age: i32,
interaction_radius: f64,
seed: i32,
) -> Self { ... }
pub fn step(&mut self) { ... }
pub fn run(&mut self, steps: i32) { ... }
pub fn summary(&self) -> String { ... }
// ... plus get_tick, population, x_positions, y_positions, energies, ages, age_histogram
}
The key teaching point: the Rust side looks like an ordinary type with methods. The R-facing API is derived from it, not hand-written separately. Every pub fn in the impl block becomes an R method; every doc comment becomes an @param/@return entry in the generated .Rd file.
#[distributed_slice] registration is automatic. No miniextendr_module! macro or manual registration call is needed beyond miniextendr_init! at the top of lib.rs.
🔗Step 3: the build and configure glue
The files that make this a real R package build are:
tests/model_project/bootstrap.Rtests/model_project/configure.actests/model_project/configuretests/model_project/src/Makevars.intests/model_project/src/rust/cargo-config.toml.intests/model_project/src/stub.c
This layer answers the questions that a toy example usually skips:
- how
R CMD INSTALLfinds Cargo and Rust - how dev and CRAN-like modes are selected
- where cargo outputs go
- how package-local Rust config is generated
- how the static library gets linked into the final package library
bootstrap.R is especially useful to study because it shows how devtools workflows force a predictable configure mode before the package build starts.
Dev mode prerequisite. In the miniextendr monorepo, always run bash ./configure (via just configure) before any R CMD operation. Configure generates Makevars from .in templates and, in dev mode, syncs workspace crates into src/rust/vendor/. Skipping this step causes stale or missing build artifacts.
🔗Step 4: wrapper generation and R-facing methods
The generated R API lives in:
tests/model_project/R/pigworld-wrappers.Rtests/model_project/man/World.Rd
This is what the user eventually interacts with from R:
world <- World$new(
n_initial = 50L,
food_per_tick = 4,
move_cost = 1,
reproduction_threshold = 80,
max_age = 100L,
interaction_radius = 2,
seed = 42L
)
world$run(100L)
world$summary()
The wrappers are generated from the Rust impl block and Rust doc comments. The R methods, Rd entries, and method dispatch shape all come out of the Rust source.
Regenerating wrappers. Whenever you change a #[miniextendr] function, add a parameter, or edit a doc comment, regenerate the wrappers:
just configure && just rcmdinstall && just devtools-document
The generated file (R/pigworld-wrappers.R) and the documentation (man/World.Rd, NAMESPACE) must be committed together with the Rust changes that produced them.
Running tests. The package ships R tests in tests/testthat/. Run them with:
just devtools-test🔗Step 5: how to read this project as a tutorial
If you want to follow it as a step-by-step walkthrough, read it in this order:
DESCRIPTIONandNAMESPACEto understand the package shell.src/rust/Cargo.tomlto understand how the nested Rust crate is configured.src/rust/lib.rsto see the actual exported Rust API.bootstrap.Randconfigure.acto understand how package builds are wired.R/pigworld-wrappers.Rto see what miniextendr generates for R users.man/World.Rdto see the final user-facing documentation shape.
That sequence gives you a full-project mental model without having to infer the missing pieces from smaller examples.
🔗What this tutorial teaches better than a minimal example
Compared with a simple add(a, b) tutorial, the model project shows:
- an exported Rust-backed class instead of just free functions
- package bootstrap and configure behavior
- Cargo feature wiring in a package-local crate
- generated R method wrappers
- generated Rd output
- the actual shape of a scaffolded package you can compare against your own
🔗Where to go next
- Read Getting Started for the minimal first package flow.
- Read Packages if you want the repository-wide map of crates and R packages.
- Read Package Map for the detailed relationship between workspace crates, R packages, and fixtures.
- Read Class Systems for env, R6, S3, S4, S7, and vctrs class generation.
- Browse the API reference for per-crate rustdoc.
- See the GitHub issues for open work.