pub(crate) unsafe fn unwrap_class_handle(sexp: SEXP) -> Option<SEXP>Expand description
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).
Tries, in order:
- Env / R6: a direct
.ptrbinding onsexpitself — most#[miniextendr(env)]classes are actually a bare classedEXTPTRSXP(the generated constructor doesclass(.val) <- "T"directly on the pointer returned by Rust, seeenv_class.rs), which already satisfies the plainEXTPTRSXPcheck and never reaches this function, but a user-authored environment that binds.ptris covered here too. Then the R6 handle chain.__enclos_env__->private->.ptr(R6 objects are the public environment;privateonly hangs off the enclosing environment stored at.__enclos_env__forportableclasses, the default — seer6_class.rs). - S4: the
ptrslot viamethods::slot()(crate::s4_helpers::s4_get_slot). Guarded byisS4(), which excludes S7 objects even though both share theS4SXP/OBJSXPSEXPTYPE— S7’snew_object(S7_object(), ...)base never sets the S4 bit. - Anything else carrying a
.ptrattribute: S7 stores properties as plain attributes on its base object (sees7_class.rs), soRf_getAttrib(x, ".ptr")recovers the pointer without going through S7’s@/prop()dispatch machinery.
Returns Some(inner) only when the unwrapped value is itself an
EXTPTRSXP — anything else (e.g. a .ptr-named field that isn’t a
pointer) is treated as “no handle found” rather than an error. No
recursion beyond one unwrap level. Any::downcast remains the type-safety
authority: unwrapping a handle for the wrong T still fails at the
caller with the existing type-mismatch error — this only loosens the
accepted R-side shape, not type safety.
§Safety
- Must be called from R’s main thread.
- The returned SEXP is reachable from
sexp(an env binding, S4 slot, or attribute) for as long assexpitself is protected. Macro-generated.Call()wrappers hold every argument alive in the call’s PROTECT stack for the duration of the call, so no additional protection is needed here.