Skip to content

Commit 2b83e60

Browse files
authored
stop leaking PyMethodDef inside macro-generated code (#5459)
* stop leaking `PyMethodDef` inside macro-generated code * newsfragment * coverage
1 parent da618c7 commit 2b83e60

File tree

9 files changed

+169
-86
lines changed

9 files changed

+169
-86
lines changed

newsfragments/5459.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Stop leaking `PyMethodDef` instances inside `#[pyfunction]` macro generated code.

pyo3-macros-backend/src/pyfunction.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -433,17 +433,18 @@ pub fn impl_wrap_pyfunction(
433433
#[doc(hidden)]
434434
#vis mod #name {
435435
pub(crate) struct MakeDef;
436-
pub const _PYO3_DEF: #pyo3_path::impl_::pymethods::PyMethodDef = MakeDef::_PYO3_DEF;
436+
pub static _PYO3_DEF: #pyo3_path::impl_::pyfunction::PyFunctionDef = MakeDef::_PYO3_DEF;
437437
#introspection_id
438438
}
439439

440-
// Generate the definition inside an anonymous function in the same scope as the original function -
440+
// Generate the definition in the same scope as the original function -
441441
// this avoids complications around the fact that the generated module has a different scope
442442
// (and `super` doesn't always refer to the outer scope, e.g. if the `#[pyfunction] is
443443
// inside a function body)
444444
#[allow(unknown_lints, non_local_definitions)]
445445
impl #name::MakeDef {
446-
const _PYO3_DEF: #pyo3_path::impl_::pymethods::PyMethodDef = #methoddef;
446+
const _PYO3_DEF: #pyo3_path::impl_::pyfunction::PyFunctionDef =
447+
#pyo3_path::impl_::pyfunction::PyFunctionDef::from_method_def(#methoddef);
447448
}
448449

449450
#[allow(non_snake_case)]

src/impl_/pyclass.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,7 +1444,7 @@ mod tests {
14441444

14451445
for items in FrozenClass::items_iter() {
14461446
methods.extend(items.methods.iter().map(|m| match m {
1447-
MaybeRuntimePyMethodDef::Static(m) => m.clone(),
1447+
MaybeRuntimePyMethodDef::Static(m) => *m,
14481448
MaybeRuntimePyMethodDef::Runtime(r) => r(),
14491449
}));
14501450
slots.extend_from_slice(items.slots);
@@ -1482,7 +1482,7 @@ mod tests {
14821482

14831483
for items in FrozenClass::items_iter() {
14841484
methods.extend(items.methods.iter().map(|m| match m {
1485-
MaybeRuntimePyMethodDef::Static(m) => m.clone(),
1485+
MaybeRuntimePyMethodDef::Static(m) => *m,
14861486
MaybeRuntimePyMethodDef::Runtime(r) => r(),
14871487
}));
14881488
slots.extend_from_slice(items.slots);

src/impl_/pyfunction.rs

Lines changed: 122 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,152 @@
1+
use std::cell::UnsafeCell;
2+
13
use crate::{
2-
types::{PyCFunction, PyModule},
4+
ffi,
5+
ffi_ptr_ext::FfiPtrExt,
6+
py_result_ext::PyResultExt,
7+
types::{PyCFunction, PyModule, PyModuleMethods},
38
Borrowed, Bound, PyResult, Python,
49
};
510

611
pub use crate::impl_::pymethods::PyMethodDef;
712

13+
/// Wrapper around `ffi::PyMethodDef` suitable to use as a static variable for `#[pyfunction]` values.
14+
///
15+
/// The `UnsafeCell` is used because the Python interpreter consumes these as `*mut ffi::PyMethodDef`.
16+
pub struct PyFunctionDef(UnsafeCell<ffi::PyMethodDef>);
17+
18+
// Safety: contents are only ever used by the Python interpreter, which uses global statics in this way.
19+
unsafe impl Sync for PyFunctionDef {}
20+
21+
impl PyFunctionDef {
22+
pub const fn new(def: ffi::PyMethodDef) -> Self {
23+
Self(UnsafeCell::new(def))
24+
}
25+
26+
pub const fn from_method_def(def: PyMethodDef) -> Self {
27+
Self::new(def.into_raw())
28+
}
29+
30+
pub(crate) fn create_py_c_function<'py>(
31+
&'static self,
32+
py: Python<'py>,
33+
module: Option<&Bound<'py, PyModule>>,
34+
) -> PyResult<Bound<'py, PyCFunction>> {
35+
// Safety: self is static
36+
unsafe { create_py_c_function(py, self.0.get(), module) }
37+
}
38+
}
39+
840
/// Trait to enable the use of `wrap_pyfunction` with both `Python` and `PyModule`,
941
/// and also to infer the return type of either `&'py PyCFunction` or `Bound<'py, PyCFunction>`.
1042
pub trait WrapPyFunctionArg<'py, T> {
11-
fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<T>;
43+
fn wrap_pyfunction(self, function_def: &'static PyFunctionDef) -> PyResult<T>;
1244
}
1345

1446
impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Bound<'py, PyModule> {
15-
fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<Bound<'py, PyCFunction>> {
16-
PyCFunction::internal_new(self.py(), method_def, Some(&self))
47+
fn wrap_pyfunction(
48+
self,
49+
function_def: &'static PyFunctionDef,
50+
) -> PyResult<Bound<'py, PyCFunction>> {
51+
function_def.create_py_c_function(self.py(), Some(&self))
1752
}
1853
}
1954

2055
impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Bound<'py, PyModule> {
21-
fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<Bound<'py, PyCFunction>> {
22-
PyCFunction::internal_new(self.py(), method_def, Some(self))
56+
fn wrap_pyfunction(
57+
self,
58+
function_def: &'static PyFunctionDef,
59+
) -> PyResult<Bound<'py, PyCFunction>> {
60+
function_def.create_py_c_function(self.py(), Some(self))
2361
}
2462
}
2563

2664
impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Borrowed<'_, 'py, PyModule> {
27-
fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<Bound<'py, PyCFunction>> {
28-
PyCFunction::internal_new(self.py(), method_def, Some(&self))
65+
fn wrap_pyfunction(
66+
self,
67+
function_def: &'static PyFunctionDef,
68+
) -> PyResult<Bound<'py, PyCFunction>> {
69+
function_def.create_py_c_function(self.py(), Some(&self))
2970
}
3071
}
3172

3273
impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Borrowed<'_, 'py, PyModule> {
33-
fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<Bound<'py, PyCFunction>> {
34-
PyCFunction::internal_new(self.py(), method_def, Some(self))
74+
fn wrap_pyfunction(
75+
self,
76+
function_def: &'static PyFunctionDef,
77+
) -> PyResult<Bound<'py, PyCFunction>> {
78+
function_def.create_py_c_function(self.py(), Some(self))
3579
}
3680
}
3781

3882
impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Python<'py> {
39-
fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<Bound<'py, PyCFunction>> {
40-
PyCFunction::internal_new(self, method_def, None)
83+
fn wrap_pyfunction(
84+
self,
85+
function_def: &'static PyFunctionDef,
86+
) -> PyResult<Bound<'py, PyCFunction>> {
87+
function_def.create_py_c_function(self, None)
88+
}
89+
}
90+
91+
/// Creates a `PyCFunction` object from a `PyMethodDef`.
92+
///
93+
/// # Safety
94+
///
95+
/// The `method_def` pointer must be valid for the lifetime of the returned `PyCFunction`
96+
/// (effectively, it must be a static variable).
97+
pub unsafe fn create_py_c_function<'py>(
98+
py: Python<'py>,
99+
method_def: *mut ffi::PyMethodDef,
100+
module: Option<&Bound<'py, PyModule>>,
101+
) -> PyResult<Bound<'py, PyCFunction>> {
102+
let (mod_ptr, module_name) = if let Some(m) = module {
103+
let mod_ptr = m.as_ptr();
104+
(mod_ptr, Some(m.name()?))
105+
} else {
106+
(std::ptr::null_mut(), None)
107+
};
108+
109+
let module_name_ptr = module_name
110+
.as_ref()
111+
.map_or(std::ptr::null_mut(), Bound::as_ptr);
112+
113+
unsafe {
114+
ffi::PyCFunction_NewEx(method_def, mod_ptr, module_name_ptr)
115+
.assume_owned_or_err(py)
116+
.cast_into_unchecked()
117+
}
118+
}
119+
120+
#[cfg(test)]
121+
#[cfg(feature = "macros")]
122+
mod tests {
123+
#[test]
124+
fn test_wrap_pyfunction_forms() {
125+
use crate::types::{PyAnyMethods, PyModule};
126+
use crate::{wrap_pyfunction, Python};
127+
128+
#[crate::pyfunction(crate = "crate")]
129+
fn f() {}
130+
131+
Python::attach(|py| {
132+
let module = PyModule::new(py, "test_wrap_pyfunction_forms").unwrap();
133+
134+
let func = wrap_pyfunction!(f, module.clone()).unwrap();
135+
func.call0().unwrap();
136+
137+
let func = wrap_pyfunction!(f, &module).unwrap();
138+
func.call0().unwrap();
139+
140+
let module_borrowed = module.as_borrowed();
141+
142+
let func = wrap_pyfunction!(f, module_borrowed).unwrap();
143+
func.call0().unwrap();
144+
145+
let func = wrap_pyfunction!(f, &module_borrowed).unwrap();
146+
func.call0().unwrap();
147+
148+
let func = wrap_pyfunction!(f, py).unwrap();
149+
func.call0().unwrap();
150+
});
41151
}
42152
}

src/impl_/pymethods.rs

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ impl IPowModulo {
5858

5959
/// `PyMethodDefType` represents different types of Python callable objects.
6060
/// It is used by the `#[pymethods]` attribute.
61-
#[cfg_attr(test, derive(Clone))]
61+
#[derive(Copy, Clone)]
6262
pub enum PyMethodDefType {
6363
/// Represents class method
6464
Class(PyMethodDef),
@@ -86,10 +86,7 @@ pub enum PyMethodType {
8686

8787
pub type PyClassAttributeFactory = for<'p> fn(Python<'p>) -> PyResult<Py<PyAny>>;
8888

89-
// TODO: it would be nice to use CStr in these types, but then the constructors can't be const fn
90-
// until `CStr::from_bytes_with_nul_unchecked` is const fn.
91-
92-
#[derive(Clone, Debug)]
89+
#[derive(Copy, Clone, Debug)]
9390
pub struct PyMethodDef {
9491
pub(crate) ml_name: &'static CStr,
9592
pub(crate) ml_meth: PyMethodType,
@@ -103,26 +100,20 @@ pub struct PyClassAttributeDef {
103100
pub(crate) meth: PyClassAttributeFactory,
104101
}
105102

106-
#[derive(Clone)]
103+
#[derive(Copy, Clone)]
107104
pub struct PyGetterDef {
108105
pub(crate) name: &'static CStr,
109106
pub(crate) meth: Getter,
110107
pub(crate) doc: &'static CStr,
111108
}
112109

113-
#[derive(Clone)]
110+
#[derive(Copy, Clone)]
114111
pub struct PySetterDef {
115112
pub(crate) name: &'static CStr,
116113
pub(crate) meth: Setter,
117114
pub(crate) doc: &'static CStr,
118115
}
119116

120-
unsafe impl Sync for PyMethodDef {}
121-
122-
unsafe impl Sync for PyGetterDef {}
123-
124-
unsafe impl Sync for PySetterDef {}
125-
126117
impl PyMethodDef {
127118
/// Define a function with no `*args` and `**kwargs`.
128119
pub const fn noargs(
@@ -172,8 +163,7 @@ impl PyMethodDef {
172163
self
173164
}
174165

175-
/// Convert `PyMethodDef` to Python method definition struct `ffi::PyMethodDef`
176-
pub(crate) fn as_method_def(&self) -> ffi::PyMethodDef {
166+
pub const fn into_raw(self) -> ffi::PyMethodDef {
177167
let meth = match self.ml_meth {
178168
PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer { PyCFunction: meth },
179169
PyMethodType::PyCFunctionWithKeywords(meth) => ffi::PyMethodDefPointer {
@@ -737,10 +727,21 @@ mod tests {
737727
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
738728
fn test_fastcall_function_with_keywords() {
739729
use super::PyMethodDef;
740-
use crate::types::{PyAnyMethods, PyCFunction};
730+
use crate::impl_::pyfunction::PyFunctionDef;
731+
use crate::types::PyAnyMethods;
741732
use crate::{ffi, Python};
742733

743734
Python::attach(|py| {
735+
let def =
736+
PyFunctionDef::from_method_def(PyMethodDef::fastcall_cfunction_with_keywords(
737+
ffi::c_str!("test"),
738+
accepts_no_arguments,
739+
ffi::c_str!("doc"),
740+
));
741+
// leak to make it 'static
742+
// deliberately done at runtime to have coverage of `PyFunctionDef::from_method_def`
743+
let def = Box::leak(Box::new(def));
744+
744745
unsafe extern "C" fn accepts_no_arguments(
745746
_slf: *mut ffi::PyObject,
746747
_args: *const *mut ffi::PyObject,
@@ -752,16 +753,7 @@ mod tests {
752753
unsafe { Python::assume_attached().None().into_ptr() }
753754
}
754755

755-
let f = PyCFunction::internal_new(
756-
py,
757-
&PyMethodDef::fastcall_cfunction_with_keywords(
758-
ffi::c_str!("test"),
759-
accepts_no_arguments,
760-
ffi::c_str!("doc"),
761-
),
762-
None,
763-
)
764-
.unwrap();
756+
let f = def.create_py_c_function(py, None).unwrap();
765757

766758
f.call0().unwrap();
767759
});

src/impl_/pymodule.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ use crate::prelude::PyTypeMethods;
2525
use crate::PyErr;
2626
use crate::{
2727
ffi,
28-
impl_::pymethods::PyMethodDef,
28+
impl_::pyfunction::PyFunctionDef,
2929
sync::PyOnceLock,
30-
types::{PyCFunction, PyModule, PyModuleMethods},
30+
types::{PyModule, PyModuleMethods},
3131
Bound, Py, PyClass, PyResult, PyTypeInfo, Python,
3232
};
3333

@@ -203,9 +203,10 @@ impl<T: PyClass> PyAddToModule for AddClassToModule<T> {
203203
}
204204

205205
/// For adding a function to a module.
206-
impl PyAddToModule for PyMethodDef {
206+
impl PyAddToModule for PyFunctionDef {
207207
fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> {
208-
module.add_function(PyCFunction::internal_new(module.py(), self, Some(module))?)
208+
// safety: self is static
209+
module.add_function(self.create_py_c_function(module.py(), Some(module))?)
209210
}
210211
}
211212

src/pyclass/create_type_object.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ impl PyTypeBuilder {
195195
.add_setter(setter),
196196
PyMethodDefType::Method(def)
197197
| PyMethodDefType::Class(def)
198-
| PyMethodDefType::Static(def) => self.method_defs.push(def.as_method_def()),
198+
| PyMethodDefType::Static(def) => self.method_defs.push(def.into_raw()),
199199
// These class attributes are added after the type gets created by LazyStaticType
200200
PyMethodDefType::ClassAttribute(_) => {}
201201
PyMethodDefType::StructMember(def) => self.member_defs.push(*def),

src/sealed.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::impl_::pyfunction::PyFunctionDef;
12
use crate::types::{
23
PyBool, PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyFloat, PyFrozenSet, PyList,
34
PyMapping, PyMappingProxy, PyModule, PyRange, PySequence, PySet, PySlice, PyString,
@@ -50,6 +51,7 @@ impl Sealed for Bound<'_, PyWeakrefReference> {}
5051
impl<T> Sealed for AddTypeToModule<T> {}
5152
impl<T> Sealed for AddClassToModule<T> {}
5253
impl Sealed for PyMethodDef {}
54+
impl Sealed for PyFunctionDef {}
5355
impl Sealed for ModuleDef {}
5456

5557
impl<T: crate::type_object::PyTypeInfo> Sealed for PyNativeTypeInitializer<T> {}

0 commit comments

Comments
 (0)