Skip to content

Commit c08c676

Browse files
committed
Initial sketch of a Rust API for CPython
This commit introduces a rough sketch of a Rust API for CPython. The overall design goals are 1. Should be very similar to PyO3 API 2. Threadstate should be passed explicitly to ensure safety 3. Should feel familiar to C API users 4. If a C API for something is missing, we can add new internal C APIs (e.g. an operation where we need to pass the threadstate explicitly where it is fetched implicitly from TLS) 5. Should be minimal to what we need, we are not re-implementing all of PyO3 This is still a work in progress so things are still subject to change significantly.
1 parent 59f2ced commit c08c676

27 files changed

Lines changed: 3624 additions & 230 deletions

File tree

Cargo.lock

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
[workspace]
22
resolver = "3"
33
members = [
4-
"Modules/_base64", "Modules/cpython-build-helper", "Modules/cpython-rust-staticlib",
5-
"Modules/cpython-sys"
4+
"Modules/_base64",
5+
"Modules/cpython-api",
6+
"Modules/cpython-api-macros",
7+
"Modules/cpython-build-helper",
8+
"Modules/cpython-rust-staticlib",
9+
"Modules/cpython-sys",
610
]

Modules/_base64/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.1.0"
44
edition = "2024"
55

66
[dependencies]
7-
cpython-sys ={ path = "../cpython-sys" }
7+
cpython-api = { path = "../cpython-api" }
88

99
[build-dependencies]
1010
cpython-build-helper = { path = "../cpython-build-helper" }

Modules/_base64/src/lib.rs

Lines changed: 69 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -1,249 +1,93 @@
1-
use std::cell::UnsafeCell;
2-
use std::ffi::{c_char, c_int, c_void};
3-
use std::mem::MaybeUninit;
4-
use std::ptr;
5-
use std::slice;
6-
7-
use cpython_sys::METH_FASTCALL;
8-
use cpython_sys::Py_DecRef;
9-
use cpython_sys::Py_buffer;
10-
use cpython_sys::Py_ssize_t;
11-
use cpython_sys::PyBuffer_Release;
12-
use cpython_sys::PyBytes_AsString;
13-
use cpython_sys::PyBytes_FromStringAndSize;
14-
use cpython_sys::PyErr_NoMemory;
15-
use cpython_sys::PyErr_SetString;
16-
use cpython_sys::PyExc_TypeError;
17-
use cpython_sys::PyMethodDef;
18-
use cpython_sys::PyMethodDefFuncPointer;
19-
use cpython_sys::PyModuleDef;
20-
use cpython_sys::PyModuleDef_HEAD_INIT;
21-
use cpython_sys::PyModuleDef_Init;
22-
use cpython_sys::PyObject;
23-
use cpython_sys::PyObject_GetBuffer;
24-
25-
const PYBUF_SIMPLE: c_int = 0;
26-
const PAD_BYTE: u8 = b'=';
27-
const ENCODE_TABLE: [u8; 64] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
28-
29-
#[inline]
30-
fn encoded_output_len(input_len: usize) -> Option<usize> {
31-
input_len
32-
.checked_add(2)
33-
.map(|n| n / 3)
34-
.and_then(|blocks| blocks.checked_mul(4))
35-
}
36-
37-
#[inline]
38-
fn encode_into(input: &[u8], output: &mut [u8]) -> usize {
39-
let mut src_index = 0;
40-
let mut dst_index = 0;
41-
let len = input.len();
42-
43-
while src_index + 3 <= len {
44-
let chunk = (u32::from(input[src_index]) << 16)
45-
| (u32::from(input[src_index + 1]) << 8)
46-
| u32::from(input[src_index + 2]);
47-
output[dst_index] = ENCODE_TABLE[((chunk >> 18) & 0x3f) as usize];
48-
output[dst_index + 1] = ENCODE_TABLE[((chunk >> 12) & 0x3f) as usize];
49-
output[dst_index + 2] = ENCODE_TABLE[((chunk >> 6) & 0x3f) as usize];
50-
output[dst_index + 3] = ENCODE_TABLE[(chunk & 0x3f) as usize];
51-
src_index += 3;
52-
dst_index += 4;
53-
}
54-
55-
match len - src_index {
56-
0 => {}
57-
1 => {
58-
let chunk = u32::from(input[src_index]) << 16;
59-
output[dst_index] = ENCODE_TABLE[((chunk >> 18) & 0x3f) as usize];
60-
output[dst_index + 1] = ENCODE_TABLE[((chunk >> 12) & 0x3f) as usize];
61-
output[dst_index + 2] = PAD_BYTE;
62-
output[dst_index + 3] = PAD_BYTE;
63-
dst_index += 4;
64-
}
65-
2 => {
66-
let chunk =
67-
(u32::from(input[src_index]) << 16) | (u32::from(input[src_index + 1]) << 8);
68-
output[dst_index] = ENCODE_TABLE[((chunk >> 18) & 0x3f) as usize];
69-
output[dst_index + 1] = ENCODE_TABLE[((chunk >> 12) & 0x3f) as usize];
70-
output[dst_index + 2] = ENCODE_TABLE[((chunk >> 6) & 0x3f) as usize];
71-
output[dst_index + 3] = PAD_BYTE;
72-
dst_index += 4;
73-
}
74-
_ => unreachable!("len - src_index cannot exceed 2"),
75-
}
76-
77-
dst_index
78-
}
79-
80-
struct BorrowedBuffer {
81-
view: Py_buffer,
82-
}
1+
//! The `_base64` module, implemented against `cpython-api`.
832
84-
impl BorrowedBuffer {
85-
fn from_object(obj: &PyObject) -> Result<Self, ()> {
86-
let mut view = MaybeUninit::<Py_buffer>::uninit();
87-
let buffer = unsafe {
88-
if PyObject_GetBuffer(obj.as_raw(), view.as_mut_ptr(), PYBUF_SIMPLE) != 0 {
89-
return Err(());
90-
}
91-
Self {
92-
view: view.assume_init(),
93-
}
94-
};
95-
Ok(buffer)
96-
}
3+
use std::mem::MaybeUninit;
974

98-
fn len(&self) -> Py_ssize_t {
99-
self.view.len
100-
}
5+
use cpython_api::prelude::*;
1016

102-
fn as_ptr(&self) -> *const u8 {
103-
self.view.buf.cast::<u8>() as *const u8
104-
}
105-
}
7+
/// Per-module state, empty since the base64 module has no state
8+
struct Base64State;
1069

107-
impl Drop for BorrowedBuffer {
108-
fn drop(&mut self) {
109-
unsafe {
110-
PyBuffer_Release(&mut self.view);
111-
}
10+
impl ModuleState for Base64State {
11+
fn new<'py>(_ts: &ThreadState<'py>, _module: &Bound<'py, PyModule>) -> PyResult<Self> {
12+
Ok(Base64State)
11213
}
11314
}
11415

115-
/// # Safety
116-
/// `module` must be a valid pointer of PyObject representing the module.
117-
/// `args` must be a valid pointer to an array of valid PyObject pointers with length `nargs`.
118-
pub unsafe extern "C" fn standard_b64encode(
119-
_module: *mut PyObject,
120-
args: *mut *mut PyObject,
121-
nargs: Py_ssize_t,
122-
) -> *mut PyObject {
123-
if nargs != 1 {
124-
unsafe {
125-
PyErr_SetString(
126-
PyExc_TypeError,
127-
c"standard_b64encode() takes exactly one argument".as_ptr(),
128-
);
129-
}
130-
return ptr::null_mut();
131-
}
132-
133-
let source = unsafe { &**args };
16+
const PAD_BYTE: u8 = b'=';
17+
const ENCODE_TABLE: [u8; 64] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
13418

135-
// Safe cast by Safety
136-
match standard_b64encode_impl(source) {
137-
Ok(result) => result,
138-
Err(_) => ptr::null_mut(),
139-
}
19+
/// 4 output bytes per started block of 3 input bytes; `None` on overflow.
20+
#[inline]
21+
fn encoded_output_len(input_len: usize) -> Option<usize> {
22+
input_len.div_ceil(3).checked_mul(4)
14023
}
14124

142-
fn standard_b64encode_impl(source: &PyObject) -> Result<*mut PyObject, ()> {
143-
let buffer = match BorrowedBuffer::from_object(source) {
144-
Ok(buf) => buf,
145-
Err(_) => return Err(()),
25+
/// Encode `input` into `output`, which must be exactly
26+
/// `encoded_output_len(input.len())` bytes and is fully initialized on
27+
/// return.
28+
fn encode_into(input: &[u8], output: &mut [MaybeUninit<u8>]) {
29+
let mut chunks = input.chunks_exact(3);
30+
let mut out = output.iter_mut();
31+
let mut put = |b: u8| {
32+
out.next()
33+
.expect("output sized to encoded_output_len")
34+
.write(b);
14635
};
14736

148-
let view_len = buffer.len();
149-
if view_len < 0 {
150-
unsafe {
151-
PyErr_SetString(
152-
PyExc_TypeError,
153-
c"standard_b64encode() argument has negative length".as_ptr(),
154-
);
155-
}
156-
return Err(());
37+
for chunk in &mut chunks {
38+
let group = (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8) | u32::from(chunk[2]);
39+
put(ENCODE_TABLE[(group >> 18 & 0x3f) as usize]);
40+
put(ENCODE_TABLE[(group >> 12 & 0x3f) as usize]);
41+
put(ENCODE_TABLE[(group >> 6 & 0x3f) as usize]);
42+
put(ENCODE_TABLE[(group & 0x3f) as usize]);
15743
}
15844

159-
let input_len = view_len as usize;
160-
let input = unsafe { slice::from_raw_parts(buffer.as_ptr(), input_len) };
161-
162-
let Some(output_len) = encoded_output_len(input_len) else {
163-
unsafe {
164-
PyErr_NoMemory();
45+
match *chunks.remainder() {
46+
[] => {}
47+
[a] => {
48+
let group = u32::from(a) << 16;
49+
put(ENCODE_TABLE[(group >> 18 & 0x3f) as usize]);
50+
put(ENCODE_TABLE[(group >> 12 & 0x3f) as usize]);
51+
put(PAD_BYTE);
52+
put(PAD_BYTE);
16553
}
166-
return Err(());
167-
};
168-
169-
if output_len > isize::MAX as usize {
170-
unsafe {
171-
PyErr_NoMemory();
54+
[a, b] => {
55+
let group = (u32::from(a) << 16) | (u32::from(b) << 8);
56+
put(ENCODE_TABLE[(group >> 18 & 0x3f) as usize]);
57+
put(ENCODE_TABLE[(group >> 12 & 0x3f) as usize]);
58+
put(ENCODE_TABLE[(group >> 6 & 0x3f) as usize]);
59+
put(PAD_BYTE);
17260
}
173-
return Err(());
61+
_ => unreachable!("chunks_exact(3) remainder is at most 2 bytes"),
17462
}
175-
176-
let result = unsafe { PyBytes_FromStringAndSize(ptr::null(), output_len as Py_ssize_t) };
177-
if result.is_null() {
178-
return Err(());
179-
}
180-
181-
let dest_ptr = unsafe { PyBytes_AsString(result) };
182-
if dest_ptr.is_null() {
183-
unsafe {
184-
Py_DecRef(result);
185-
}
186-
return Err(());
187-
}
188-
let dest = unsafe { slice::from_raw_parts_mut(dest_ptr.cast::<u8>(), output_len) };
189-
190-
let written = encode_into(input, dest);
191-
debug_assert_eq!(written, output_len);
192-
Ok(result)
193-
}
194-
195-
pub extern "C" fn _base64_clear(_obj: *mut PyObject) -> c_int {
196-
//TODO
197-
0
198-
}
199-
200-
pub extern "C" fn _base64_free(_o: *mut c_void) {
201-
//TODO
20263
}
20364

204-
pub struct ModuleDef {
205-
ffi: UnsafeCell<PyModuleDef>,
65+
/// Encode a bytes-like object with the standard Base64 alphabet.
66+
#[pyfunction(signature = (data, /))]
67+
fn standard_b64encode<'py>(
68+
ts: &ThreadState<'py>,
69+
_state: &Base64State,
70+
data: PyBuffer<'py>,
71+
) -> PyResult<Bound<'py, PyBytes>> {
72+
let input = data.as_bytes();
73+
let Some(output_len) = encoded_output_len(input.len()) else {
74+
return Err(PyMemoryError::raise(ts, "encoded result too long"));
75+
};
76+
// Write straight into the bytes object's buffer — no intermediate Vec.
77+
PyBytes::new_with(ts, output_len, |output| {
78+
encode_into(input, output);
79+
Ok(())
80+
})
20681
}
20782

208-
impl ModuleDef {
209-
fn init_multi_phase(&'static self) -> *mut PyObject {
210-
unsafe { PyModuleDef_Init(self.ffi.get()) }
211-
}
83+
fn base64_exec<'py>(_ts: &ThreadState<'py>, _module: &Bound<'py, PyModule>) -> PyResult<()> {
84+
Ok(())
21285
}
21386

214-
unsafe impl Sync for ModuleDef {}
215-
216-
pub static _BASE64_MODULE_METHODS: [PyMethodDef; 2] = {
217-
[
218-
PyMethodDef {
219-
ml_name: c"standard_b64encode".as_ptr() as *mut c_char,
220-
ml_meth: PyMethodDefFuncPointer {
221-
PyCFunctionFast: standard_b64encode,
222-
},
223-
ml_flags: METH_FASTCALL,
224-
ml_doc: c"Demo for the _base64 module".as_ptr() as *mut c_char,
225-
},
226-
PyMethodDef::zeroed(),
227-
]
228-
};
229-
230-
pub static _BASE64_MODULE: ModuleDef = {
231-
ModuleDef {
232-
ffi: UnsafeCell::new(PyModuleDef {
233-
m_base: PyModuleDef_HEAD_INIT,
234-
m_name: c"_base64".as_ptr() as *mut _,
235-
m_doc: c"A test Rust module".as_ptr() as *mut _,
236-
m_size: 0,
237-
m_methods: &_BASE64_MODULE_METHODS as *const PyMethodDef as *mut _,
238-
m_slots: std::ptr::null_mut(),
239-
m_traverse: None,
240-
m_clear: Some(_base64_clear),
241-
m_free: Some(_base64_free),
242-
}),
243-
}
244-
};
245-
246-
#[unsafe(no_mangle)]
247-
pub extern "C" fn PyInit__base64() -> *mut PyObject {
248-
_BASE64_MODULE.init_multi_phase()
87+
export_module! {
88+
name: _base64,
89+
doc: c"Base64 encoding implemented in Rust",
90+
state: Base64State,
91+
methods: [standard_b64encode],
92+
exec: base64_exec,
24993
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "cpython-api-macros"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lib]
7+
proc-macro = true
8+
9+
[dependencies]
10+
proc-macro2 = "1"
11+
quote = "1"
12+
syn = { version = "2", features = ["full"] }

0 commit comments

Comments
 (0)