Skip to content

Commit 3d42829

Browse files
authored
Add RWeakObject (#835)
1 parent 5dc1c4f commit 3d42829

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

crates/harp/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub mod traits;
4141
pub mod utils;
4242
pub mod vec_format;
4343
pub mod vector;
44+
pub mod weak_ref;
4445

4546
// Reexport API
4647
pub use column_names::*;
@@ -70,6 +71,7 @@ pub use harp::object::list_poke;
7071
pub use harp::object::RObject;
7172
pub use harp::symbol::RSymbol;
7273
pub use harp::utils::get_option;
74+
pub use harp::weak_ref::RWeakRef;
7375
pub use harp_macros::register;
7476

7577
// Allow `crate::` references within the crate

crates/harp/src/weak_ref.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//
2+
// weak_ref.rs
3+
//
4+
// Copyright (C) 2025 Posit Software, PBC. All rights reserved.
5+
//
6+
//
7+
8+
use crate::RObject;
9+
10+
/// Weak reference to an R object.
11+
///
12+
/// This is a wrapper around R weak references (see
13+
/// <https://cran.r-project.org/doc/manuals/r-devel/R-exts.html#External-pointers-and-weak-references>).
14+
///
15+
/// The weak reference points to an R object that you can dereference with the
16+
/// `deref()` method, without preventing R from garbage collecting this object.
17+
/// When it gets GC'd by R, or when the weak ref is dropped, the supplied
18+
/// `finalizer` is run. This technique allows you to monitor the existence of R
19+
/// objects in the session.
20+
///
21+
/// Note that just because the weak reference is active does not mean that the
22+
/// object is still reachable. It might be lingering in memory until the next
23+
/// GC.
24+
#[derive(Debug)]
25+
pub struct RWeakRef {
26+
weak_ref: RObject,
27+
}
28+
29+
impl RWeakRef {
30+
pub fn new(obj: libr::SEXP, finalizer: impl FnOnce()) -> Self {
31+
// Convert generic `FnOnce` to a boxed trait object. This must be an
32+
// explicit `Box<dyn FnOnce()>`, not a `Box<impl FnOnce()>`, which you
33+
// get if you don't specify the type. The latter is not a proper trait
34+
// object.
35+
let finalizer: Box<dyn FnOnce()> = Box::new(finalizer);
36+
37+
// Since `finalizer` is a trait object we need to double box it before
38+
// calling `Box::into_raw()`. If we call `into_raw()` directly on the
39+
// boxed trait object it returns a fat pointer with a vtable, which is
40+
// not a valid C pointer we can pass across the FFI boundary. So we
41+
// rebox it and convert the outer box to a pointer. See
42+
// https://users.rust-lang.org/t/how-to-convert-box-dyn-fn-into-raw-pointer-and-then-call-it/104410.
43+
let finalizer = Box::new(finalizer);
44+
45+
// Create a C pointer to the outer box and prevent it from being
46+
// destructed on drop. The resource will now be managed by R.
47+
let finalizer = Box::into_raw(finalizer);
48+
49+
// Wrap that address in an R external pointer.
50+
let finalizer = RObject::new(unsafe {
51+
libr::R_MakeExternalPtr(
52+
finalizer as *mut std::ffi::c_void,
53+
RObject::null().sexp,
54+
RObject::null().sexp,
55+
)
56+
});
57+
58+
// This is the C callback that unpacks the external pointer when the
59+
// weakref is finalized.
60+
unsafe extern "C-unwind" fn finalize_weak_ref(key: libr::SEXP) {
61+
let finalizer = libr::R_ExternalPtrAddr(key) as *mut Box<dyn FnOnce()>;
62+
63+
if finalizer.is_null() {
64+
log::warn!("Weakref finalizer is unexpectedly NULL");
65+
return;
66+
}
67+
68+
let finalizer = Box::from_raw(finalizer);
69+
finalizer();
70+
}
71+
72+
// Finally, the weakref wraps `obj` and our finalizer.
73+
let weak_ref = RObject::new(unsafe {
74+
libr::R_MakeWeakRefC(
75+
finalizer.sexp, // Protected by weakref
76+
obj, // Not protected by weakref
77+
Some(finalize_weak_ref),
78+
libr::Rboolean_FALSE,
79+
)
80+
});
81+
82+
Self { weak_ref }
83+
}
84+
85+
/// Derefence weakref.
86+
///
87+
/// If the value is `None`, it means the weakref is now stale and the
88+
/// finalizer has been run.
89+
pub fn deref(&self) -> Option<RObject> {
90+
// If finalizer is `NULL` we know for sure the weakref is stale
91+
let key = unsafe { libr::R_WeakRefKey(self.weak_ref.sexp) };
92+
if key == RObject::null().sexp {
93+
return None;
94+
}
95+
96+
Some(RObject::new(unsafe {
97+
libr::R_WeakRefValue(self.weak_ref.sexp)
98+
}))
99+
}
100+
}
101+
102+
impl Drop for RWeakRef {
103+
// It's fine to run this even when weakref is stale
104+
fn drop(&mut self) {
105+
unsafe { libr::R_RunWeakRefFinalizer(self.weak_ref.sexp) }
106+
}
107+
}
108+
109+
#[cfg(test)]
110+
mod tests {
111+
use super::*;
112+
use crate::environment::Environment;
113+
use crate::parse_eval_base;
114+
115+
#[test]
116+
fn test_weakref() {
117+
crate::r_task(|| {
118+
let env = Environment::new(parse_eval_base("new.env()").unwrap());
119+
120+
// Destructor runs when weakref is dropped
121+
let mut has_run = false;
122+
let weak_ref = RWeakRef::new(env.inner.sexp, || has_run = true);
123+
drop(weak_ref);
124+
assert!(has_run);
125+
126+
// Destructor runs when referee is gc'd
127+
let mut has_run = false;
128+
let _weak_ref = RWeakRef::new(env.inner.sexp, || has_run = true);
129+
drop(env);
130+
parse_eval_base("gc(full = TRUE)").unwrap();
131+
assert!(has_run);
132+
})
133+
}
134+
}

crates/libr/src/r.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ functions::generate! {
6060

6161
pub fn R_MakeExternalPtr(p: *mut std::ffi::c_void, tag: SEXP, prot: SEXP) -> SEXP;
6262

63+
pub fn R_MakeWeakRefC(key: SEXP, val: SEXP, fin: R_CFinalizer_t, onexit: Rboolean) -> SEXP;
64+
65+
pub fn R_WeakRefKey(w: SEXP) -> SEXP;
66+
67+
pub fn R_WeakRefValue(w: SEXP) -> SEXP;
68+
69+
pub fn R_RunWeakRefFinalizer(w: SEXP);
70+
6371
pub fn R_IsNA(arg1: f64) -> std::ffi::c_int;
6472

6573
pub fn R_IsNaN(arg1: f64) -> std::ffi::c_int;

crates/libr/src/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ pub const ParseStatus_PARSE_EOF: ParseStatus = 4;
8383

8484
pub type DL_FUNC = Option<unsafe extern "C-unwind" fn() -> *mut std::ffi::c_void>;
8585
pub type R_NativePrimitiveArgType = std::ffi::c_uint;
86+
pub type R_CFinalizer_t = Option<unsafe extern "C-unwind" fn(SEXP)>;
8687

8788
#[repr(C)]
8889
#[derive(Debug, Copy, Clone)]

0 commit comments

Comments
 (0)