Skip to content

Commit 26e4220

Browse files
committed
feat: raw pointer capsule instantiation method
1 parent 7c8f8aa commit 26e4220

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed

newsfragments/5689.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `PyCapsule::new_pointer` and `PyCapsule::new_pointer_with_destructor` for creating capsules with raw pointers.

src/types/capsule.rs

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,129 @@ impl PyCapsule {
135135
}
136136
}
137137

138+
/// Constructs a new capsule with a raw pointer.
139+
///
140+
/// Unlike [`PyCapsule::new`], which stores a value and sets the capsule's pointer
141+
/// to the address of that stored value, this method sets the capsule's pointer
142+
/// directly to the provided address. This is useful when interfacing with APIs
143+
/// that expect the capsule pointer to *be* a specific address (such as a function
144+
/// pointer) rather than point to stored data.
145+
///
146+
/// # Safety
147+
///
148+
/// - `pointer` must be non-null and valid for the intended use case.
149+
/// - If the pointer refers to data, the caller must ensure that data outlives
150+
/// the capsule or is otherwise managed appropriately.
151+
/// - No destructor is set, so no cleanup will occur when the capsule is destroyed.
152+
/// Use [`PyCapsule::new_pointer_with_destructor`] if cleanup is needed.
153+
///
154+
/// # Example
155+
///
156+
/// ```
157+
/// use pyo3::{prelude::*, types::PyCapsule};
158+
/// use std::ffi::{c_void, CString};
159+
///
160+
/// // Example: wrapping a function pointer for FFI
161+
/// extern "C" fn my_handler(arg: *mut c_void) -> *mut c_void {
162+
/// std::ptr::null_mut()
163+
/// }
164+
///
165+
/// Python::attach(|py| {
166+
/// let capsule = unsafe {
167+
/// PyCapsule::new_pointer(
168+
/// py,
169+
/// my_handler as *mut c_void,
170+
/// None,
171+
/// )
172+
/// }.unwrap();
173+
///
174+
/// // The capsule's pointer IS the function address
175+
/// let ptr = capsule.pointer_checked(None).unwrap();
176+
/// assert_eq!(ptr.as_ptr(), my_handler as *mut c_void);
177+
/// });
178+
/// ```
179+
pub unsafe fn new_pointer(
180+
py: Python<'_>,
181+
pointer: *mut c_void,
182+
name: Option<CString>,
183+
) -> PyResult<Bound<'_, Self>> {
184+
// SAFETY: caller guarantees pointer validity
185+
unsafe { Self::new_pointer_with_destructor(py, pointer, name, None) }
186+
}
187+
188+
/// Constructs a new capsule with a raw pointer and an optional destructor.
189+
///
190+
/// This is the full-featured version of [`PyCapsule::new_pointer`], allowing
191+
/// specification of a destructor that will be called when the capsule is
192+
/// garbage collected.
193+
///
194+
/// # Safety
195+
///
196+
/// - `pointer` must be non-null and valid for the intended use case.
197+
/// - If the pointer refers to data, the caller must ensure that data remains
198+
/// valid for the lifetime of the capsule, or that the destructor properly
199+
/// cleans it up.
200+
/// - The destructor, if provided, must be safe to call from any thread.
201+
///
202+
/// # Example
203+
///
204+
/// ```
205+
/// use pyo3::{prelude::*, types::PyCapsule, ffi::PyCapsule_Destructor};
206+
/// use std::ffi::{c_void, CString};
207+
///
208+
/// // A destructor that will be called when the capsule is destroyed
209+
/// unsafe extern "C" fn my_destructor(capsule: *mut pyo3::ffi::PyObject) {
210+
/// // Clean up resources here
211+
/// println!("Capsule destroyed!");
212+
/// }
213+
///
214+
/// Python::attach(|py| {
215+
/// let data = Box::into_raw(Box::new(42u32));
216+
///
217+
/// let capsule = unsafe {
218+
/// PyCapsule::new_pointer_with_destructor(
219+
/// py,
220+
/// data as *mut c_void,
221+
/// None,
222+
/// Some(my_destructor),
223+
/// )
224+
/// }.unwrap();
225+
/// });
226+
/// ```
227+
pub unsafe fn new_pointer_with_destructor(
228+
py: Python<'_>,
229+
pointer: *mut c_void,
230+
name: Option<CString>,
231+
destructor: Option<ffi::PyCapsule_Destructor>,
232+
) -> PyResult<Bound<'_, Self>> {
233+
if pointer.is_null() {
234+
return Err(crate::exceptions::PyValueError::new_err(
235+
"PyCapsule pointer must not be null",
236+
));
237+
}
238+
239+
let name_ptr = name.as_ref().map_or(std::ptr::null(), |n| n.as_ptr());
240+
241+
// We need to keep the name alive if it was provided, but since we're not
242+
// using PyO3's destructor mechanism, we need to leak it or store it.
243+
// For simplicity, we leak the name - this matches Python's expectation
244+
// that capsule names are static or long-lived.
245+
if let Some(name) = name {
246+
std::mem::forget(name);
247+
}
248+
249+
// SAFETY:
250+
// - `pointer` is guaranteed non-null by the check above
251+
// - `name_ptr` is either null or points to a valid C string
252+
// - `destructor` is either None or a valid destructor function
253+
// - thread is attached to the Python interpreter
254+
unsafe {
255+
ffi::PyCapsule_New(pointer, name_ptr, destructor)
256+
.assume_owned_or_err(py)
257+
.cast_into_unchecked()
258+
}
259+
}
260+
138261
/// Imports an existing capsule.
139262
///
140263
/// The `name` should match the path to the module attribute exactly in the form
@@ -677,4 +800,100 @@ mod tests {
677800
assert_eq!(cap.context().unwrap(), std::ptr::null_mut());
678801
});
679802
}
803+
804+
#[test]
805+
fn test_pycapsule_new_pointer() {
806+
extern "C" fn dummy_handler(_: *mut c_void) -> *mut c_void {
807+
std::ptr::null_mut()
808+
}
809+
810+
Python::attach(|py| {
811+
let fn_ptr = dummy_handler as *mut c_void;
812+
813+
// SAFETY: `fn_ptr` is a valid, non-null function pointer
814+
let capsule = unsafe { PyCapsule::new_pointer(py, fn_ptr, None) }.unwrap();
815+
816+
// The capsule's pointer should BE the function address, not point to it
817+
let retrieved_ptr = capsule.pointer_checked(None).unwrap();
818+
assert_eq!(retrieved_ptr.as_ptr(), fn_ptr);
819+
});
820+
}
821+
822+
#[test]
823+
fn test_pycapsule_new_pointer_with_name() {
824+
use std::ffi::CString;
825+
826+
extern "C" fn dummy_handler(_: *mut c_void) -> *mut c_void {
827+
std::ptr::null_mut()
828+
}
829+
830+
Python::attach(|py| {
831+
let fn_ptr = dummy_handler as *mut c_void;
832+
let name = CString::new("my_module.my_function").unwrap();
833+
834+
// SAFETY: `fn_ptr` is a valid, non-null function pointer
835+
let capsule = unsafe { PyCapsule::new_pointer(py, fn_ptr, Some(name)) }.unwrap();
836+
837+
let retrieved_ptr = capsule
838+
.pointer_checked(Some(c"my_module.my_function"))
839+
.unwrap();
840+
assert_eq!(retrieved_ptr.as_ptr(), fn_ptr);
841+
842+
// Verify the name was set correctly
843+
let capsule_name = capsule.name().unwrap().unwrap();
844+
// SAFETY: we know the capsule has a name that outlives this usage
845+
assert_eq!(unsafe { capsule_name.as_cstr() }, c"my_module.my_function");
846+
});
847+
}
848+
849+
#[test]
850+
fn test_pycapsule_new_pointer_null_rejected() {
851+
Python::attach(|py| {
852+
// SAFETY: Testing that null pointers are properly rejected
853+
let result = unsafe { PyCapsule::new_pointer(py, std::ptr::null_mut(), None) };
854+
855+
assert!(result.is_err());
856+
});
857+
}
858+
859+
#[test]
860+
fn test_pycapsule_new_pointer_with_destructor() {
861+
use std::sync::mpsc::channel;
862+
863+
let (tx, rx) = channel::<bool>();
864+
865+
unsafe extern "C" fn destructor_fn(capsule: *mut crate::ffi::PyObject) {
866+
// SAFETY:
867+
// - `capsule` is a valid capsule object being destroyed by Python
868+
// - The context was set to a valid `Box<Sender<bool>>` below
869+
unsafe {
870+
let ctx = crate::ffi::PyCapsule_GetContext(capsule);
871+
if !ctx.is_null() {
872+
let sender: Box<Sender<bool>> = Box::from_raw(ctx.cast());
873+
let _ = sender.send(true);
874+
}
875+
}
876+
}
877+
878+
Python::attach(|py| {
879+
let dummy_ptr = 0xDEADBEEF as *mut c_void;
880+
881+
// SAFETY:
882+
// - `dummy_ptr` is non-null (it's a made-up address for testing)
883+
// - We're providing a valid destructor function
884+
let capsule = unsafe {
885+
PyCapsule::new_pointer_with_destructor(py, dummy_ptr, None, Some(destructor_fn))
886+
}
887+
.unwrap();
888+
889+
// Store the sender in the capsule's context
890+
let sender_box = Box::new(tx);
891+
capsule
892+
.set_context(Box::into_raw(sender_box).cast())
893+
.unwrap();
894+
});
895+
896+
// After Python::attach scope ends, the capsule should be destroyed
897+
assert_eq!(rx.recv(), Ok(true));
898+
}
680899
}

0 commit comments

Comments
 (0)