#![allow(clippy::undocumented_unsafe_blocks)]
use crate::exceptions::PyStopAsyncIteration;
use crate::impl_::callback::IntoPyCallbackOutput;
#[cfg(feature = "experimental-inspect")]
use crate::impl_::introspection::PyReturnType;
use crate::impl_::panic::PanicTrap;
use crate::impl_::pyclass::PyClassDict as _;
#[cfg(feature = "experimental-inspect")]
use crate::inspect::PyStaticExpr;
use crate::instance::PyBorrowedUnbound;
use crate::internal::get_slot::{get_slot, TP_BASE, TP_CLEAR, TP_TRAVERSE};
use crate::internal::pyclass_init::PyClassInit;
use crate::internal::state::ForbidAttaching;
use crate::pycell::impl_::PyClassObjectLayout;
use crate::pyclass::gc::{PyClassTraverseGuard, PyTraverseError, PyVisit};
use crate::types::PyType;
use crate::{ffi, Borrowed, Bound, Py, PyAny, PyClass, PyErr, PyResult, Python};
use core::ffi::CStr;
use core::ffi::{c_int, c_void};
use core::fmt;
use core::marker::PhantomData;
use core::panic::AssertUnwindSafe;
use core::ptr::{null_mut, NonNull};
use std::panic::catch_unwind;
use super::pyclass::PyClassImpl;
use super::trampoline;
use crate::internal_tricks::{clear_eq, traverse_eq};
#[derive(Copy, Clone)]
pub enum PyMethodDefType {
Method(PyMethodDef),
ClassAttribute(PyClassAttributeDef),
Getter(PyGetterDef),
Setter(PySetterDef),
Deleter(PyDeleterDef),
StructMember(ffi::PyMemberDef),
}
#[derive(Copy, Clone, Debug)]
pub enum PyMethodType {
PyCFunction(ffi::PyCFunction),
PyCFunctionWithKeywords(ffi::PyCFunctionWithKeywords),
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
PyCFunctionFastWithKeywords(ffi::PyCFunctionFastWithKeywords),
}
pub type PyClassAttributeFactory = for<'p> fn(Python<'p>) -> PyResult<Py<PyAny>>;
#[derive(Copy, Clone, Debug)]
pub struct PyMethodDef {
pub(crate) ml_name: &'static CStr,
pub(crate) ml_meth: PyMethodType,
pub(crate) ml_flags: c_int,
pub(crate) ml_doc: &'static CStr,
}
#[derive(Copy, Clone)]
pub struct PyClassAttributeDef {
pub(crate) name: &'static CStr,
pub(crate) meth: PyClassAttributeFactory,
}
#[derive(Copy, Clone)]
pub struct PyGetterDef {
pub(crate) name: &'static CStr,
pub(crate) meth: Getter,
pub(crate) doc: Option<&'static CStr>,
}
#[derive(Copy, Clone)]
pub struct PySetterDef {
pub(crate) name: &'static CStr,
pub(crate) meth: Setter,
pub(crate) doc: Option<&'static CStr>,
}
#[derive(Copy, Clone)]
pub struct PyDeleterDef {
pub(crate) name: &'static CStr,
pub(crate) meth: Deleter,
pub(crate) doc: Option<&'static CStr>,
}
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
#[macro_export]
#[doc(hidden)]
macro_rules! maybe_define_fastcall_function_with_keywords {
($name:ident, $py:ident, $slf:ident, $args:ident, $nargs:ident, $kwargs:ident, $body:block) => {
#[allow(non_snake_case)]
unsafe fn $name<'py>(
$py: $crate::Python<'py>,
$slf: *mut $crate::ffi::PyObject,
$args: *const *mut $crate::ffi::PyObject,
$nargs: $crate::ffi::Py_ssize_t,
$kwargs: *mut $crate::ffi::PyObject
) -> $crate::PyResult<*mut $crate::ffi::PyObject> $body
};
}
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
#[macro_export]
#[doc(hidden)]
macro_rules! maybe_define_fastcall_function_with_keywords {
($name:ident, $py:ident, $slf:ident, $args:ident, $nargs:ident, $kwargs:ident, $body:block) => {
#[allow(non_snake_case)]
unsafe fn $name<'py>(
$py: $crate::Python<'py>,
$slf: *mut $crate::ffi::PyObject,
$args: *mut $crate::ffi::PyObject,
$kwargs: *mut $crate::ffi::PyObject
) -> $crate::PyResult<*mut $crate::ffi::PyObject> $body
};
}
pub use crate::maybe_define_fastcall_function_with_keywords;
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
#[macro_export]
#[doc(hidden)]
macro_rules! maybe_extract_arguments_fastcall {
($description:ident, $py:ident, $args:ident, $nargs:ident, $kwargs:ident, $args_array:ident, $args_handler:ty, $kwargs_handler:ty) => {
unsafe {
$description.extract_arguments_fastcall::<$args_handler, $kwargs_handler>(
$py,
$args,
$nargs,
$kwargs,
&mut $args_array,
)
}
};
}
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
#[macro_export]
#[doc(hidden)]
macro_rules! maybe_extract_arguments_fastcall {
($description:ident, $py:ident, $args:ident, $nargs:ident, $kwargs:ident, $args_array:ident, $args_handler:ty, $kwargs_handler:ty) => {
unsafe {
$description.extract_arguments_tuple_dict::<$args_handler, $kwargs_handler>(
$py,
$args,
$kwargs,
&mut $args_array,
)
}
};
}
pub use crate::maybe_extract_arguments_fastcall;
impl PyMethodDef {
pub const fn noargs(
ml_name: &'static CStr,
cfunction: ffi::PyCFunction,
ml_doc: &'static CStr,
) -> Self {
Self {
ml_name,
ml_meth: PyMethodType::PyCFunction(cfunction),
ml_flags: ffi::METH_NOARGS,
ml_doc,
}
}
pub const fn cfunction_with_keywords(
ml_name: &'static CStr,
cfunction: ffi::PyCFunctionWithKeywords,
ml_doc: &'static CStr,
) -> Self {
Self {
ml_name,
ml_meth: PyMethodType::PyCFunctionWithKeywords(cfunction),
ml_flags: ffi::METH_VARARGS | ffi::METH_KEYWORDS,
ml_doc,
}
}
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
pub const fn fastcall_cfunction_with_keywords(
ml_name: &'static CStr,
cfunction: ffi::PyCFunctionFastWithKeywords,
ml_doc: &'static CStr,
) -> Self {
Self {
ml_name,
ml_meth: PyMethodType::PyCFunctionFastWithKeywords(cfunction),
ml_flags: ffi::METH_FASTCALL | ffi::METH_KEYWORDS,
ml_doc,
}
}
pub const fn maybe_fastcall_cfunction_with_keywords(
ml_name: &'static CStr,
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] cfunction: ffi::PyCFunctionFastWithKeywords,
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] cfunction: ffi::PyCFunctionWithKeywords,
ml_doc: &'static CStr,
) -> Self {
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
{
Self::fastcall_cfunction_with_keywords(ml_name, cfunction, ml_doc)
}
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
{
Self::cfunction_with_keywords(ml_name, cfunction, ml_doc)
}
}
pub const fn flags(mut self, flags: c_int) -> Self {
self.ml_flags |= flags;
self
}
pub const fn into_raw(self) -> ffi::PyMethodDef {
let meth = match self.ml_meth {
PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer { PyCFunction: meth },
PyMethodType::PyCFunctionWithKeywords(meth) => ffi::PyMethodDefPointer {
PyCFunctionWithKeywords: meth,
},
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
PyMethodType::PyCFunctionFastWithKeywords(meth) => ffi::PyMethodDefPointer {
PyCFunctionFastWithKeywords: meth,
},
};
ffi::PyMethodDef {
ml_name: self.ml_name.as_ptr(),
ml_meth: meth,
ml_flags: self.ml_flags,
ml_doc: self.ml_doc.as_ptr(),
}
}
}
impl PyClassAttributeDef {
pub const fn new(name: &'static CStr, meth: PyClassAttributeFactory) -> Self {
Self { name, meth }
}
}
impl fmt::Debug for PyClassAttributeDef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PyClassAttributeDef")
.field("name", &self.name)
.finish()
}
}
pub(crate) type Getter =
for<'py> unsafe fn(Python<'py>, NonNull<ffi::PyObject>) -> PyResult<*mut ffi::PyObject>;
pub(crate) type Setter = for<'py> unsafe fn(
Python<'py>,
NonNull<ffi::PyObject>,
NonNull<ffi::PyObject>,
) -> PyResult<c_int>;
pub(crate) type Deleter =
for<'py> unsafe fn(Python<'py>, NonNull<ffi::PyObject>) -> PyResult<c_int>;
impl PyGetterDef {
pub const fn new(name: &'static CStr, getter: Getter, doc: Option<&'static CStr>) -> Self {
Self {
name,
meth: getter,
doc,
}
}
}
impl PySetterDef {
pub const fn new(name: &'static CStr, setter: Setter, doc: Option<&'static CStr>) -> Self {
Self {
name,
meth: setter,
doc,
}
}
}
impl PyDeleterDef {
pub const fn new(name: &'static CStr, deleter: Deleter, doc: Option<&'static CStr>) -> Self {
Self {
name,
meth: deleter,
doc,
}
}
}
#[doc(hidden)]
pub unsafe fn _call_traverse<T>(
slf: *mut ffi::PyObject,
impl_: fn(&T, PyVisit<'_>) -> Result<(), PyTraverseError>,
visit: ffi::visitproc,
arg: *mut c_void,
current_traverse: ffi::traverseproc,
) -> c_int
where
T: PyClass,
{
let trap = PanicTrap::new("uncaught panic inside __traverse__ handler");
let lock = ForbidAttaching::during_traverse();
let slf = unsafe {
PyBorrowedUnbound::from_non_null(NonNull::new_unchecked(slf)).cast_unchecked::<T>()
};
let retval = unsafe { traverse_impl(slf, impl_, visit, arg, current_traverse) };
drop(lock);
trap.disarm();
retval
}
unsafe fn traverse_impl<T>(
slf: PyBorrowedUnbound<'_, T>,
impl_: fn(&T, PyVisit<'_>) -> Result<(), PyTraverseError>,
visit: ffi::visitproc,
arg: *mut c_void,
current_traverse: ffi::traverseproc,
) -> c_int
where
T: PyClass,
{
let super_retval = unsafe { call_super_traverse(slf.as_ptr(), visit, arg, current_traverse) };
if super_retval != 0 {
return super_retval;
}
let dict_retval = unsafe {
T::Layout::contents_during_gc(slf)
.dict
.traverse_dict(visit, arg)
};
if dict_retval != 0 {
return dict_retval;
}
let Some(guard) = PyClassTraverseGuard::<T>::try_from_class_object(slf) else {
return 0;
};
let visit = PyVisit {
visit,
arg,
_guard: PhantomData,
};
match catch_unwind(AssertUnwindSafe(move || impl_(&guard, visit))) {
Ok(Ok(())) => 0,
Ok(Err(traverse_error)) => traverse_error.into_inner(),
Err(_err) => -1,
}
}
unsafe fn call_super_traverse(
obj: *mut ffi::PyObject,
visit: ffi::visitproc,
arg: *mut c_void,
current_traverse: ffi::traverseproc,
) -> c_int {
let mut ty = unsafe { ffi::Py_TYPE(obj) };
let mut traverse: Option<ffi::traverseproc>;
loop {
traverse = unsafe { get_slot(ty, TP_TRAVERSE) };
if traverse_eq(traverse, current_traverse) {
break;
}
ty = unsafe { get_slot(ty, TP_BASE) };
if ty.is_null() {
return 0;
}
}
while traverse_eq(traverse, current_traverse) {
ty = unsafe { get_slot(ty, TP_BASE) };
if ty.is_null() {
break;
}
traverse = unsafe { get_slot(ty, TP_TRAVERSE) };
}
if let Some(traverse) = traverse {
return unsafe { traverse(obj, visit, arg) };
}
0
}
pub unsafe fn _call_clear<T>(
slf: *mut ffi::PyObject,
impl_: for<'py> unsafe fn(Python<'py>, *mut ffi::PyObject) -> PyResult<()>,
current_clear: ffi::inquiry,
) -> c_int
where
T: PyClass,
{
unsafe {
trampoline::trampoline(move |py| {
let super_retval = call_super_clear(py, slf, current_clear);
if super_retval != 0 {
return Err(PyErr::fetch(py));
}
impl_(py, slf)?;
let class_object: &<T as PyClassImpl>::Layout = &*slf.cast();
class_object.contents().dict.clear_dict(py);
Ok(0)
})
}
}
pub unsafe extern "C" fn synthesized_traverse<T>(
slf: *mut ffi::PyObject,
visit: ffi::visitproc,
arg: *mut c_void,
) -> c_int
where
T: PyClass,
{
let super_retval = unsafe { call_super_traverse(slf, visit, arg, synthesized_traverse::<T>) };
if super_retval != 0 {
return super_retval;
}
let slf = unsafe {
PyBorrowedUnbound::from_non_null(NonNull::new_unchecked(slf)).cast_unchecked::<T>()
};
unsafe {
T::Layout::contents_during_gc(slf)
.dict
.traverse_dict(visit, arg)
}
}
pub unsafe extern "C" fn synthesized_clear<T>(slf: *mut ffi::PyObject) -> c_int
where
T: PyClass,
{
unsafe { _call_clear::<T>(slf, |_, _| Ok(()), synthesized_clear::<T>) }
}
unsafe fn call_super_clear(
py: Python<'_>,
obj: *mut ffi::PyObject,
current_clear: ffi::inquiry,
) -> c_int {
let mut ty = unsafe { PyType::from_borrowed_type_ptr(py, ffi::Py_TYPE(obj)) };
let mut clear: Option<ffi::inquiry>;
loop {
clear = ty.get_slot(TP_CLEAR);
if clear_eq(clear, current_clear) {
break;
}
let base = ty.get_slot(TP_BASE);
if base.is_null() {
return 0;
}
ty = unsafe { PyType::from_borrowed_type_ptr(py, base) };
}
while clear_eq(clear, current_clear) {
let base = ty.get_slot(TP_BASE);
if base.is_null() {
break;
}
ty = unsafe { PyType::from_borrowed_type_ptr(py, base) };
clear = ty.get_slot(TP_CLEAR);
}
if let Some(clear) = clear {
return unsafe { clear(obj) };
}
0
}
macro_rules! iter_next_output {
($wrapper:ident, $convert_fallback:ident, $type_fallback:ident, exhausted: $exhausted:expr) => {
pub struct $wrapper<T>(pub T);
pub trait $convert_fallback {
type Value;
fn convert<'py, Target>(self, py: Python<'py>) -> PyResult<Target>
where
Self::Value: IntoPyCallbackOutput<'py, Target>;
}
impl<Value> $convert_fallback for $wrapper<Value> {
type Value = Value;
#[inline]
fn convert<'py, Target>(self, py: Python<'py>) -> PyResult<Target>
where
Value: IntoPyCallbackOutput<'py, Target>,
{
self.0.convert(py)
}
}
#[cfg(feature = "experimental-inspect")]
pub trait $type_fallback {
const OUTPUT_TYPE: PyStaticExpr;
}
#[cfg(feature = "experimental-inspect")]
impl<T: PyReturnType> $type_fallback for $wrapper<T> {
const OUTPUT_TYPE: PyStaticExpr = <T as PyReturnType>::OUTPUT_TYPE;
}
impl<Value> $wrapper<Option<Value>> {
#[inline]
pub fn convert<'py>(self, py: Python<'py>) -> PyResult<*mut ffi::PyObject>
where
Value: IntoPyCallbackOutput<'py, *mut ffi::PyObject>,
{
match self.0 {
Some(value) => value.convert(py),
None => $exhausted,
}
}
}
#[cfg(feature = "experimental-inspect")]
impl<Value: PyReturnType> $wrapper<Option<Value>> {
pub const OUTPUT_TYPE: PyStaticExpr = <Value as PyReturnType>::OUTPUT_TYPE;
}
impl<Value, Error> $wrapper<Result<Option<Value>, Error>> {
#[inline]
pub fn convert<'py>(self, py: Python<'py>) -> PyResult<*mut ffi::PyObject>
where
Value: IntoPyCallbackOutput<'py, *mut ffi::PyObject>,
Error: Into<PyErr>,
{
match self.0 {
Ok(Some(value)) => value.convert(py),
Ok(None) => $exhausted,
Err(err) => Err(err.into()),
}
}
}
#[cfg(feature = "experimental-inspect")]
impl<Value: PyReturnType, Error> $wrapper<Result<Option<Value>, Error>> {
pub const OUTPUT_TYPE: PyStaticExpr = <Value as PyReturnType>::OUTPUT_TYPE;
}
};
}
iter_next_output!(
IterNextOutput,
IterNextConvertFallback,
IterNextTypeFallback,
exhausted: Ok(null_mut())
);
iter_next_output!(
AsyncIterNextOutput,
AsyncIterNextConvertFallback,
AsyncIterNextTypeFallback,
exhausted: Err(PyStopAsyncIteration::new_err(()))
);
pub use crate::internal::pyclass_init::tp_new_resolver;
#[expect(
private_bounds,
reason = "`PyClassInit` is not a public trait, bound exist for diagnostics"
)]
pub unsafe fn tp_new_impl<'py, InitializerT, ClassT>(
py: Python<'py>,
initializer: InitializerT,
cls: *mut ffi::PyTypeObject,
) -> PyResult<*mut ffi::PyObject>
where
InitializerT: PyClassInit<'py, ClassT>,
{
unsafe { initializer.init(Borrowed::from_ptr_unchecked(py, cls.cast()).cast_unchecked()) }
.map(Bound::into_ptr)
}
#[cfg(test)]
mod tests {
#[test]
#[cfg(feature = "experimental-inspect")]
fn iter_next_output_type() {
use super::{AsyncIterNextOutput, AsyncIterNextTypeFallback as _};
use super::{IterNextOutput, IterNextTypeFallback as _};
use crate::PyResult;
for hint in [
IterNextOutput::<Option<usize>>::OUTPUT_TYPE,
IterNextOutput::<PyResult<Option<usize>>>::OUTPUT_TYPE,
AsyncIterNextOutput::<Option<usize>>::OUTPUT_TYPE,
AsyncIterNextOutput::<PyResult<Option<usize>>>::OUTPUT_TYPE,
IterNextOutput::<PyResult<usize>>::OUTPUT_TYPE,
AsyncIterNextOutput::<usize>::OUTPUT_TYPE,
] {
assert_eq!(hint.to_string(), "builtins.int");
}
assert_eq!(
IterNextOutput::<Vec<Option<usize>>>::OUTPUT_TYPE.to_string(),
"builtins.list[builtins.int | None]"
);
}
#[test]
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
fn test_fastcall_function_with_keywords() {
use super::PyMethodDef;
use crate::impl_::pyfunction::PyFunctionDef;
use crate::types::PyAnyMethods;
use crate::{ffi, Python};
Python::attach(|py| {
let def =
PyFunctionDef::from_method_def(PyMethodDef::fastcall_cfunction_with_keywords(
c"test",
accepts_no_arguments,
c"doc",
));
let def = Box::leak(Box::new(def));
unsafe extern "C" fn accepts_no_arguments(
_slf: *mut ffi::PyObject,
_args: *const *mut ffi::PyObject,
nargs: ffi::Py_ssize_t,
kwargs: *mut ffi::PyObject,
) -> *mut ffi::PyObject {
assert_eq!(nargs, 0);
assert!(kwargs.is_null());
unsafe { Python::assume_attached().None().into_ptr() }
}
let f = def.create_py_c_function(py, None).unwrap();
f.call0().unwrap();
});
}
}