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