Reference page
ExternalPtr
ExternalPtr<T> is a Box-like owned pointer that wraps R's EXTPTRSXP. It lets you hand ownership of Rust-allocated data to R and let R's garbage collector decide when to drop it.
ExternalPtr<T> is a Box-like owned pointer that wraps Rβs EXTPTRSXP. It lets you hand ownership of Rust-allocated data to R and let Rβs garbage collector decide when to drop it.
Source: miniextendr-api/src/externalptr.rs
πWhy ExternalPtr Exists
R has no native way to hold arbitrary Rust data. EXTPTRSXP is Rβs mechanism for storing opaque C pointers, but it provides no type safety, no RAII cleanup, and no protection against use-after-free. ExternalPtr<T> wraps EXTPTRSXP with:
- Type-safe access via
TypedExternaltrait and R symbol comparison - Automatic cleanup via R GC finalizer that calls
Drop - Box-like API (
Deref,DerefMut,Clone,into_inner,into_raw,pin, etc.) - Thread-safe construction β
new()routes R API calls to the main thread when called off-thread (e.g., with theworker-threadfeature)
πWhen to Use ExternalPtr
| Strategy | Lifetime | Use Case |
|---|---|---|
ExternalPtr | Until R GCs | Rust data owned by R (structs returned to R) |
ProtectScope | Within .Call | Temporary R allocations |
| Preserve list | Across .Calls | Long-lived R objects (not Rust values) |
Use ExternalPtr when you want R to own a Rust value and drop it when R garbage-collects the pointer. This is the standard mechanism for exposing Rust structs to R code.
πCreating an ExternalPtr
πWith #[derive(ExternalPtr)] (recommended)
The derive macro implements TypedExternal and IntoExternalPtr, so returning your struct from a #[miniextendr] function automatically wraps it:
#[derive(ExternalPtr)]
pub struct MyData {
pub value: f64,
}
#[miniextendr]
pub fn create_data(v: f64) -> MyData {
MyData { value: v } // Automatically wrapped in ExternalPtr
}πManual construction
let ptr = ExternalPtr::new(MyData { value: 3.14 });
new() works from any thread β if called off the main thread (e.g., from the worker thread with the worker-thread feature), R API calls are automatically dispatched to the main thread via with_r_thread.
πUnchecked construction (ALTREP callbacks, main-thread-only code)
// SAFETY: must be on R's main thread
let ptr = unsafe { ExternalPtr::new_unchecked(MyData { value: 3.14 }) };
Skips thread safety assertions for performance-critical paths.
πFrom raw pointers
let raw = Box::into_raw(Box::new(MyData { value: 1.0 }));
// SAFETY: raw was allocated by Box, is non-null, caller transfers ownership
let ptr = unsafe { ExternalPtr::from_raw(raw) };πAccessing the Value
ExternalPtr<T> implements Deref<Target = T> and DerefMut, so you can use it like a reference:
let ptr = ExternalPtr::new(MyData { value: 3.14 });
println!("{}", ptr.value); // Deref to &MyData
Explicit access methods:
| Method | Returns | Notes |
|---|---|---|
as_ref() | Option<&T> | Always Some for valid ptrs |
as_mut() | Option<&mut T> | Always Some for valid ptrs |
as_ptr() | *const T | Raw pointer, no ownership transfer |
as_sexp() | SEXP | The underlying R object |
πConsuming and Dropping
| Method | Effect |
|---|---|
into_inner(this) | Moves value out, deallocates, neutralizes finalizer |
into_raw(this) | Returns *mut T, neutralizes finalizer, caller owns memory |
leak(this) | Returns &'a mut T, memory is never freed |
Rβs GC finalizer handles cleanup when the ExternalPtr goes out of scope in Rust without being explicitly consumed. The Rust Drop impl is a no-op to avoid double-free.
πType Identification with TypedExternal
Every ExternalPtr<T> requires T: TypedExternal. This trait provides two identifiers stored in the SEXP:
TYPE_NAME_CSTRβ Short display name, stored in thetagslot (visible when printing in R)TYPE_ID_CSTRβ Namespaced identifier (crate@version::module::Type), stored inprot[0]for type checking
Type checking uses Rβs interned symbols (Rf_install), which enables fast pointer comparison rather than string comparison.
πImplementing TypedExternal
Via derive (recommended):
#[derive(ExternalPtr)]
pub struct MyData { /* ... */ }
Via macro:
impl_typed_external!(MyData);
// also works for generic types:
impl_typed_external!(MyWrapper<i32>);
Manually:
impl TypedExternal for MyData {
const TYPE_NAME: &'static str = "MyData";
const TYPE_NAME_CSTR: &'static [u8] = b"MyData\0";
const TYPE_ID_CSTR: &'static [u8] =
concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION"),
"::", module_path!(), "::MyData\0").as_bytes();
}πBuilt-in TypedExternal Implementations
Source: miniextendr-api/src/externalptr_std.rs
The following standard library types have built-in TypedExternal impls, so they can be stored in ExternalPtr<T> without any manual implementation:
| Category | Types |
|---|---|
| Primitives | bool, char, i8βi128, isize, u8βu128, usize, f32, f64 |
| Strings | String, CString, OsString, PathBuf |
| Collections | Vec<T>, VecDeque<T>, LinkedList<T>, BinaryHeap<T>, HashMap<K,V>, BTreeMap<K,V>, HashSet<T>, BTreeSet<T> |
| Smart pointers | Box<T>, Box<[T]>, Rc<T>, Arc<T>, Cell<T>, RefCell<T>, UnsafeCell<T>, Mutex<T>, RwLock<T>, OnceLock<T>, Pin<T>, ManuallyDrop<T>, MaybeUninit<T>, PhantomData<T> |
| Option/Result | Option<T>, Result<T, E> |
| Ranges | Range<T>, RangeInclusive<T>, RangeFrom<T>, RangeTo<T>, RangeToInclusive<T>, RangeFull |
| I/O | File, BufReader<R>, BufWriter<W>, Cursor<T> |
| Time | Duration, Instant, SystemTime |
| Networking | TcpStream, TcpListener, UdpSocket, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6 |
| Threading | Thread, JoinHandle<T>, Sender<T>, SyncSender<T>, Receiver<T>, Barrier, BarrierWaitResult |
| Atomics | AtomicBool, AtomicI8βAtomicI64, AtomicIsize, AtomicU8βAtomicU64, AtomicUsize |
| Numeric wrappers | NonZeroI8βNonZeroI128, NonZeroIsize, NonZeroU8βNonZeroU128, NonZeroUsize, Wrapping<T>, Saturating<T> |
| Tuples | (A,) through (A, B, C, D, E, F, G, H, I, J, K, L) (1β12 elements) |
| Arrays | [T; N] (const generic, any size) |
| Static slices | &'static [T], &'static mut [T] |
Note on generic types: For generic types like Vec<T>, the type name does not include the type parameter (e.g., Vec<i32> and Vec<String> both have type name "Vec"). R-level type checking wonβt distinguish between different instantiations. For stricter type safety, create a newtype wrapper and derive ExternalPtr.
Note on ManuallyDrop<T>: Shares Tβs type symbols, allowing ExternalPtr<ManuallyDrop<T>> to interoperate with ExternalPtr<T>. This is safe because ManuallyDrop<T> is #[repr(transparent)].
Note on static slices: &'static [T] and &'static mut [T] are fat pointers (ptr + len) that satisfy 'static + Sized, so they can be stored directly in ExternalPtr. Use cases include const arrays (&DATA), leaked data (Box::leak), and memory-mapped files.
πIntoExternalPtr
The IntoExternalPtr marker trait triggers a blanket IntoR implementation that wraps the value in ExternalPtr<T> when returning from #[miniextendr] functions. #[derive(ExternalPtr)] implements both TypedExternal and IntoExternalPtr.
πCross-Package Safety
The TYPE_ID_CSTR format (crate@version::module::Type) ensures:
- Same type from same crate+version: compatible (can share ExternalPtr)
- Same type name from different crates: incompatible (different crate prefix)
- Same type from different crate versions: incompatible (different version)
When wrapping a SEXP, wrap_sexp() compares the stored symbol pointer against the expected symbol pointer. A mismatch returns None (or TypeMismatchError from wrap_sexp_with_error).
For cross-package trait dispatch, see Trait ABI.
πType-Erased Pointers (ErasedExternalPtr)
pub type ErasedExternalPtr = ExternalPtr<()>;
ErasedExternalPtr wraps any EXTPTRSXP without checking the stored type. Useful for:
- Inspecting the stored type name before downcasting
- Working with external pointers from unknown sources
let erased = unsafe { ErasedExternalPtr::from_sexp(some_sexp) };
// Check what type is stored
if erased.is::<MyData>() {
let data: &MyData = erased.downcast_ref::<MyData>().unwrap();
}
// Or read the stored type name
if let Some(name) = erased.stored_type_name() {
println!("stored type: {}", name);
}
Methods on ErasedExternalPtr:
| Method | Returns |
|---|---|
is::<T>() | bool β does the stored type match T? |
downcast_ref::<T>() | Option<&T> |
downcast_mut::<T>() | Option<&mut T> |
stored_type_name() | Option<&'static str> |
πExternalSlice
ExternalSlice<T> stores a Vec<T> as a raw pointer + length + capacity, suitable for wrapping in ExternalPtr:
impl_typed_external!(ExternalSlice<f64>);
let data = vec![1.0, 2.0, 3.0];
let ptr = ExternalPtr::new(ExternalSlice::new(data));
assert_eq!(ptr.as_slice(), &[1.0, 2.0, 3.0]);
This is useful when you need R to own a Rust slice and access it by index (e.g., in ALTREP elt callbacks).
| Method | Returns |
|---|---|
new(vec) | Creates from Vec<T> |
from_boxed(boxed) | Creates from Box<[T]> |
as_slice() | &[T] |
as_mut_slice() | &mut [T] |
len() / is_empty() | Length queries |
πALTREP data1/data2 Helpers
ALTREP objects have two data slots (data1, data2). These helpers extract typed ExternalPtrs from those slots:
// In an ALTREP callback:
fn length(x: SEXP) -> R_xlen_t {
match unsafe { altrep_data1_as::<MyAltrepData>(x) } {
Some(ext) => ext.data.len() as R_xlen_t,
None => 0,
}
}| Function | Description |
|---|---|
altrep_data1_as::<T>(x) | Extract data1 as ExternalPtr<T> with type check |
altrep_data1_as_unchecked::<T>(x) | Same, skips thread safety assertions |
altrep_data2_as::<T>(x) | Extract data2 as ExternalPtr<T> with type check |
altrep_data2_as_unchecked::<T>(x) | Same, skips thread safety assertions |
altrep_data1_mut::<T>(x) | Mutable &'static mut T reference from data1 |
altrep_data1_mut_unchecked::<T>(x) | Same, skips thread safety assertions |
The _unchecked variants are for performance-critical ALTREP callbacks where you are guaranteed to be on the main thread.
πRSidecar (R Data Fields)
RSidecar is a zero-sized marker type that enables R-facing getter/setter generation for struct fields annotated with #[r_data]:
#[derive(ExternalPtr)]
pub struct MyType {
pub x: i32,
#[r_data]
r: RSidecar, // Enables R wrapper generation
#[r_data]
pub count: i32, // Generates MyType_get_count() / MyType_set_count()
#[r_data]
pub name: String, // Generates MyType_get_name() / MyType_set_name()
}
Only pub fields with #[r_data] get R wrapper functions. Supported field types: SEXP, i32, f64, bool, u8, and any type implementing IntoR.
πSEXP Layout
The internal layout of an ExternalPtr-created EXTPTRSXP:
EXTPTRSXP
addr β *mut T (heap-allocated Rust value)
tag β SYMSXP (TYPE_NAME_CSTR, for display)
prot β VECSXP[2]
[0] β SYMSXP (TYPE_ID_CSTR, for type checking)
[1] β user-protected SEXP (set via set_protected)
The prot slot holds a two-element list. Slot 0 is the namespaced type ID symbol for fast pointer-based type comparison. Slot 1 is available for user-protected R objects that should be kept alive alongside the pointer.
πThread Safety
ExternalPtr is Send when T: Send, allowing it to be transferred between threads. All R API calls are serialized through the main thread. Concurrent access is not supported β Rβs runtime is single-threaded.
πTrait Implementations
ExternalPtr<T> mirrors Box<T>βs trait implementations:
Deref<Target = T>/DerefMutAsRef<T>/AsMut<T>/Borrow<T>/BorrowMut<T>Clone(deep clone, whenT: Clone)Default(whenT: Default)Debug/Display/PointerPartialEq/Eq/PartialOrd/Ord/Hash(compare pointee values)Iterator/DoubleEndedIterator/ExactSizeIterator/FusedIteratorFrom<T>/From<Box<T>>Pinsupport viapin(),pin_unchecked(),into_pin()