Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/capi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub mod setobject;
pub mod sliceobject;
pub mod traceback;
pub mod tupleobject;
pub mod typeobject;
pub mod unicodeobject;
mod util;
pub mod warnings;
Expand Down
61 changes: 29 additions & 32 deletions crates/capi/src/methodobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use crate::object::define_py_check;
use crate::pystate::with_vm;
use crate::util::CStrExt;
use core::ffi::{c_char, c_int};
use core::ptr::NonNull;
use rustpython_vm::function::{FuncArgs, HeapMethodDef, PosArgs, PyMethodFlags};
use rustpython_vm::types::c_slots::{kwargs_ptr, ret_ptr_to_pyresult, split_args};
use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine};

define_py_check!(fn PyCFunction_Check, types.builtin_function_or_method_type);
Expand All @@ -19,27 +19,36 @@ pub struct PyMethodDef {
pub ml_doc: *const c_char,
}

pub type PyCFunction =
unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject;

pub type PyCFunctionWithKeywords = unsafe extern "C" fn(
slf: *mut PyObject,
args: *mut PyObject,
kwargs: *mut PyObject,
) -> *mut PyObject;

pub type PyCFunctionFast = unsafe extern "C" fn(
slf: *mut PyObject,
args: *const *mut PyObject,
nargs: isize,
) -> *mut PyObject;

pub type PyCFunctionFastWithKeywords = unsafe extern "C" fn(
slf: *mut PyObject,
args: *const *mut PyObject,
nargs: isize,
kwnames: *mut PyObject,
) -> *mut PyObject;

#[repr(C)]
#[derive(Copy, Clone)]
#[allow(non_snake_case)]
pub union PyMethodPointer {
pub PyCFunction: unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject,
pub PyCFunctionWithKeywords: unsafe extern "C" fn(
slf: *mut PyObject,
args: *mut PyObject,
kwargs: *mut PyObject,
) -> *mut PyObject,
pub PyCFunctionFast: unsafe extern "C" fn(
slf: *mut PyObject,
args: *const *mut PyObject,
nargs: isize,
) -> *mut PyObject,
pub PyCFunctionFastWithKeywords: unsafe extern "C" fn(
slf: *mut PyObject,
args: *const *mut PyObject,
nargs: isize,
kwnames: *mut PyObject,
) -> *mut PyObject,
pub PyCFunction: PyCFunction,
pub PyCFunctionWithKeywords: PyCFunctionWithKeywords,
pub PyCFunctionFast: PyCFunctionFast,
pub PyCFunctionFastWithKeywords: PyCFunctionFastWithKeywords,
}

pub(crate) fn build_method_def(
Expand Down Expand Up @@ -175,16 +184,12 @@ unsafe fn call_function_with_keywords(
.as_ref()
.map(|obj| obj.as_object().as_raw().cast_mut())
.unwrap_or_default();
let arg_tuple = vm.ctx.new_tuple(args.args);
let kwargs = vm.ctx.new_dict();
for (k, v) in args.kwargs {
kwargs.set_item(&*k, v, vm)?;
}
let (arg_tuple, kwargs) = split_args(vm, args)?;
let ret_ptr = unsafe {
f(
slf_ptr,
arg_tuple.as_object().as_raw().cast_mut(),
kwargs.as_object().as_raw().cast_mut(),
kwargs_ptr(kwargs.as_ref()),
)
};
ret_ptr_to_pyresult(vm, ret_ptr)
Expand Down Expand Up @@ -247,14 +252,6 @@ unsafe fn call_fast_function(
ret_ptr_to_pyresult(vm, ret_ptr)
}

fn ret_ptr_to_pyresult(vm: &VirtualMachine, ret_ptr: *mut PyObject) -> PyResult {
let ret_ptr = NonNull::new(ret_ptr).ok_or_else(|| {
vm.take_raised_exception()
.expect("Native function returned NULL, but there was no exception set")
})?;
Ok(unsafe { PyObjectRef::from_raw(ret_ptr) })
}

fn take_self_arg(args: &mut FuncArgs, flags: PyMethodFlags) -> Option<PyObjectRef> {
if flags.contains(PyMethodFlags::STATIC) {
None
Expand Down
290 changes: 290 additions & 0 deletions crates/capi/src/typeobject.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
//! C level type slots.
//!
//! The slot table keeps its Rust signatures, so a `newfunc` from an extension
//! goes into the type's [`CSlots`] table and `new` gets the trampoline that
//! reads it. The table pointer is inherited alongside the trampoline, so a
//! subclass reaches the C function without a lookup, the way an inherited
//! `tp_new` pointer does.
//!
//! `__new__` is the same wrapper a native type gets, so a call through it is
//! checked by `PyType::__new__` before it reaches the slot.

use crate::object::PyTypeObject;
use core::ffi::{c_int, c_void};
use core::ptr;
use rustpython_vm::builtins::PyType;
use rustpython_vm::function::PyMethodFlags;
use rustpython_vm::types::{CNewFunc, CSlotId, CSlots};
use rustpython_vm::{Py, PyResult, VirtualMachine, identifier};

#[allow(non_camel_case_types)]
pub type newfunc = CNewFunc;

#[allow(non_upper_case_globals)]
pub const Py_tp_new: c_int = CSlotId::TpNew as c_int;

/// Install a C `newfunc` as the type's tp_new.
///
/// `ty` must be a heap type that does not already define `__new__` itself.
pub fn set_tp_new(vm: &VirtualMachine, ty: &Py<PyType>, tp_new: newfunc) -> PyResult<()> {
Comment on lines +26 to +29
let c_slots = CSlots::new();
c_slots.new.store(Some(tp_new));
ty.set_c_slots(c_slots, vm)?;

// The wrapper that reaches the slot the checked way, as a native type gets
// from `extend_class`. Stored without the attribute protocol so that
// `update_slot` does not read it back as a definition of `__new__`.
let def = vm
.ctx
.new_method_def("__new__", PyType::__new__, PyMethodFlags::METHOD, None);
let wrapper = def.build_function(vm, Some(ty.to_owned().into()));
ty.set_attr(identifier!(vm, __new__), wrapper.into());
Ok(())
}

/// Only slots installed from C are reported. A slot backed by a Rust function
/// has no C ABI entry point yet and reads as empty, as does a slot id this
/// layer does not handle.
#[unsafe(no_mangle)]
#[allow(non_upper_case_globals)]
pub unsafe extern "C" fn PyType_GetSlot(ty: *const PyTypeObject, slot: c_int) -> *mut c_void {
let ty = unsafe { &*ty };
let Some(c_slots) = ty.slots.c_slots() else {
return ptr::null_mut();
};
match CSlotId::from_raw(slot) {
Some(CSlotId::TpNew) => c_slots
.new
.load()
.map_or(ptr::null_mut(), |f| f as *mut c_void),
None => ptr::null_mut(),
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::PyObject;
use pyo3::Python;
use rustpython_vm::builtins::{PyStrRef, PyTuple, PyTypeRef};
use rustpython_vm::function::{FuncArgs, KwArgs};
use rustpython_vm::vm::thread::{current_vm_is_set, with_current_vm};
use rustpython_vm::{AsObject, PyObjectRef, PyRef};

/// Returns `(subtype.__name__, args, kwds is NULL)`.
unsafe extern "C" fn echo_new(
subtype: *mut PyTypeObject,
args: *mut PyObject,
kwds: *mut PyObject,
) -> *mut PyObject {
assert!(current_vm_is_set());
with_current_vm(|vm| {
let subtype = unsafe { &*subtype };
let args = unsafe { &*args }.to_owned();
let echo = vm.ctx.new_tuple(vec![
vm.ctx.new_str(subtype.name().to_string()).into(),
args,
vm.ctx.new_bool(kwds.is_null()).into(),
]);
PyObjectRef::from(echo).into_raw().as_ptr()
})
}

/// Returns an empty tuple, so it is told apart from `echo_new` by result.
unsafe extern "C" fn other_new(
_subtype: *mut PyTypeObject,
_args: *mut PyObject,
_kwds: *mut PyObject,
) -> *mut PyObject {
with_current_vm(|vm| {
PyObjectRef::from(vm.ctx.new_tuple(vec![]))
.into_raw()
.as_ptr()
})
}

fn heap_type(name: &str, base: &Py<PyType>, vm: &VirtualMachine) -> PyTypeRef {
PyType::new_simple_heap(name, base, &vm.ctx).unwrap()
}

fn call(ty: &Py<PyType>, args: FuncArgs, vm: &VirtualMachine) -> PyRef<PyTuple> {
ty.as_object()
.call(args, vm)
.unwrap()
.downcast::<PyTuple>()
.unwrap()
}

fn echoed_name(echo: &PyTuple) -> String {
let name: PyStrRef = echo[0].clone().downcast().unwrap();
name.to_string()
}

#[test]
fn c_tp_new_is_called() {
Python::attach(|_py| {
with_current_vm(|vm| {
let ty = heap_type("CType", vm.ctx.types.object_type, vm);
set_tp_new(vm, &ty, echo_new).unwrap();

let echo = call(&ty, FuncArgs::default(), vm);
assert_eq!(echoed_name(&echo), "CType");
assert_eq!(echo[1].clone().downcast::<PyTuple>().unwrap().len(), 0);
// No keywords were passed, so kwds must be NULL.
assert!(echo[2].clone().try_to_bool(vm).unwrap());

let echo = call(&ty, FuncArgs::from(vec![vm.ctx.new_int(1).into()]), vm);
assert_eq!(echo[1].clone().downcast::<PyTuple>().unwrap().len(), 1);

let kwargs: KwArgs =
core::iter::once(("k".to_owned(), vm.ctx.new_int(2).into())).collect();
let echo = call(&ty, FuncArgs::new(vec![], kwargs), vm);
assert!(!echo[2].clone().try_to_bool(vm).unwrap());
})
})
}

/// A subclass reaches the C slot through the inherited slot pair, and the
/// type it is instantiated with is the one handed to the slot.
#[test]
fn c_tp_new_is_inherited() {
Python::attach(|_py| {
with_current_vm(|vm| {
let base = heap_type("CBase", vm.ctx.types.object_type, vm);
set_tp_new(vm, &base, echo_new).unwrap();
let sub = heap_type("CSub", &base, vm);

assert!(sub.slots.c_slots().and_then(|c| c.new.load()).is_some());
assert_eq!(echoed_name(&call(&sub, FuncArgs::default(), vm)), "CSub");
})
})
}

/// Every C type reaches its slot through one shared trampoline, so the
/// "is not safe" check has to compare the C functions behind it, not the
/// trampoline. Without that, `CBase.__new__(CSub)` would silently run
/// CSub's tp_new where the caller asked for CBase's.
#[test]
fn cross_type_dunder_new_call_is_rejected() {
Python::attach(|_py| {
with_current_vm(|vm| {
let base = heap_type("COuter", vm.ctx.types.object_type, vm);
set_tp_new(vm, &base, echo_new).unwrap();
let sub = heap_type("CInner", &base, vm);
set_tp_new(vm, &sub, other_new).unwrap();

// Each type reaches its own C function.
assert_eq!(echoed_name(&call(&base, FuncArgs::default(), vm)), "COuter");
assert!(call(&sub, FuncArgs::default(), vm).is_empty());

// But COuter.__new__(CInner) must not reach CInner's.
let dunder_new = base
.as_object()
.get_attr(identifier!(vm, __new__), vm)
.unwrap();
let err = dunder_new.call((sub,), vm).unwrap_err();
let msg = err.as_object().str(vm).unwrap().to_string();
assert!(
msg.contains("is not safe"),
"expected an is-not-safe error, got {msg}"
);
})
})
}

/// `__new__` reaches the slot through `PyType::__new__`, so a direct call
/// is argument-checked instead of handing the raw pointer to the callee.
#[test]
fn direct_dunder_new_call_is_checked() {
Python::attach(|_py| {
with_current_vm(|vm| {
let ty = heap_type("CChecked", vm.ctx.types.object_type, vm);
set_tp_new(vm, &ty, echo_new).unwrap();
let dunder_new = ty
.as_object()
.get_attr(identifier!(vm, __new__), vm)
.unwrap();

// No type argument at all.
assert!(dunder_new.call((), vm).is_err());
// First argument is not a type.
assert!(dunder_new.call((vm.ctx.new_int(42),), vm).is_err());
// First argument is a type, but not a subtype of this one.
assert!(
dunder_new
.call((vm.ctx.types.dict_type.to_owned(),), vm)
.is_err()
);

// The type itself and a subclass of it are accepted.
let echo = dunder_new
.call((ty.clone(),), vm)
.unwrap()
.downcast::<PyTuple>()
.unwrap();
assert_eq!(echoed_name(&echo), "CChecked");

let sub = heap_type("CCheckedSub", &ty, vm);
let echo = dunder_new
.call((sub,), vm)
.unwrap()
.downcast::<PyTuple>()
.unwrap();
assert_eq!(echoed_name(&echo), "CCheckedSub");
})
})
}

/// A Python-level `__new__` on a subclass replaces the inherited C slot.
#[test]
fn python_subclass_new_overrides_the_slot() {
Python::attach(|_py| {
with_current_vm(|vm| {
let base = heap_type("COverBase", vm.ctx.types.object_type, vm);
set_tp_new(vm, &base, echo_new).unwrap();
let sub = heap_type("COverSub", &base, vm);

let py_new = vm.ctx.new_method_def(
"__new__",
|_args: FuncArgs, vm: &VirtualMachine| -> PyResult {
Ok(vm.ctx.new_str("from python").into())
},
PyMethodFlags::STATIC,
None,
);
let py_new = py_new.build_function(vm, None);
sub.as_object()
.set_attr(identifier!(vm, __new__), py_new, vm)
.unwrap();

assert!(sub.slots.c_slots().is_none());
let obj = sub.as_object().call((), vm).unwrap();
let obj: PyStrRef = obj.downcast().unwrap();
assert_eq!(obj.to_string(), "from python");
})
})
}

#[test]
fn get_slot_round_trips() {
Python::attach(|_py| {
with_current_vm(|vm| {
let ty = heap_type("CGet", vm.ctx.types.object_type, vm);
assert!(unsafe { PyType_GetSlot(&*ty, Py_tp_new) }.is_null());

set_tp_new(vm, &ty, echo_new).unwrap();
assert_eq!(
unsafe { PyType_GetSlot(&*ty, Py_tp_new) },
echo_new as *mut c_void
);

// Inherited by pointer, as tp_new is.
let sub = heap_type("CGetSub", &ty, vm);
assert_eq!(
unsafe { PyType_GetSlot(&*sub, Py_tp_new) },
echo_new as *mut c_void
);
})
})
}
}
Loading
Loading