Change function calls to use vectorcall (#5948)
* Make argument_vector re-usable for other types.
* Attempt to collect args into array for vectorcall
* Revert "Attempt to collect args into array for vectorcall"
This reverts commit 418a034195c5c8b517e7404686555c097efe7a4b.
* Implement vectorcall args collector
* pre-commit fixes
* Checkpoint in moving to METH_FASTCALL
* pre-commit fixes
* Use the names tuple directly, cleaner code and less reference counting
* Fix unit test, the code now holds more references
It cannot re-use the incoming tuple as before, because it is no longer a tuple at all. So a new tuple must be created, which then holds references for each member.
* Make clangtidy happy
* Oops, _v is C++14
* style: pre-commit fixes
* Minor code cleanup
* Fix signed conversions
* Fix args expansion
This would be easier with `if constexpr`
* style: pre-commit fixes
* Code cleanup
* fix(tests): Install multiple-interpreter test modules into wheel
The `mod_per_interpreter_gil`, `mod_shared_interpreter_gil`, and
`mod_per_interpreter_gil_with_singleton` modules were being built
but not installed into the wheel when using scikit-build-core
(SKBUILD=true). This caused iOS (and potentially Android) CIBW
tests to fail with ModuleNotFoundError.
Root cause analysis:
- The main test targets have install() commands (line 531)
- The PYBIND11_MULTIPLE_INTERPRETERS_TEST_MODULES were missing
equivalent install() commands
- For regular CMake builds, this wasn't a problem because
LIBRARY_OUTPUT_DIRECTORY places the modules next to pybind11_tests
- For wheel builds, only targets with explicit install() commands
are included in the wheel
This issue was latent until commit fee2527d changed the test imports
from `pytest.importorskip()` (graceful skip) to direct `import`
statements (hard failure), which exposed the missing modules.
Failing tests:
- test_multiple_interpreters.py::test_independent_subinterpreters
- test_multiple_interpreters.py::test_dependent_subinterpreters
Error: ModuleNotFoundError: No module named 'mod_per_interpreter_gil'
* tests: Pin numpy 2.4.0 for Python 3.14 CI tests
Add numpy==2.4.0 requirement for Python 3.14 (both default and
free-threaded builds). NumPy 2.4.0 is the first version to provide
official PyPI wheels for Python 3.14:
- numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64...whl (default)
- numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64...whl (free-threaded)
Previously, CI was skipping all numpy-dependent tests for Python 3.14
because PIP_ONLY_BINARY was set and no wheels were available:
SKIPPED [...] test_numpy_array.py:8: could not import 'numpy':
No module named 'numpy'
With this change, the full numpy test suite will run on Python 3.14,
providing better test coverage for the newest Python version.
Note: Using exact pin (==2.4.0) rather than compatible release (~=2.4.0)
to ensure reproducible CI results with the first known-working version.
* tests: Add verbose flag to CIBW pytest command
Add `-v` to the pytest command in tests/pyproject.toml to help
diagnose hanging tests in CIBW jobs (particularly iOS).
This will show each test name as it runs, making it easier to
identify which specific test is hanging.
* tests: Skip subinterpreter tests on iOS, add pytest timeout
- Add `IOS` platform constant to `tests/env.py` for consistency with
existing `ANDROID`, `LINUX`, `MACOS`, `WIN`, `FREEBSD` constants.
- Skip `test_multiple_interpreters.py` module on iOS. Subinterpreters
are not supported in the iOS simulator environment. These tests were
previously skipped implicitly because the modules weren't installed
in the wheel; now that they are (commit 6ed6d5a8), we need an
explicit skip.
- Change pytest timeout from 0 (disabled) to 120 seconds. This provides
a safety net to catch hanging tests before the CI job times out after
hours. Normal test runs complete in 33-55 seconds total (~1100 tests),
so 120 seconds per test is very generous.
- Add `-v` flag for verbose output to help diagnose any future issues.
* More cleanups in argument vector, per comments.
* Per Cursor, move all versions to Vectorcall since it has been supported since 3.8.
This means getting rid of simple_collector, we can do the same with a constexpr if in the unpacking_collector.
* Switch to a bool vec for the used_kwargs flag...
This makes more sense and saves a sort, and the small_vector implementation means it will actually take less space than a vector of size_t elements.
The most common case is that all kwargs are used.
* Fix signedness for clang
* Another signedness issue
* tests: Disable pytest-timeout for Pyodide (no signal.setitimer)
Pyodide runs in a WebAssembly sandbox without POSIX signals, so
`signal.setitimer` is not available. This causes pytest-timeout to
crash with `AttributeError: module 'signal' has no attribute 'setitimer'`
when timeout > 0.
Override the test-command for Pyodide to keep timeout=0 (disabled).
* Combine temp storage and args into one vector
It's a good bit faster at the cost of this one scary reinterpret_cast.
* Phrasing
* Delete incorrect comment
At 6, the struct is 144 bytes (not 128 bytes as the comment said).
* Fix push_back
* Update push_back in argument_vector.h
Co-authored-by: Aaron Gokaslan <aaronGokaslan@gmail.com>
* style: pre-commit fixes
* Use real types for these instead of object
They can be null if you "steal" a null handle.
* refactor: Replace small_vector<object> with ref_small_vector for explicit ownership
Introduce `ref_small_vector` to manage PyObject* references in `unpacking_collector`,
replacing the previous `small_vector<object>` approach.
Primary goals:
1. **Maintainability**: The previous implementation relied on
`sizeof(object) == sizeof(PyObject*)` and used a reinterpret_cast to
pass the object array to vectorcall. This coupling to py::object's
internal layout could break if someone adds a debug field or other
member to py::handle/py::object in the future.
2. **Readability**: The new `push_back_steal()` vs `push_back_borrow()`
API makes reference counting intent explicit at each call site,
rather than relying on implicit py::object semantics.
3. **Intuitive code**: Storing `PyObject*` directly and passing it to
`_PyObject_Vectorcall` without casts is straightforward and matches
what the C API expects. No "scary" reinterpret_cast needed.
Additional benefits:
- `PyObject*` is trivially copyable, simplifying vector operations
- Batch decref in destructor (tight loop vs N individual object destructors)
- Self-documenting ownership semantics
Design consideration: We considered folding the ref-counting functionality
directly into `small_vector` via template specialization for `PyObject*`.
We decided against this because:
- It would give `small_vector<PyObject*, N>` a different interface than the
generic `small_vector<T, N>` (steal/borrow vs push_back)
- Someone might want a non-ref-counting `small_vector<PyObject*, N>`
- The specialization behavior could surprise users expecting uniform semantics
A separate `ref_small_vector` type makes the ref-counting behavior explicit
and self-documenting, while keeping `small_vector` generic and predictable.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
Co-authored-by: Aaron Gokaslan <aaronGokaslan@gmail.com>
diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h
index 310b77b..f5a94da 100644
--- a/include/pybind11/cast.h
+++ b/include/pybind11/cast.h
@@ -2078,8 +2078,7 @@
// forward declaration (definition in attr.h)
struct function_record;
-/// (Inline size chosen mostly arbitrarily; 6 should pad function_call out to two cache lines
-/// (16 pointers) in size.)
+/// Inline size chosen mostly arbitrarily.
constexpr std::size_t arg_vector_small_size = 6;
/// Internal data associated with a single function call
@@ -2191,86 +2190,121 @@
std::tuple<make_caster<Args>...> argcasters;
};
-/// Helper class which collects only positional arguments for a Python function call.
-/// A fancier version below can collect any argument, but this one is optimal for simple calls.
-template <return_value_policy policy>
-class simple_collector {
-public:
- template <typename... Ts>
- explicit simple_collector(Ts &&...values)
- : m_args(pybind11::make_tuple<policy>(std::forward<Ts>(values)...)) {}
-
- const tuple &args() const & { return m_args; }
- dict kwargs() const { return {}; }
-
- tuple args() && { return std::move(m_args); }
-
- /// Call a Python function and pass the collected arguments
- object call(PyObject *ptr) const {
- PyObject *result = PyObject_CallObject(ptr, m_args.ptr());
- if (!result) {
- throw error_already_set();
- }
- return reinterpret_steal<object>(result);
- }
-
-private:
- tuple m_args;
-};
+// [workaround(intel)] Separate function required here
+// We need to put this into a separate function because the Intel compiler
+// fails to compile enable_if_t<!all_of<is_positional<Args>...>::value>
+// (tested with ICC 2021.1 Beta 20200827).
+template <typename... Args>
+constexpr bool args_has_keyword_or_ds() {
+ return any_of<is_keyword_or_ds<Args>...>::value;
+}
/// Helper class which collects positional, keyword, * and ** arguments for a Python function call
template <return_value_policy policy>
class unpacking_collector {
public:
template <typename... Ts>
- explicit unpacking_collector(Ts &&...values) {
- // Tuples aren't (easily) resizable so a list is needed for collection,
- // but the actual function call strictly requires a tuple.
- auto args_list = list();
- using expander = int[];
- (void) expander{0, (process(args_list, std::forward<Ts>(values)), 0)...};
+ explicit unpacking_collector(Ts &&...values)
+ : m_names(reinterpret_steal<tuple>(
+ handle())) // initialize to null to avoid useless allocation of 0-length tuple
+ {
+ /*
+ Python can sometimes utilize an extra space before the arguments to prepend `self`.
+ This is important enough that there is a special flag for it:
+ PY_VECTORCALL_ARGUMENTS_OFFSET.
+ All we have to do is allocate an extra space at the beginning of this array, and set the
+ flag. Note that the extra space is not passed directly in to vectorcall.
+ */
+ m_args.reserve(sizeof...(values) + 1);
+ m_args.push_back_null();
- m_args = std::move(args_list);
+ if (args_has_keyword_or_ds<Ts...>()) {
+ list names_list;
+
+ // collect_arguments guarantees this can't be constructed with kwargs before the last
+ // positional so we don't need to worry about Ts... being in anything but normal python
+ // order.
+ using expander = int[];
+ (void) expander{0, (process(names_list, std::forward<Ts>(values)), 0)...};
+
+ m_names = reinterpret_steal<tuple>(PyList_AsTuple(names_list.ptr()));
+ } else {
+ auto not_used
+ = reinterpret_steal<list>(handle()); // initialize as null (to avoid an allocation)
+
+ using expander = int[];
+ (void) expander{0, (process(not_used, std::forward<Ts>(values)), 0)...};
+ }
}
- const tuple &args() const & { return m_args; }
- const dict &kwargs() const & { return m_kwargs; }
-
- tuple args() && { return std::move(m_args); }
- dict kwargs() && { return std::move(m_kwargs); }
-
/// Call a Python function and pass the collected arguments
object call(PyObject *ptr) const {
- PyObject *result = PyObject_Call(ptr, m_args.ptr(), m_kwargs.ptr());
+ size_t nargs = m_args.size() - 1; // -1 for PY_VECTORCALL_ARGUMENTS_OFFSET (see ctor)
+ if (m_names) {
+ nargs -= m_names.size();
+ }
+ PyObject *result = _PyObject_Vectorcall(
+ ptr, m_args.data() + 1, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, m_names.ptr());
if (!result) {
throw error_already_set();
}
return reinterpret_steal<object>(result);
}
+ tuple args() const {
+ size_t nargs = m_args.size() - 1; // -1 for PY_VECTORCALL_ARGUMENTS_OFFSET (see ctor)
+ if (m_names) {
+ nargs -= m_names.size();
+ }
+ tuple val(nargs);
+ for (size_t i = 0; i < nargs; ++i) {
+ // +1 for PY_VECTORCALL_ARGUMENTS_OFFSET (see ctor)
+ val[i] = reinterpret_borrow<object>(m_args[i + 1]);
+ }
+ return val;
+ }
+
+ dict kwargs() const {
+ dict val;
+ if (m_names) {
+ size_t offset = m_args.size() - m_names.size();
+ for (size_t i = 0; i < m_names.size(); ++i, ++offset) {
+ val[m_names[i]] = reinterpret_borrow<object>(m_args[offset]);
+ }
+ }
+ return val;
+ }
+
private:
+ // normal argument, possibly needing conversion
template <typename T>
- void process(list &args_list, T &&x) {
- auto o = reinterpret_steal<object>(
- detail::make_caster<T>::cast(std::forward<T>(x), policy, {}));
- if (!o) {
+ void process(list & /*names_list*/, T &&x) {
+ handle h = detail::make_caster<T>::cast(std::forward<T>(x), policy, {});
+ if (!h) {
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES)
- throw cast_error_unable_to_convert_call_arg(std::to_string(args_list.size()));
+ throw cast_error_unable_to_convert_call_arg(std::to_string(m_args.size() - 1));
#else
- throw cast_error_unable_to_convert_call_arg(std::to_string(args_list.size()),
+ throw cast_error_unable_to_convert_call_arg(std::to_string(m_args.size() - 1),
type_id<T>());
#endif
}
- args_list.append(std::move(o));
+ m_args.push_back_steal(h.ptr()); // cast returns a new reference
}
- void process(list &args_list, detail::args_proxy ap) {
+ // * unpacking
+ void process(list & /*names_list*/, detail::args_proxy ap) {
+ if (!ap) {
+ return;
+ }
for (auto a : ap) {
- args_list.append(a);
+ m_args.push_back_borrow(a.ptr());
}
}
- void process(list & /*args_list*/, arg_v a) {
+ // named argument
+ // NOLINTNEXTLINE(performance-unnecessary-value-param)
+ void process(list &names_list, arg_v a) {
+ assert(names_list);
if (!a.name) {
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES)
nameless_argument_error();
@@ -2278,7 +2312,8 @@
nameless_argument_error(a.type);
#endif
}
- if (m_kwargs.contains(a.name)) {
+ auto name = str(a.name);
+ if (names_list.contains(name)) {
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES)
multiple_values_error();
#else
@@ -2292,22 +2327,27 @@
throw cast_error_unable_to_convert_call_arg(a.name, a.type);
#endif
}
- m_kwargs[a.name] = std::move(a.value);
+ names_list.append(std::move(name));
+ m_args.push_back_borrow(a.value.ptr());
}
- void process(list & /*args_list*/, detail::kwargs_proxy kp) {
+ // ** unpacking
+ void process(list &names_list, detail::kwargs_proxy kp) {
if (!kp) {
return;
}
- for (auto k : reinterpret_borrow<dict>(kp)) {
- if (m_kwargs.contains(k.first)) {
+ assert(names_list);
+ for (auto &&k : reinterpret_borrow<dict>(kp)) {
+ auto name = str(k.first);
+ if (names_list.contains(name)) {
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES)
multiple_values_error();
#else
- multiple_values_error(str(k.first));
+ multiple_values_error(name);
#endif
}
- m_kwargs[k.first] = k.second;
+ names_list.append(std::move(name));
+ m_args.push_back_borrow(k.second.ptr());
}
}
@@ -2333,39 +2373,20 @@
}
private:
- tuple m_args;
- dict m_kwargs;
+ ref_small_vector<arg_vector_small_size> m_args;
+ tuple m_names;
};
-// [workaround(intel)] Separate function required here
-// We need to put this into a separate function because the Intel compiler
-// fails to compile enable_if_t<!all_of<is_positional<Args>...>::value>
-// (tested with ICC 2021.1 Beta 20200827).
-template <typename... Args>
-constexpr bool args_are_all_positional() {
- return all_of<is_positional<Args>...>::value;
-}
-
-/// Collect only positional arguments for a Python function call
-template <return_value_policy policy,
- typename... Args,
- typename = enable_if_t<args_are_all_positional<Args...>()>>
-simple_collector<policy> collect_arguments(Args &&...args) {
- return simple_collector<policy>(std::forward<Args>(args)...);
-}
-
-/// Collect all arguments, including keywords and unpacking (only instantiated when needed)
-template <return_value_policy policy,
- typename... Args,
- typename = enable_if_t<!args_are_all_positional<Args...>()>>
+/// Collect all arguments, including keywords and unpacking
+template <return_value_policy policy, typename... Args>
unpacking_collector<policy> collect_arguments(Args &&...args) {
// Following argument order rules for generalized unpacking according to PEP 448
- static_assert(constexpr_last<is_positional, Args...>()
- < constexpr_first<is_keyword_or_ds, Args...>()
- && constexpr_last<is_s_unpacking, Args...>()
- < constexpr_first<is_ds_unpacking, Args...>(),
- "Invalid function call: positional args must precede keywords and ** unpacking; "
- "* unpacking must precede ** unpacking");
+ static_assert(
+ constexpr_last<is_positional, Args...>() < constexpr_first<is_keyword_or_ds, Args...>(),
+ "Invalid function call: positional args must precede keywords and */** unpacking;");
+ static_assert(constexpr_last<is_s_unpacking, Args...>()
+ < constexpr_first<is_ds_unpacking, Args...>(),
+ "Invalid function call: * unpacking must precede ** unpacking");
return unpacking_collector<policy>(std::forward<Args>(args)...);
}
diff --git a/include/pybind11/detail/argument_vector.h b/include/pybind11/detail/argument_vector.h
index e15a3cf..6e2c2ec 100644
--- a/include/pybind11/detail/argument_vector.h
+++ b/include/pybind11/detail/argument_vector.h
@@ -66,24 +66,23 @@
inline_array iarray;
heap_vector hvector;
- static_assert(std::is_trivially_move_constructible<ArrayT>::value,
- "ArrayT must be trivially move constructible");
- static_assert(std::is_trivially_destructible<ArrayT>::value,
- "ArrayT must be trivially destructible");
-
inline_array_or_vector() : iarray() {}
+
~inline_array_or_vector() {
- if (!is_inline()) {
+ if (is_inline()) {
+ iarray.~inline_array();
+ } else {
hvector.~heap_vector();
}
}
+
// Disable copy ctor and assignment.
inline_array_or_vector(const inline_array_or_vector &) = delete;
inline_array_or_vector &operator=(const inline_array_or_vector &) = delete;
inline_array_or_vector(inline_array_or_vector &&rhs) noexcept {
if (rhs.is_inline()) {
- std::memcpy(&iarray, &rhs.iarray, sizeof(iarray));
+ new (&iarray) inline_array(std::move(rhs.iarray));
} else {
new (&hvector) heap_vector(std::move(rhs.hvector));
}
@@ -95,17 +94,16 @@
return *this;
}
- if (rhs.is_inline()) {
- if (!is_inline()) {
- hvector.~heap_vector();
- }
- std::memcpy(&iarray, &rhs.iarray, sizeof(iarray));
+ if (is_inline()) {
+ iarray.~inline_array();
} else {
- if (is_inline()) {
- new (&hvector) heap_vector(std::move(rhs.hvector));
- } else {
- hvector = std::move(rhs.hvector);
- }
+ hvector.~heap_vector();
+ }
+
+ if (rhs.is_inline()) {
+ new (&iarray) inline_array(std::move(rhs.iarray));
+ } else {
+ new (&hvector) heap_vector(std::move(rhs.hvector));
}
return *this;
}
@@ -126,18 +124,16 @@
}
};
-// small_vector-like container to avoid heap allocation for N or fewer
-// arguments.
-template <std::size_t N>
-struct argument_vector {
+template <typename T, std::size_t InlineSize>
+struct small_vector {
public:
- argument_vector() = default;
+ small_vector() = default;
// Disable copy ctor and assignment.
- argument_vector(const argument_vector &) = delete;
- argument_vector &operator=(const argument_vector &) = delete;
- argument_vector(argument_vector &&) noexcept = default;
- argument_vector &operator=(argument_vector &&) noexcept = default;
+ small_vector(const small_vector &) = delete;
+ small_vector &operator=(const small_vector &) = delete;
+ small_vector(small_vector &&) noexcept = default;
+ small_vector &operator=(small_vector &&) noexcept = default;
std::size_t size() const {
if (is_inline()) {
@@ -146,7 +142,14 @@
return m_repr.hvector.vec.size();
}
- handle &operator[](std::size_t idx) {
+ T const *data() const {
+ if (is_inline()) {
+ return m_repr.iarray.arr.data();
+ }
+ return m_repr.hvector.vec.data();
+ }
+
+ T &operator[](std::size_t idx) {
assert(idx < size());
if (is_inline()) {
return m_repr.iarray.arr[idx];
@@ -154,7 +157,7 @@
return m_repr.hvector.vec[idx];
}
- handle operator[](std::size_t idx) const {
+ T const &operator[](std::size_t idx) const {
assert(idx < size());
if (is_inline()) {
return m_repr.iarray.arr[idx];
@@ -162,28 +165,28 @@
return m_repr.hvector.vec[idx];
}
- void push_back(handle x) {
+ void push_back(const T &x) { emplace_back(x); }
+
+ void push_back(T &&x) { emplace_back(std::move(x)); }
+
+ template <typename... Args>
+ void emplace_back(Args &&...x) {
if (is_inline()) {
auto &ha = m_repr.iarray;
- if (ha.size == N) {
- move_to_heap_vector_with_reserved_size(N + 1);
- push_back_slow_path(x);
+ if (ha.size == InlineSize) {
+ move_to_heap_vector_with_reserved_size(InlineSize + 1);
+ m_repr.hvector.vec.emplace_back(std::forward<Args>(x)...);
} else {
- ha.arr[ha.size++] = x;
+ ha.arr[ha.size++] = T(std::forward<Args>(x)...);
}
} else {
- push_back_slow_path(x);
+ m_repr.hvector.vec.emplace_back(std::forward<Args>(x)...);
}
}
- template <typename Arg>
- void emplace_back(Arg &&x) {
- push_back(handle(x));
- }
-
void reserve(std::size_t sz) {
if (is_inline()) {
- if (sz > N) {
+ if (sz > InlineSize) {
move_to_heap_vector_with_reserved_size(sz);
}
} else {
@@ -192,7 +195,7 @@
}
private:
- using repr_type = inline_array_or_vector<handle, N>;
+ using repr_type = inline_array_or_vector<T, InlineSize>;
repr_type m_repr;
PYBIND11_NOINLINE void move_to_heap_vector_with_reserved_size(std::size_t reserved_size) {
@@ -201,32 +204,33 @@
using heap_vector = typename repr_type::heap_vector;
heap_vector hv;
hv.vec.reserve(reserved_size);
- std::copy(ha.arr.begin(), ha.arr.begin() + ha.size, std::back_inserter(hv.vec));
+ static_assert(std::is_nothrow_move_constructible<T>::value,
+ "this conversion is not exception safe");
+ static_assert(std::is_nothrow_move_constructible<heap_vector>::value,
+ "this conversion is not exception safe");
+ std::move(ha.arr.begin(), ha.arr.begin() + ha.size, std::back_inserter(hv.vec));
new (&m_repr.hvector) heap_vector(std::move(hv));
}
- PYBIND11_NOINLINE void push_back_slow_path(handle x) { m_repr.hvector.vec.push_back(x); }
-
PYBIND11_NOINLINE void reserve_slow_path(std::size_t sz) { m_repr.hvector.vec.reserve(sz); }
bool is_inline() const { return m_repr.is_inline(); }
};
-// small_vector-like container to avoid heap allocation for N or fewer
-// arguments.
+// Container to avoid heap allocation for kRequestedInlineSize or fewer booleans.
template <std::size_t kRequestedInlineSize>
-struct args_convert_vector {
+struct small_vector<bool, kRequestedInlineSize> {
private:
public:
- args_convert_vector() = default;
+ small_vector() = default;
// Disable copy ctor and assignment.
- args_convert_vector(const args_convert_vector &) = delete;
- args_convert_vector &operator=(const args_convert_vector &) = delete;
- args_convert_vector(args_convert_vector &&) noexcept = default;
- args_convert_vector &operator=(args_convert_vector &&) noexcept = default;
+ small_vector(const small_vector &) = delete;
+ small_vector &operator=(const small_vector &) = delete;
+ small_vector(small_vector &&) noexcept = default;
+ small_vector &operator=(small_vector &&) noexcept = default;
- args_convert_vector(std::size_t count, bool value) {
+ small_vector(std::size_t count, bool value) {
if (count > kInlineSize) {
new (&m_repr.hvector) typename repr_type::heap_vector(count, value);
} else {
@@ -284,7 +288,24 @@
}
}
- void swap(args_convert_vector &rhs) noexcept { std::swap(m_repr, rhs.m_repr); }
+ void set(std::size_t idx, bool value = true) {
+ if (is_inline()) {
+ auto &ha = m_repr.iarray;
+ assert(ha.size < kInlineSize);
+ const auto wbi = word_and_bit_index(idx);
+ assert(wbi.word < kWords);
+ assert(wbi.bit < kBitsPerWord);
+ if (value) {
+ ha.arr[wbi.word] |= (static_cast<std::size_t>(1) << wbi.bit);
+ } else {
+ ha.arr[wbi.word] &= ~(static_cast<std::size_t>(1) << wbi.bit);
+ }
+ } else {
+ m_repr.hvector.vec[idx] = value;
+ }
+ }
+
+ void swap(small_vector &rhs) noexcept { std::swap(m_repr, rhs.m_repr); }
private:
struct WordAndBitIndex {
@@ -326,5 +347,71 @@
bool is_inline() const { return m_repr.is_inline(); }
};
+// Container to avoid heap allocation for N or fewer arguments.
+template <size_t N>
+using argument_vector = small_vector<handle, N>;
+
+// Container to avoid heap allocation for N or fewer booleans.
+template <size_t N>
+using args_convert_vector = small_vector<bool, N>;
+
+/// A small_vector of PyObject* that holds references and releases them on destruction.
+/// This provides explicit ownership semantics without relying on py::object's
+/// destructor, and avoids the need for reinterpret_cast when passing to vectorcall.
+template <std::size_t InlineSize>
+class ref_small_vector {
+public:
+ ref_small_vector() = default;
+
+ ~ref_small_vector() {
+ for (std::size_t i = 0; i < m_ptrs.size(); ++i) {
+ Py_XDECREF(m_ptrs[i]);
+ }
+ }
+
+ // Disable copy (prevent accidental double-decref)
+ ref_small_vector(const ref_small_vector &) = delete;
+ ref_small_vector &operator=(const ref_small_vector &) = delete;
+
+ // Move is allowed
+ ref_small_vector(ref_small_vector &&other) noexcept : m_ptrs(std::move(other.m_ptrs)) {
+ // other.m_ptrs is now empty, so its destructor won't decref anything
+ }
+
+ ref_small_vector &operator=(ref_small_vector &&other) noexcept {
+ if (this != &other) {
+ // Decref our current contents
+ for (std::size_t i = 0; i < m_ptrs.size(); ++i) {
+ Py_XDECREF(m_ptrs[i]);
+ }
+ m_ptrs = std::move(other.m_ptrs);
+ }
+ return *this;
+ }
+
+ /// Add a pointer, taking ownership (no incref, will decref on destruction)
+ void push_back_steal(PyObject *p) { m_ptrs.push_back(p); }
+
+ /// Add a pointer, borrowing (increfs now, will decref on destruction)
+ void push_back_borrow(PyObject *p) {
+ Py_XINCREF(p);
+ m_ptrs.push_back(p);
+ }
+
+ /// Add a null pointer (for PY_VECTORCALL_ARGUMENTS_OFFSET slot)
+ void push_back_null() { m_ptrs.push_back(nullptr); }
+
+ void reserve(std::size_t sz) { m_ptrs.reserve(sz); }
+
+ std::size_t size() const { return m_ptrs.size(); }
+
+ PyObject *operator[](std::size_t idx) const { return m_ptrs[idx]; }
+
+ PyObject *const *data() const { return m_ptrs.data(); }
+
+private:
+ small_vector<PyObject *, InlineSize> m_ptrs;
+};
+
PYBIND11_NAMESPACE_END(detail)
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h
index f932726..45e8e46 100644
--- a/include/pybind11/pybind11.h
+++ b/include/pybind11/pybind11.h
@@ -684,7 +684,7 @@
rec->def->ml_name = rec->name;
rec->def->ml_meth
= reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)()>(dispatcher));
- rec->def->ml_flags = METH_VARARGS | METH_KEYWORDS;
+ rec->def->ml_flags = METH_FASTCALL | METH_KEYWORDS;
object py_func_rec = detail::function_record_PyObject_New();
(reinterpret_cast<detail::function_record_PyObject *>(py_func_rec.ptr()))->cpp_func_rec
@@ -847,7 +847,8 @@
}
/// Main dispatch logic for calls to functions bound using pybind11
- static PyObject *dispatcher(PyObject *self, PyObject *args_in, PyObject *kwargs_in) {
+ static PyObject *
+ dispatcher(PyObject *self, PyObject *const *args_in_arr, size_t nargsf, PyObject *kwnames_in) {
using namespace detail;
const function_record *overloads = function_record_ptr_from_PyObject(self);
assert(overloads != nullptr);
@@ -857,9 +858,9 @@
/* Need to know how many arguments + keyword arguments there are to pick the right
overload */
- const auto n_args_in = static_cast<size_t>(PyTuple_GET_SIZE(args_in));
+ const auto n_args_in = static_cast<size_t>(PyVectorcall_NARGS(nargsf));
- handle parent = n_args_in > 0 ? PyTuple_GET_ITEM(args_in, 0) : nullptr,
+ handle parent = n_args_in > 0 ? args_in_arr[0] : nullptr,
result = PYBIND11_TRY_NEXT_OVERLOAD;
auto self_value_and_holder = value_and_holder();
@@ -948,7 +949,7 @@
self_value_and_holder.type->dealloc(self_value_and_holder);
}
- call.init_self = PyTuple_GET_ITEM(args_in, 0);
+ call.init_self = args_in_arr[0];
call.args.emplace_back(reinterpret_cast<PyObject *>(&self_value_and_holder));
call.args_convert.push_back(false);
++args_copied;
@@ -959,17 +960,24 @@
for (; args_copied < args_to_copy; ++args_copied) {
const argument_record *arg_rec
= args_copied < func.args.size() ? &func.args[args_copied] : nullptr;
- if (kwargs_in && arg_rec && arg_rec->name
- && dict_getitemstring(kwargs_in, arg_rec->name)) {
+
+ /* if the argument is listed in the call site's kwargs, but the argument is
+ also fulfilled positionally, then the call can't match this overload. for
+ example, the call site is: foo(0, key=1) but our overload is foo(key:int) then
+ this call can't be for us, because it would be invalid.
+ */
+ if (kwnames_in && arg_rec && arg_rec->name
+ && keyword_index(kwnames_in, arg_rec->name) >= 0) {
bad_arg = true;
break;
}
- handle arg(PyTuple_GET_ITEM(args_in, args_copied));
+ handle arg(args_in_arr[args_copied]);
if (arg_rec && !arg_rec->none && arg.is_none()) {
bad_arg = true;
break;
}
+
call.args.push_back(arg);
call.args_convert.push_back(arg_rec ? arg_rec->convert : true);
}
@@ -981,20 +989,12 @@
// to copy the rest into a py::args argument.
size_t positional_args_copied = args_copied;
- // We'll need to copy this if we steal some kwargs for defaults
- dict kwargs = reinterpret_borrow<dict>(kwargs_in);
-
// 1.5. Fill in any missing pos_only args from defaults if they exist
if (args_copied < func.nargs_pos_only) {
for (; args_copied < func.nargs_pos_only; ++args_copied) {
const auto &arg_rec = func.args[args_copied];
- handle value;
-
if (arg_rec.value) {
- value = arg_rec.value;
- }
- if (value) {
- call.args.push_back(value);
+ call.args.push_back(arg_rec.value);
call.args_convert.push_back(arg_rec.convert);
} else {
break;
@@ -1007,46 +1007,42 @@
}
// 2. Check kwargs and, failing that, defaults that may help complete the list
+ small_vector<bool, arg_vector_small_size> used_kwargs(
+ kwnames_in ? static_cast<size_t>(PyTuple_GET_SIZE(kwnames_in)) : 0, false);
+ size_t used_kwargs_count = 0;
if (args_copied < num_args) {
- bool copied_kwargs = false;
-
for (; args_copied < num_args; ++args_copied) {
const auto &arg_rec = func.args[args_copied];
handle value;
- if (kwargs_in && arg_rec.name) {
- value = dict_getitemstring(kwargs.ptr(), arg_rec.name);
+ if (kwnames_in && arg_rec.name) {
+ ssize_t i = keyword_index(kwnames_in, arg_rec.name);
+ if (i >= 0) {
+ value = args_in_arr[n_args_in + static_cast<size_t>(i)];
+ used_kwargs.set(static_cast<size_t>(i), true);
+ used_kwargs_count++;
+ }
}
- if (value) {
- // Consume a kwargs value
- if (!copied_kwargs) {
- kwargs = reinterpret_steal<dict>(PyDict_Copy(kwargs.ptr()));
- copied_kwargs = true;
- }
- if (PyDict_DelItemString(kwargs.ptr(), arg_rec.name) == -1) {
- throw error_already_set();
- }
- } else if (arg_rec.value) {
+ if (!value) {
value = arg_rec.value;
+ if (!value) {
+ break;
+ }
}
if (!arg_rec.none && value.is_none()) {
break;
}
- if (value) {
- // If we're at the py::args index then first insert a stub for it to be
- // replaced later
- if (func.has_args && call.args.size() == func.nargs_pos) {
- call.args.push_back(none());
- }
-
- call.args.push_back(value);
- call.args_convert.push_back(arg_rec.convert);
- } else {
- break;
+ // If we're at the py::args index then first insert a stub for it to be
+ // replaced later
+ if (func.has_args && call.args.size() == func.nargs_pos) {
+ call.args.push_back(none());
}
+
+ call.args.push_back(value);
+ call.args_convert.push_back(arg_rec.convert);
}
if (args_copied < num_args) {
@@ -1056,47 +1052,46 @@
}
// 3. Check everything was consumed (unless we have a kwargs arg)
- if (kwargs && !kwargs.empty() && !func.has_kwargs) {
+ if (!func.has_kwargs && used_kwargs_count < used_kwargs.size()) {
continue; // Unconsumed kwargs, but no py::kwargs argument to accept them
}
// 4a. If we have a py::args argument, create a new tuple with leftovers
if (func.has_args) {
- tuple extra_args;
- if (args_to_copy == 0) {
- // We didn't copy out any position arguments from the args_in tuple, so we
- // can reuse it directly without copying:
- extra_args = reinterpret_borrow<tuple>(args_in);
- } else if (positional_args_copied >= n_args_in) {
- extra_args = tuple(0);
+ if (positional_args_copied >= n_args_in) {
+ call.args_ref = tuple(0);
} else {
size_t args_size = n_args_in - positional_args_copied;
- extra_args = tuple(args_size);
+ tuple extra_args(args_size);
for (size_t i = 0; i < args_size; ++i) {
- extra_args[i] = PyTuple_GET_ITEM(args_in, positional_args_copied + i);
+ extra_args[i] = args_in_arr[positional_args_copied + i];
}
+ call.args_ref = std::move(extra_args);
}
if (call.args.size() <= func.nargs_pos) {
- call.args.push_back(extra_args);
+ call.args.push_back(call.args_ref);
} else {
- call.args[func.nargs_pos] = extra_args;
+ call.args[func.nargs_pos] = call.args_ref;
}
call.args_convert.push_back(false);
- call.args_ref = std::move(extra_args);
}
// 4b. If we have a py::kwargs, pass on any remaining kwargs
if (func.has_kwargs) {
- if (!kwargs.ptr()) {
- kwargs = dict(); // If we didn't get one, send an empty one
+ dict kwargs;
+ for (size_t i = 0; i < used_kwargs.size(); ++i) {
+ if (!used_kwargs[i]) {
+ kwargs[PyTuple_GET_ITEM(kwnames_in, i)] = args_in_arr[n_args_in + i];
+ }
}
call.args.push_back(kwargs);
call.args_convert.push_back(false);
call.kwargs_ref = std::move(kwargs);
}
-// 5. Put everything in a vector. Not technically step 5, we've been building it
-// in `call.args` all along.
+ // 5. Put everything in a vector. Not technically step 5, we've been building it
+ // in `call.args` all along.
+
#if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
if (call.args.size() != func.nargs || call.args_convert.size() != func.nargs) {
pybind11_fail("Internal error: function call dispatcher inserted wrong number "
@@ -1227,40 +1222,37 @@
msg += '\n';
}
msg += "\nInvoked with: ";
- auto args_ = reinterpret_borrow<tuple>(args_in);
bool some_args = false;
- for (size_t ti = overloads->is_constructor ? 1 : 0; ti < args_.size(); ++ti) {
+ for (size_t ti = overloads->is_constructor ? 1 : 0; ti < n_args_in; ++ti) {
if (!some_args) {
some_args = true;
} else {
msg += ", ";
}
try {
- msg += pybind11::repr(args_[ti]);
+ msg += pybind11::repr(args_in_arr[ti]);
} catch (const error_already_set &) {
msg += "<repr raised Error>";
}
}
- if (kwargs_in) {
- auto kwargs = reinterpret_borrow<dict>(kwargs_in);
- if (!kwargs.empty()) {
- if (some_args) {
- msg += "; ";
+ if (kwnames_in && PyTuple_GET_SIZE(kwnames_in) > 0) {
+ if (some_args) {
+ msg += "; ";
+ }
+ msg += "kwargs: ";
+ bool first = true;
+ for (size_t i = 0; i < static_cast<size_t>(PyTuple_GET_SIZE(kwnames_in)); ++i) {
+ if (first) {
+ first = false;
+ } else {
+ msg += ", ";
}
- msg += "kwargs: ";
- bool first = true;
- for (const auto &kwarg : kwargs) {
- if (first) {
- first = false;
- } else {
- msg += ", ";
- }
- msg += pybind11::str("{}=").format(kwarg.first);
- try {
- msg += pybind11::repr(kwarg.second);
- } catch (const error_already_set &) {
- msg += "<repr raised Error>";
- }
+ msg += reinterpret_borrow<pybind11::str>(PyTuple_GET_ITEM(kwnames_in, i));
+ msg += '=';
+ try {
+ msg += pybind11::repr(args_in_arr[n_args_in + i]);
+ } catch (const error_already_set &) {
+ msg += "<repr raised Error>";
}
}
}
@@ -1295,6 +1287,28 @@
}
return result.ptr();
}
+
+ static ssize_t keyword_index(PyObject *haystack, char const *needle) {
+ /* kwargs is usually very small (<= 5 entries). The arg strings are typically interned.
+ * CPython itself implements the search this way, first comparing all pointers ... which is
+ * cheap and will work if the strings are interned. If it fails, then it falls back to a
+ * second lexicographic check. This is wildly expensive for huge argument lists, but those
+ * are incredibly rare so we optimize for the vastly common case of just a couple of args.
+ */
+ auto n = PyTuple_GET_SIZE(haystack);
+ auto s = reinterpret_steal<pybind11::str>(PyUnicode_InternFromString(needle));
+ for (ssize_t i = 0; i < n; ++i) {
+ if (PyTuple_GET_ITEM(haystack, i) == s.ptr()) {
+ return i;
+ }
+ }
+ for (ssize_t i = 0; i < n; ++i) {
+ if (PyUnicode_Compare(PyTuple_GET_ITEM(haystack, i), s.ptr()) == 0) {
+ return i;
+ }
+ }
+ return -1;
+ }
};
PYBIND11_NAMESPACE_BEGIN(detail)
diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py
index d41e505..a7745d1 100644
--- a/tests/test_kwargs_and_defaults.py
+++ b/tests/test_kwargs_and_defaults.py
@@ -458,7 +458,8 @@
assert refcount(myval) == expected
exp3 = refcount(myval, myval, myval)
- assert m.args_refcount(myval, myval, myval) == (exp3, exp3, exp3)
+ # if we have to create a new tuple internally, then it will hold an extra reference for each item in it.
+ assert m.args_refcount(myval, myval, myval) == (exp3 + 3, exp3 + 3, exp3 + 3)
assert refcount(myval) == expected
# This function takes the first arg as a `py::object` and the rest as a `py::args`. Unlike the