From fc6fa9ead6339a84d3ee769341e454dd1953d0a9 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 19 Nov 2020 14:02:57 +0000 Subject: [PATCH 1/3] ffi: correct vectorcall bindings --- CHANGELOG.md | 2 + src/ffi/cpython/abstract_.rs | 306 +++++++++++++++++++++++++++++++++++ src/ffi/cpython/mod.rs | 3 + src/ffi/methodobject.rs | 34 ---- src/ffi/mod.rs | 6 + src/ffi/object.rs | 2 +- src/ffi/objectabstract.rs | 80 ++------- 7 files changed, 327 insertions(+), 106 deletions(-) create mode 100644 src/ffi/cpython/abstract_.rs create mode 100644 src/ffi/cpython/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 18e90ff9..823a7f60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Add context.h functions (`PyContext_New`, etc) to FFI. [#1259](https://github.com/PyO3/pyo3/pull/1259) - Add `PyAny::is_instance()` method. [#1276](https://github.com/PyO3/pyo3/pull/1276) - Add support for conversion between `char` and `PyString`. [#1282](https://github.com/PyO3/pyo3/pull/1282) +- Add FFI definitions for `PyBuffer_SizeFromFormat`, `PyObject_LengthHint`, `PyObject_CallNoArgs`, `PyObject_CallOneArg`, `PyObject_CallMethodNoArgs`, `PyObject_CallMethodOneArg`, `PyObject_VectorcallDict`, and `PyObject_VectorcallMethod`. [#1285](https://github.com/PyO3/pyo3/pull/1285) ### Changed - Change return type `PyType::name()` from `Cow` to `PyResult<&str>`. [#1152](https://github.com/PyO3/pyo3/pull/1152) @@ -38,6 +39,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed - Fix missing field in `PyCodeObject` struct (`co_posonlyargcount`) - caused invalid access to other fields in Python >3.7. [#1260](https://github.com/PyO3/pyo3/pull/1260) - Fix building for `x86_64-unknown-linux-musl` target from `x86_65-unknown-linux-gnu` host. [#1267](https://github.com/PyO3/pyo3/pull/1267) +- Fix FFI definitions for `PyObject_Vectorcall` and `PyVectorcall_Call`. [#1285](https://github.com/PyO3/pyo3/pull/1285) - Fix `#[text_signature]` interacting badly with rust `r#raw_identifiers`. [#1286](https://github.com/PyO3/pyo3/pull/1286) - Fix building with Anaconda python inside a virtualenv. [#1290](https://github.com/PyO3/pyo3/pull/1290) diff --git a/src/ffi/cpython/abstract_.rs b/src/ffi/cpython/abstract_.rs new file mode 100644 index 00000000..a2246708 --- /dev/null +++ b/src/ffi/cpython/abstract_.rs @@ -0,0 +1,306 @@ +use crate::ffi::{PyObject, Py_TYPE, Py_buffer, Py_ssize_t}; +use libc::{c_char, c_int, c_void}; + +#[cfg(all(Py_3_8, not(PyPy)))] +use crate::ffi::{ + vectorcallfunc, PyCallable_Check, PyThreadState, PyThreadState_GET, PyTuple_Check, + PyType_HasFeature, Py_TPFLAGS_HAVE_VECTORCALL, +}; +#[cfg(all(Py_3_8, not(PyPy)))] +use libc::size_t; + +extern "C" { + #[cfg(all(Py_3_8, not(PyPy)))] + pub fn _PyStack_AsDict(values: *const *mut PyObject, kwnames: *mut PyObject) -> *mut PyObject; +} + +#[cfg(all(Py_3_8, not(PyPy)))] +const _PY_FASTCALL_SMALL_STACK: size_t = 5; + +extern "C" { + #[cfg(all(Py_3_8, not(PyPy)))] + pub fn _Py_CheckFunctionResult( + tstate: *mut PyThreadState, + callable: *mut PyObject, + result: *mut PyObject, + where_: *const c_char, + ) -> *mut PyObject; + + #[cfg(all(Py_3_8, not(PyPy)))] + pub fn _PyObject_MakeTpCall( + tstate: *mut PyThreadState, + callable: *mut PyObject, + args: *const *mut PyObject, + nargs: Py_ssize_t, + keywords: *mut PyObject, + ) -> *mut PyObject; +} + +#[cfg(all(Py_3_8, not(PyPy)))] +const PY_VECTORCALL_ARGUMENTS_OFFSET: Py_ssize_t = + 1 << (8 * std::mem::size_of::() as Py_ssize_t - 1); + +#[cfg(all(Py_3_8, not(PyPy)))] +#[inline(always)] +pub unsafe fn PyVectorcall_NARGS(n: size_t) -> Py_ssize_t { + assert!(n <= (Py_ssize_t::MAX as size_t)); + (n as Py_ssize_t) & !PY_VECTORCALL_ARGUMENTS_OFFSET +} + +#[cfg(all(Py_3_8, not(PyPy)))] +#[inline(always)] +pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option { + assert!(!callable.is_null()); + let tp = Py_TYPE(callable); + if PyType_HasFeature(tp, Py_TPFLAGS_HAVE_VECTORCALL) == 0 { + return None; + } + assert!(PyCallable_Check(callable) > 0); + let offset = (*tp).tp_vectorcall_offset; + assert!(offset > 0); + let ptr = (callable as *const c_char).offset(offset) as *const Option; + *ptr +} + +#[cfg(all(Py_3_8, not(PyPy)))] +#[inline(always)] +pub unsafe fn _PyObject_VectorcallTstate( + tstate: *mut PyThreadState, + callable: *mut PyObject, + args: *const *mut PyObject, + nargsf: size_t, + kwnames: *mut PyObject, +) -> *mut PyObject { + assert!(kwnames.is_null() || PyTuple_Check(kwnames) > 0); + assert!(!args.is_null() || PyVectorcall_NARGS(nargsf) == 0); + + match PyVectorcall_Function(callable) { + None => { + let nargs = PyVectorcall_NARGS(nargsf); + _PyObject_MakeTpCall(tstate, callable, args, nargs, kwnames) + } + Some(func) => { + let res = func(callable, args, nargsf, kwnames); + _Py_CheckFunctionResult(tstate, callable, res, std::ptr::null_mut()) + } + } +} + +#[cfg(all(Py_3_8, not(PyPy)))] +#[inline(always)] +pub unsafe fn PyObject_Vectorcall( + callable: *mut PyObject, + args: *const *mut PyObject, + nargsf: size_t, + kwnames: *mut PyObject, +) -> *mut PyObject { + _PyObject_VectorcallTstate(PyThreadState_GET(), callable, args, nargsf, kwnames) +} + +extern "C" { + #[cfg(all(Py_3_8, not(PyPy)))] + #[cfg_attr(not(Py_3_9), link_name = "_PyObject_VectorcallDict")] + pub fn PyObject_VectorcallDict( + callable: *mut PyObject, + args: *const *mut PyObject, + nargsf: size_t, + kwargs: *mut PyObject, + ) -> *mut PyObject; + + #[cfg(all(Py_3_8, not(PyPy)))] + #[cfg_attr(not(Py_3_9), link_name = "_PyVectorcall_Call")] + pub fn PyVectorcall_Call( + callable: *mut PyObject, + tuple: *mut PyObject, + dict: *mut PyObject, + ) -> *mut PyObject; +} + +#[cfg(all(Py_3_8, not(PyPy)))] +#[inline(always)] +pub unsafe fn _PyObject_FastCallTstate( + tstate: *mut PyThreadState, + func: *mut PyObject, + args: *const *mut PyObject, + nargs: Py_ssize_t, +) -> *mut PyObject { + _PyObject_VectorcallTstate(tstate, func, args, nargs as size_t, std::ptr::null_mut()) +} + +#[cfg(all(Py_3_8, not(PyPy)))] +#[inline(always)] +pub unsafe fn _PyObject_FastCall( + func: *mut PyObject, + args: *const *mut PyObject, + nargs: Py_ssize_t, +) -> *mut PyObject { + _PyObject_FastCallTstate(PyThreadState_GET(), func, args, nargs) +} + +#[cfg(all(Py_3_8, not(PyPy)))] +#[inline(always)] +pub unsafe fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject { + _PyObject_VectorcallTstate( + PyThreadState_GET(), + func, + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + ) +} + +#[cfg(all(Py_3_8, not(PyPy)))] +#[inline(always)] +pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *mut PyObject { + assert!(!arg.is_null()); + let _args = [std::ptr::null_mut(), arg]; + let args = _args.as_ptr().offset(1); // For PY_VECTORCALL_ARGUMENTS_OFFSET + let tstate = PyThreadState_GET(); + let nargsf = 1 | PY_VECTORCALL_ARGUMENTS_OFFSET; + _PyObject_VectorcallTstate(tstate, func, args, nargsf as size_t, std::ptr::null_mut()) +} + +extern "C" { + #[cfg(all(Py_3_9, not(PyPy)))] + pub fn PyObject_VectorcallMethod( + name: *mut PyObject, + args: *const *mut PyObject, + nargsf: size_t, + kwnames: *mut PyObject, + ) -> *mut PyObject; +} + +#[cfg(all(Py_3_9, not(PyPy)))] +#[inline(always)] +pub unsafe fn PyObject_CallMethodNoArgs( + self_: *mut PyObject, + name: *mut PyObject, +) -> *mut PyObject { + PyObject_VectorcallMethod( + name, + &self_, + 1 | PY_VECTORCALL_ARGUMENTS_OFFSET as size_t, + std::ptr::null_mut(), + ) +} + +#[cfg(all(Py_3_9, not(PyPy)))] +#[inline(always)] +pub unsafe fn PyObject_CallMethodOneArg( + self_: *mut PyObject, + name: *mut PyObject, + arg: *mut PyObject, +) -> *mut PyObject { + let args = [self_, arg]; + assert!(!arg.is_null()); + PyObject_VectorcallMethod( + name, + args.as_ptr(), + 2 | PY_VECTORCALL_ARGUMENTS_OFFSET as size_t, + std::ptr::null_mut(), + ) +} + +// skipped _PyObject_VectorcallMethodId +// skipped _PyObject_CallMethodIdNoArgs +// skipped _PyObject_CallMethodIdOneArg + +// skipped _PyObject_HasLen + +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPyObject_LengthHint")] + pub fn PyObject_LengthHint(o: *mut PyObject, arg1: Py_ssize_t) -> Py_ssize_t; + + #[cfg(all(Py_3_9, not(PyPy)))] + pub fn PyObject_CheckBuffer(obj: *mut PyObject) -> c_int; +} + +#[cfg(not(any(Py_3_9, PyPy)))] +#[inline] +pub unsafe fn PyObject_CheckBuffer(o: *mut PyObject) -> c_int { + let tp_as_buffer = (*Py_TYPE(o)).tp_as_buffer; + (!tp_as_buffer.is_null() && (*tp_as_buffer).bf_getbuffer.is_some()) as c_int +} + +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPyObject_GetBuffer")] + pub fn PyObject_GetBuffer(obj: *mut PyObject, view: *mut Py_buffer, flags: c_int) -> c_int; + #[cfg_attr(PyPy, link_name = "PyPyBuffer_GetPointer")] + pub fn PyBuffer_GetPointer(view: *mut Py_buffer, indices: *mut Py_ssize_t) -> *mut c_void; + #[cfg_attr(PyPy, link_name = "PyPyBuffer_SizeFromFormat")] + pub fn PyBuffer_SizeFromFormat(format: *const c_char) -> Py_ssize_t; + #[cfg_attr(PyPy, link_name = "PyPyBuffer_ToContiguous")] + pub fn PyBuffer_ToContiguous( + buf: *mut c_void, + view: *mut Py_buffer, + len: Py_ssize_t, + order: c_char, + ) -> c_int; + #[cfg_attr(PyPy, link_name = "PyPyBuffer_FromContiguous")] + pub fn PyBuffer_FromContiguous( + view: *mut Py_buffer, + buf: *mut c_void, + len: Py_ssize_t, + order: c_char, + ) -> c_int; + pub fn PyObject_CopyData(dest: *mut PyObject, src: *mut PyObject) -> c_int; + #[cfg_attr(PyPy, link_name = "PyPyBuffer_IsContiguous")] + pub fn PyBuffer_IsContiguous(view: *const Py_buffer, fort: c_char) -> c_int; + pub fn PyBuffer_FillContiguousStrides( + ndims: c_int, + shape: *mut Py_ssize_t, + strides: *mut Py_ssize_t, + itemsize: c_int, + fort: c_char, + ); + #[cfg_attr(PyPy, link_name = "PyPyBuffer_FillInfo")] + pub fn PyBuffer_FillInfo( + view: *mut Py_buffer, + o: *mut PyObject, + buf: *mut c_void, + len: Py_ssize_t, + readonly: c_int, + flags: c_int, + ) -> c_int; + #[cfg_attr(PyPy, link_name = "PyPyBuffer_Release")] + pub fn PyBuffer_Release(view: *mut Py_buffer); +} + +#[inline] +pub unsafe fn PyIter_Check(o: *mut PyObject) -> c_int { + (match (*Py_TYPE(o)).tp_iternext { + Some(tp_iternext) => { + tp_iternext as *const c_void != crate::ffi::object::_PyObject_NextNotImplemented as _ + } + None => false, + }) as c_int +} + +// skipped PySequence_ITEM + +pub const PY_ITERSEARCH_COUNT: c_int = 1; +pub const PY_ITERSEARCH_INDEX: c_int = 2; +pub const PY_ITERSEARCH_CONTAINS: c_int = 3; + +extern "C" { + #[cfg(not(PyPy))] + pub fn _PySequence_IterSearch( + seq: *mut PyObject, + obj: *mut PyObject, + operation: c_int, + ) -> Py_ssize_t; +} + +// skipped _PyObject_RealIsInstance +// skipped _PyObject_RealIsSubclass + +// skipped _PySequence_BytesToCharpArray + +// skipped _Py_FreeCharPArray + +// skipped _Py_add_one_to_index_F +// skipped _Py_add_one_to_index_C + +// skipped _Py_convert_optional_to_ssize_t + +// skipped _PyNumber_Index(*mut PyObject o) diff --git a/src/ffi/cpython/mod.rs b/src/ffi/cpython/mod.rs new file mode 100644 index 00000000..7164f260 --- /dev/null +++ b/src/ffi/cpython/mod.rs @@ -0,0 +1,3 @@ +pub mod abstract_; + +pub use self::abstract_::*; diff --git a/src/ffi/methodobject.rs b/src/ffi/methodobject.rs index 193dde8b..91dd2d6f 100644 --- a/src/ffi/methodobject.rs +++ b/src/ffi/methodobject.rs @@ -16,40 +16,6 @@ pub unsafe fn PyCFunction_Check(op: *mut PyObject) -> c_int { pub type PyCFunction = unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject; -// TODO(davidhewitt)[1283] - Fix this definition -// #[cfg(all(Py_3_8, not(Py_LIMITED_API)))] -// #[cfg_attr(Py_3_8, link_name = "_PyObject_Vectorcall")] -// pub type PyObject_Vectorcall = unsafe extern "C" fn( -// slf: *mut PyObject, -// // positional and keyword arguments -// args: *const *mut PyObject, -// // number of position arguments in args, after which values are kwargs -// nargs: crate::ffi::pyport::Py_ssize_t, -// // tuple of kwargs, if given, or null -// kwnames: *mut PyObject, -// ) -> *mut PyObject; - -// TODO(davidhewitt)[1283] - Fix this definition -// #[cfg(all(Py_3_8, not(Py_LIMITED_API)))] -// #[cfg_attr(Py_3_8, link_name = "PyVectorcall_Call")] -// pub type PyVectorcall_Call = unsafe extern "C" fn( -// obj: *mut PyObject, -// tuple: *mut PyObject, -// dict: *mut PyObject, -// ) -> *mut PyObject; - -#[cfg(all(Py_3_7, not(Py_LIMITED_API)))] -const PY_VECTORCALL_ARGUMENTS_OFFSET: crate::ffi::pyport::Py_ssize_t = - 1 << (8 * std::mem::size_of::() - 1); - -#[cfg(all(Py_3_7, not(Py_LIMITED_API)))] -#[inline(always)] -pub unsafe fn PyVectorcall_NARGS( - n: crate::ffi::pyport::Py_ssize_t, -) -> crate::ffi::pyport::Py_ssize_t { - n & !PY_VECTORCALL_ARGUMENTS_OFFSET -} - #[cfg(all(Py_3_7, not(Py_LIMITED_API)))] pub type _PyCFunctionFast = unsafe extern "C" fn( slf: *mut PyObject, diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index cd901103..2edb2768 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -61,6 +61,9 @@ pub use self::unicodeobject::*; pub use self::warnings::*; pub use self::weakrefobject::*; +#[cfg(not(Py_LIMITED_API))] +pub use self::cpython::*; + mod pyport; // mod pymacro; contains nothing of interest for Rust // mod pyatomic; contains nothing of interest for Rust @@ -169,3 +172,6 @@ pub(crate) mod datetime; pub(crate) mod marshal; pub(crate) mod funcobject; + +#[cfg(not(Py_LIMITED_API))] +mod cpython; diff --git a/src/ffi/object.rs b/src/ffi/object.rs index 10255639..99fc3798 100644 --- a/src/ffi/object.rs +++ b/src/ffi/object.rs @@ -758,7 +758,7 @@ pub const Py_TPFLAGS_BASETYPE: c_ulong = 1 << 10; /// Set if the type implements the vectorcall protocol (PEP 590) #[cfg(all(Py_3_8, not(Py_LIMITED_API)))] -pub const _Py_TPFLAGS_HAVE_VECTORCALL: c_ulong = 1 << 11; +pub const Py_TPFLAGS_HAVE_VECTORCALL: c_ulong = 1 << 11; /// Set if the type is 'ready' -- fully initialized pub const Py_TPFLAGS_READY: c_ulong = 1 << 12; diff --git a/src/ffi/objectabstract.rs b/src/ffi/objectabstract.rs index bead5ebf..07116e4e 100644 --- a/src/ffi/objectabstract.rs +++ b/src/ffi/objectabstract.rs @@ -1,6 +1,6 @@ use crate::ffi::object::*; use crate::ffi::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int, c_void}; +use std::os::raw::{c_char, c_int}; use std::ptr; #[inline] @@ -16,6 +16,11 @@ pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_ } extern "C" { + #[cfg(all( + not(PyPy), + any(not(Py_LIMITED_API), Py_3_9) // Added to limited API in 3.9 + ))] + pub fn PyObject_CallNoArgs(func: *mut PyObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyObject_Call")] pub fn PyObject_Call( callable_object: *mut PyObject, @@ -61,10 +66,6 @@ pub unsafe fn PyObject_Length(o: *mut PyObject) -> Py_ssize_t { } extern "C" { - #[cfg(not(Py_LIMITED_API))] - #[cfg_attr(PyPy, link_name = "PyPyObject_LengthHint")] - pub fn PyObject_LengthHint(o: *mut PyObject, arg1: Py_ssize_t) -> Py_ssize_t; - #[cfg_attr(PyPy, link_name = "PyPyObject_GetItem")] pub fn PyObject_GetItem(o: *mut PyObject, key: *mut PyObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyObject_SetItem")] @@ -73,78 +74,15 @@ extern "C" { pub fn PyObject_DelItem(o: *mut PyObject, key: *mut PyObject) -> c_int; } -#[cfg(not(Py_LIMITED_API))] -#[inline] -pub unsafe fn PyObject_CheckBuffer(o: *mut PyObject) -> c_int { - let tp_as_buffer = (*Py_TYPE(o)).tp_as_buffer; - (!tp_as_buffer.is_null() && (*tp_as_buffer).bf_getbuffer.is_some()) as c_int -} - -#[cfg(not(Py_LIMITED_API))] -extern "C" { - #[cfg_attr(PyPy, link_name = "PyPyObject_GetBuffer")] - pub fn PyObject_GetBuffer(obj: *mut PyObject, view: *mut Py_buffer, flags: c_int) -> c_int; - #[cfg_attr(PyPy, link_name = "PyPyBuffer_GetPointer")] - pub fn PyBuffer_GetPointer(view: *mut Py_buffer, indices: *mut Py_ssize_t) -> *mut c_void; - #[cfg_attr(PyPy, link_name = "PyPyBuffer_ToContiguous")] - pub fn PyBuffer_ToContiguous( - buf: *mut c_void, - view: *mut Py_buffer, - len: Py_ssize_t, - order: c_char, - ) -> c_int; - #[cfg_attr(PyPy, link_name = "PyPyBuffer_FromContiguous")] - pub fn PyBuffer_FromContiguous( - view: *mut Py_buffer, - buf: *mut c_void, - len: Py_ssize_t, - order: c_char, - ) -> c_int; - pub fn PyObject_CopyData(dest: *mut PyObject, src: *mut PyObject) -> c_int; - #[cfg_attr(PyPy, link_name = "PyPyBuffer_IsContiguous")] - pub fn PyBuffer_IsContiguous(view: *const Py_buffer, fort: c_char) -> c_int; - pub fn PyBuffer_FillContiguousStrides( - ndims: c_int, - shape: *mut Py_ssize_t, - strides: *mut Py_ssize_t, - itemsize: c_int, - fort: c_char, - ); - #[cfg_attr(PyPy, link_name = "PyPyBuffer_FillInfo")] - pub fn PyBuffer_FillInfo( - view: *mut Py_buffer, - o: *mut PyObject, - buf: *mut c_void, - len: Py_ssize_t, - readonly: c_int, - flags: c_int, - ) -> c_int; - #[cfg_attr(PyPy, link_name = "PyPyBuffer_Release")] - pub fn PyBuffer_Release(view: *mut Py_buffer); -} - extern "C" { #[cfg_attr(PyPy, link_name = "PyPyObject_Format")] pub fn PyObject_Format(obj: *mut PyObject, format_spec: *mut PyObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyObject_GetIter")] pub fn PyObject_GetIter(arg1: *mut PyObject) -> *mut PyObject; -} -#[cfg(not(Py_LIMITED_API))] -#[inline] -#[cfg_attr(PyPy, link_name = "PyPyIter_Check")] -pub unsafe fn PyIter_Check(o: *mut PyObject) -> c_int { - (match (*Py_TYPE(o)).tp_iternext { - Some(tp_iternext) => { - tp_iternext as *const c_void - != crate::ffi::object::_PyObject_NextNotImplemented as *const c_void - } - None => false, - }) as c_int -} - -#[cfg(all(Py_LIMITED_API, Py_3_8))] -extern "C" { + // PyIter_Check for unlimited API is in cpython/abstract_.rs + #[cfg(all(Py_LIMITED_API, Py_3_8))] + #[cfg_attr(PyPy, link_name = "PyPyIter_Check")] pub fn PyIter_Check(obj: *mut PyObject) -> c_int; } From d1248d5743f5948ce5a491b59c883184b6c3796d Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 19 Nov 2020 14:27:04 +0000 Subject: [PATCH 2/3] benches: add bench_call --- benches/bench_call.rs | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 benches/bench_call.rs diff --git a/benches/bench_call.rs b/benches/bench_call.rs new file mode 100644 index 00000000..45b4ffd3 --- /dev/null +++ b/benches/bench_call.rs @@ -0,0 +1,53 @@ +#![feature(test)] + +extern crate test; +use pyo3::prelude::*; +use test::Bencher; + +macro_rules! test_module { + ($py:ident, $code:literal) => { + PyModule::from_code($py, indoc::indoc!($code), file!(), "test_module") + .expect("module creation failed") + }; +} + +#[bench] +fn bench_call_0(b: &mut Bencher) { + Python::with_gil(|py| { + let module = test_module!( + py, + r#" + def foo(): pass + "# + ); + + let foo = module.getattr("foo").unwrap(); + + b.iter(|| { + for _ in 0..1000 { + foo.call0().unwrap(); + } + }); + }) +} + +#[bench] +fn bench_call_method_0(b: &mut Bencher) { + Python::with_gil(|py| { + let module = test_module!( + py, + r#" + class Foo: + def foo(self): pass + "# + ); + + let foo = module.getattr("Foo").unwrap().call0().unwrap(); + + b.iter(|| { + for _ in 0..1000 { + foo.call_method0("foo").unwrap(); + } + }); + }) +} From 87bacf1be56c26ab2dac4120f13152431049a9c2 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 19 Nov 2020 14:34:44 +0000 Subject: [PATCH 3/3] performance: use vectorcall for call0 and call_method0 --- CHANGELOG.md | 5 ++-- Cargo.toml | 1 + src/instance.rs | 25 +++++++++++++++++-- src/types/any.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 823a7f60..ddbae8e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Add context.h functions (`PyContext_New`, etc) to FFI. [#1259](https://github.com/PyO3/pyo3/pull/1259) - Add `PyAny::is_instance()` method. [#1276](https://github.com/PyO3/pyo3/pull/1276) - Add support for conversion between `char` and `PyString`. [#1282](https://github.com/PyO3/pyo3/pull/1282) -- Add FFI definitions for `PyBuffer_SizeFromFormat`, `PyObject_LengthHint`, `PyObject_CallNoArgs`, `PyObject_CallOneArg`, `PyObject_CallMethodNoArgs`, `PyObject_CallMethodOneArg`, `PyObject_VectorcallDict`, and `PyObject_VectorcallMethod`. [#1285](https://github.com/PyO3/pyo3/pull/1285) +- Add FFI definitions for `PyBuffer_SizeFromFormat`, `PyObject_LengthHint`, `PyObject_CallNoArgs`, `PyObject_CallOneArg`, `PyObject_CallMethodNoArgs`, `PyObject_CallMethodOneArg`, `PyObject_VectorcallDict`, and `PyObject_VectorcallMethod`. [#1287](https://github.com/PyO3/pyo3/pull/1287) ### Changed - Change return type `PyType::name()` from `Cow` to `PyResult<&str>`. [#1152](https://github.com/PyO3/pyo3/pull/1152) @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Change `Debug` and `Display` impls for `PyException` to be consistent with `PyAny`. [#1275](https://github.com/PyO3/pyo3/pull/1275) - Change `Debug` impl of `PyErr` to output more helpful information (acquiring the GIL if necessary). [#1275](https://github.com/PyO3/pyo3/pull/1275) - Rename `PyTypeInfo::is_instance` and `PyTypeInfo::is_exact_instance` to `PyTypeInfo::is_type_of` and `PyTypeInfo::is_exact_type_of`. [#1278](https://github.com/PyO3/pyo3/pull/1278) +- Optimize `PyAny::call0`, `Py::call0` and `PyAny::call_method0` and `Py::call_method0` on Python 3.9 and up. [#1287](https://github.com/PyO3/pyo3/pull/1285) - Deprecate `Python::is_instance`, `Python::is_subclass`, `Python::release`, and `Python::xdecref`. [#1292](https://github.com/PyO3/pyo3/pull/1292) ### Removed @@ -39,8 +40,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed - Fix missing field in `PyCodeObject` struct (`co_posonlyargcount`) - caused invalid access to other fields in Python >3.7. [#1260](https://github.com/PyO3/pyo3/pull/1260) - Fix building for `x86_64-unknown-linux-musl` target from `x86_65-unknown-linux-gnu` host. [#1267](https://github.com/PyO3/pyo3/pull/1267) -- Fix FFI definitions for `PyObject_Vectorcall` and `PyVectorcall_Call`. [#1285](https://github.com/PyO3/pyo3/pull/1285) - Fix `#[text_signature]` interacting badly with rust `r#raw_identifiers`. [#1286](https://github.com/PyO3/pyo3/pull/1286) +- Fix FFI definitions for `PyObject_Vectorcall` and `PyVectorcall_Call`. [#1287](https://github.com/PyO3/pyo3/pull/1285) - Fix building with Anaconda python inside a virtualenv. [#1290](https://github.com/PyO3/pyo3/pull/1290) ## [0.12.3] - 2020-10-12 diff --git a/Cargo.toml b/Cargo.toml index 29a26741..9850271f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ build = "build.rs" edition = "2018" [dependencies] +cfg-if = { version = "1.0" } ctor = { version = "0.1", optional = true } indoc = { version = "1.0.3", optional = true } inventory = { version = "0.1.4", optional = true } diff --git a/src/instance.rs b/src/instance.rs index 9cfc4002..45d3a169 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -272,7 +272,18 @@ impl Py { /// /// This is equivalent to the Python expression `self()`. pub fn call0(&self, py: Python) -> PyResult { - self.call(py, (), None) + cfg_if::cfg_if! { + // TODO: Use PyObject_CallNoArgs instead after https://bugs.python.org/issue42415. + // Once the issue is resolved, we can enable this optimization for limited API. + if #[cfg(all(Py_3_9, not(Py_LIMITED_API)))] { + // Optimized path on python 3.9+ + unsafe { + PyObject::from_owned_ptr_or_err(py, ffi::_PyObject_CallNoArg(self.as_ptr())) + } + } else { + self.call(py, (), None) + } + } } /// Calls a method on the object. @@ -316,7 +327,17 @@ impl Py { /// /// This is equivalent to the Python expression `self.name()`. pub fn call_method0(&self, py: Python, name: &str) -> PyResult { - self.call_method(py, name, (), None) + cfg_if::cfg_if! { + if #[cfg(all(Py_3_9, not(Py_LIMITED_API)))] { + // Optimized path on python 3.9+ + unsafe { + let name = name.into_py(py); + PyObject::from_owned_ptr_or_err(py, ffi::PyObject_CallMethodNoArgs(self.as_ptr(), name.as_ptr())) + } + } else { + self.call_method(py, name, (), None) + } + } } /// Create a `Py` instance by taking ownership of the given FFI pointer. diff --git a/src/types/any.rs b/src/types/any.rs index 1a79aa58..5e8c2ff1 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -226,7 +226,18 @@ impl PyAny { /// /// This is equivalent to the Python expression `self()`. pub fn call0(&self) -> PyResult<&PyAny> { - self.call((), None) + cfg_if::cfg_if! { + // TODO: Use PyObject_CallNoArgs instead after https://bugs.python.org/issue42415. + // Once the issue is resolved, we can enable this optimization for limited API. + if #[cfg(all(Py_3_9, not(Py_LIMITED_API)))] { + // Optimized path on python 3.9+ + unsafe { + self.py().from_owned_ptr_or_err(ffi::_PyObject_CallNoArg(self.as_ptr())) + } + } else { + self.call((), None) + } + } } /// Calls the object with only positional arguments. @@ -283,7 +294,17 @@ impl PyAny { /// /// This is equivalent to the Python expression `self.name()`. pub fn call_method0(&self, name: &str) -> PyResult<&PyAny> { - self.call_method(name, (), None) + cfg_if::cfg_if! { + if #[cfg(all(Py_3_9, not(Py_LIMITED_API)))] { + // Optimized path on python 3.9+ + unsafe { + let name = name.into_py(self.py()); + self.py().from_owned_ptr_or_err(ffi::PyObject_CallMethodNoArgs(self.as_ptr(), name.as_ptr())) + } + } else { + self.call_method(name, (), None) + } + } } /// Calls a method on the object with only positional arguments. @@ -464,8 +485,17 @@ impl PyAny { #[cfg(test)] mod test { - use crate::types::{IntoPyDict, PyList, PyLong}; - use crate::{Python, ToPyObject}; + use crate::{ + types::{IntoPyDict, PyList, PyLong, PyModule}, + Python, ToPyObject, + }; + + macro_rules! test_module { + ($py:ident, $code:literal) => { + PyModule::from_code($py, indoc::indoc!($code), file!(), "test_module") + .expect("module creation failed") + }; + } #[test] fn test_call_for_non_existing_method() { @@ -488,6 +518,30 @@ mod test { assert_eq!(list.extract::>(py).unwrap(), vec![7, 6, 5, 4, 3]); } + #[test] + fn test_call_method0() { + Python::with_gil(|py| { + let module = test_module!( + py, + r#" + class SimpleClass: + def foo(self): + return 42 + "# + ); + + let simple_class = module.getattr("SimpleClass").unwrap().call0().unwrap(); + assert_eq!( + simple_class + .call_method0("foo") + .unwrap() + .extract::() + .unwrap(), + 42 + ); + }) + } + #[test] fn test_type() { let gil = Python::acquire_gil();