Reference page
Native Runtime
miniR's native story is not only about compiling package src/ trees. It also includes a Rust implementation of a large slice of the R C API, callback plumbing back into the interpreter, and an error boundary that can survive longjmp-style native failures.
miniR's native story is not only about compiling package src/ trees. It also includes a Rust implementation of a large slice of the R C API, callback plumbing back into the interpreter, and an error boundary that can survive longjmp-style native failures.
The Core Idea
Package shared libraries resolve symbols such as Rf_allocVector, Rf_eval, Rf_findVar, and R_registerRoutines against the miniR process itself. That means miniR needs an in-process R ABI surface, not just a loader.
The center of that surface is src/interpreter/native/runtime.rs.
What Lives In The Native Runtime
The native runtime provides:
- exported sentinel globals such as
R_NilValue,R_BaseEnv, andR_GlobalEnv - many
extern "C"functions implementing the R C API - native invocation state for the current thread
- callbacks that let native code ask miniR to evaluate, look up, define, or parse objects
- conversion helpers between
RValueandSEXP
This is what lets loaded packages treat miniR like an R runtime instead of like an unrelated host program.
Thread-Local Native State
During native execution, miniR keeps thread-local runtime state for:
- allocation tracking
- a protect stack
- interpreter callbacks
- temporary language-object stashes used when parsing or evaluating across the boundary
That state is scoped to native entry points and cleared afterwards. The goal is to support reentrant native calls without leaking interpreter state across sessions.
Crossing The .Call() Boundary
The rough .Call() flow is:
native/dll.rsresolves the symbol in a loaded shared library.- miniR installs interpreter callbacks into the native runtime.
- Rust-side values are converted into
SEXP. - The native function runs.
- Returned
SEXPvalues are converted back intoRValue. - Temporary callbacks and runtime state are cleared.
That same layer also handles registered routines and native error propagation.
Global Initialization
There are two important setup steps before package native code can behave sanely:
init_globals()seeds sentinel SEXPs and well-known symbols.init_global_envs()maps the current interpreter's base and global environments toR_BaseEnvandR_GlobalEnv.
Without those mappings, package init code sees meaningless pointers instead of a usable runtime environment.
Errors And Longjmp Boundaries
Rf_error()-style control flow is not implemented as a pure Rust unwind. miniR uses a C trampoline around the setjmp/longjmp boundary and then converts the failure back into a Rust-side error.
That bridge is important because it allows all of these at once:
- native errors unwinding safely back into miniR
- R tracebacks and native backtraces being captured in the same failure
- native code re-entering the interpreter through callbacks
This is one of the sharpest parts of the runtime and should stay explicit in the code.
Routine Registration
Real packages do not always rely on a bare exported symbol table. Many register routines during package init, and some depend on C-callable lookup helpers.
That is why miniR includes support for things like:
R_registerRoutines()- registered
.Call()and.External()entry points - C-callable lookup across package boundaries
If registered routines fail while plain exported symbols work, the problem is in runtime bookkeeping, not in the compiler step.
Native ABI Coverage
The runtime covers a wide surface area, including:
- allocation and protection
- attribute access and mutation
- symbols and environments
- evaluation helpers
- coercion and type predicates
- strings and character translation
- RNG hooks
- connections and parts of graphics-facing ABI
- Rmath-facing entry points
The important point is not the raw count. The important point is that the runtime needs enough coverage for real packages to survive contact with the interpreter.
Backtraces Are Part Of The Native Story
miniR couples native runtime support to stack unwinding support so native failures are debuggable:
object,gimli, andaddr2lineresolve raw instruction pointers- the captured frames are stitched into the higher-level error story
- package authors can see both the R stack and the native call chain
That makes native support materially more useful than a loader that only reports "symbol call failed".
Where To Debug Native Failures
Start in the native runtime when the symptom looks like:
- a package shared library loads but C API calls crash or misbehave
Rf_evalor environment lookups return nonsense from native codeR_BaseEnvorR_GlobalEnvappears unsetRf_error()does not unwind cleanly- registered routines exist but cannot be called
- native backtraces disappear or resolve badly
Those are runtime integration bugs, not just build-system bugs.