🌐 AI搜索 & 代理 主页
Skip to content

Commit 95afe5d

Browse files
committed
rustpython-cpython
1 parent 0e6e256 commit 95afe5d

File tree

5 files changed

+339
-0
lines changed

5 files changed

+339
-0
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ ssl-rustls = ["ssl", "rustpython-stdlib/ssl-rustls"]
2525
ssl-openssl = ["ssl", "rustpython-stdlib/ssl-openssl"]
2626
ssl-vendor = ["ssl-openssl", "rustpython-stdlib/ssl-vendor"]
2727
tkinter = ["rustpython-stdlib/tkinter"]
28+
cpython = ["dep:rustpython-cpython"]
2829

2930
[build-dependencies]
3031
winresource = "0.1"
@@ -34,6 +35,7 @@ rustpython-compiler = { workspace = true }
3435
rustpython-pylib = { workspace = true, optional = true }
3536
rustpython-stdlib = { workspace = true, optional = true, features = ["compiler"] }
3637
rustpython-vm = { workspace = true, features = ["compiler"] }
38+
rustpython-cpython = { workspace = true, optional = true }
3739
ruff_python_parser = { workspace = true }
3840

3941
cfg-if = { workspace = true }
@@ -150,6 +152,7 @@ rustpython-stdlib = { path = "crates/stdlib", default-features = false, version
150152
rustpython-sre_engine = { path = "crates/sre_engine", version = "0.4.0" }
151153
rustpython-wtf8 = { path = "crates/wtf8", version = "0.4.0" }
152154
rustpython-doc = { path = "crates/doc", version = "0.4.0" }
155+
rustpython-cpython = { path = "crates/cpython", version = "0.4.0" }
153156

154157
ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", tag = "0.14.1" }
155158
ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", tag = "0.14.1" }

crates/cpython/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "rustpython-cpython"
3+
description = "RustPython to CPython bridge via PyO3"
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+
[dependencies]
12+
rustpython-vm = { workspace = true }
13+
rustpython-derive = { workspace = true }
14+
pyo3 = { version = "0.26", features = ["auto-initialize"] }
15+
16+
[lints]
17+
workspace = true

crates/cpython/src/lib.rs

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
//! RustPython to CPython bridge via PyO3
2+
//!
3+
//! This crate provides interoperability between RustPython and CPython,
4+
//! allowing RustPython code to execute functions in the CPython runtime.
5+
//!
6+
//! # Background
7+
//!
8+
//! RustPython does not implement all CPython C extension modules.
9+
//! This crate enables calling into the real CPython runtime for functionality
10+
//! that is not yet available in RustPython.
11+
//!
12+
//! # Architecture
13+
//!
14+
//! Communication between RustPython and CPython uses PyO3 for in-process calls.
15+
//! Data is serialized using Python's `pickle` protocol:
16+
//!
17+
//! ```text
18+
//! RustPython CPython
19+
//! │ │
20+
//! │ pickle.dumps(args, kwargs) │
21+
//! │ ──────────────────────────────► │
22+
//! │ │ exec(source)
23+
//! │ │ result = func(*args, **kwargs)
24+
//! │ pickle.dumps(result) │
25+
//! │ ◄────────────────────────────── │
26+
//! │ │
27+
//! │ pickle.loads(result) │
28+
//! ```
29+
//!
30+
//! # Limitations
31+
//!
32+
//! - **File-based functions only**: Functions defined in REPL or via `exec()` will fail
33+
//! (`inspect.getsource()` requires source file access)
34+
//! - **Picklable data only**: Cannot pass functions, classes, file handles, etc.
35+
//! - **Performance overhead**: pickle serialization + CPython GIL acquisition
36+
//! - **CPython required**: System must have CPython installed (linked via PyO3)
37+
38+
#[macro_use]
39+
extern crate rustpython_derive;
40+
41+
use rustpython_vm::{PyRef, VirtualMachine, builtins::PyModule};
42+
43+
/// Create the _cpython module
44+
pub fn make_module(vm: &VirtualMachine) -> PyRef<PyModule> {
45+
_cpython::make_module(vm)
46+
}
47+
48+
#[pymodule]
49+
mod _cpython {
50+
use pyo3::PyErr;
51+
use pyo3::prelude::PyAnyMethods;
52+
use pyo3::types::PyBytes as Pyo3Bytes;
53+
use pyo3::types::PyBytesMethods;
54+
use pyo3::types::PyDictMethods;
55+
use rustpython_vm::{
56+
Py, PyObjectRef, PyPayload, PyResult, VirtualMachine,
57+
builtins::{PyBytes as RustPyBytes, PyBytesRef, PyDict, PyStrRef, PyTypeRef},
58+
function::FuncArgs,
59+
types::{Callable, Constructor, Representable},
60+
};
61+
62+
/// Wrapper class for executing functions in CPython.
63+
/// Used as a decorator: @_cpython.call
64+
#[pyattr]
65+
#[pyclass(name = "call")]
66+
#[derive(Debug, PyPayload)]
67+
struct CPythonCall {
68+
source: String,
69+
func_name: String,
70+
}
71+
72+
impl Constructor for CPythonCall {
73+
type Args = PyObjectRef;
74+
75+
fn py_new(cls: PyTypeRef, func: Self::Args, vm: &VirtualMachine) -> PyResult {
76+
// Get function name
77+
let func_name = func
78+
.get_attr("__name__", vm)?
79+
.downcast::<rustpython_vm::builtins::PyStr>()
80+
.map_err(|_| vm.new_type_error("function must have __name__".to_owned()))?
81+
.as_str()
82+
.to_owned();
83+
84+
// Get source using inspect.getsource(func)
85+
let inspect = vm.import("inspect", 0)?;
86+
let getsource = inspect.get_attr("getsource", vm)?;
87+
let source_obj = getsource.call((func.clone(),), vm)?;
88+
let source_full = source_obj
89+
.downcast::<rustpython_vm::builtins::PyStr>()
90+
.map_err(|_| vm.new_type_error("getsource did not return str".to_owned()))?
91+
.as_str()
92+
.to_owned();
93+
94+
// Strip decorator lines from source (lines starting with @)
95+
// Find the first line that starts with 'def ' or 'async def '
96+
let source = strip_decorators(&source_full);
97+
98+
Self { source, func_name }
99+
.into_ref_with_type(vm, cls)
100+
.map(Into::into)
101+
}
102+
}
103+
104+
/// Strip decorator lines from function source code.
105+
/// Returns source starting from 'def' or 'async def'.
106+
fn strip_decorators(source: &str) -> String {
107+
let lines = source.lines();
108+
let mut result_lines = Vec::new();
109+
let mut found_def = false;
110+
111+
for line in lines {
112+
let trimmed = line.trim_start();
113+
if !found_def {
114+
if trimmed.starts_with("def ") || trimmed.starts_with("async def ") {
115+
found_def = true;
116+
result_lines.push(line);
117+
}
118+
// Skip decorator lines (starting with @) and blank lines before def
119+
} else {
120+
result_lines.push(line);
121+
}
122+
}
123+
124+
result_lines.join("\n")
125+
}
126+
127+
impl Callable for CPythonCall {
128+
type Args = FuncArgs;
129+
130+
fn call(zelf: &Py<Self>, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
131+
// Import pickle module
132+
let pickle = vm.import("pickle", 0)?;
133+
let dumps = pickle.get_attr("dumps", vm)?;
134+
let loads = pickle.get_attr("loads", vm)?;
135+
136+
// Pickle args and kwargs
137+
let args_tuple = vm.ctx.new_tuple(args.args);
138+
let kwargs_dict = PyDict::default().into_ref(&vm.ctx);
139+
for (key, value) in args.kwargs {
140+
kwargs_dict.set_item(&key, value, vm)?;
141+
}
142+
143+
let pickled_args = dumps.call((args_tuple,), vm)?;
144+
let pickled_kwargs = dumps.call((kwargs_dict,), vm)?;
145+
146+
let pickled_args_bytes = pickled_args
147+
.downcast::<RustPyBytes>()
148+
.map_err(|_| vm.new_type_error("pickle.dumps did not return bytes".to_owned()))?;
149+
let pickled_kwargs_bytes = pickled_kwargs
150+
.downcast::<RustPyBytes>()
151+
.map_err(|_| vm.new_type_error("pickle.dumps did not return bytes".to_owned()))?;
152+
153+
// Call execute_impl()
154+
let result_bytes = execute_impl(
155+
&zelf.source,
156+
&zelf.func_name,
157+
pickled_args_bytes.as_bytes(),
158+
pickled_kwargs_bytes.as_bytes(),
159+
vm,
160+
)?;
161+
162+
// Unpickle result
163+
let result_py_bytes = RustPyBytes::from(result_bytes).into_ref(&vm.ctx);
164+
loads.call((result_py_bytes,), vm)
165+
}
166+
}
167+
168+
impl Representable for CPythonCall {
169+
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
170+
Ok(format!("<_cpython.call wrapper for '{}'>", zelf.func_name))
171+
}
172+
}
173+
174+
#[pyclass(with(Constructor, Callable, Representable))]
175+
impl CPythonCall {}
176+
177+
/// Internal implementation for executing Python code in CPython.
178+
fn execute_impl(
179+
source: &str,
180+
func_name: &str,
181+
args_bytes: &[u8],
182+
kwargs_bytes: &[u8],
183+
vm: &VirtualMachine,
184+
) -> PyResult<Vec<u8>> {
185+
// Build the CPython code to execute
186+
let cpython_code = format!(
187+
r#"
188+
import pickle as __pickle
189+
190+
# Unpickle arguments
191+
__args__ = __pickle.loads(__pickled_args__)
192+
__kwargs__ = __pickle.loads(__pickled_kwargs__)
193+
# Execute the source code (defines the function)
194+
{source}
195+
196+
# Call the function and pickle the result
197+
__result__ = {func_name}(*__args__, **__kwargs__)
198+
__pickled_result__ = __pickle.dumps(__result__, protocol=4)
199+
"#,
200+
source = source,
201+
func_name = func_name,
202+
);
203+
204+
// Execute in CPython via PyO3
205+
pyo3::Python::attach(|py| -> Result<Vec<u8>, PyErr> {
206+
// Create Python bytes for pickled data
207+
let py_args = Pyo3Bytes::new(py, args_bytes);
208+
let py_kwargs = Pyo3Bytes::new(py, kwargs_bytes);
209+
210+
// Create globals dict with pickled args
211+
let globals = pyo3::types::PyDict::new(py);
212+
globals.set_item("__pickled_args__", &py_args)?;
213+
globals.set_item("__pickled_kwargs__", &py_kwargs)?;
214+
215+
// Execute using compile + exec pattern
216+
let builtins = py.import("builtins")?;
217+
let compile = builtins.getattr("compile")?;
218+
let exec_fn = builtins.getattr("exec")?;
219+
220+
// Compile the code
221+
let code = compile.call1((&cpython_code, "<cpython_bridge>", "exec"))?;
222+
223+
// Execute with globals
224+
exec_fn.call1((code, &globals))?;
225+
226+
// Get the pickled result
227+
let result = globals.get_item("__pickled_result__")?.ok_or_else(|| {
228+
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("No result returned")
229+
})?;
230+
let result_bytes: &pyo3::Bound<'_, Pyo3Bytes> = result.downcast()?;
231+
Ok(result_bytes.as_bytes().to_vec())
232+
})
233+
.map_err(|e| vm.new_runtime_error(format!("CPython error: {}", e)))
234+
}
235+
236+
/// Execute a Python function in CPython runtime.
237+
///
238+
/// # Arguments
239+
/// * `source` - The complete source code of the function
240+
/// * `func_name` - The name of the function to call
241+
/// * `pickled_args` - Pickled positional arguments (bytes)
242+
/// * `pickled_kwargs` - Pickled keyword arguments (bytes)
243+
///
244+
/// # Returns
245+
/// Pickled result from CPython (bytes)
246+
#[pyfunction]
247+
fn execute(
248+
source: PyStrRef,
249+
func_name: PyStrRef,
250+
pickled_args: PyBytesRef,
251+
pickled_kwargs: PyBytesRef,
252+
vm: &VirtualMachine,
253+
) -> PyResult<PyBytesRef> {
254+
let result_bytes = execute_impl(
255+
source.as_str(),
256+
func_name.as_str(),
257+
pickled_args.as_bytes(),
258+
pickled_kwargs.as_bytes(),
259+
vm,
260+
)?;
261+
Ok(RustPyBytes::from(result_bytes).into_ref(&vm.ctx))
262+
}
263+
264+
/// Execute arbitrary Python code in CPython and return pickled result.
265+
///
266+
/// # Arguments
267+
/// * `code` - Python code to execute (should assign result to `__result__`)
268+
///
269+
/// # Returns
270+
/// Pickled result from CPython (bytes)
271+
#[pyfunction]
272+
fn eval_code(code: PyStrRef, vm: &VirtualMachine) -> PyResult<PyBytesRef> {
273+
let code_str = code.as_str();
274+
275+
let wrapper_code = format!(
276+
r#"
277+
import pickle
278+
{code}
279+
__pickled_result__ = pickle.dumps(__result__, protocol=4)
280+
"#,
281+
code = code_str,
282+
);
283+
284+
let result_bytes = pyo3::Python::attach(|py| -> Result<Vec<u8>, PyErr> {
285+
let globals = pyo3::types::PyDict::new(py);
286+
287+
let builtins = py.import("builtins")?;
288+
let compile = builtins.getattr("compile")?;
289+
let exec_fn = builtins.getattr("exec")?;
290+
291+
let code = compile.call1((&wrapper_code, "<cpython_bridge>", "exec"))?;
292+
exec_fn.call1((code, &globals))?;
293+
294+
let result = globals.get_item("__pickled_result__")?.ok_or_else(|| {
295+
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("No __result__ defined in code")
296+
})?;
297+
let result_bytes: &pyo3::Bound<'_, Pyo3Bytes> = result.downcast()?;
298+
Ok(result_bytes.as_bytes().to_vec())
299+
})
300+
.map_err(|e| vm.new_runtime_error(format!("CPython error: {}", e)))?;
301+
302+
Ok(RustPyBytes::from(result_bytes).into_ref(&vm.ctx))
303+
}
304+
}

src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ pub fn run(init: impl FnOnce(&mut VirtualMachine) + 'static) -> ExitCode {
108108
}
109109
config = config.init_hook(Box::new(init));
110110

111+
#[cfg(feature = "cpython")]
112+
{
113+
config = config.add_native_module("_cpython".to_owned(), rustpython_cpython::make_module);
114+
}
115+
111116
let interp = config.interpreter();
112117
let exitcode = interp.run(move |vm| run_rustpython(vm, run_mode));
113118

0 commit comments

Comments
 (0)