Skip to content

Commit 0a01cb4

Browse files
Add minimal capi lifecycle support
1 parent d5921d1 commit 0a01cb4

8 files changed

Lines changed: 198 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/capi/.cargo/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[env]
2+
PYO3_CONFIG_FILE = { value = "pyo3-rustpython.config", relative = true }
3+
PYO3_NO_PYTHON = { value = "1" }

crates/capi/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "rustpython-capi"
3+
description = "Minimal CPython C-API compatibility exports for RustPython"
4+
version.workspace = true
5+
authors.workspace = true
6+
edition.workspace = true
7+
rust-version.workspace = true
8+
repository.workspace = true
9+
license.workspace = true
10+
11+
[lib]
12+
crate-type = ["cdylib"]
13+
14+
[dependencies]
15+
rustpython-vm = { workspace = true, features = ["threading"] }
16+
17+
[dev-dependencies]
18+
pyo3 = { version = "0.28", features = ["auto-initialize", "abi3"] }
19+
20+
[lints]
21+
workspace = true

crates/capi/pyo3-rustpython.config

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
implementation=CPython
2+
version=3.14
3+
shared=true
4+
abi3=true
5+
suppress_build_script_link_lines=true

crates/capi/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pub use rustpython_vm::PyObject;
2+
3+
extern crate alloc;
4+
5+
pub mod pylifecycle;
6+
pub mod pystate;
7+
pub mod refcount;

crates/capi/src/pylifecycle.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use crate::pystate::attach_vm_to_thread;
2+
use core::ffi::c_int;
3+
use rustpython_vm::Interpreter;
4+
use rustpython_vm::vm::thread::ThreadedVirtualMachine;
5+
use std::sync::{Once, OnceLock, mpsc};
6+
7+
static VM_REQUEST_TX: OnceLock<mpsc::Sender<mpsc::SyncSender<ThreadedVirtualMachine>>> =
8+
OnceLock::new();
9+
pub(crate) static INITIALIZED: Once = Once::new();
10+
11+
/// Request a vm from the main interpreter
12+
pub(crate) fn request_vm_from_interpreter() -> ThreadedVirtualMachine {
13+
let tx = VM_REQUEST_TX
14+
.get()
15+
.expect("VM request channel not initialized");
16+
let (response_tx, response_rx) = mpsc::sync_channel(1);
17+
tx.send(response_tx).expect("Failed to send VM request");
18+
response_rx.recv().expect("Failed to receive VM response")
19+
}
20+
21+
#[unsafe(no_mangle)]
22+
pub extern "C" fn Py_IsInitialized() -> c_int {
23+
INITIALIZED.is_completed() as _
24+
}
25+
26+
#[unsafe(no_mangle)]
27+
pub extern "C" fn Py_Initialize() {
28+
Py_InitializeEx(0);
29+
}
30+
31+
#[unsafe(no_mangle)]
32+
pub extern "C" fn Py_InitializeEx(_initsigs: c_int) {
33+
if INITIALIZED.is_completed() {
34+
panic!("Initialize called multiple times");
35+
}
36+
37+
INITIALIZED.call_once(|| {
38+
let (tx, rx) = mpsc::channel();
39+
VM_REQUEST_TX
40+
.set(tx)
41+
.expect("VM request channel was already initialized");
42+
43+
std::thread::spawn(move || {
44+
let interp = Interpreter::with_init(Default::default(), |_vm| {});
45+
interp.enter(|vm| {
46+
while let Ok(request) = rx.recv() {
47+
request
48+
.send(vm.new_thread())
49+
.expect("Failed to send VM response");
50+
}
51+
})
52+
});
53+
});
54+
55+
attach_vm_to_thread();
56+
}
57+
58+
#[unsafe(no_mangle)]
59+
pub extern "C" fn Py_Finalize() {
60+
let _ = Py_FinalizeEx();
61+
}
62+
63+
#[unsafe(no_mangle)]
64+
pub extern "C" fn Py_FinalizeEx() -> c_int {
65+
0
66+
}
67+
68+
#[unsafe(no_mangle)]
69+
pub extern "C" fn Py_IsFinalizing() -> c_int {
70+
0
71+
}

crates/capi/src/pystate.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use crate::pylifecycle::request_vm_from_interpreter;
2+
use core::cell::RefCell;
3+
use core::ffi::c_int;
4+
use core::ptr;
5+
use rustpython_vm::vm::thread::ThreadedVirtualMachine;
6+
7+
thread_local! {
8+
static VM: RefCell<Option<ThreadedVirtualMachine>> = const { RefCell::new(None) };
9+
}
10+
11+
#[allow(non_camel_case_types)]
12+
type PyGILState_STATE = c_int;
13+
14+
#[repr(C)]
15+
pub struct PyThreadState {
16+
_interp: *mut core::ffi::c_void,
17+
}
18+
19+
pub(crate) fn attach_vm_to_thread() {
20+
VM.with(|vm| {
21+
vm.borrow_mut()
22+
.get_or_insert_with(request_vm_from_interpreter);
23+
});
24+
}
25+
26+
#[unsafe(no_mangle)]
27+
pub extern "C" fn PyGILState_Ensure() -> PyGILState_STATE {
28+
attach_vm_to_thread();
29+
30+
0
31+
}
32+
33+
#[unsafe(no_mangle)]
34+
pub extern "C" fn PyGILState_Release(_state: PyGILState_STATE) {}
35+
36+
#[unsafe(no_mangle)]
37+
pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState {
38+
ptr::null_mut()
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use crate::pystate::VM;
44+
use pyo3::prelude::*;
45+
46+
#[test]
47+
fn test_new_thread() {
48+
Python::attach(|_py| {
49+
assert!(
50+
VM.with(|vm| vm.borrow().is_some()),
51+
"This thread did not have a vm attached"
52+
);
53+
54+
std::thread::spawn(move || {
55+
Python::attach(|_py| {
56+
assert!(
57+
VM.with(|vm| vm.borrow().is_some()),
58+
"This thread did not have a vm attached"
59+
);
60+
});
61+
})
62+
.join()
63+
.unwrap();
64+
})
65+
}
66+
}

crates/capi/src/refcount.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use crate::PyObject;
2+
use core::ptr::NonNull;
3+
use rustpython_vm::PyObjectRef;
4+
5+
#[unsafe(no_mangle)]
6+
#[allow(clippy::not_unsafe_ptr_arg_deref)]
7+
pub extern "C" fn _Py_DecRef(op: *mut PyObject) {
8+
// By dropping PyObjectRef, we will decrement the reference count.
9+
unsafe { PyObjectRef::from_raw(NonNull::new_unchecked(op)) };
10+
}
11+
12+
#[unsafe(no_mangle)]
13+
#[allow(clippy::not_unsafe_ptr_arg_deref)]
14+
pub extern "C" fn _Py_IncRef(op: *mut PyObject) {
15+
// Don't drop the owned value, as we just want to increment the refcount.
16+
core::mem::forget(unsafe { (*op).to_owned() });
17+
}

0 commit comments

Comments
 (0)