Skip to main content

Module externalptr

Module externalptr 

Source
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

ModuleContents
altrep_helpersALTREP data1/data2 slot access helpers + Sidecar marker type

§Core Types

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:

StrategyModuleLifetimeRelease OrderUse Case
PROTECT stackgc_protectWithin .CallLIFO (stack)Temporary allocations
VECSXP poolprotect_poolAcross .CallsAny orderLong-lived R objects
R ownershipExternalPtrUntil R GCsR decidesRust 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.

ConstantRoleVisible to R asAuthoritative?
TYPE_NAME_CSTRDisplay tagclass() / print()No
TYPE_ID_CSTRError-message identifier on downcast failureStored 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 T to 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 than ExternalPtr.

§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§

AbortIfUnwinding 🔒
Guard that aborts the process if dropped while a panic is in progress.
ExternalPtr
An owned pointer stored in R’s external pointer SEXP.
ExternalSlice
A slice stored as a standalone struct, suitable for wrapping in ExternalPtr.

Enums§

TypeMismatchError
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 (a VECSXP list)
PROT_USER_INDEX 🔒
Index of user-protected objects contained in the prot (a VECSXP list)
PROT_VEC_LEN 🔒
Length of the prot list (VECSXP)

Traits§

IntoExternalPtr
Marker trait for types that should be converted to R as ExternalPtr.
TypedExternal
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, not Rf_findVar).
is_type_erased 🔒
release_any 🔒
Non-generic C finalizer called by R’s garbage collector.
root_owned 🔒
Root an owning handle’s EXTPTRSXP in 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 EXTPTRSXP it carries, so ExternalPtr::<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§

ErasedExternalPtr
Type-erased ExternalPtr for cases where the concrete T is not needed.
SendableAnyPtr 🔒
A wrapper around a raw pointer that implements Send.