🌐 AI搜索 & 代理 主页
Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
rustpython-cpython
  • Loading branch information
youknowone committed Nov 28, 2025
commit e70fb0446d2f17d4511658f069028f1a89198f2b
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ssl-rustls = ["ssl", "rustpython-stdlib/ssl-rustls"]
ssl-openssl = ["ssl", "rustpython-stdlib/ssl-openssl"]
ssl-vendor = ["ssl-openssl", "rustpython-stdlib/ssl-vendor"]
tkinter = ["rustpython-stdlib/tkinter"]
cpython = ["dep:rustpython-cpython"]

[build-dependencies]
winresource = "0.1"
Expand All @@ -34,6 +35,7 @@ rustpython-compiler = { workspace = true }
rustpython-pylib = { workspace = true, optional = true }
rustpython-stdlib = { workspace = true, optional = true, features = ["compiler"] }
rustpython-vm = { workspace = true, features = ["compiler"] }
rustpython-cpython = { workspace = true, optional = true }
ruff_python_parser = { workspace = true }

cfg-if = { workspace = true }
Expand Down Expand Up @@ -150,6 +152,7 @@ rustpython-stdlib = { path = "crates/stdlib", default-features = false, version
rustpython-sre_engine = { path = "crates/sre_engine", version = "0.4.0" }
rustpython-wtf8 = { path = "crates/wtf8", version = "0.4.0" }
rustpython-doc = { path = "crates/doc", version = "0.4.0" }
rustpython-cpython = { path = "crates/cpython", version = "0.4.0" }

ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", tag = "0.14.1" }
ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", tag = "0.14.1" }
Expand Down
17 changes: 17 additions & 0 deletions crates/cpython/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "rustpython-cpython"
description = "RustPython to CPython bridge via PyO3"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
repository.workspace = true
license.workspace = true

[dependencies]
rustpython-vm = { workspace = true }
rustpython-derive = { workspace = true }
pyo3 = { version = "0.26", features = ["auto-initialize"] }

[lints]
workspace = true
304 changes: 304 additions & 0 deletions crates/cpython/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
//! RustPython to CPython bridge via PyO3
//!
//! This crate provides interoperability between RustPython and CPython,
//! allowing RustPython code to execute functions in the CPython runtime.
//!
//! # Background
//!
//! RustPython does not implement all CPython C extension modules.
//! This crate enables calling into the real CPython runtime for functionality
//! that is not yet available in RustPython.
//!
//! # Architecture
//!
//! Communication between RustPython and CPython uses PyO3 for in-process calls.
//! Data is serialized using Python's `pickle` protocol:
//!
//! ```text
//! RustPython CPython
//! │ │
//! │ pickle.dumps(args, kwargs) │
//! │ ──────────────────────────────► │
//! │ │ exec(source)
//! │ │ result = func(*args, **kwargs)
//! │ pickle.dumps(result) │
//! │ ◄────────────────────────────── │
//! │ │
//! │ pickle.loads(result) │
//! ```
//!
//! # Limitations
//!
//! - **File-based functions only**: Functions defined in REPL or via `exec()` will fail
//! (`inspect.getsource()` requires source file access)
//! - **Picklable data only**: Cannot pass functions, classes, file handles, etc.
//! - **Performance overhead**: pickle serialization + CPython GIL acquisition
//! - **CPython required**: System must have CPython installed (linked via PyO3)

#[macro_use]
extern crate rustpython_derive;

use rustpython_vm::{PyRef, VirtualMachine, builtins::PyModule};

/// Create the _cpython module
pub fn make_module(vm: &VirtualMachine) -> PyRef<PyModule> {
_cpython::make_module(vm)
}

#[pymodule]
mod _cpython {
use pyo3::PyErr;
use pyo3::prelude::PyAnyMethods;
use pyo3::types::PyBytes as Pyo3Bytes;
use pyo3::types::PyBytesMethods;
use pyo3::types::PyDictMethods;
use rustpython_vm::{
Py, PyObjectRef, PyPayload, PyResult, VirtualMachine,
builtins::{PyBytes as RustPyBytes, PyBytesRef, PyDict, PyStrRef, PyTypeRef},
function::FuncArgs,
types::{Callable, Constructor, Representable},
};

/// Wrapper class for executing functions in CPython.
/// Used as a decorator: @_cpython.call
#[pyattr]
#[pyclass(name = "call")]
#[derive(Debug, PyPayload)]
struct CPythonCall {
source: String,
func_name: String,
}

impl Constructor for CPythonCall {
type Args = PyObjectRef;

fn py_new(cls: PyTypeRef, func: Self::Args, vm: &VirtualMachine) -> PyResult {
// Get function name
let func_name = func
.get_attr("__name__", vm)?
.downcast::<rustpython_vm::builtins::PyStr>()
.map_err(|_| vm.new_type_error("function must have __name__".to_owned()))?
.as_str()
.to_owned();

// Get source using inspect.getsource(func)
let inspect = vm.import("inspect", 0)?;
let getsource = inspect.get_attr("getsource", vm)?;
let source_obj = getsource.call((func.clone(),), vm)?;
let source_full = source_obj
.downcast::<rustpython_vm::builtins::PyStr>()
.map_err(|_| vm.new_type_error("getsource did not return str".to_owned()))?
.as_str()
.to_owned();

// Strip decorator lines from source (lines starting with @)
// Find the first line that starts with 'def ' or 'async def '
let source = strip_decorators(&source_full);

Self { source, func_name }
.into_ref_with_type(vm, cls)
.map(Into::into)
}
}

/// Strip decorator lines from function source code.
/// Returns source starting from 'def' or 'async def'.
fn strip_decorators(source: &str) -> String {
let lines = source.lines();
let mut result_lines = Vec::new();
let mut found_def = false;

for line in lines {
let trimmed = line.trim_start();
if !found_def {
if trimmed.starts_with("def ") || trimmed.starts_with("async def ") {
found_def = true;
result_lines.push(line);
}
// Skip decorator lines (starting with @) and blank lines before def
} else {
result_lines.push(line);
}
}

result_lines.join("\n")
}

impl Callable for CPythonCall {
type Args = FuncArgs;

fn call(zelf: &Py<Self>, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
// Import pickle module
let pickle = vm.import("pickle", 0)?;
let dumps = pickle.get_attr("dumps", vm)?;
let loads = pickle.get_attr("loads", vm)?;

// Pickle args and kwargs
let args_tuple = vm.ctx.new_tuple(args.args);
let kwargs_dict = PyDict::default().into_ref(&vm.ctx);
for (key, value) in args.kwargs {
kwargs_dict.set_item(&key, value, vm)?;
}

let pickled_args = dumps.call((args_tuple,), vm)?;
let pickled_kwargs = dumps.call((kwargs_dict,), vm)?;

let pickled_args_bytes = pickled_args
.downcast::<RustPyBytes>()
.map_err(|_| vm.new_type_error("pickle.dumps did not return bytes".to_owned()))?;
let pickled_kwargs_bytes = pickled_kwargs
.downcast::<RustPyBytes>()
.map_err(|_| vm.new_type_error("pickle.dumps did not return bytes".to_owned()))?;

// Call execute_impl()
let result_bytes = execute_impl(
&zelf.source,
&zelf.func_name,
pickled_args_bytes.as_bytes(),
pickled_kwargs_bytes.as_bytes(),
vm,
)?;

// Unpickle result
let result_py_bytes = RustPyBytes::from(result_bytes).into_ref(&vm.ctx);
loads.call((result_py_bytes,), vm)
}
}

impl Representable for CPythonCall {
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
Ok(format!("<_cpython.call wrapper for '{}'>", zelf.func_name))
}
}

#[pyclass(with(Constructor, Callable, Representable))]
impl CPythonCall {}

/// Internal implementation for executing Python code in CPython.
fn execute_impl(
source: &str,
func_name: &str,
args_bytes: &[u8],
kwargs_bytes: &[u8],
vm: &VirtualMachine,
) -> PyResult<Vec<u8>> {
// Build the CPython code to execute
let cpython_code = format!(
r#"
import pickle as __pickle

# Unpickle arguments
__args__ = __pickle.loads(__pickled_args__)
__kwargs__ = __pickle.loads(__pickled_kwargs__)
# Execute the source code (defines the function)
{source}

# Call the function and pickle the result
__result__ = {func_name}(*__args__, **__kwargs__)
__pickled_result__ = __pickle.dumps(__result__, protocol=4)
"#,
source = source,
func_name = func_name,
);

// Execute in CPython via PyO3
pyo3::Python::attach(|py| -> Result<Vec<u8>, PyErr> {
// Create Python bytes for pickled data
let py_args = Pyo3Bytes::new(py, args_bytes);
let py_kwargs = Pyo3Bytes::new(py, kwargs_bytes);

// Create globals dict with pickled args
let globals = pyo3::types::PyDict::new(py);
globals.set_item("__pickled_args__", &py_args)?;
globals.set_item("__pickled_kwargs__", &py_kwargs)?;

// Execute using compile + exec pattern
let builtins = py.import("builtins")?;
let compile = builtins.getattr("compile")?;
let exec_fn = builtins.getattr("exec")?;

// Compile the code
let code = compile.call1((&cpython_code, "<cpython_bridge>", "exec"))?;

// Execute with globals
exec_fn.call1((code, &globals))?;

// Get the pickled result
let result = globals.get_item("__pickled_result__")?.ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("No result returned")
})?;
let result_bytes: &pyo3::Bound<'_, Pyo3Bytes> = result.downcast()?;
Ok(result_bytes.as_bytes().to_vec())
})
.map_err(|e| vm.new_runtime_error(format!("CPython error: {}", e)))
}

/// Execute a Python function in CPython runtime.
///
/// # Arguments
/// * `source` - The complete source code of the function
/// * `func_name` - The name of the function to call
/// * `pickled_args` - Pickled positional arguments (bytes)
/// * `pickled_kwargs` - Pickled keyword arguments (bytes)
///
/// # Returns
/// Pickled result from CPython (bytes)
#[pyfunction]
fn execute(
source: PyStrRef,
func_name: PyStrRef,
pickled_args: PyBytesRef,
pickled_kwargs: PyBytesRef,
vm: &VirtualMachine,
) -> PyResult<PyBytesRef> {
let result_bytes = execute_impl(
source.as_str(),
func_name.as_str(),
pickled_args.as_bytes(),
pickled_kwargs.as_bytes(),
vm,
)?;
Ok(RustPyBytes::from(result_bytes).into_ref(&vm.ctx))
}

/// Execute arbitrary Python code in CPython and return pickled result.
///
/// # Arguments
/// * `code` - Python code to execute (should assign result to `__result__`)
///
/// # Returns
/// Pickled result from CPython (bytes)
#[pyfunction]
fn eval_code(code: PyStrRef, vm: &VirtualMachine) -> PyResult<PyBytesRef> {
let code_str = code.as_str();

let wrapper_code = format!(
r#"
import pickle
{code}
__pickled_result__ = pickle.dumps(__result__, protocol=4)
"#,
code = code_str,
);

let result_bytes = pyo3::Python::attach(|py| -> Result<Vec<u8>, PyErr> {
let globals = pyo3::types::PyDict::new(py);

let builtins = py.import("builtins")?;
let compile = builtins.getattr("compile")?;
let exec_fn = builtins.getattr("exec")?;

let code = compile.call1((&wrapper_code, "<cpython_bridge>", "exec"))?;
exec_fn.call1((code, &globals))?;

let result = globals.get_item("__pickled_result__")?.ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("No __result__ defined in code")
})?;
let result_bytes: &pyo3::Bound<'_, Pyo3Bytes> = result.downcast()?;
Ok(result_bytes.as_bytes().to_vec())
})
.map_err(|e| vm.new_runtime_error(format!("CPython error: {}", e)))?;

Ok(RustPyBytes::from(result_bytes).into_ref(&vm.ctx))
}
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ pub fn run(init: impl FnOnce(&mut VirtualMachine) + 'static) -> ExitCode {
}
config = config.init_hook(Box::new(init));

#[cfg(feature = "cpython")]
{
config = config.add_native_module("_cpython".to_owned(), rustpython_cpython::make_module);
}

let interp = config.interpreter();
let exitcode = interp.run(move |vm| run_rustpython(vm, run_mode));

Expand Down