--- src/lib.rs.before
+++ src/lib.rs.experiment
@@ -2,6 +2,7 @@
mod completed;
mod http;
mod net;
+mod vectorcall;
use pyo3::exceptions::{PyOSError, PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyDict, PyList};
@@ -591,10 +592,12 @@
let send = Py::new(
py,
Sender {
+ vectorcall: vectorcall::call,
state: Rc::downgrade(self),
request: r.clone(),
},
)?;
+ vectorcall::enable(py, &send);
let receive = Py::new(
py,
Receiver {
@@ -734,8 +737,9 @@
Ok(())
}
-#[pyclass(unsendable, frozen)]
+#[pyclass(unsendable, frozen, immutable_type)]
struct Sender {
+ vectorcall: pyo3::ffi::vectorcallfunc,
state: Weak<State>,
request: Request,
}
--- /dev/null
+++ src/vectorcall.rs
@@ -0,0 +1,80 @@
+//! Vectorcall for the existing PyO3 Sender; ownership and thread checks remain
+//! PyO3's. No Rust pointer is kept outside its original Python object.
+use super::Sender;
+use pyo3::{ffi, prelude::*, types::PyTuple};
+
+pub fn enable(py: Python<'_>, sender: &Py<Sender>) {
+ let data = sender.bind(py).borrow();
+ let offset = (&data.vectorcall as *const _ as usize) - sender.as_ptr() as usize;
+ // SAFETY: Sender is frozen, non-subclassable and its type is immutable. The
+ // actual field address determines the offset, without assuming PyO3's layout.
+ // All instances have the same layout and are initialized with call before
+ // exposure. The GIL serializes installation; PyO3 still owns allocation/drop.
+ unsafe {
+ let ty = ffi::Py_TYPE(sender.as_ptr());
+ if (*ty).tp_vectorcall_offset == 0 {
+ (*ty).tp_vectorcall_offset = offset as ffi::Py_ssize_t;
+ (*ty).tp_call = Some(ffi::PyVectorcall_Call);
+ (*ty).tp_flags |= ffi::Py_TPFLAGS_HAVE_VECTORCALL;
+ }
+ }
+}
+
+struct Call;
+impl
+ pyo3::impl_::trampoline::MethodDef<
+ pyo3::impl_::trampoline::fastcall_cfunction_with_keywords::Func,
+ > for Call
+{
+ const METH: pyo3::impl_::trampoline::fastcall_cfunction_with_keywords::Func = invoke;
+}
+
+// SAFETY: CPython invokes this only with a live Sender and a valid vectorcall
+// argument array under the GIL. The PyO3 trampoline attaches its thread guard,
+// catches Rust panics and translates Python errors, as for a generated method.
+pub unsafe extern "C" fn call(
+ sender: *mut ffi::PyObject,
+ args: *const *mut ffi::PyObject,
+ nargs: usize,
+ names: *mut ffi::PyObject,
+) -> *mut ffi::PyObject {
+ unsafe {
+ pyo3::impl_::trampoline::fastcall_cfunction_with_keywords::<Call>(
+ sender,
+ args,
+ ffi::PyVectorcall_NARGS(nargs),
+ names,
+ )
+ }
+}
+
+unsafe fn invoke(
+ py: Python<'_>,
+ sender: *mut ffi::PyObject,
+ args: *const *mut ffi::PyObject,
+ nargs: ffi::Py_ssize_t,
+ names: *mut ffi::PyObject,
+) -> PyResult<*mut ffi::PyObject> {
+ // SAFETY: call and the PyO3 trampoline establish the interpreter/argument
+ // guarantees. Argument count is checked before dereferencing the array.
+ unsafe {
+ let keywords = if names.is_null() {
+ None
+ } else {
+ Some(Bound::from_borrowed_ptr(py, names).cast_into::<PyTuple>()?)
+ };
+ let valid = if let Some(names) = keywords {
+ nargs == 0 && names.len() == 1 && names.get_item(0)?.eq("msg")?
+ } else {
+ nargs == 1
+ };
+ if !valid {
+ return Err(pyo3::exceptions::PyTypeError::new_err(
+ "send() takes one message",
+ ));
+ }
+ let msg = Bound::from_borrowed_ptr(py, *args);
+ let sender = Bound::from_borrowed_ptr(py, sender).cast_into::<Sender>()?;
+ Ok(sender.borrow().__call__(py, msg.cast()?)?.into_ptr())
+ }
+}