Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
54156f6
export c api
bschoenmaeckers Apr 4, 2026
2250a1c
type mapping v1
bschoenmaeckers Apr 5, 2026
bc96211
Type mapping v2
bschoenmaeckers Apr 5, 2026
46307f6
Implement type flags
bschoenmaeckers Apr 5, 2026
b08d41a
Implement `PyUnicode_FromStringAndSize`
bschoenmaeckers Apr 5, 2026
34c13f4
Do not use `C-unwind`
bschoenmaeckers Apr 5, 2026
24b2c63
implement `PyLong_AsLong`
bschoenmaeckers Apr 5, 2026
384b378
Add `with_vm` helper
bschoenmaeckers Apr 5, 2026
f49e859
Move `PyThreadState` to `pystate.rs`
bschoenmaeckers Apr 5, 2026
e18eef7
Basic multi-threading support
bschoenmaeckers Apr 5, 2026
565bf5e
Cleanup
bschoenmaeckers Apr 5, 2026
85b2928
Attach thread in `Py_InitializeEx`
bschoenmaeckers Apr 5, 2026
c603d37
Map RustPython type flags to CPython type flags
bschoenmaeckers Apr 7, 2026
cee9403
Use `*const Py<PyType>` instead of `*mut PyTypeObject`
bschoenmaeckers Apr 7, 2026
0a50fcc
Implement `PyUnicode_AsUTF8AndSize`
bschoenmaeckers Apr 7, 2026
f588979
Implement `PyType_GetName`
bschoenmaeckers Apr 7, 2026
edaca0c
Implement alternative `PyLong` constructors
bschoenmaeckers Apr 7, 2026
e4c8bd7
Move `init_static_type_pointers` to `pylifecycle.rs`
bschoenmaeckers Apr 7, 2026
26c2777
Add support for `None`, `True`, `False`, `Ellipsis` & `NotImplemented`
bschoenmaeckers Apr 7, 2026
09466e2
Add support for PyBytes
bschoenmaeckers Apr 8, 2026
94cc39b
Add basic exception support
bschoenmaeckers Apr 8, 2026
2ef64d2
Add import support
bschoenmaeckers Apr 8, 2026
55b1e14
Add basic call support
bschoenmaeckers Apr 8, 2026
85b1ed4
Use pyo3 as test harness
bschoenmaeckers Apr 8, 2026
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
28 changes: 18 additions & 10 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions crates/capi/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[profile.test.env]
PYO3_CONFIG_FILE = { value = "pyo3-rustpython.config", relative = true }
21 changes: 21 additions & 0 deletions crates/capi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "rustpython-capi"
description = "Minimal CPython C-API compatibility exports for RustPython"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
repository.workspace = true
license.workspace = true

[lib]
crate-type = ["staticlib"]

[dependencies]
rustpython-vm = { workspace = true, features = ["threading"]}

[dev-dependencies]
pyo3 = { version = "0.28.3", features = ["auto-initialize", "abi3-py314"] }

[lints]
workspace = true
6 changes: 6 additions & 0 deletions crates/capi/pyo3-rustpython.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
implementation=CPython
version=3.14
shared=true
abi3=true
build_flags=
suppress_build_script_link_lines=true
91 changes: 91 additions & 0 deletions crates/capi/src/abstract_.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use crate::pystate::with_vm;
use rustpython_vm::PyObject;
use rustpython_vm::builtins::{PyStr, PyTuple};
use std::slice;

const PY_VECTORCALL_ARGUMENTS_OFFSET: usize = 1usize << (usize::BITS as usize - 1);

#[unsafe(no_mangle)]
pub extern "C" fn PyObject_CallNoArgs(callable: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
if callable.is_null() {
vm.push_exception(Some(vm.new_system_error(
"PyObject_CallNoArgs called with null callable".to_owned(),
)));
return std::ptr::null_mut();
}

let callable = unsafe { &*callable };
callable.call((), vm).map_or_else(
|err| {
vm.push_exception(Some(err));
std::ptr::null_mut()
},
|result| result.into_raw().as_ptr(),
)
})
}

#[unsafe(no_mangle)]
pub extern "C" fn PyObject_VectorcallMethod(
name: *mut PyObject,
args: *const *mut PyObject,
nargsf: usize,
kwnames: *mut PyObject,
) -> *mut PyObject {
with_vm(|vm| {
let args_len = nargsf & !PY_VECTORCALL_ARGUMENTS_OFFSET;
let num_positional_args = args_len - 1;

let (receiver, args) = unsafe { slice::from_raw_parts(args, args_len) }
.split_first()
.expect("PyObject_VectorcallMethod should always have at least one argument");

let method_name = unsafe { (&*name).downcast_unchecked_ref::<PyStr>() };
let callable = match unsafe {
(&**receiver)
.get_attr(method_name, vm)
} {
Ok(obj) => obj,
Err(err) => {
vm.push_exception(Some(err));
return std::ptr::null_mut()
},
};

let args = args
.iter()
.map(|arg| unsafe { &**arg }.to_owned())
.collect::<Vec<_>>();

let kwnames = unsafe {
kwnames
.as_ref()
.map(|tuple| &***tuple.downcast_unchecked_ref::<PyTuple>())
};

callable
.vectorcall(args, num_positional_args, kwnames, vm)
.map_or_else(
|err| {
vm.push_exception(Some(err));
std::ptr::null_mut()
},
|obj| obj.into_raw().as_ptr(),
)
})
}

#[cfg(test)]
mod tests {
use pyo3::prelude::*;
use pyo3::types::PyString;

#[test]
fn test_call_method1() {
Python::attach(|py| {
let string = PyString::new(py, "Hello, World!");
assert!(string.call_method1("endswith", ("!",)).unwrap().is_truthy().unwrap());
})
}
}
56 changes: 56 additions & 0 deletions crates/capi/src/bytesobject.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use crate::PyObject;
use crate::pystate::with_vm;
use core::ffi::c_char;
use rustpython_vm::builtins::PyBytes;
use rustpython_vm::convert::IntoObject;

#[unsafe(no_mangle)]
pub extern "C" fn PyBytes_FromStringAndSize(bytes: *mut c_char, len: isize) -> *mut PyObject {
with_vm(|vm| {
if bytes.is_null() {
std::ptr::null_mut()
} else {
let bytes_slice =
unsafe { core::slice::from_raw_parts(bytes as *const u8, len as usize) };
vm.ctx
.new_bytes(bytes_slice.to_vec())
.into_object()
.into_raw()
.as_ptr()
}
})
}

#[unsafe(no_mangle)]
pub extern "C" fn PyBytes_Size(bytes: *mut PyObject) -> isize {
with_vm(|_vm| {
let bytes = unsafe { &*bytes }
.downcast_ref::<PyBytes>()
.expect("PyBytes_Size argument must be a bytes object");
bytes.as_bytes().len() as _
})
}

#[unsafe(no_mangle)]
pub extern "C" fn PyBytes_AsString(bytes: *mut PyObject) -> *mut c_char {
with_vm(|_vm| {
let bytes = unsafe { &*bytes }
.downcast_ref::<PyBytes>()
.expect("PyBytes_AsString argument must be a bytes object");
bytes.as_bytes().as_ptr() as _
})
}

#[cfg(test)]
mod tests {
use pyo3::prelude::*;
use pyo3::types::PyBytes;

#[test]
fn test_bytes() {
Python::attach(|py| {
let bytes = PyBytes::new(py, b"Hello, World!");
assert_eq!(bytes.as_bytes(), b"Hello, World!");
})
}
}
29 changes: 29 additions & 0 deletions crates/capi/src/import.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use crate::PyObject;
use crate::pystate::with_vm;
use rustpython_vm::builtins::PyStr;

#[unsafe(no_mangle)]
pub extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
let name = unsafe { (&*name).downcast_unchecked_ref::<PyStr>() };
vm.import(name, 0).map_or_else(
|err| {
vm.push_exception(Some(err));
std::ptr::null_mut()
},
|module| module.into_raw().as_ptr(),
)
})
}

#[cfg(test)]
mod tests {
use pyo3::prelude::*;

#[test]
fn test_import() {
Python::attach(|py| {
let _module = py.import("sys").unwrap();
})
}
}
21 changes: 21 additions & 0 deletions crates/capi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
pub use rustpython_vm::PyObject;

extern crate alloc;

pub mod abstract_;
pub mod bytesobject;
pub mod import;
pub mod longobject;
pub mod object;
pub mod pyerrors;
pub mod pylifecycle;
pub mod pystate;
pub mod refcount;
pub mod traceback;
pub mod tupleobject;
pub mod unicodeobject;

#[inline]
pub(crate) fn log_stub(name: &str) {
eprintln!("[rustpython-capi stub] {name} called");
}
75 changes: 75 additions & 0 deletions crates/capi/src/longobject.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong};

use crate::PyObject;
use crate::pyerrors::{PyErr_SetString, PyExc_OverflowError};
use crate::pystate::with_vm;
use rustpython_vm::builtins::PyInt;
use rustpython_vm::convert::IntoObject;

#[unsafe(no_mangle)]
pub extern "C" fn PyLong_FromLong(value: c_long) -> *mut PyObject {
with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr())
}

#[unsafe(no_mangle)]
pub extern "C" fn PyLong_FromLongLong(value: c_longlong) -> *mut PyObject {
with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr())
}

#[unsafe(no_mangle)]
pub extern "C" fn PyLong_FromSsize_t(value: isize) -> *mut PyObject {
with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr())
}

#[unsafe(no_mangle)]
pub extern "C" fn PyLong_FromSize_t(value: usize) -> *mut PyObject {
with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr())
}

#[unsafe(no_mangle)]
pub extern "C" fn PyLong_FromUnsignedLong(value: c_ulong) -> *mut PyObject {
with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr())
}

#[unsafe(no_mangle)]
pub extern "C" fn PyLong_FromUnsignedLongLong(value: c_ulonglong) -> *mut PyObject {
with_vm(|vm| vm.ctx.new_int(value).into_object().into_raw().as_ptr())
}

#[unsafe(no_mangle)]
pub extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long {
if obj.is_null() {
panic!("PyLong_AsLong called with null object");
}

with_vm(|_vm| {
// SAFETY: non-null checked above; caller promises a valid PyObject pointer.
let obj_ref = unsafe { &*obj };
let int_obj = obj_ref
.downcast_ref::<PyInt>()
.expect("PyLong_AsLong currently only accepts int instances");

int_obj.as_bigint().try_into().unwrap_or_else(|_| unsafe {
PyErr_SetString(
PyExc_OverflowError.assume_init(),
c"Python int too large to convert to C long".as_ptr(),
);
-1
})
})
}

#[cfg(test)]
mod tests {
use pyo3::prelude::*;
use pyo3::types::PyInt;

#[test]
fn test_py_int() {
Python::attach(|py| {
let number = PyInt::new(py, 123);
assert!(number.is_instance_of::<PyInt>());
assert_eq!(number.extract::<i32>().unwrap(), 123);
})
}
}
Loading