Skip to content

Commit

Permalink
use "fastcall" convention on abi3, if >3.10 (#4415)
Browse files Browse the repository at this point in the history
* use "fastcall" convention on abi3, if >3.10

* improve coverage

* coverage, clippy

* use apis available on all versions
  • Loading branch information
davidhewitt authored Aug 16, 2024
1 parent 6087a15 commit 52dc139
Show file tree
Hide file tree
Showing 12 changed files with 89 additions and 35 deletions.
2 changes: 1 addition & 1 deletion examples/string-sum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ static mut METHODS: &[PyMethodDef] = &[
PyMethodDef {
ml_name: c_str!("sum_as_string").as_ptr(),
ml_meth: PyMethodDefPointer {
_PyCFunctionFast: sum_as_string,
PyCFunctionFast: sum_as_string,
},
ml_flags: METH_FASTCALL,
ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(),
Expand Down
1 change: 1 addition & 0 deletions newsfragments/4415.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add FFI definitions `PyCFunctionFast` and `PyCFunctionFastWithKeywords`
1 change: 1 addition & 0 deletions newsfragments/4415.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use "fastcall" Python calling convention for `#[pyfunction]`s when compiling on abi3 for Python 3.10 and up.
2 changes: 1 addition & 1 deletion pyo3-ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ static mut METHODS: [PyMethodDef; 2] = [
PyMethodDef {
ml_name: c_str!("sum_as_string").as_ptr(),
ml_meth: PyMethodDefPointer {
_PyCFunctionFast: sum_as_string,
PyCFunctionFast: sum_as_string,
},
ml_flags: METH_FASTCALL,
ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(),
Expand Down
2 changes: 1 addition & 1 deletion pyo3-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
//! PyMethodDef {
//! ml_name: c_str!("sum_as_string").as_ptr(),
//! ml_meth: PyMethodDefPointer {
//! _PyCFunctionFast: sum_as_string,
//! PyCFunctionFast: sum_as_string,
//! },
//! ml_flags: METH_FASTCALL,
//! ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(),
Expand Down
30 changes: 24 additions & 6 deletions pyo3-ffi/src/methodobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,34 @@ pub type PyCFunction =
unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject;

#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
pub type _PyCFunctionFast = unsafe extern "C" fn(
pub type PyCFunctionFast = unsafe extern "C" fn(
slf: *mut PyObject,
args: *mut *mut PyObject,
nargs: crate::pyport::Py_ssize_t,
) -> *mut PyObject;

#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
#[deprecated(note = "renamed to `PyCFunctionFast`")]
pub type _PyCFunctionFast = PyCFunctionFast;

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

#[cfg(not(Py_LIMITED_API))]
pub type _PyCFunctionFastWithKeywords = unsafe extern "C" fn(
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
pub type PyCFunctionFastWithKeywords = unsafe extern "C" fn(
slf: *mut PyObject,
args: *const *mut PyObject,
nargs: crate::pyport::Py_ssize_t,
kwnames: *mut PyObject,
) -> *mut PyObject;

#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
#[deprecated(note = "renamed to `PyCFunctionFastWithKeywords`")]
pub type _PyCFunctionFastWithKeywords = PyCFunctionFastWithKeywords;

#[cfg(all(Py_3_9, not(Py_LIMITED_API)))]
pub type PyCMethod = unsafe extern "C" fn(
slf: *mut PyObject,
Expand Down Expand Up @@ -144,11 +152,21 @@ pub union PyMethodDefPointer {

/// This variant corresponds with [`METH_FASTCALL`].
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
pub _PyCFunctionFast: _PyCFunctionFast,
#[deprecated(note = "renamed to `PyCFunctionFast`")]
pub _PyCFunctionFast: PyCFunctionFast,

/// This variant corresponds with [`METH_FASTCALL`].
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
pub PyCFunctionFast: PyCFunctionFast,

/// This variant corresponds with [`METH_FASTCALL`] | [`METH_KEYWORDS`].
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
#[deprecated(note = "renamed to `PyCFunctionFastWithKeywords`")]
pub _PyCFunctionFastWithKeywords: PyCFunctionFastWithKeywords,

/// This variant corresponds with [`METH_FASTCALL`] | [`METH_KEYWORDS`].
#[cfg(not(Py_LIMITED_API))]
pub _PyCFunctionFastWithKeywords: _PyCFunctionFastWithKeywords,
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
pub PyCFunctionFastWithKeywords: PyCFunctionFastWithKeywords,

/// This variant corresponds with [`METH_METHOD`] | [`METH_FASTCALL`] | [`METH_KEYWORDS`].
#[cfg(all(Py_3_9, not(Py_LIMITED_API)))]
Expand Down
15 changes: 8 additions & 7 deletions pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::{ext::IdentExt, spanned::Spanned, Ident, Result};

use crate::deprecations::deprecate_trailing_option_default;
use crate::pyversions::is_abi3_before;
use crate::utils::{Ctx, LitCStr};
use crate::{
attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue},
Expand All @@ -15,7 +16,7 @@ use crate::{
FunctionSignature, PyFunctionArgPyO3Attributes, PyFunctionOptions, SignatureAttribute,
},
quotes,
utils::{self, is_abi3, PythonDoc},
utils::{self, PythonDoc},
};

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -374,7 +375,7 @@ impl SelfType {
pub enum CallingConvention {
Noargs, // METH_NOARGS
Varargs, // METH_VARARGS | METH_KEYWORDS
Fastcall, // METH_FASTCALL | METH_KEYWORDS (not compatible with `abi3` feature)
Fastcall, // METH_FASTCALL | METH_KEYWORDS (not compatible with `abi3` feature before 3.10)
TpNew, // special convention for tp_new
}

Expand All @@ -386,11 +387,11 @@ impl CallingConvention {
pub fn from_signature(signature: &FunctionSignature<'_>) -> Self {
if signature.python_signature.has_no_args() {
Self::Noargs
} else if signature.python_signature.kwargs.is_some() {
// for functions that accept **kwargs, always prefer varargs
Self::Varargs
} else if !is_abi3() {
// FIXME: available in the stable ABI since 3.10
} else if signature.python_signature.kwargs.is_none() && !is_abi3_before(3, 10) {
// For functions that accept **kwargs, always prefer varargs for now based on
// historical performance testing.
//
// FASTCALL not compatible with `abi3` before 3.10
Self::Fastcall
} else {
Self::Varargs
Expand Down
11 changes: 4 additions & 7 deletions pyo3-macros-backend/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ use crate::pymethod::{
impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType,
SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, __STR__,
};
use crate::pyversions;
use crate::utils::{self, apply_renaming_rule, LitCStr, PythonDoc};
use crate::utils::{is_abi3, Ctx};
use crate::pyversions::is_abi3_before;
use crate::utils::{self, apply_renaming_rule, Ctx, LitCStr, PythonDoc};
use crate::PyFunctionOptions;

/// If the class is derived from a Rust `struct` or `enum`.
Expand Down Expand Up @@ -186,13 +185,11 @@ impl PyClassPyO3Options {
};
}

let python_version = pyo3_build_config::get().version;

match option {
PyClassPyO3Option::Crate(krate) => set_option!(krate),
PyClassPyO3Option::Dict(dict) => {
ensure_spanned!(
python_version >= pyversions::PY_3_9 || !is_abi3(),
!is_abi3_before(3, 9),
dict.span() => "`dict` requires Python >= 3.9 when using the `abi3` feature"
);
set_option!(dict);
Expand All @@ -216,7 +213,7 @@ impl PyClassPyO3Options {
PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable),
PyClassPyO3Option::Weakref(weakref) => {
ensure_spanned!(
python_version >= pyversions::PY_3_9 || !is_abi3(),
!is_abi3_before(3, 9),
weakref.span() => "`weakref` requires Python >= 3.9 when using the `abi3` feature"
);
set_option!(weakref);
Expand Down
5 changes: 4 additions & 1 deletion pyo3-macros-backend/src/pyversions.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use pyo3_build_config::PythonVersion;

pub const PY_3_9: PythonVersion = PythonVersion { major: 3, minor: 9 };
pub fn is_abi3_before(major: u8, minor: u8) -> bool {
let config = pyo3_build_config::get();
config.abi3 && config.version < PythonVersion { major, minor }
}
4 changes: 0 additions & 4 deletions pyo3-macros-backend/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,6 @@ pub fn apply_renaming_rule(rule: RenamingRule, name: &str) -> String {
}
}

pub(crate) fn is_abi3() -> bool {
pyo3_build_config::get().abi3
}

pub(crate) enum IdentOrStr<'a> {
Str(&'a str),
Ident(syn::Ident),
Expand Down
2 changes: 1 addition & 1 deletion src/impl_/extract_argument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ impl FunctionDescription {
/// - `args` must be a pointer to a C-style array of valid `ffi::PyObject` pointers, or NULL.
/// - `kwnames` must be a pointer to a PyTuple, or NULL.
/// - `nargs + kwnames.len()` is the total length of the `args` array.
#[cfg(not(Py_LIMITED_API))]
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
pub unsafe fn extract_arguments_fastcall<'py, V, K>(
&self,
py: Python<'py>,
Expand Down
49 changes: 43 additions & 6 deletions src/impl_/pymethods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ pub enum PyMethodDefType {
pub enum PyMethodType {
PyCFunction(ffi::PyCFunction),
PyCFunctionWithKeywords(ffi::PyCFunctionWithKeywords),
#[cfg(not(Py_LIMITED_API))]
PyCFunctionFastWithKeywords(ffi::_PyCFunctionFastWithKeywords),
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
PyCFunctionFastWithKeywords(ffi::PyCFunctionFastWithKeywords),
}

pub type PyClassAttributeFactory = for<'p> fn(Python<'p>) -> PyResult<PyObject>;
Expand Down Expand Up @@ -145,10 +145,10 @@ impl PyMethodDef {
}

/// Define a function that can take `*args` and `**kwargs`.
#[cfg(not(Py_LIMITED_API))]
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
pub const fn fastcall_cfunction_with_keywords(
ml_name: &'static CStr,
cfunction: ffi::_PyCFunctionFastWithKeywords,
cfunction: ffi::PyCFunctionFastWithKeywords,
ml_doc: &'static CStr,
) -> Self {
Self {
Expand All @@ -171,9 +171,9 @@ impl PyMethodDef {
PyMethodType::PyCFunctionWithKeywords(meth) => ffi::PyMethodDefPointer {
PyCFunctionWithKeywords: meth,
},
#[cfg(not(Py_LIMITED_API))]
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
PyMethodType::PyCFunctionFastWithKeywords(meth) => ffi::PyMethodDefPointer {
_PyCFunctionFastWithKeywords: meth,
PyCFunctionFastWithKeywords: meth,
},
};

Expand Down Expand Up @@ -519,3 +519,40 @@ pub unsafe fn tp_new_impl<T: PyClass>(
.create_class_object_of_type(py, target_type)
.map(Bound::into_ptr)
}

#[cfg(test)]
mod tests {
#[test]
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
fn test_fastcall_function_with_keywords() {
use super::PyMethodDef;
use crate::types::{PyAnyMethods, PyCFunction};
use crate::{ffi, Python};

Python::with_gil(|py| {
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());
Python::assume_gil_acquired().None().into_ptr()
}

let f = PyCFunction::internal_new(
py,
&PyMethodDef::fastcall_cfunction_with_keywords(
ffi::c_str!("test"),
accepts_no_arguments,
ffi::c_str!("doc"),
),
None,
)
.unwrap();

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

0 comments on commit 52dc139

Please sign in to comment.