Skip to content

Commit a5dcce9

Browse files
feat(pyo3): migrate to pyo3 0.28.3 and pyo3-async-runtimes 0.28.0 (p1)
- Upgrade pyo3 to 0.28.3 and replace pyo3-asyncio with pyo3-async-runtimes 0.28.0. - Migrate codebase to use the modern Bound API (Python::attach, py.detach, etc.). - Modernize #[pyclass] with (from_py_object) and replace deprecated downcast with cast. - Update tests to align with new PyO3 APIs and remove outdated initialization calls. - Fix clippy warnings and remove unnecessary unsafe blocks in Python bindings. - Note: JIT and Vortex features are pending full migration in p2. Co-authored-by: Iris Seravelle <iris.seravelle@gmail.com>
1 parent cd2ba21 commit a5dcce9

20 files changed

Lines changed: 1174 additions & 2045 deletions

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ crossbeam-channel = "0.5"
2424
crossbeam-queue = "0.3.11"
2525

2626
# Python (Default)
27-
pyo3 = { version = "0.20", features = ["auto-initialize"], optional = true}
28-
pyo3-asyncio = { version = "0.20", features = ["tokio-runtime"], optional = true }
27+
pyo3 = { version = "0.28.3", features = ["auto-initialize"], optional = true}
28+
pyo3-async-runtimes = { version = "0.28.0", features = ["tokio-runtime"], optional = true }
2929

3030
# JIT (Optional and experimental)
3131
cranelift = { version = "0.87", optional = true }
@@ -43,7 +43,7 @@ napi-build = { version = "2.0", optional = true }
4343

4444
[features]
4545
default = ["pyo3"]
46-
pyo3 = ["dep:pyo3", "dep:pyo3-asyncio"]
46+
pyo3 = ["dep:pyo3", "dep:pyo3-async-runtimes"]
4747
extension-module = ["pyo3/extension-module"]
4848
jit = ["dep:cranelift", "dep:cranelift-module", "dep:cranelift-jit", "dep:cranelift-native", "dep:wide"]
4949
# node = ["dep:napi", "dep:napi-derive", "dep:napi-build"]
@@ -52,7 +52,7 @@ vortex = ["pyo3"]
5252
[dev-dependencies]
5353
futures = "0.3"
5454
tracing-subscriber = "0.3"
55-
pyo3 = { version = "0.20", features = ["auto-initialize"] }
55+
pyo3 = { version = "0.28.3", features = ["auto-initialize"] }
5656

5757
[profile.release]
5858
opt-level = 3

src/core/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ impl Runtime {
126126

127127
#[cfg(feature = "pyo3")]
128128
{
129-
pyo3::prepare_freethreaded_python();
129+
// In modern PyO3, this is often handled by auto-initialize or not needed for library usage.
130+
// pyo3::prepare_freethreaded_python();
130131
}
131132

132133
let (telemetry_mgr, _event_rx) = telemetry::TelemetryManager::new();

src/py/jit_stub.rs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,42 +13,42 @@ use pyo3::wrap_pyfunction;
1313
#[cfg(feature = "pyo3")]
1414
#[pyfunction]
1515
fn register_offload(
16-
func: PyObject,
16+
func: Py<PyAny>,
1717
_strategy: Option<String>,
1818
_return_type: Option<String>,
1919
_source_expr: Option<String>,
2020
_arg_names: Option<Vec<String>>,
21-
) -> PyResult<PyObject> {
21+
) -> PyResult<Py<PyAny>> {
2222
Ok(func)
2323
}
2424

2525
#[cfg(feature = "pyo3")]
2626
#[pyfunction]
2727
fn offload_call(
28-
py: Python,
29-
func: PyObject,
30-
args: &PyTuple,
31-
kwargs: Option<&PyDict>,
32-
) -> PyResult<PyObject> {
33-
func.as_ref(py).call(args, kwargs).map(|v| v.into_py(py))
28+
py: Python<'_>,
29+
func: Py<PyAny>,
30+
args: &Bound<'_, PyTuple>,
31+
kwargs: Option<&Bound<'_, PyDict>>,
32+
) -> PyResult<Py<PyAny>> {
33+
func.bind(py).call(args, kwargs).map(|v| v.unbind())
3434
}
3535

3636
#[cfg(feature = "pyo3")]
3737
#[pyfunction]
3838
fn call_jit(
39-
_py: Python,
40-
_func: PyObject,
41-
_args: &PyTuple,
42-
_kwargs: Option<&PyDict>,
43-
) -> PyResult<PyObject> {
39+
_py: Python<'_>,
40+
_func: Py<PyAny>,
41+
_args: &Bound<'_, PyTuple>,
42+
_kwargs: Option<&Bound<'_, PyDict>>,
43+
) -> PyResult<Py<PyAny>> {
4444
Err(pyo3::exceptions::PyRuntimeError::new_err(
4545
"JIT feature is disabled in this build",
4646
))
4747
}
4848

4949
#[cfg(feature = "pyo3")]
5050
#[pyfunction]
51-
fn call_jit_step_loop_f64(_func: PyObject, _seed: f64, _count: usize) -> PyResult<f64> {
51+
fn call_jit_step_loop_f64(_func: Py<PyAny>, _seed: f64, _count: usize) -> PyResult<f64> {
5252
Err(pyo3::exceptions::PyRuntimeError::new_err(
5353
"JIT feature is disabled in this build",
5454
))
@@ -83,13 +83,13 @@ fn is_quantum_speculation_enabled() -> PyResult<bool> {
8383

8484
#[cfg(feature = "pyo3")]
8585
#[pyfunction]
86-
fn get_quantum_profile(_func: PyObject) -> PyResult<Vec<(usize, f64, u64, u64)>> {
86+
fn get_quantum_profile(_func: Py<PyAny>) -> PyResult<Vec<(usize, f64, u64, u64)>> {
8787
Ok(Vec::new())
8888
}
8989

9090
#[cfg(feature = "pyo3")]
9191
#[pyfunction]
92-
fn seed_quantum_profile(_func: PyObject, _rows: Vec<(usize, f64, u64, u64)>) -> PyResult<bool> {
92+
fn seed_quantum_profile(_func: Py<PyAny>, _rows: Vec<(usize, f64, u64, u64)>) -> PyResult<bool> {
9393
Ok(false)
9494
}
9595

@@ -158,7 +158,7 @@ fn get_quantum_cooldown() -> PyResult<(u64, u64)> {
158158
}
159159

160160
#[cfg(feature = "pyo3")]
161-
pub(crate) fn init_py(m: &PyModule) -> PyResult<()> {
161+
pub(crate) fn init_py(m: &Bound<'_, PyModule>) -> PyResult<()> {
162162
m.add_function(wrap_pyfunction!(register_offload, m)?)?;
163163
m.add_function(wrap_pyfunction!(offload_call, m)?)?;
164164
m.add_function(wrap_pyfunction!(call_jit, m)?)?;

src/py/mailbox.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use tokio::sync::Mutex as TokioMutex;
99

1010
/// A wrapper around a live MailboxReceiver for Python actors.
1111
/// Revamped: Now purely blocking/synchronous to Python, running in dedicated threads.
12-
#[pyclass]
12+
#[pyclass(from_py_object)]
1313
#[derive(Clone)]
1414
pub struct PyMailbox {
1515
pub(crate) inner: Arc<TokioMutex<crate::mailbox::MailboxReceiver>>,
@@ -19,7 +19,8 @@ pub struct PyMailbox {
1919
impl PyMailbox {
2020
/// Receive the next message (Blocking).
2121
/// Releases the GIL while waiting. Checks for Python signals cleanly to allow Ctrl+C escapes.
22-
fn recv(&self, py: Python, timeout: Option<f64>) -> PyResult<PyObject> {
22+
#[pyo3(signature = (timeout=None))]
23+
fn recv(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<Py<PyAny>> {
2324
let rx = self.inner.clone();
2425
let start = std::time::Instant::now();
2526
let timeout_dur = timeout.map(std::time::Duration::from_secs_f64);
@@ -40,7 +41,7 @@ impl PyMailbox {
4041
};
4142

4243
// Release GIL to allow other threads to run while we block on the channel
43-
let res = py.allow_threads(|| {
44+
let res = py.detach(|| {
4445
crate::RUNTIME.block_on(async {
4546
let fut = async {
4647
let mut guard = rx.lock().await;
@@ -68,12 +69,13 @@ impl PyMailbox {
6869

6970
/// Selectively receive a message matching a Python predicate (Blocking).
7071
/// Releases the GIL while waiting. Checks for Python signals cleanly.
72+
#[pyo3(signature = (matcher, timeout=None))]
7173
fn selective_recv(
7274
&self,
73-
py: Python,
74-
matcher: PyObject,
75+
py: Python<'_>,
76+
matcher: Py<PyAny>,
7577
timeout: Option<f64>,
76-
) -> PyResult<PyObject> {
78+
) -> PyResult<Py<PyAny>> {
7779
let rx = self.inner.clone();
7880
let start = std::time::Instant::now();
7981
let timeout_dur = timeout.map(std::time::Duration::from_secs_f64);
@@ -92,13 +94,16 @@ impl PyMailbox {
9294
wait_time
9395
};
9496

95-
let res = py.allow_threads(|| {
97+
let res = py.detach(|| {
9698
crate::RUNTIME.block_on(async {
9799
let fut = async {
98100
let mut guard = rx.lock().await;
99101
guard
100102
.selective_recv(|msg| {
101-
Python::with_gil(|py| run_python_matcher(py, &matcher, msg))
103+
Python::attach(|py| {
104+
Ok::<bool, PyErr>(run_python_matcher(py, matcher.bind(py), msg))
105+
})
106+
.unwrap_or(false)
102107
})
103108
.await
104109
};

src/py/pool.rs

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,18 @@ use crate::Runtime;
1313
#[cfg(feature = "pyo3")]
1414
use pyo3::prelude::*;
1515
use pyo3::types::PyBytes;
16-
use pyo3::PyObject;
1716

1817
/// Task variants sent to dedicated or pooled GIL workers.
1918
#[cfg(feature = "pyo3")]
2019
pub(crate) enum PoolTask {
2120
Execute {
22-
behavior: Arc<parking_lot::RwLock<PyObject>>,
21+
behavior: Arc<parking_lot::RwLock<Py<PyAny>>>,
2322
bytes: bytes::Bytes,
2423
pid_holder: Arc<AtomicU64>,
2524
rt: Arc<Runtime>,
2625
},
2726
HotSwap {
28-
behavior: Arc<parking_lot::RwLock<PyObject>>,
27+
behavior: Arc<parking_lot::RwLock<Py<PyAny>>>,
2928
ptr: usize,
3029
},
3130
}
@@ -68,7 +67,7 @@ impl GilPool {
6867
}
6968
let success = crate::py::utils::run_python_callback(|py| {
7069
let guard = behavior.read();
71-
let cb = guard.as_ref(py);
70+
let cb = guard.bind(py);
7271
let pybytes = PyBytes::new(py, &bytes);
7372
cb.call1((pybytes,)).map(|_| ())
7473
});
@@ -83,11 +82,14 @@ impl GilPool {
8382
if unsafe { pyo3::ffi::Py_IsInitialized() } == 0 {
8483
break;
8584
}
86-
Python::with_gil(|py| unsafe {
85+
Python::attach(|py| unsafe {
8786
let new_obj =
88-
PyObject::from_owned_ptr(py, ptr as *mut pyo3::ffi::PyObject);
87+
Bound::from_owned_ptr(py, ptr as *mut pyo3::ffi::PyObject)
88+
.unbind();
8989
*behavior.write() = new_obj;
90-
});
90+
Ok::<(), PyErr>(())
91+
})
92+
.unwrap();
9193
}
9294
},
9395
Err(cb_channel::RecvTimeoutError::Timeout) => continue,
@@ -107,7 +109,7 @@ impl GilPool {
107109
pub(crate) fn make_release_gil_channel(
108110
rt: &Runtime,
109111
release: bool,
110-
behavior: Arc<parking_lot::RwLock<PyObject>>,
112+
behavior: Arc<parking_lot::RwLock<Py<PyAny>>>,
111113
pid_holder: Arc<AtomicU64>,
112114
rt_arc: Arc<Runtime>,
113115
) -> PyResult<Option<cb_channel::Sender<DedicatedPoolTask>>> {
@@ -160,7 +162,7 @@ pub(crate) fn make_release_gil_channel(
160162
}
161163
let success = crate::py::utils::run_python_callback(|py| {
162164
let guard = _b_thread.read();
163-
let cb = guard.as_ref(py);
165+
let cb = guard.bind(py);
164166
let pybytes = PyBytes::new(py, &bytes);
165167
cb.call1((pybytes,)).map(|_| ())
166168
});
@@ -175,10 +177,13 @@ pub(crate) fn make_release_gil_channel(
175177
if unsafe { pyo3::ffi::Py_IsInitialized() } == 0 {
176178
continue;
177179
}
178-
Python::with_gil(|py| unsafe {
179-
let new_obj = PyObject::from_owned_ptr(py, ptr as *mut pyo3::ffi::PyObject);
180+
Python::attach(|py| unsafe {
181+
let new_obj =
182+
Bound::from_owned_ptr(py, ptr as *mut pyo3::ffi::PyObject).unbind();
180183
*_b_thread.write() = new_obj;
181-
});
184+
Ok::<(), PyErr>(())
185+
})
186+
.unwrap();
182187
}
183188
},
184189
Err(cb_channel::RecvTimeoutError::Timeout) => continue,
@@ -197,14 +202,12 @@ mod tests {
197202

198203
#[test]
199204
fn max_threads_zero_forces_shared_pool_even_in_strict_mode() {
200-
pyo3::prepare_freethreaded_python();
201-
202205
let rt = Runtime::new();
203206
rt.set_release_gil_limits(0, 1);
204207
rt.set_release_gil_strict(true);
205208

206-
pyo3::Python::with_gil(|py| {
207-
let behavior = Arc::new(parking_lot::RwLock::new(py.None().into_py(py)));
209+
Python::attach(|py| {
210+
let behavior = Arc::new(parking_lot::RwLock::new(py.None()));
208211
let ch = make_release_gil_channel(
209212
&rt,
210213
true,
@@ -214,6 +217,8 @@ mod tests {
214217
)
215218
.expect("max_threads=0 should force shared pool mode without strict error");
216219
assert!(ch.is_none());
217-
});
220+
Ok::<(), PyErr>(())
221+
})
222+
.unwrap();
218223
}
219224
}

0 commit comments

Comments
 (0)