FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Own generated native closures and retain their method definitions by 1ndahous3 · Pull Request #8962 · RustPython/RustPython · GitHub

Repository navigation

Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .rs  (4) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
39 changes: 35 additions & 4 deletions crates/vm/src/builtins/builtin_func.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ use crate::{
common::wtf8::Wtf8,
convert::TryFromObject,
function::{Callee, FuncArgs, PyComparisonValue, PyMethodDef, PyMethodFlags, PyNativeFn},
object::{Traverse, TraverseFn},
types::{Callable, Comparable, PyComparisonOp, Representable},
};
use alloc::fmt;

// PyCFunctionObject in CPython
#[repr(C)]
#[pyclass(name = "builtin_function_or_method", module = false, traverse)]
#[pyclass(
name = "builtin_function_or_method",
module = false,
traverse = "manual"
)]
pub struct PyNativeFunction {
#[pytraverse(skip)]
pub(crate) value: &'static PyMethodDef,
pub(crate) zelf: Option<PyObjectRef>,
// Module that owns this function. Not passed as a call argument.
Expand All @@ -24,6 +28,23 @@ pub struct PyNativeFunction {
pub(crate) _method_def_owner: Option<PyObjectRef>,
}

unsafe impl Traverse for PyNativeFunction {
fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
self.zelf.traverse(tracer_fn);
self.module_object.traverse(tracer_fn);
self.module.traverse(tracer_fn);
self._method_def_owner.traverse(tracer_fn);
}

fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
out.extend(self.zelf.take());
out.extend(self.module_object.take());
// GC has exclusive access while clearing this unreachable object.
out.extend(unsafe { self.module.swap(None) });
// Keep the definition owner until deallocation: `value` borrows it.
}
}

impl PyPayload for PyNativeFunction {
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.builtin_function_or_method_type
Expand Down Expand Up @@ -78,7 +99,7 @@ impl PyNativeFunction {
self.zelf.as_deref().or(self.module_object.as_deref())
}

pub const fn as_func(&self) -> &'static dyn PyNativeFn {
pub const fn as_func(&self) -> &dyn PyNativeFn {
self.value.func
}
}
Expand Down Expand Up @@ -234,12 +255,22 @@ impl PyNativeFunction {
}

// PyCMethodObject in CPython
#[pyclass(name = "builtin_function_or_method", module = false, base = PyNativeFunction, ctx = "builtin_function_or_method_type")]
#[pyclass(name = "builtin_function_or_method", module = false, base = PyNativeFunction, ctx = "builtin_function_or_method_type", traverse = "manual")]
pub struct PyNativeMethod {
pub(crate) func: PyNativeFunction,
pub(crate) class: &'static Py<PyType>, // TODO: the actual life is &'self
}

unsafe impl Traverse for PyNativeMethod {
fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
self.func.traverse(tracer_fn);
}

fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
self.func.clear(out);
}
}

// All Python-visible behavior (getters, slots) is registered by PyNativeFunction::extend_class.
// PyNativeMethod only extends the Rust-side struct with the defining class reference.
// The func field at offset 0 (#[repr(C)]) allows NativeFunctionOrMethod to read it safely.
Expand Down
41 changes: 35 additions & 6 deletions crates/vm/src/builtins/descriptor.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
function::{
Callee, FuncArgs, ItemDoc, PyMethodDef, PyMethodFlags, PySetterValue, PySsize, plain_doc,
},
object::{Traverse, TraverseFn},
protocol::{PyNumberBinaryFunc, PyNumberTernaryFunc, PyNumberUnaryFunc},
types::{
Callable, Comparable, DelFunc, DescrGetFunc, DescrSetFunc, GenericMethod, GetDescriptor,
Expand All @@ -34,17 +35,37 @@ pub struct PyDescriptorOwned {
pub qualname: PyRwLock<Option<String>>,
}

#[pyclass(name = "method_descriptor", module = false)]
impl PyDescriptor {
fn bind(
&self,
method: &'static PyMethodDef,
owner: Option<PyObjectRef>,
obj: PyObjectRef,
ctx: &Context,
) -> PyRef<PyNativeMethod> {
let mut bound = method.to_bound_method(obj, self.typ);
bound.func._method_def_owner = owner;
bound.into_ref(ctx)
}
}

#[pyclass(name = "method_descriptor", module = false, traverse = "manual")]
pub struct PyMethodDescriptor {
#[pymember(name = "__objclass__", path = "typ")]
#[pymember(name = "__name__", path = "name")]
pub common: PyDescriptor,
pub method: &'static PyMethodDef,
pub(crate) method: &'static PyMethodDef,
// vectorcall: vector_call_func,
/// Prevent HeapMethodDef from being freed while this descriptor references it
pub(crate) _method_def_owner: Option<PyObjectRef>,
}

unsafe impl Traverse for PyMethodDescriptor {
fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
self._method_def_owner.traverse(tracer_fn);
}
}

impl PyMethodDescriptor {
pub fn new(method: &'static PyMethodDef, typ: &'static Py<PyType>, ctx: &Context) -> Self {
Self {
Expand Down Expand Up @@ -125,7 +146,8 @@ impl Callable for PyMethodDescriptor {

impl PyMethodDescriptor {
pub fn bind(&self, obj: PyObjectRef, ctx: &Context) -> PyRef<PyNativeMethod> {
self.method.build_bound_method(ctx, obj, self.common.typ)
self.common
.bind(self.method, self._method_def_owner.clone(), obj, ctx)
}
}

Expand Down Expand Up @@ -173,15 +195,21 @@ impl Representable for PyMethodDescriptor {
}

// METH_CLASS descriptors. Same layout as method_descriptor; a distinct type.
#[pyclass(name = "classmethod_descriptor", module = false)]
#[pyclass(name = "classmethod_descriptor", module = false, traverse = "manual")]
pub struct PyClassMethodDescriptor {
#[pymember(name = "__objclass__", path = "typ")]
#[pymember(name = "__name__", path = "name")]
pub common: PyDescriptor,
pub method: &'static PyMethodDef,
pub(crate) method: &'static PyMethodDef,
pub(crate) _method_def_owner: Option<PyObjectRef>,
}

unsafe impl Traverse for PyClassMethodDescriptor {
fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
self._method_def_owner.traverse(tracer_fn);
}
}

impl PyClassMethodDescriptor {
pub fn new(method: &'static PyMethodDef, typ: &'static Py<PyType>, ctx: &Context) -> Self {
Self {
Expand All @@ -196,7 +224,8 @@ impl PyClassMethodDescriptor {
}

pub fn bind(&self, obj: PyObjectRef, ctx: &Context) -> PyRef<PyNativeMethod> {
self.method.build_bound_method(ctx, obj, self.common.typ)
self.common
.bind(self.method, self._method_def_owner.clone(), obj, ctx)
}
}

Expand Down
90 changes: 88 additions & 2 deletions crates/vm/src/function/method.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -375,15 +375,51 @@ impl core::fmt::Debug for PyMethodDef {
// This is not a part of CPython API.
// But useful to support dynamically generated methods
#[pyclass(name, module = false, ctx = "method_def")]
#[derive(Debug)]
pub struct HeapMethodDef {
method: PyMethodDef,
_function_owner: Option<Box<dyn PyNativeFn>>,
}

impl core::fmt::Debug for HeapMethodDef {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.method.fmt(f)
}
}

impl HeapMethodDef {
#[must_use]
pub const fn new(method: PyMethodDef) -> Self {
Self { method }
Self {
method,
_function_owner: None,
}
}

pub(crate) fn with_owned_function(
name: &'static str,
function: Box<dyn PyNativeFn>,
flags: PyMethodFlags,
doc: super::ItemDoc,
) -> Self {
// SAFETY: the box has a stable address and is retained by this payload.
// The definition is private; all functions/descriptors referring to it
// retain this HeapMethodDef and expose no static reference to callers.
let func = unsafe { &*core::ptr::from_ref(&*function) };
Self {
method: PyMethodDef {
name,
func,
flags,
#[cfg(feature = "doc")]
doc_off: doc.offset,
#[cfg(feature = "doc")]
doc_len: doc.len,
#[cfg(feature = "doc")]
doc_body_pending: false,
doc: doc.text,
},
_function_owner: Some(function),
}
}
}

Expand Down Expand Up @@ -431,3 +467,53 @@ pub(crate) fn init(ctx: &'static Context) {

let _ = ctx.intern_str(HeapMethodDef::NAME);
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{AsObject, function::FuncArgs};

#[test]
fn generated_functions_own_and_release_their_native_captures() {
use alloc::sync::Arc;
let captured = Arc::new(17);
crate::Interpreter::without_stdlib(Default::default()).enter(|vm| {
let capture = captured.clone();
let definition = vm.ctx.new_method_def(
"captured",
move |_args: FuncArgs| *capture,
PyMethodFlags::empty(),
super::super::ItemDoc::NONE,
);
let function = definition.build_function(vm, None);
let descriptor = definition.build_method(vm.ctx.types.object_type, vm);
drop(definition);
assert_eq!(Arc::strong_count(&captured), 2);
assert_eq!(
function
.as_object()
.call((), vm)
.unwrap()
.try_into_value::<i32>(vm)
.unwrap(),
17
);
let method = descriptor.bind(vm.ctx.none(), &vm.ctx);
assert!(method.func._method_def_owner.is_some());
drop(function);
drop(descriptor);
assert_eq!(Arc::strong_count(&captured), 2);
assert_eq!(
method
.as_object()
.call((), vm)
.unwrap()
.try_into_value::<i32>(vm)
.unwrap(),
17
);
drop(method);
assert_eq!(Arc::strong_count(&captured), 1);
});
}
}
14 changes: 1 addition & 13 deletions crates/vm/src/vm/context.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -659,19 +659,7 @@ impl Context {
where
F: IntoPyNativeFn<FKind>,
{
let def = PyMethodDef {
name,
func: Box::leak(Box::new(f.into_func())),
flags,
#[cfg(feature = "doc")]
doc_off: doc.offset,
#[cfg(feature = "doc")]
doc_len: doc.len,
#[cfg(feature = "doc")]
doc_body_pending: false,
doc: doc.text,
};
let payload = HeapMethodDef::new(def);
let payload = HeapMethodDef::with_owned_function(name, Box::new(f.into_func()), flags, doc);
PyRef::new_ref(payload, self.types.method_def.to_owned(), None)
}

Expand Down
Loading

Back | FazBrowse Home | New Git URL