Expand description
ExternalPtr<T> — a Box-like owned pointer that wraps R’s EXTPTRSXP.
This provides ownership semantics similar to Box<T>, with the key difference
that cleanup is deferred to R’s garbage collector via finalizers.
§Submodules
| Module | Contents |
|---|---|
altrep_helpers | ALTREP data1/data2 slot access helpers + Sidecar marker type |
§Core Types
ExternalPtr<T>— owned pointer wrapping EXTPTRSXPTypedExternal— trait for type-safe identification across packagesExternalSlice<T>— helper for slice data in external pointersErasedExternalPtr— type-erasedExternalPtr<()>aliasIntoExternalPtr— conversion trait for wrapping values
PartialEq/PartialOrd compare the pointee values (like Box<T>). Use
ptr_eq when you care about pointer identity, and as_ref()/as_mut() for
explicit by-value comparisons.
§Protection Strategies in miniextendr
miniextendr provides three complementary protection mechanisms for different scenarios:
| Strategy | Module | Lifetime | Release Order | Use Case |
|---|---|---|---|---|
| PROTECT stack | gc_protect | Within .Call | LIFO (stack) | Temporary allocations |
| VECSXP pool | protect_pool | Across .Calls | Any order | Long-lived R objects |
| R ownership | ExternalPtr | Until R GCs | R decides | Rust data owned by R |
§When to Use ExternalPtr
Use ExternalPtr (this module) when:
- You want R to own a Rust value
- The Rust value should be dropped when R garbage collects the pointer
- You’re exposing Rust structs to R code
Use gc_protect instead when:
- You’re allocating temporary R objects during computation
- Protection is short-lived (within a single
.Call)
Use ProtectPool instead when:
- You need R objects (not Rust values) to survive across
.Calls - You need arbitrary-order release of protections
§How ExternalPtr Protection Works
┌─────────────────────────────────────────────────────────────────┐
│ ExternalPtr<MyStruct>::new(value) │
│ ├── Rf_protect() during construction (temporary) │
│ ├── R_MakeExternalPtr() creates EXTPTRSXP │
│ ├── R_RegisterCFinalizerEx() registers cleanup callback │
│ ├── pool.insert() roots it for the Rust handle's lifetime │
│ └── Rf_unprotect() after construction complete │
│ │
│ Held in Rust (even across other R allocations, e.g. in a Vec) │
│ └── stays alive — the pool's GC-traced VECSXP slot roots it │
│ │
│ Return to R → R now also references the EXTPTRSXP │
│ └── Rust handle drops → pool.release(key) drops the root, │
│ but R's own reference keeps it live │
│ │
│ R GC runs (no refs left) → finalizer (release_any) frees value │
└─────────────────────────────────────────────────────────────────┘Owning handles (new / from_raw / Clone) root their EXTPTRSXP in a
process-wide ProtectPool so they survive R
allocations while held in Rust; borrowed views (wrap_sexp / from_sexp
/ reborrow) take no root — the object is kept alive by whatever R-side
reference handed it to them. The pool (O(1) any-order release) is used
rather than R_PreserveObject because a Vec<ExternalPtr> releases its
roots front-to-back, the O(n²) worst case for R_ReleaseObject’s
precious-list scan — see analysis/gc-protection-benchmarks-results.md.
When the end goal is an R list() of external pointers (rather than a
Vec<ExternalPtr> you keep working with in Rust), prefer
ExternalPtr::collect_into_r_list — it builds each
EXTPTRSXP straight into the protected result list, so the list roots every
element and the pool is never touched at all.
§Type Identification
Type safety is enforced via Any::downcast (Rust’s TypeId). R symbols
in the tag and prot slots are retained for display and error messages
but are never authoritative for downcast safety — the Any vtable is.
Internally, data is stored as Box<Box<dyn Any>> — a thin pointer (fits
in R’s R_ExternalPtrAddr) pointing to a fat pointer (carries the Any
vtable for runtime downcasting). The outer Box keeps the heap address
stable so ExternalPtr::cached_ptr can be cached
once at construction.
The tag slot holds a symbol (type name, for display).
The prot slot holds a VECSXP (list) with two elements:
- Index 0: SYMSXP (interned type ID symbol, for error messages)
- Index 1: User-protected SEXP slot (for preventing GC of R objects)
§TYPE_NAME_CSTR vs TYPE_ID_CSTR
TypedExternal exposes two associated constants with distinct roles —
mixing them up does not break type safety (Any::downcast is the real
gate) but produces noisy diagnostics.
| Constant | Role | Visible to R as | Authoritative? |
|---|---|---|---|
TYPE_NAME_CSTR | Display tag | class() / print() | No |
TYPE_ID_CSTR | Error-message identifier on downcast failure | Stored in prot[0] | No (cosmetic; downcast uses TypeId) |
#[derive(ExternalPtr)] fills both with sensible defaults; only override
manually when implementing TypedExternal by hand.
§Pointer provenance for cached_ptr
ExternalPtr caches the data pointer at construction so as_ref /
as_mut avoid an FFI call on every access. The cached *mut T must
be derived from a mutable path so writes through as_mut are sound under
Stacked Borrows:
Box::into_raw(Box::new(value))— preferred (the constructor path).&mut T— when you already hold an exclusive reference.<Box<dyn Any>>::downcast_mut::<T>()— when extracting from the inner box.std::ptr::from_mut— when promoting a&mut Tto a raw pointer.
Caching a pointer derived from &T or downcast_ref::<T>() is UB
the moment anything writes through it. Internal sites that touch
cached_ptr are audited; the rule matters for the (rare) hand-rolled
TypedExternal impl that bypasses ExternalPtr::new.
§See also
crate::altrep— when the alternative (an ALTREP class) makes more sense thanExternalPtr.
§ExternalPtr is Not an R Native Type
Unlike R’s native atomic types (integer, double, character, etc.),
external pointers cannot be coerced to vectors or used in R’s vectorized
operations. This is an R limitation, not a miniextendr limitation:
> matrix(new("externalptr"), 1, 1)
Error in `as.vector()`:
! cannot coerce type 'externalptr' to vector of type 'any'If you need your Rust type to participate in R’s vector/matrix operations,
consider implementing IntoList (via #[derive(IntoList)])
to convert your struct to a named R list, or use ALTREP to expose Rust
iterators as lazy R vectors.
Re-exports§
pub use altrep_helpers::*;
Modules§
- altrep_
helpers 🔒 - ALTREP helpers for
ExternalPtr— data1/data2 slot access.
Structs§
- Abort
IfUnwinding 🔒 - Guard that aborts the process if dropped while a panic is in progress.
- External
Ptr - An owned pointer stored in R’s external pointer SEXP.
- External
Slice - A slice stored as a standalone struct, suitable for wrapping in ExternalPtr.
Enums§
- Type
Mismatch Error - Error returned when type checking fails in
try_from_sexp_with_error.
Constants§
- EXTPTR_
ROOTS 🔒 - PROT_
TYPE_ 🔒ID_ INDEX - Index of the type SYMSXP contained in the
prot(aVECSXPlist) - PROT_
USER_ 🔒INDEX - Index of user-protected objects contained in the
prot(aVECSXPlist) - PROT_
VEC_ 🔒LEN - Length of the
protlist (VECSXP)
Traits§
- Into
External Ptr - Marker trait for types that should be converted to R as ExternalPtr.
- Typed
External - Trait for types that can be stored in an ExternalPtr.
Functions§
- drop_
catching_ 👻panic - Run a destructor closure, aborting the process if the closure panics.
- env_
binding 🔒 ⚠ - Look up a variable bound directly in a single environment frame (no search
of enclosing frames — this is
Rf_findVarInFrame, notRf_findVar). - is_
type_ 🔒erased - release_
any 🔒 - Non-generic C finalizer called by R’s garbage collector.
- root_
owned 🔒 - Root an owning handle’s
EXTPTRSXPin the main-thread pool. - sendable_
any_ 🔒ptr_ into_ ptr - Get the raw pointer, consuming the sendable wrapper.
- sendable_
any_ 🔒 ⚠ptr_ new - Create a new sendable pointer from a raw
*mut Box<dyn Any>. - symbol_
name 🔒 - Get the type name from a stored symbol SEXP.
- type_
id_ 🔒 ⚠symbol - Get the namespaced type ID symbol for type checking.
- type_
id_ 🔒 ⚠symbol_ unchecked - Unchecked version of
type_id_symbol. - type_
symbol 🔒 ⚠ - Get the interned R symbol for a type’s name.
- type_
symbol_ 🔒 ⚠unchecked - Unchecked version of
type_symbol- no thread safety checks. - unroot_
owned 🔒 - Release an owning handle’s pool root. Stale keys are a safe no-op.
- unwrap_
class_ 🔒 ⚠handle - Attempt to unwrap a class-wrapped handle down to the bare
EXTPTRSXPit carries, soExternalPtr::<T>argument conversion accepts the ergonomic class handle (e.g.Foo$new(...)) in addition to the raw pointer returned by a low-level constructor (audit finding A9 —audit/2026-07-03-api-sense-conversions-dataframe-errors.md#5).
Type Aliases§
- Erased
External Ptr - Type-erased
ExternalPtrfor cases where the concreteTis not needed. - Sendable
AnyPtr 🔒 - A wrapper around a raw pointer that implements
Send.