diff --git a/Cargo.lock b/Cargo.lock index 132b913b06d5e5f..6c0b127a84075dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,8 +6,8 @@ version = 4 name = "_base64" version = "0.1.0" dependencies = [ + "cpython-api", "cpython-build-helper", - "cpython-sys", ] [[package]] @@ -71,6 +71,23 @@ dependencies = [ "libloading", ] +[[package]] +name = "cpython-api" +version = "0.1.0" +dependencies = [ + "cpython-api-macros", + "cpython-sys", +] + +[[package]] +name = "cpython-api-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cpython-build-helper" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 768d289fd77653e..03f9c4d28986a83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,10 @@ [workspace] resolver = "3" members = [ - "Modules/_base64", "Modules/cpython-build-helper", "Modules/cpython-rust-staticlib", - "Modules/cpython-sys" + "Modules/_base64", + "Modules/cpython-api", + "Modules/cpython-api-macros", + "Modules/cpython-build-helper", + "Modules/cpython-rust-staticlib", + "Modules/cpython-sys", ] diff --git a/Modules/_base64/Cargo.toml b/Modules/_base64/Cargo.toml index f90350624729d65..63c2b167131ab35 100644 --- a/Modules/_base64/Cargo.toml +++ b/Modules/_base64/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -cpython-sys ={ path = "../cpython-sys" } +cpython-api = { path = "../cpython-api" } [build-dependencies] cpython-build-helper = { path = "../cpython-build-helper" } diff --git a/Modules/_base64/src/lib.rs b/Modules/_base64/src/lib.rs index 64e4d718789372c..3f97eec7ca6faf7 100644 --- a/Modules/_base64/src/lib.rs +++ b/Modules/_base64/src/lib.rs @@ -1,249 +1,93 @@ -use std::cell::UnsafeCell; -use std::ffi::{c_char, c_int, c_void}; -use std::mem::MaybeUninit; -use std::ptr; -use std::slice; - -use cpython_sys::METH_FASTCALL; -use cpython_sys::Py_DecRef; -use cpython_sys::Py_buffer; -use cpython_sys::Py_ssize_t; -use cpython_sys::PyBuffer_Release; -use cpython_sys::PyBytes_AsString; -use cpython_sys::PyBytes_FromStringAndSize; -use cpython_sys::PyErr_NoMemory; -use cpython_sys::PyErr_SetString; -use cpython_sys::PyExc_TypeError; -use cpython_sys::PyMethodDef; -use cpython_sys::PyMethodDefFuncPointer; -use cpython_sys::PyModuleDef; -use cpython_sys::PyModuleDef_HEAD_INIT; -use cpython_sys::PyModuleDef_Init; -use cpython_sys::PyObject; -use cpython_sys::PyObject_GetBuffer; - -const PYBUF_SIMPLE: c_int = 0; -const PAD_BYTE: u8 = b'='; -const ENCODE_TABLE: [u8; 64] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -#[inline] -fn encoded_output_len(input_len: usize) -> Option { - input_len - .checked_add(2) - .map(|n| n / 3) - .and_then(|blocks| blocks.checked_mul(4)) -} - -#[inline] -fn encode_into(input: &[u8], output: &mut [u8]) -> usize { - let mut src_index = 0; - let mut dst_index = 0; - let len = input.len(); - - while src_index + 3 <= len { - let chunk = (u32::from(input[src_index]) << 16) - | (u32::from(input[src_index + 1]) << 8) - | u32::from(input[src_index + 2]); - output[dst_index] = ENCODE_TABLE[((chunk >> 18) & 0x3f) as usize]; - output[dst_index + 1] = ENCODE_TABLE[((chunk >> 12) & 0x3f) as usize]; - output[dst_index + 2] = ENCODE_TABLE[((chunk >> 6) & 0x3f) as usize]; - output[dst_index + 3] = ENCODE_TABLE[(chunk & 0x3f) as usize]; - src_index += 3; - dst_index += 4; - } - - match len - src_index { - 0 => {} - 1 => { - let chunk = u32::from(input[src_index]) << 16; - output[dst_index] = ENCODE_TABLE[((chunk >> 18) & 0x3f) as usize]; - output[dst_index + 1] = ENCODE_TABLE[((chunk >> 12) & 0x3f) as usize]; - output[dst_index + 2] = PAD_BYTE; - output[dst_index + 3] = PAD_BYTE; - dst_index += 4; - } - 2 => { - let chunk = - (u32::from(input[src_index]) << 16) | (u32::from(input[src_index + 1]) << 8); - output[dst_index] = ENCODE_TABLE[((chunk >> 18) & 0x3f) as usize]; - output[dst_index + 1] = ENCODE_TABLE[((chunk >> 12) & 0x3f) as usize]; - output[dst_index + 2] = ENCODE_TABLE[((chunk >> 6) & 0x3f) as usize]; - output[dst_index + 3] = PAD_BYTE; - dst_index += 4; - } - _ => unreachable!("len - src_index cannot exceed 2"), - } - - dst_index -} - -struct BorrowedBuffer { - view: Py_buffer, -} +//! The `_base64` module, implemented against `cpython-api`. -impl BorrowedBuffer { - fn from_object(obj: &PyObject) -> Result { - let mut view = MaybeUninit::::uninit(); - let buffer = unsafe { - if PyObject_GetBuffer(obj.as_raw(), view.as_mut_ptr(), PYBUF_SIMPLE) != 0 { - return Err(()); - } - Self { - view: view.assume_init(), - } - }; - Ok(buffer) - } +use std::mem::MaybeUninit; - fn len(&self) -> Py_ssize_t { - self.view.len - } +use cpython_api::prelude::*; - fn as_ptr(&self) -> *const u8 { - self.view.buf.cast::() as *const u8 - } -} +/// Per-module state, empty since the base64 module has no state +struct Base64State; -impl Drop for BorrowedBuffer { - fn drop(&mut self) { - unsafe { - PyBuffer_Release(&mut self.view); - } +impl ModuleState for Base64State { + fn new<'py>(_ts: &ThreadState<'py>, _module: &Bound<'py, PyModule>) -> PyResult { + Ok(Base64State) } } -/// # Safety -/// `module` must be a valid pointer of PyObject representing the module. -/// `args` must be a valid pointer to an array of valid PyObject pointers with length `nargs`. -pub unsafe extern "C" fn standard_b64encode( - _module: *mut PyObject, - args: *mut *mut PyObject, - nargs: Py_ssize_t, -) -> *mut PyObject { - if nargs != 1 { - unsafe { - PyErr_SetString( - PyExc_TypeError, - c"standard_b64encode() takes exactly one argument".as_ptr(), - ); - } - return ptr::null_mut(); - } - - let source = unsafe { &**args }; +const PAD_BYTE: u8 = b'='; +const ENCODE_TABLE: [u8; 64] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - // Safe cast by Safety - match standard_b64encode_impl(source) { - Ok(result) => result, - Err(_) => ptr::null_mut(), - } +/// 4 output bytes per started block of 3 input bytes; `None` on overflow. +#[inline] +fn encoded_output_len(input_len: usize) -> Option { + input_len.div_ceil(3).checked_mul(4) } -fn standard_b64encode_impl(source: &PyObject) -> Result<*mut PyObject, ()> { - let buffer = match BorrowedBuffer::from_object(source) { - Ok(buf) => buf, - Err(_) => return Err(()), +/// Encode `input` into `output`, which must be exactly +/// `encoded_output_len(input.len())` bytes and is fully initialized on +/// return. +fn encode_into(input: &[u8], output: &mut [MaybeUninit]) { + let mut chunks = input.chunks_exact(3); + let mut out = output.iter_mut(); + let mut put = |b: u8| { + out.next() + .expect("output sized to encoded_output_len") + .write(b); }; - let view_len = buffer.len(); - if view_len < 0 { - unsafe { - PyErr_SetString( - PyExc_TypeError, - c"standard_b64encode() argument has negative length".as_ptr(), - ); - } - return Err(()); + for chunk in &mut chunks { + let group = (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8) | u32::from(chunk[2]); + put(ENCODE_TABLE[(group >> 18 & 0x3f) as usize]); + put(ENCODE_TABLE[(group >> 12 & 0x3f) as usize]); + put(ENCODE_TABLE[(group >> 6 & 0x3f) as usize]); + put(ENCODE_TABLE[(group & 0x3f) as usize]); } - let input_len = view_len as usize; - let input = unsafe { slice::from_raw_parts(buffer.as_ptr(), input_len) }; - - let Some(output_len) = encoded_output_len(input_len) else { - unsafe { - PyErr_NoMemory(); + match *chunks.remainder() { + [] => {} + [a] => { + let group = u32::from(a) << 16; + put(ENCODE_TABLE[(group >> 18 & 0x3f) as usize]); + put(ENCODE_TABLE[(group >> 12 & 0x3f) as usize]); + put(PAD_BYTE); + put(PAD_BYTE); } - return Err(()); - }; - - if output_len > isize::MAX as usize { - unsafe { - PyErr_NoMemory(); + [a, b] => { + let group = (u32::from(a) << 16) | (u32::from(b) << 8); + put(ENCODE_TABLE[(group >> 18 & 0x3f) as usize]); + put(ENCODE_TABLE[(group >> 12 & 0x3f) as usize]); + put(ENCODE_TABLE[(group >> 6 & 0x3f) as usize]); + put(PAD_BYTE); } - return Err(()); + _ => unreachable!("chunks_exact(3) remainder is at most 2 bytes"), } - - let result = unsafe { PyBytes_FromStringAndSize(ptr::null(), output_len as Py_ssize_t) }; - if result.is_null() { - return Err(()); - } - - let dest_ptr = unsafe { PyBytes_AsString(result) }; - if dest_ptr.is_null() { - unsafe { - Py_DecRef(result); - } - return Err(()); - } - let dest = unsafe { slice::from_raw_parts_mut(dest_ptr.cast::(), output_len) }; - - let written = encode_into(input, dest); - debug_assert_eq!(written, output_len); - Ok(result) -} - -pub extern "C" fn _base64_clear(_obj: *mut PyObject) -> c_int { - //TODO - 0 -} - -pub extern "C" fn _base64_free(_o: *mut c_void) { - //TODO } -pub struct ModuleDef { - ffi: UnsafeCell, +/// Encode a bytes-like object with the standard Base64 alphabet. +#[pyfunction(signature = (data, /))] +fn standard_b64encode<'py>( + ts: &ThreadState<'py>, + _state: &Base64State, + data: PyBuffer<'py>, +) -> PyResult> { + let input = data.as_bytes(); + let Some(output_len) = encoded_output_len(input.len()) else { + return Err(PyMemoryError::raise(ts, "encoded result too long")); + }; + // Write straight into the bytes object's buffer — no intermediate Vec. + PyBytes::new_with(ts, output_len, |output| { + encode_into(input, output); + Ok(()) + }) } -impl ModuleDef { - fn init_multi_phase(&'static self) -> *mut PyObject { - unsafe { PyModuleDef_Init(self.ffi.get()) } - } +fn base64_exec<'py>(_ts: &ThreadState<'py>, _module: &Bound<'py, PyModule>) -> PyResult<()> { + Ok(()) } -unsafe impl Sync for ModuleDef {} - -pub static _BASE64_MODULE_METHODS: [PyMethodDef; 2] = { - [ - PyMethodDef { - ml_name: c"standard_b64encode".as_ptr() as *mut c_char, - ml_meth: PyMethodDefFuncPointer { - PyCFunctionFast: standard_b64encode, - }, - ml_flags: METH_FASTCALL, - ml_doc: c"Demo for the _base64 module".as_ptr() as *mut c_char, - }, - PyMethodDef::zeroed(), - ] -}; - -pub static _BASE64_MODULE: ModuleDef = { - ModuleDef { - ffi: UnsafeCell::new(PyModuleDef { - m_base: PyModuleDef_HEAD_INIT, - m_name: c"_base64".as_ptr() as *mut _, - m_doc: c"A test Rust module".as_ptr() as *mut _, - m_size: 0, - m_methods: &_BASE64_MODULE_METHODS as *const PyMethodDef as *mut _, - m_slots: std::ptr::null_mut(), - m_traverse: None, - m_clear: Some(_base64_clear), - m_free: Some(_base64_free), - }), - } -}; - -#[unsafe(no_mangle)] -pub extern "C" fn PyInit__base64() -> *mut PyObject { - _BASE64_MODULE.init_multi_phase() +export_module! { + name: _base64, + doc: c"Base64 encoding implemented in Rust", + state: Base64State, + methods: [standard_b64encode], + exec: base64_exec, } diff --git a/Modules/cpython-api-macros/Cargo.toml b/Modules/cpython-api-macros/Cargo.toml new file mode 100644 index 000000000000000..374ded3c806df0d --- /dev/null +++ b/Modules/cpython-api-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cpython-api-macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } diff --git a/Modules/cpython-api-macros/src/lib.rs b/Modules/cpython-api-macros/src/lib.rs new file mode 100644 index 000000000000000..562e0b4e5fa3966 --- /dev/null +++ b/Modules/cpython-api-macros/src/lib.rs @@ -0,0 +1,729 @@ +//! Proc macros for `cpython-api`: `#[pyfunction]` and `#[pymethods]`. +//! +//! These generate the `extern "C"` trampolines and argument-parsing glue — +//! the mechanical, error-prone part of binding Rust functions to Python. +//! Module and class *definitions* stay explicit (`export_module!`, +//! `ClassDef`); see RUST_API.md. +//! +//! Parameter classification is positional, verified by the type system +//! rather than by name matching (a proc macro sees only tokens — it cannot +//! resolve types or trait impls, so anything name-based would be a +//! heuristic): +//! +//! - after an optional `&self`, the **first** parameter is the thread-state +//! token (`ts: &ThreadState<'py>`); +//! - the **second** is the module state (`state: &T` where `T: ModuleState`) +//! — always present, mirroring how C module functions always receive the +//! module object; stateless functions name it `_state`; +//! - the remaining parameters are Python-visible arguments extracted with +//! `FromPyObject`. +//! +//! The macro only checks the *shape* (shared references in the right +//! positions); the generated code pins the real types, so a wrong type shows +//! up as an ordinary compile error at the user signature. +//! +//! ```ignore +//! #[pyfunction(signature = (data, value = 1, /))] +//! fn adler32(ts: &ThreadState<'_>, _state: &ZlibState, +//! data: PyBuffer<'_>, value: u32) -> PyResult { .. } +//! // => mod adler32 { pub const DEF: PyMethodDef; ... } +//! ``` +//! +//! `#[pymethods]` on an inherent impl block processes `#[pyfunction]`, +//! `#[getter]` and `#[new]` items and emits `{TYPE}_METHODS`, +//! `{TYPE}_GETSETS` and (if `#[new]` is present) `{TYPE}_TP_NEW` statics for +//! use with `ClassDef`. Methods are uniformly compiled as `METH_METHOD` +//! (PyCMethod), so the defining class — and through it the module state — is +//! always available. `#[getter]`s stay minimal (`&self, ts`), as does +//! `#[new]` (`ts, args...`). +//! +//! Note the inner attributes never expand on their own inside `#[pymethods]`: +//! attribute macros expand outermost-first, so the impl-block macro consumes +//! the markers before rustc would try to expand them. + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::{ + Error, Expr, FnArg, Ident, ImplItem, ItemFn, ItemImpl, LitByteStr, LitStr, Pat, Result, Token, + Type, +}; + +// --- signature attribute parsing ------------------------------------------- + +enum SigEntry { + Param { name: Ident, default: Option }, + PosOnlyMarker, +} + +impl Parse for SigEntry { + fn parse(input: ParseStream<'_>) -> Result { + if input.peek(Token![/]) { + input.parse::()?; + return Ok(SigEntry::PosOnlyMarker); + } + let name: Ident = input.parse()?; + let default = if input.peek(Token![=]) { + input.parse::()?; + Some(input.parse::()?) + } else { + None + }; + Ok(SigEntry::Param { name, default }) + } +} + +/// The `signature = (...)` attribute payload. +struct PyFunctionAttr { + entries: Option>, +} + +impl Parse for PyFunctionAttr { + fn parse(input: ParseStream<'_>) -> Result { + if input.is_empty() { + return Ok(PyFunctionAttr { entries: None }); + } + let key: Ident = input.parse()?; + if key != "signature" { + return Err(Error::new(key.span(), "expected `signature = (...)`")); + } + input.parse::()?; + let content; + syn::parenthesized!(content in input); + let entries: Punctuated = + content.parse_terminated(SigEntry::parse, Token![,])?; + Ok(PyFunctionAttr { + entries: Some(entries.into_iter().collect()), + }) + } +} + +// --- function model -------------------------------------------------------- + +struct PyParam { + name: Ident, + default: Option, +} + +/// Which special leading parameters the function shape carries. +#[derive(PartialEq, Clone, Copy)] +enum FnShape { + /// `([&self,] ts, state, python-args...)` + Regular, + /// `(&self, ts)` + Getter, + /// `(ts, python-args...)` + New, +} + +struct FnModel { + rust_name: Ident, + has_receiver: bool, + /// The token was declared `&mut ThreadState` (needed for `detach`). + ts_mut: bool, + /// Type behind the state reference (`Some` iff shape is `Regular`). + state_ty: Option, + params: Vec, + pos_only: usize, +} + +impl FnModel { + /// `let [mut] __ts = ...` binding and the `&[mut] __ts` expression passed + /// to the user function. + fn ts_tokens(&self) -> (TokenStream2, TokenStream2) { + if self.ts_mut { + (quote!(mut __ts), quote!(&mut __ts)) + } else { + (quote!(__ts), quote!(&__ts)) + } + } +} + +/// Positional slot for a special leading parameter: require a shared +/// reference and return its target type. The *actual* type is pinned by the +/// generated code, so this only guards the shape for a clear early error. +fn shared_ref_target(ty: &Type, what: &str) -> Result { + let Type::Reference(r) = ty else { + return Err(Error::new( + ty.span(), + format!("this parameter must be {what}"), + )); + }; + if r.mutability.is_some() { + return Err(Error::new( + ty.span(), + format!("this parameter must be a shared reference: {what}"), + )); + } + Ok(r.elem.as_ref().clone()) +} + +/// Analyze a function signature and reconcile it with the `signature = (...)` +/// attribute. +fn analyze_fn(sig: &syn::Signature, attr: &PyFunctionAttr, shape: FnShape) -> Result { + let mut has_receiver = false; + let mut saw_ts = false; + let mut ts_mut = false; + let mut state_ty: Option = None; + let mut py_names: Vec = Vec::new(); + + for input in sig.inputs.iter() { + match input { + FnArg::Receiver(recv) => { + if recv.mutability.is_some() || recv.reference.is_none() { + return Err(Error::new( + recv.span(), + "methods must take `&self`; use interior mutability (e.g. Mutex) for state", + )); + } + has_receiver = true; + } + FnArg::Typed(pat_ty) => { + if !saw_ts { + // `&ThreadState` or `&mut ThreadState` (the latter allows + // `ts.detach(..)` in the body). + let Type::Reference(r) = &*pat_ty.ty else { + return Err(Error::new( + pat_ty.ty.span(), + "the first parameter must be the thread-state token \ + (`ts: &ThreadState` or `ts: &mut ThreadState`)", + )); + }; + ts_mut = r.mutability.is_some(); + saw_ts = true; + continue; + } + if shape == FnShape::Regular && state_ty.is_none() { + state_ty = Some(shared_ref_target( + &pat_ty.ty, + "the module state (`state: &YourModuleState`)", + )?); + continue; + } + let Pat::Ident(pi) = pat_ty.pat.as_ref() else { + return Err(Error::new( + pat_ty.pat.span(), + "parameter patterns are not supported; use a plain name", + )); + }; + py_names.push(pi.ident.clone()); + } + } + } + if !saw_ts { + return Err(Error::new( + sig.span(), + "a `ts: &ThreadState` parameter is required (first after `&self`)", + )); + } + if shape == FnShape::Regular && state_ty.is_none() { + return Err(Error::new( + sig.span(), + "a module-state parameter is required after `ts` (`state: &YourModuleState`; \ + name it `_state` if unused)", + )); + } + if shape == FnShape::Getter && !py_names.is_empty() { + return Err(Error::new( + sig.span(), + "#[getter] methods must be `fn name(&self, ts: &ThreadState) -> T`", + )); + } + + // Reconcile with the signature attribute. + let (params, pos_only) = match &attr.entries { + None => ( + py_names + .into_iter() + .map(|name| PyParam { + name, + default: None, + }) + .collect::>(), + 0, + ), + Some(entries) => { + let mut params = Vec::new(); + let mut pos_only = None; + for entry in entries { + match entry { + SigEntry::PosOnlyMarker => { + if pos_only.is_some() { + return Err(Error::new(sig.span(), "duplicate `/` in signature")); + } + pos_only = Some(params.len()); + } + SigEntry::Param { name, default } => params.push(PyParam { + name: name.clone(), + default: default.clone(), + }), + } + } + let sig_names: Vec = params.iter().map(|p| p.name.to_string()).collect(); + let fn_names: Vec = py_names.iter().map(|i| i.to_string()).collect(); + if sig_names != fn_names { + return Err(Error::new( + sig.span(), + format!( + "signature parameters ({}) do not match function parameters ({})", + sig_names.join(", "), + fn_names.join(", ") + ), + )); + } + (params, pos_only.unwrap_or(0)) + } + }; + + Ok(FnModel { + rust_name: sig.ident.clone(), + has_receiver, + ts_mut, + state_ty, + params, + pos_only, + }) +} + +// --- code generation ------------------------------------------------------- + +fn cstr_lit(s: &str, span: proc_macro2::Span) -> LitByteStr { + LitByteStr::new(format!("{s}\0").as_bytes(), span) +} + +fn gen_spec(model: &FnModel) -> TokenStream2 { + let fn_name = LitStr::new(&model.rust_name.to_string(), model.rust_name.span()); + let pos_only = model.pos_only; + let params = model.params.iter().map(|p| { + let name = cstr_lit(&p.name.to_string(), p.name.span()); + let required = p.default.is_none(); + quote! { + ::cpython_api::args::Param { + name: ::cpython_api::const_cstr(#name), + required: #required, + } + } + }); + quote! { + pub const SPEC: ::cpython_api::args::ParamSpec = ::cpython_api::args::ParamSpec { + fn_name: #fn_name, + params: &[#(#params),*], + pos_only: #pos_only, + }; + } +} + +/// Generate the `let vN = ...` extraction statements (shared by all +/// trampoline kinds). +fn gen_extractions(model: &FnModel) -> (Vec, Vec) { + let mut stmts = Vec::new(); + let mut vars = Vec::new(); + for (i, param) in model.params.iter().enumerate() { + let var = format_ident!("__arg{i}"); + let stmt = match ¶m.default { + Some(default) => quote! { + let #var = match ::cpython_api::args::arg_bound(&__slots, #i) { + Some(obj) => ::cpython_api::FromPyObject::extract(&__ts, obj)?, + None => #default, + }; + }, + None => quote! { + let #var = match ::cpython_api::args::arg_bound(&__slots, #i) { + Some(obj) => ::cpython_api::FromPyObject::extract(&__ts, obj)?, + None => unreachable!("required argument enforced by parse"), + }; + }, + }; + stmts.push(stmt); + vars.push(var); + } + (stmts, vars) +} + +enum TrampolineKind<'a> { + /// Free function; `self` is the module object (state source). + Function, + /// Instance method of `self_ty`; METH_METHOD, state via defining class. + Method { self_ty: &'a Type }, +} + +/// Generate the trampoline plus `DEF` for a function or method. +fn gen_trampoline_and_def(model: &FnModel, kind: TrampolineKind<'_>) -> Result { + let n = model.params.len(); + let (extractions, vars) = gen_extractions(model); + let name = &model.rust_name; + let ml_name = cstr_lit(&model.rust_name.to_string(), model.rust_name.span()); + let state_ty = model + .state_ty + .as_ref() + .expect("Regular shape always has a state type"); + + let (ts_binding, ts_arg) = model.ts_tokens(); + let (state_setup, call) = match &kind { + TrampolineKind::Function => ( + quote! { + let __state: &#state_ty = unsafe { + ::cpython_api::module::state_from_module_ptr::<#state_ty>(&__ts, __slf)? + }; + }, + quote! { super::#name(#ts_arg, __state, #(#vars),*) }, + ), + TrampolineKind::Method { self_ty } => ( + quote! { + let __this: &#self_ty = + unsafe { ::cpython_api::class::payload_ref::<#self_ty>(__slf) }; + let __state: &#state_ty = unsafe { + ::cpython_api::class::state_from_defining_class::<#state_ty>(&__ts, __cls)? + }; + }, + quote! { super::#self_ty::#name(__this, #ts_arg, __state, #(#vars),*) }, + ), + }; + + let body = quote! { + let #ts_binding = unsafe { ::cpython_api::ThreadState::current_unchecked() }; + let __result: ::cpython_api::PyResult<*mut ::cpython_api::ffi::PyObject> = (|| { + #state_setup + let __slots = unsafe { + ::cpython_api::args::parse_fastcall::<#n>( + &__ts, &SPEC, __args as *const _, __nargs, __kwnames, + ) + }?; + #(#extractions)* + let __ret = #call; + ::cpython_api::conversion::IntoPyCallbackOutput::convert(__ret, &__ts) + })(); + match __result { + Ok(ptr) => ptr, + Err(_raised) => ::std::ptr::null_mut(), + } + }; + + let (trampoline, def) = match &kind { + TrampolineKind::Method { .. } => ( + quote! { + pub(crate) unsafe extern "C" fn trampoline( + __slf: *mut ::cpython_api::ffi::PyObject, + __cls: *mut ::cpython_api::ffi::PyTypeObject, + __args: *mut *mut ::cpython_api::ffi::PyObject, + __nargs: ::cpython_api::ffi::Py_ssize_t, + __kwnames: *mut ::cpython_api::ffi::PyObject, + ) -> *mut ::cpython_api::ffi::PyObject { + #body + } + }, + quote! { + pub const DEF: ::cpython_api::ffi::PyMethodDef = ::cpython_api::ffi::PyMethodDef { + ml_name: ::cpython_api::const_cstr(#ml_name).as_ptr().cast_mut(), + ml_meth: ::cpython_api::ffi::PyMethodDefFuncPointer { + PyCMethod: trampoline, + }, + ml_flags: ::cpython_api::ffi::METH_FASTCALL + | ::cpython_api::ffi::METH_KEYWORDS + | ::cpython_api::ffi::METH_METHOD, + ml_doc: ::std::ptr::null_mut(), + }; + }, + ), + TrampolineKind::Function => ( + quote! { + pub(crate) unsafe extern "C" fn trampoline( + __slf: *mut ::cpython_api::ffi::PyObject, + __args: *mut *mut ::cpython_api::ffi::PyObject, + __nargs: ::cpython_api::ffi::Py_ssize_t, + __kwnames: *mut ::cpython_api::ffi::PyObject, + ) -> *mut ::cpython_api::ffi::PyObject { + #body + } + }, + quote! { + pub const DEF: ::cpython_api::ffi::PyMethodDef = ::cpython_api::ffi::PyMethodDef { + ml_name: ::cpython_api::const_cstr(#ml_name).as_ptr().cast_mut(), + ml_meth: ::cpython_api::ffi::PyMethodDefFuncPointer { + PyCFunctionFastWithKeywords: trampoline, + }, + ml_flags: ::cpython_api::ffi::METH_FASTCALL + | ::cpython_api::ffi::METH_KEYWORDS, + ml_doc: ::std::ptr::null_mut(), + }; + }, + ), + }; + + let spec = gen_spec(model); + Ok(quote! { + #spec + #trampoline + #def + }) +} + +// --- #[pyfunction] on free functions --------------------------------------- + +#[proc_macro_attribute] +pub fn pyfunction(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = syn::parse_macro_input!(attr as PyFunctionAttr); + let func = syn::parse_macro_input!(item as ItemFn); + + let model = match analyze_fn(&func.sig, &attr, FnShape::Regular) { + Ok(m) => m, + Err(e) => return e.to_compile_error().into(), + }; + if model.has_receiver { + return Error::new( + func.sig.span(), + "#[pyfunction] on methods must be used inside #[pymethods]", + ) + .to_compile_error() + .into(); + } + + let generated = match gen_trampoline_and_def(&model, TrampolineKind::Function) { + Ok(g) => g, + Err(e) => return e.to_compile_error().into(), + }; + + let vis = &func.vis; + let mod_name = &model.rust_name; + let out = quote! { + #func + + #[doc(hidden)] + #[allow(non_upper_case_globals)] + #vis mod #mod_name { + use super::*; + #generated + } + }; + out.into() +} + +// --- #[pymethods] on impl blocks ------------------------------------------- + +enum MethodKind { + Regular, + Getter, + New, +} + +#[proc_macro_attribute] +pub fn pymethods(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut block = syn::parse_macro_input!(item as ItemImpl); + match expand_pymethods(&mut block) { + Ok(extra) => quote! { #block #extra }.into(), + Err(e) => e.to_compile_error().into(), + } +} + +fn expand_pymethods(block: &mut ItemImpl) -> Result { + if block.trait_.is_some() { + return Err(Error::new( + block.span(), + "#[pymethods] only supports inherent impl blocks", + )); + } + let self_ty = block.self_ty.as_ref().clone(); + let Type::Path(self_path) = &self_ty else { + return Err(Error::new(block.self_ty.span(), "unsupported self type")); + }; + let type_ident = &self_path.path.segments.last().unwrap().ident; + let prefix = type_ident.to_string().to_uppercase(); + + let mut method_defs: Vec = Vec::new(); + let mut getset_entries: Vec = Vec::new(); + let mut generated_mods: Vec = Vec::new(); + let mut tp_new: Option = None; + + for item in block.items.iter_mut() { + let ImplItem::Fn(method) = item else { continue }; + + // Recognize and strip our marker attributes. + let mut kind = MethodKind::Regular; + let mut fn_attr = PyFunctionAttr { entries: None }; + let mut keep = Vec::new(); + let mut is_py = false; + for attr in method.attrs.drain(..) { + if attr.path().is_ident("pyfunction") { + is_py = true; + fn_attr = match &attr.meta { + syn::Meta::Path(_) => PyFunctionAttr { entries: None }, + _ => attr.parse_args::()?, + }; + } else if attr.path().is_ident("getter") { + is_py = true; + kind = MethodKind::Getter; + } else if attr.path().is_ident("new") { + is_py = true; + kind = MethodKind::New; + } else { + keep.push(attr); + } + } + method.attrs = keep; + if !is_py { + continue; + } + + let shape = match kind { + MethodKind::Regular => FnShape::Regular, + MethodKind::Getter => FnShape::Getter, + MethodKind::New => FnShape::New, + }; + let model = analyze_fn(&method.sig, &fn_attr, shape)?; + let rust_name = model.rust_name.clone(); + let mod_name = format_ident!("__cpy_{}_{}", type_ident, rust_name); + + match kind { + MethodKind::Regular => { + if !model.has_receiver { + return Err(Error::new( + method.sig.span(), + "#[pyfunction] methods must take &self (use #[new] for constructors)", + )); + } + let generated = + gen_trampoline_and_def(&model, TrampolineKind::Method { self_ty: &self_ty })?; + generated_mods.push(quote! { + #[doc(hidden)] + #[allow(non_snake_case, non_upper_case_globals)] + mod #mod_name { + use super::*; + #generated + } + }); + method_defs.push(quote!(#mod_name::DEF)); + } + MethodKind::Getter => { + if !model.has_receiver { + return Err(Error::new( + method.sig.span(), + "#[getter] methods must take &self", + )); + } + let name_lit = cstr_lit(&rust_name.to_string(), rust_name.span()); + let (ts_binding, ts_arg) = model.ts_tokens(); + generated_mods.push(quote! { + #[doc(hidden)] + #[allow(non_snake_case)] + mod #mod_name { + use super::*; + pub(crate) unsafe extern "C" fn getter( + __slf: *mut ::cpython_api::ffi::PyObject, + _closure: *mut ::std::os::raw::c_void, + ) -> *mut ::cpython_api::ffi::PyObject { + let #ts_binding = unsafe { ::cpython_api::ThreadState::current_unchecked() }; + let __result: ::cpython_api::PyResult<*mut ::cpython_api::ffi::PyObject> = (|| { + let __this: &#self_ty = + unsafe { ::cpython_api::class::payload_ref::<#self_ty>(__slf) }; + let __ret = super::#self_ty::#rust_name(__this, #ts_arg); + ::cpython_api::conversion::IntoPyCallbackOutput::convert(__ret, &__ts) + })(); + match __result { + Ok(ptr) => ptr, + Err(_raised) => ::std::ptr::null_mut(), + } + } + } + }); + getset_entries.push(quote! { + ::cpython_api::ffi::PyGetSetDef { + name: ::cpython_api::const_cstr(#name_lit).as_ptr(), + get: Some(#mod_name::getter), + set: None, + doc: ::std::ptr::null(), + closure: ::std::ptr::null_mut(), + } + }); + } + MethodKind::New => { + if model.has_receiver { + return Err(Error::new( + method.sig.span(), + "#[new] must not take a receiver; it returns PyResult", + )); + } + if tp_new.is_some() { + return Err(Error::new(method.sig.span(), "duplicate #[new]")); + } + let n = model.params.len(); + let (extractions, vars) = gen_extractions(&model); + let spec = gen_spec(&model); + let (ts_binding, ts_arg) = model.ts_tokens(); + generated_mods.push(quote! { + #[doc(hidden)] + #[allow(non_snake_case)] + mod #mod_name { + use super::*; + #spec + pub(crate) unsafe extern "C" fn tp_new( + __subtype: *mut ::cpython_api::ffi::PyTypeObject, + __args: *mut ::cpython_api::ffi::PyObject, + __kwargs: *mut ::cpython_api::ffi::PyObject, + ) -> *mut ::cpython_api::ffi::PyObject { + let #ts_binding = unsafe { ::cpython_api::ThreadState::current_unchecked() }; + let __result: ::cpython_api::PyResult<*mut ::cpython_api::ffi::PyObject> = (|| { + let __slots = unsafe { + ::cpython_api::args::parse_tuple_dict::<#n>( + &__ts, &SPEC, __args, __kwargs, + ) + }?; + #(#extractions)* + let __payload: #self_ty = + super::#self_ty::#rust_name(#ts_arg, #(#vars),*)?; + unsafe { + ::cpython_api::class::alloc_with::<#self_ty>( + &__ts, __subtype, __payload, + ) + } + })(); + match __result { + Ok(ptr) => ptr, + Err(_raised) => ::std::ptr::null_mut(), + } + } + } + }); + tp_new = Some(quote!(#mod_name::tp_new)); + } + } + } + + let methods_ident = format_ident!("{prefix}_METHODS"); + let getsets_ident = format_ident!("{prefix}_GETSETS"); + let n_methods = method_defs.len() + 1; + let n_getsets = getset_entries.len() + 1; + + let tp_new_item = tp_new.map(|path| { + let ident = format_ident!("{prefix}_TP_NEW"); + quote! { + pub(crate) const #ident: ::cpython_api::class::NewFunc = #path; + } + }); + + Ok(quote! { + #(#generated_mods)* + + pub(crate) static #methods_ident: ::cpython_api::module::MethodDefs<#n_methods> = + ::cpython_api::module::MethodDefs([ + #(#method_defs,)* + ::cpython_api::ffi::PyMethodDef::zeroed(), + ]); + + pub(crate) static #getsets_ident: ::cpython_api::module::GetSetDefs<#n_getsets> = + ::cpython_api::module::GetSetDefs([ + #(#getset_entries,)* + ::cpython_api::ffi::PyGetSetDef { + name: ::std::ptr::null(), + get: None, + set: None, + doc: ::std::ptr::null(), + closure: ::std::ptr::null_mut(), + }, + ]); + + #tp_new_item + }) +} diff --git a/Modules/cpython-api/Cargo.toml b/Modules/cpython-api/Cargo.toml new file mode 100644 index 000000000000000..be541d53b66a9aa --- /dev/null +++ b/Modules/cpython-api/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cpython-api" +version = "0.1.0" +edition = "2024" + +[dependencies] +cpython-sys = { path = "../cpython-sys" } +cpython-api-macros = { path = "../cpython-api-macros" } + +[lib] +name = "cpython_api" diff --git a/Modules/cpython-api/build.rs b/Modules/cpython-api/build.rs new file mode 100644 index 000000000000000..fbc2dc545443def --- /dev/null +++ b/Modules/cpython-api/build.rs @@ -0,0 +1,57 @@ +use std::path::PathBuf; + +fn main() { + // Test binaries reference Python C API symbols through the crate's glue + // (trampolines, statics). Link test executables the way an extension + // module is linked — Python symbols may stay unresolved — so `cargo + // test` still compiles in a tree where libpython has not been built yet + // (as in CI, which runs configure + cargo test without make). When the + // build dir does contain a static libpython, link it so the symbols the + // test binary touches at load time resolve for real and the tests can + // run. + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + if target_os == "windows" { + return; + } + println!("cargo:rustc-link-arg-tests=-Wl,--unresolved-symbols=ignore-all"); + // Rust links with `-z now` by default, which would make the dynamic + // loader abort on any interpreter symbol the link left unresolved even + // though the tests never call one. Lazy binding defers function + // resolution to first call. + println!("cargo:rustc-link-arg-tests=-Wl,-z,lazy"); + + println!("cargo:rerun-if-env-changed=PYTHON_BUILD_DIR"); + let Ok(build_dir) = std::env::var("PYTHON_BUILD_DIR") else { + return; + }; + let build_dir = PathBuf::from(build_dir); + println!( + "cargo:rerun-if-changed={}", + build_dir.join("libpython3.15.a").display() + ); + let mut libs: Vec = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&build_dir) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().into_owned(); + if let Some(stem) = name.strip_prefix("lib").and_then(|n| n.strip_suffix(".a")) + && stem.starts_with("python") + { + libs.push(stem.to_owned()); + } + } + } + // Highest version wins if several are lying around. + libs.sort(); + println!("cargo:rustc-check-cfg=cfg(has_libpython)"); + if let Some(lib) = libs.last() { + println!("cargo:rustc-link-arg-tests=-L{}", build_dir.display()); + println!("cargo:rustc-link-arg-tests=-l{lib}"); + for sys in ["m", "dl", "pthread", "util"] { + println!("cargo:rustc-link-arg-tests=-l{sys}"); + } + // Tests that need real interpreter symbols at load time are gated on + // this cfg, so `cargo test` stays green in a tree where CPython + // itself hasn't been built. + println!("cargo:rustc-cfg=has_libpython"); + } +} diff --git a/Modules/cpython-api/src/args.rs b/Modules/cpython-api/src/args.rs new file mode 100644 index 000000000000000..2118cf55f40a4c0 --- /dev/null +++ b/Modules/cpython-api/src/args.rs @@ -0,0 +1,232 @@ +//! Runtime argument binding for generated trampolines. +//! +//! The `#[pyfunction]` macro emits a static [`ParamSpec`] describing the +//! Python-visible signature and calls [`parse_fastcall`] (vectorcall +//! functions/methods) or [`parse_tuple_dict`] (`tp_new`) to bind incoming +//! arguments to parameter slots. Extraction to Rust types then happens in +//! generated code via `FromPyObject`. + +use std::ffi::CStr; +use std::ptr::NonNull; + +use crate::err::{PyErrRaised, PyResult, PyTypeError}; +use crate::ffi; +use crate::instance::Bound; +use crate::sys_calls; +use crate::threadstate::ThreadState; +use crate::types::PyAny; + +/// One Python-visible parameter. +pub struct Param { + /// Keyword name (also used in error messages). + pub name: &'static CStr, + /// False if the parameter has a default in the generated code. + pub required: bool, +} + +/// The Python-visible signature of a function. +pub struct ParamSpec { + /// Function name for error messages (e.g. `"compress"`). + pub fn_name: &'static str, + pub params: &'static [Param], + /// The first `pos_only` parameters are positional-only (`/` marker). + pub pos_only: usize, +} + +/// Bound argument slots: a borrowed object pointer per parameter, `None` +/// where the default applies. +pub type ArgSlots = [Option>; N]; + +/// View a bound slot as a borrowed `&Bound`. +#[inline] +pub fn arg_bound<'py, const N: usize>( + slots: &ArgSlots, + index: usize, +) -> Option<&Bound<'py, PyAny>> { + slots[index] + .as_ref() + .map(|p| unsafe { Bound::ref_from_ptr(p) }) +} + +fn raise_type_error(ts: &ThreadState<'_>, msg: std::fmt::Arguments<'_>) -> PyErrRaised { + PyTypeError::raise(ts, msg) +} + +/// Bind a vectorcall argument array (`METH_FASTCALL | METH_KEYWORDS`) to +/// parameter slots. +/// +/// # Safety +/// +/// `args`/`nargs`/`kwnames` must be exactly what the interpreter passed to a +/// fastcall entry point; the returned pointers are borrowed from that array +/// and valid for the duration of the call. +pub unsafe fn parse_fastcall<'py, const N: usize>( + ts: &ThreadState<'py>, + spec: &ParamSpec, + args: *const *mut ffi::PyObject, + nargs: ffi::Py_ssize_t, + kwnames: *mut ffi::PyObject, +) -> PyResult> { + debug_assert_eq!(spec.params.len(), N); + let mut slots: ArgSlots = [None; N]; + + let nargs = nargs as usize; + if nargs > N { + return Err(raise_type_error( + ts, + format_args!( + "{}() takes at most {} argument{} ({} given)", + spec.fn_name, + N, + if N == 1 { "" } else { "s" }, + nargs + ), + )); + } + for (i, slot) in slots.iter_mut().enumerate().take(nargs) { + // SAFETY: the interpreter guarantees `nargs` valid entries. + *slot = NonNull::new(unsafe { *args.add(i) }); + debug_assert!(slot.is_some()); + } + + if !kwnames.is_null() { + let nkw = unsafe { sys_calls::tuple_size(ts, kwnames) }; + for k in 0..nkw { + let name = unsafe { sys_calls::tuple_get_item(ts, kwnames, k) }; + let value = NonNull::new(unsafe { *args.add(nargs + k as usize) }); + let mut matched = false; + for (i, param) in spec.params.iter().enumerate().skip(spec.pos_only) { + if unsafe { sys_calls::unicode_eq_ascii(name, param.name.as_ptr()) } != 0 { + if slots[i].is_some() { + return Err(raise_type_error( + ts, + format_args!( + "argument for {}() given by name ('{}') and position ({})", + spec.fn_name, + param.name.to_str().unwrap_or("?"), + i + 1, + ), + )); + } + slots[i] = value; + matched = true; + break; + } + } + if !matched { + return Err(raise_type_error( + ts, + format_args!("{}() got an unexpected keyword argument", spec.fn_name), + )); + } + } + } + + check_required(ts, spec, &slots)?; + Ok(slots) +} + +/// Bind a classic `(args_tuple, kwargs_dict)` pair (`tp_new`) to parameter +/// slots. +/// +/// The returned pointers are borrowed: positional values are borrowed from +/// the tuple (alive for the call), keyword values are borrowed from the +/// call-private kwargs dict. +/// +/// # Safety +/// +/// `args` must be the argument tuple and `kwargs` the (possibly null) keyword +/// dict the interpreter passed to a `tp_new` entry point. +pub unsafe fn parse_tuple_dict<'py, const N: usize>( + ts: &ThreadState<'py>, + spec: &ParamSpec, + args: *mut ffi::PyObject, + kwargs: *mut ffi::PyObject, +) -> PyResult> { + debug_assert_eq!(spec.params.len(), N); + let mut slots: ArgSlots = [None; N]; + + let nargs = unsafe { sys_calls::tuple_size(ts, args) } as usize; + if nargs > N { + return Err(raise_type_error( + ts, + format_args!( + "{}() takes at most {} argument{} ({} given)", + spec.fn_name, + N, + if N == 1 { "" } else { "s" }, + nargs + ), + )); + } + for (i, slot) in slots.iter_mut().enumerate().take(nargs) { + let item = unsafe { sys_calls::tuple_get_item(ts, args, i as ffi::Py_ssize_t) }; + if item.is_null() { + return Err(unsafe { PyErrRaised::assume_set(ts) }); + } + *slot = NonNull::new(item); + } + + if !kwargs.is_null() { + let total = unsafe { sys_calls::dict_size(ts, kwargs) }; + let mut matched: ffi::Py_ssize_t = 0; + for (i, param) in spec.params.iter().enumerate().skip(spec.pos_only) { + let mut value: *mut ffi::PyObject = std::ptr::null_mut(); + let found = unsafe { + sys_calls::dict_get_item_string_ref(ts, kwargs, param.name.as_ptr(), &mut value) + }; + match found { + -1 => return Err(unsafe { PyErrRaised::assume_set(ts) }), + 0 => {} + _ => { + // Downgrade the strong reference to a borrow: the kwargs + // dict is private to this call and keeps the value alive. + unsafe { sys_calls::decref(ts, value) }; + if slots[i].is_some() { + return Err(raise_type_error( + ts, + format_args!( + "argument for {}() given by name ('{}') and position ({})", + spec.fn_name, + param.name.to_str().unwrap_or("?"), + i + 1, + ), + )); + } + slots[i] = NonNull::new(value); + matched += 1; + } + } + } + if matched != total { + return Err(raise_type_error( + ts, + format_args!("{}() got an unexpected keyword argument", spec.fn_name), + )); + } + } + + check_required(ts, spec, &slots)?; + Ok(slots) +} + +fn check_required( + ts: &ThreadState<'_>, + spec: &ParamSpec, + slots: &ArgSlots, +) -> PyResult<()> { + for (i, param) in spec.params.iter().enumerate() { + if param.required && slots[i].is_none() { + return Err(raise_type_error( + ts, + format_args!( + "{}() missing required argument '{}' (pos {})", + spec.fn_name, + param.name.to_str().unwrap_or("?"), + i + 1, + ), + )); + } + } + Ok(()) +} diff --git a/Modules/cpython-api/src/buffer.rs b/Modules/cpython-api/src/buffer.rs new file mode 100644 index 000000000000000..7a5407f2bbc20c2 --- /dev/null +++ b/Modules/cpython-api/src/buffer.rs @@ -0,0 +1,106 @@ +//! Read-only buffer-protocol access (`PyBUF_SIMPLE`). + +use std::marker::PhantomData; +use std::mem::MaybeUninit; +use std::os::raw::c_int; + +use crate::conversion::FromPyObject; +use crate::err::{PyErrRaised, PyResult}; +use crate::ffi; +use crate::instance::Bound; +use crate::sys_calls; +use crate::threadstate::ThreadState; +use crate::types::PyAny; + +/// A read-only, C-contiguous byte view of a buffer-protocol object +/// (`bytes`, `bytearray`, `memoryview`, ...). +/// +/// Requested with `PyBUF_SIMPLE`, which by definition yields a C-contiguous +/// `u8` buffer — no separate contiguity check is needed. The view holds its +/// own reference to the exporting object and releases the buffer on drop. +/// +/// Note (free-threaded builds): a mutable exporter such as `bytearray` can be +/// mutated by other threads while the view is held. Reads may then see +/// inconsistent *data*, but never dangling memory — the same exposure the C +/// implementations accept. +pub struct PyBuffer<'py> { + view: ffi::Py_buffer, + /// Ties the view to the attached scope; `*mut ()` keeps it `!Send+!Sync`. + _marker: PhantomData<(&'py PyAny, *mut ())>, +} + +impl<'py> PyBuffer<'py> { + /// Acquire a simple buffer view of `obj`. + /// + /// Raises `TypeError`/`BufferError` (from `PyObject_GetBuffer`) if `obj` + /// does not support a simple contiguous view. + pub fn get(ts: &ThreadState<'py>, obj: &Bound<'py, PyAny>) -> PyResult> { + let mut view = MaybeUninit::::uninit(); + let ret = unsafe { + sys_calls::get_buffer( + ts, + obj.as_ptr(), + view.as_mut_ptr(), + ffi::PyBUF_SIMPLE as c_int, + ) + }; + if ret != 0 { + return Err(unsafe { PyErrRaised::assume_set(ts) }); + } + Ok(PyBuffer { + view: unsafe { view.assume_init() }, + _marker: PhantomData, + }) + } + + /// The buffer contents. + #[inline] + pub fn as_bytes(&self) -> &[u8] { + if self.view.len == 0 { + // `buf` may be NULL for an empty buffer. + &[] + } else { + // SAFETY: PyBUF_SIMPLE guarantees a C-contiguous byte buffer of + // `len` bytes, alive until PyBuffer_Release. + unsafe { + std::slice::from_raw_parts(self.view.buf as *const u8, self.view.len as usize) + } + } + } + + /// Length of the buffer in bytes. + #[inline] + pub fn len(&self) -> usize { + self.view.len as usize + } + + /// True if the buffer is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.view.len == 0 + } +} + +impl Drop for PyBuffer<'_> { + fn drop(&mut self) { + // Same attachment policy as Bound/Py drops: release when attached, + // leak (never UB) if someone smuggled the view past a detach. + unsafe { + if !ffi::PyThreadState_GetUnchecked().is_null() { + // tls-fallback: Drop has nowhere to store a token borrow. + ffi::PyBuffer_Release(&mut self.view); + } else { + debug_assert!( + false, + "PyBuffer dropped while thread state is detached; leaking" + ); + } + } + } +} + +impl<'py> FromPyObject<'py> for PyBuffer<'py> { + fn extract(ts: &ThreadState<'py>, obj: &Bound<'py, PyAny>) -> PyResult { + PyBuffer::get(ts, obj) + } +} diff --git a/Modules/cpython-api/src/class.rs b/Modules/cpython-api/src/class.rs new file mode 100644 index 000000000000000..8441d9a5d0f7ade --- /dev/null +++ b/Modules/cpython-api/src/class.rs @@ -0,0 +1,295 @@ +//! Class definition via PEP 820 (`PySlot` arrays + `PyType_FromSlots`). +//! +//! A class is a plain Rust payload struct embedded after the `PyObject` +//! header, described by a `static` [`ClassDef`] built with `const fn`s: +//! +//! ```ignore +//! struct Compress { state: Mutex } +//! +//! static COMPRESS_CLASS: ClassDef = ClassDef::new(c"zlib._Compress") +//! .methods(&COMPRESS_METHODS) // emitted by #[pymethods] +//! .getsets(&COMPRESS_GETSETS); // emitted by #[pymethods] +//! ``` +//! +//! The type object itself is created at module-exec time with +//! [`ClassDef::create`], which associates it with the module (making +//! `PyType_GetModuleState(defining_class)` work from methods) and stores the +//! `ClassDef`'s address as the type's `Py_tp_token`. +//! +//! Types are final (`Py_TPFLAGS_IMMUTABLETYPE`, no `BASETYPE`) and, unless a +//! `tp_new` is provided, not instantiable from Python +//! (`Py_TPFLAGS_DISALLOW_INSTANTIATION`) — instances are made from Rust +//! factories via [`new_instance`]. Finality is what keeps the +//! defining-class-based module-state lookup safe. + +use std::ffi::CStr; +use std::marker::PhantomData; +use std::os::raw::c_void; +use std::ptr::NonNull; + +use crate::err::{PyErrRaised, PyResult, PySystemError}; +use crate::ffi; +use crate::instance::Bound; +use crate::module::{ + GetSetDefs, MethodDefs, ModuleState, SLOT_END, SlotArray, slot_data, slot_func, slot_size, + slot_static_data, slot_uint64, +}; +use crate::sys_calls; +use crate::threadstate::ThreadState; +use crate::types::{PyAny, PyModule, PyType}; + +/// `tp_new` slot function type (generated by `#[pymethods]` for `#[new]`). +pub type NewFunc = unsafe extern "C" fn( + subtype: *mut ffi::PyTypeObject, + args: *mut ffi::PyObject, + kwargs: *mut ffi::PyObject, +) -> *mut ffi::PyObject; + +/// Byte offset of the `T` payload behind the `PyObject` header. +pub const fn payload_offset() -> usize { + let header = std::mem::size_of::(); + let align = std::mem::align_of::(); + header.div_ceil(align) * align +} + +/// Raw pointer to the payload of an instance. +/// +/// # Safety +/// +/// `obj` must be an instance of a type defined by a `ClassDef`. +#[inline] +pub unsafe fn payload_ptr(obj: *mut ffi::PyObject) -> *mut T { + unsafe { obj.byte_add(payload_offset::()) as *mut T } +} + +/// Borrow the payload of an instance. +/// +/// # Safety +/// +/// `obj` must be a valid instance of a type defined by a `ClassDef`, alive +/// for `'a`. +#[doc(hidden)] +#[inline] +pub unsafe fn payload_ref<'a, T>(obj: *mut ffi::PyObject) -> &'a T { + unsafe { &*payload_ptr::(obj) } +} + +/// `tp_dealloc` for payload-carrying heap types: drop the payload, free the +/// instance, release the reference the instance holds on its heap type. +/// +/// Deallocation always runs with an attached thread state, so the payload's +/// `Drop` may (transitively) use TLS-checked reference drops. +/// +/// # Safety +/// +/// Interpreter-only entry point, installed by `ClassDef`. +unsafe extern "C" fn dealloc_impl(obj: *mut ffi::PyObject) { + unsafe { + let tp = (*(obj as *mut ffi::_object)).ob_type; + std::ptr::drop_in_place(payload_ptr::(obj)); + let free = (*tp).tp_free.unwrap_or(ffi::PyObject_Free); + free(obj as *mut c_void); + // Heap-type instances own a reference to their type. + ffi::Py_DecRef(tp as *mut ffi::PyObject); + } +} + +const MAX_CLASS_SLOTS: usize = 10; + +// Fixed indices for slots that builder methods rewrite. +const FLAGS_SLOT: usize = 2; + +/// A `const`-built PEP 820 class definition for payload type `T`. +/// +/// Must be stored in a `static` (its address becomes the type's +/// `Py_tp_token`). +pub struct ClassDef { + slots: SlotArray, + len: usize, + _marker: PhantomData T>, +} + +// SAFETY: logically immutable after const construction; every stored pointer +// targets 'static data (name string, method/getset tables, glue fns). +unsafe impl Sync for ClassDef {} + +impl ClassDef { + /// Start a class definition. `name` is the fully qualified name shown to + /// Python (e.g. `c"zlib._Compress"`). + pub const fn new(name: &'static CStr) -> Self { + // PyType_GenericAlloc guarantees at least pymalloc alignment (8); + // payloads needing more must not rely on the header offset. + assert!( + std::mem::align_of::() <= 8, + "class payload alignment above 8 is not supported" + ); + let mut slots = [SLOT_END; MAX_CLASS_SLOTS]; + slots[0] = slot_static_data(ffi::Py_tp_name, name.as_ptr() as *mut c_void); + slots[1] = slot_size( + ffi::Py_tp_basicsize, + (payload_offset::() + std::mem::size_of::()) as ffi::Py_ssize_t, + ); + slots[FLAGS_SLOT] = slot_uint64( + ffi::Py_tp_flags, + (ffi::Py_TPFLAGS_DEFAULT + | ffi::Py_TPFLAGS_IMMUTABLETYPE + | ffi::Py_TPFLAGS_DISALLOW_INSTANTIATION) as u64, + ); + slots[3] = slot_func( + ffi::Py_tp_dealloc, + Some(unsafe { + std::mem::transmute::< + unsafe extern "C" fn(*mut ffi::PyObject), + unsafe extern "C" fn(), + >(dealloc_impl::) + }), + ); + ClassDef { + slots: SlotArray(slots), + len: 4, + _marker: PhantomData, + } + } + + const fn push(mut self, slot: ffi::PySlot) -> Self { + assert!(self.len < MAX_CLASS_SLOTS - 1, "too many class slots"); + self.slots.0[self.len] = slot; + self.len += 1; + self + } + + /// Attach the method table (emitted by `#[pymethods]`). + pub const fn methods(self, defs: &'static MethodDefs) -> Self { + self.push(slot_static_data( + ffi::Py_tp_methods, + defs.0.as_ptr() as *mut c_void, + )) + } + + /// Attach the getter table (emitted by `#[pymethods]`). + pub const fn getsets(self, defs: &'static GetSetDefs) -> Self { + self.push(slot_static_data( + ffi::Py_tp_getset, + defs.0.as_ptr() as *mut c_void, + )) + } + + /// Make the type constructible from Python with the given `tp_new` + /// (emitted by `#[pymethods]` for a `#[new]` method). Clears + /// `Py_TPFLAGS_DISALLOW_INSTANTIATION`. + pub const fn tp_new(mut self, new: NewFunc) -> Self { + // SAFETY: reading back the value this builder wrote in `new()`. + let flags = unsafe { self.slots.0[FLAGS_SLOT].__bindgen_anon_2.sl_uint64 }; + self.slots.0[FLAGS_SLOT] = slot_uint64( + ffi::Py_tp_flags, + flags & !(ffi::Py_TPFLAGS_DISALLOW_INSTANTIATION as u64), + ); + self.push(slot_func( + ffi::Py_tp_new, + Some(unsafe { std::mem::transmute::(new) }), + )) + } +} + +impl ClassDef { + /// The type's token: the address of this definition. + pub fn token(&'static self) -> *mut c_void { + self as *const Self as *mut c_void + } + + /// Create the heap type, associated with `module`. + /// + /// Called from the module's exec function; the returned type object then + /// lives in the module state as a `Py`. The module association is + /// what makes `PyType_GetModuleState(defining_class)` (used by methods + /// with a state parameter) resolve to this module's state. + pub fn create<'py>( + &'static self, + ts: &ThreadState<'py>, + module: &Bound<'py, PyModule>, + ) -> PyResult> { + // The static half of the slots is nested via Py_slot_subslots; the + // runtime-only slots (module pointer, token) are built on the stack. + let mut dynamic = [ + slot_data(ffi::Py_tp_module, module.as_ptr() as *mut c_void), + slot_static_data(ffi::Py_tp_token, self.token()), + slot_static_data(ffi::Py_slot_subslots, self.slots.as_ptr() as *mut c_void), + SLOT_END, + ]; + let ptr = unsafe { sys_calls::type_from_slots(ts, dynamic.as_mut_ptr()) }; + match NonNull::new(ptr) { + Some(p) => { + let any = unsafe { Bound::<'_, PyAny>::from_owned_ptr(ts, p) }; + Ok(unsafe { any.cast_unchecked::() }) + } + None => Err(unsafe { PyErrRaised::assume_set(ts) }), + } + } +} + +/// Allocate an instance of `cls` and move `payload` into it. +/// +/// This is the Rust-side constructor used by factory functions (e.g. +/// `compressobj()` building a `_Compress`), and by generated `tp_new` glue. +/// `cls` must be a type created from a `ClassDef` (debug-checked in +/// generated callers via the token where possible). +pub fn new_instance<'py, T: Send + Sync + 'static>( + ts: &ThreadState<'py>, + cls: &Bound<'py, PyType>, + payload: T, +) -> PyResult> { + let obj = + unsafe { sys_calls::type_generic_alloc(ts, cls.as_ptr() as *mut ffi::PyTypeObject, 0) }; + let Some(ptr) = NonNull::new(obj) else { + return Err(unsafe { PyErrRaised::assume_set(ts) }); + }; + unsafe { payload_ptr::(obj).write(payload) }; + Ok(unsafe { Bound::from_owned_ptr(ts, ptr) }) +} + +/// Raw-pointer variant of [`new_instance`] for generated `tp_new` glue. +/// +/// # Safety +/// +/// `cls` must be a valid type object created from a `ClassDef`. +#[doc(hidden)] +pub unsafe fn alloc_with( + ts: &ThreadState<'_>, + cls: *mut ffi::PyTypeObject, + payload: T, +) -> PyResult<*mut ffi::PyObject> { + let obj = unsafe { sys_calls::type_generic_alloc(ts, cls, 0) }; + if obj.is_null() { + return Err(unsafe { PyErrRaised::assume_set(ts) }); + } + unsafe { payload_ptr::(obj).write(payload) }; + Ok(obj) +} + +/// Borrow the typed module state via a method's defining class +/// (`METH_METHOD` calling convention; generated glue). +/// +/// # Safety +/// +/// `defining_class` must be the defining class the interpreter passed to a +/// `PyCMethod` trampoline, for a type created by [`ClassDef::create`] from a +/// module whose state type is `T`. Finality of our types guarantees the +/// defining class *is* the created type, making `PyType_GetModuleState` +/// safe (cf. the analogous invariant comment in `Modules/zlibmodule.c`). +#[doc(hidden)] +pub unsafe fn state_from_defining_class<'a, T: ModuleState>( + ts: &ThreadState<'_>, + defining_class: *mut ffi::PyTypeObject, +) -> PyResult<&'a T> { + let ptr = unsafe { sys_calls::type_get_module_state(defining_class) }; + if ptr.is_null() { + return Err(PySystemError::raise( + ts, + "defining class has no module state", + )); + } + // The module state block is a StateStorage; go through the module + // helper's layout by reinterpreting identically. + let storage = ptr as *mut crate::module::StateStorage; + unsafe { crate::module::storage_state_ref(ts, storage) } +} diff --git a/Modules/cpython-api/src/conversion.rs b/Modules/cpython-api/src/conversion.rs new file mode 100644 index 000000000000000..964af0e302c65cb --- /dev/null +++ b/Modules/cpython-api/src/conversion.rs @@ -0,0 +1,253 @@ +//! Conversions between Python objects and Rust values. + +use std::os::raw::c_char; +use std::ptr::NonNull; + +use crate::err::{PyErrRaised, PyOverflowError, PyResult}; +use crate::ffi; +use crate::instance::{Bound, Py}; +use crate::sys_calls; +use crate::threadstate::ThreadState; +use crate::types::PyAny; + +/// Extract a Rust value from a Python object (argument conversion). +pub trait FromPyObject<'py>: Sized { + fn extract(ts: &ThreadState<'py>, obj: &Bound<'py, PyAny>) -> PyResult; +} + +/// Convert a Rust value into a Python object (return-value conversion). +pub trait IntoPyObject<'py> { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult>; +} + +#[inline] +fn none_ptr() -> *mut ffi::PyObject { + (&raw const ffi::_Py_NoneStruct).cast_mut() +} + +#[inline] +fn owned_or_raised<'py>( + ts: &ThreadState<'py>, + ptr: *mut ffi::PyObject, +) -> PyResult> { + match NonNull::new(ptr) { + Some(p) => Ok(unsafe { Bound::from_owned_ptr(ts, p) }), + None => Err(unsafe { PyErrRaised::assume_set(ts) }), + } +} + +// --- integers -------------------------------------------------------------- + +fn extract_i64(ts: &ThreadState<'_>, obj: &Bound<'_, PyAny>) -> PyResult { + let v = unsafe { sys_calls::long_as_longlong(ts, obj.as_ptr()) }; + if v == -1 && ts.exception_set() { + return Err(unsafe { PyErrRaised::assume_set(ts) }); + } + Ok(v) +} + +macro_rules! int_via_i64 { + ($($ty:ty => $cname:literal;)*) => { + $( + impl<'py> FromPyObject<'py> for $ty { + fn extract(ts: &ThreadState<'py>, obj: &Bound<'py, PyAny>) -> PyResult { + let v = extract_i64(ts, obj)?; + <$ty>::try_from(v).map_err(|_| { + PyOverflowError::raise( + ts, + concat!("Python int out of range for C ", $cname), + ) + }) + } + } + )* + }; +} + +int_via_i64! { + i32 => "int"; + u32 => "unsigned int"; + i64 => "long long"; +} + +impl<'py> FromPyObject<'py> for u64 { + fn extract(ts: &ThreadState<'py>, obj: &Bound<'py, PyAny>) -> PyResult { + let v = unsafe { sys_calls::long_as_unsigned_longlong(ts, obj.as_ptr()) }; + if v == u64::MAX && ts.exception_set() { + return Err(unsafe { PyErrRaised::assume_set(ts) }); + } + Ok(v) + } +} + +impl<'py> FromPyObject<'py> for isize { + fn extract(ts: &ThreadState<'py>, obj: &Bound<'py, PyAny>) -> PyResult { + let v = unsafe { sys_calls::long_as_ssize_t(ts, obj.as_ptr()) }; + if v == -1 && ts.exception_set() { + return Err(unsafe { PyErrRaised::assume_set(ts) }); + } + Ok(v as isize) + } +} + +impl<'py> FromPyObject<'py> for usize { + fn extract(ts: &ThreadState<'py>, obj: &Bound<'py, PyAny>) -> PyResult { + let v = unsafe { sys_calls::long_as_size_t(ts, obj.as_ptr()) }; + if v == usize::MAX && ts.exception_set() { + return Err(unsafe { PyErrRaised::assume_set(ts) }); + } + Ok(v) + } +} + +impl<'py> FromPyObject<'py> for bool { + fn extract(ts: &ThreadState<'py>, obj: &Bound<'py, PyAny>) -> PyResult { + let v = unsafe { sys_calls::object_is_true(ts, obj.as_ptr()) }; + if v == -1 { + return Err(unsafe { PyErrRaised::assume_set(ts) }); + } + Ok(v != 0) + } +} + +impl<'py, T: FromPyObject<'py>> FromPyObject<'py> for Option { + fn extract(ts: &ThreadState<'py>, obj: &Bound<'py, PyAny>) -> PyResult { + if obj.as_ptr() == none_ptr() { + Ok(None) + } else { + T::extract(ts, obj).map(Some) + } + } +} + +impl<'py> FromPyObject<'py> for Bound<'py, PyAny> { + fn extract(ts: &ThreadState<'py>, obj: &Bound<'py, PyAny>) -> PyResult { + Ok(obj.clone_ref(ts)) + } +} + +// --- IntoPyObject ---------------------------------------------------------- + +macro_rules! int_into_signed { + ($($ty:ty),*) => { + $( + impl<'py> IntoPyObject<'py> for $ty { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult> { + let p = unsafe { sys_calls::long_from_longlong(ts, self as i64) }; + owned_or_raised(ts, p) + } + } + )* + }; +} + +int_into_signed!(i8, i16, i32, i64); + +macro_rules! int_into_unsigned { + ($($ty:ty),*) => { + $( + impl<'py> IntoPyObject<'py> for $ty { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult> { + let p = unsafe { sys_calls::long_from_unsigned_longlong(ts, self as u64) }; + owned_or_raised(ts, p) + } + } + )* + }; +} + +int_into_unsigned!(u8, u16, u32, u64); + +impl<'py> IntoPyObject<'py> for isize { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult> { + let p = unsafe { sys_calls::long_from_ssize_t(ts, self as ffi::Py_ssize_t) }; + owned_or_raised(ts, p) + } +} + +impl<'py> IntoPyObject<'py> for usize { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult> { + let p = unsafe { sys_calls::long_from_unsigned_longlong(ts, self as u64) }; + owned_or_raised(ts, p) + } +} + +impl<'py> IntoPyObject<'py> for bool { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult> { + let p = unsafe { sys_calls::bool_from_long(ts, self as std::os::raw::c_long) }; + owned_or_raised(ts, p) + } +} + +impl<'py> IntoPyObject<'py> for &str { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult> { + let p = unsafe { + sys_calls::unicode_from_string_and_size( + ts, + self.as_ptr() as *const c_char, + self.len() as ffi::Py_ssize_t, + ) + }; + owned_or_raised(ts, p) + } +} + +impl<'py> IntoPyObject<'py> for String { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult> { + self.as_str().into_pyobject(ts) + } +} + +impl<'py> IntoPyObject<'py> for () { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult> { + // None is immortal; the incref is a no-op but keeps the ownership + // model uniform. + let none = none_ptr(); + unsafe { + sys_calls::incref(ts, none); + Ok(Bound::from_owned_ptr(ts, NonNull::new_unchecked(none))) + } + } +} + +impl<'py, T> IntoPyObject<'py> for Bound<'py, T> { + fn into_pyobject(self, _ts: &ThreadState<'py>) -> PyResult> { + Ok(self.into_any()) + } +} + +impl<'py, T> IntoPyObject<'py> for Py { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult> { + Ok(self.into_bound(ts).into_any()) + } +} + +impl<'py, T: IntoPyObject<'py>> IntoPyObject<'py> for Option { + fn into_pyobject(self, ts: &ThreadState<'py>) -> PyResult> { + match self { + Some(v) => v.into_pyobject(ts), + None => ().into_pyobject(ts), + } + } +} + +// --- trampoline return conversion ------------------------------------------ + +/// Return-value conversion for generated trampolines: accepts both plain +/// values and `PyResult`s of them. +#[doc(hidden)] +pub trait IntoPyCallbackOutput<'py> { + fn convert(self, ts: &ThreadState<'py>) -> PyResult<*mut ffi::PyObject>; +} + +impl<'py, T: IntoPyObject<'py>> IntoPyCallbackOutput<'py> for T { + fn convert(self, ts: &ThreadState<'py>) -> PyResult<*mut ffi::PyObject> { + Ok(self.into_pyobject(ts)?.into_ptr()) + } +} + +impl<'py, T: IntoPyObject<'py>> IntoPyCallbackOutput<'py> for PyResult { + fn convert(self, ts: &ThreadState<'py>) -> PyResult<*mut ffi::PyObject> { + Ok(self?.into_pyobject(ts)?.into_ptr()) + } +} diff --git a/Modules/cpython-api/src/err.rs b/Modules/cpython-api/src/err.rs new file mode 100644 index 000000000000000..c0d5f60328a07d3 --- /dev/null +++ b/Modules/cpython-api/src/err.rs @@ -0,0 +1,137 @@ +//! Error handling: the zero-sized "exception is set" marker and raise helpers. +//! +//! The exception itself lives where C puts it — in the thread state. Rust +//! code only carries the [`PyErrRaised`] marker, so `Result` +//! is the same size as `T` and there is no duplicate error state to +//! reconcile. + +use std::ffi::CString; +use std::fmt; +use std::marker::PhantomData; + +use crate::Py; +use crate::ffi; +use crate::instance::Bound; +use crate::sys_calls; +use crate::threadstate::ThreadState; +use crate::types::PyType; + +/// Zero-sized marker: a Python exception has been set on the current thread. +/// +/// Produced by the `raise` methods on exception types; consumed by returning +/// it (usually with `?`) until a trampoline turns it into a `NULL` return. +#[must_use = "a raised exception must be propagated (usually with `?` or `return Err(..)`)"] +pub struct PyErrRaised { + /// `*mut ()` keeps the marker `!Send`: it refers to per-thread state. + _priv: PhantomData<*mut ()>, +} + +/// The result of any fallible operation against the interpreter. +pub type PyResult = Result; + +impl PyErrRaised { + /// Assert that an exception is already set (e.g. after an FFI call + /// returned `NULL`/`-1`). + /// + /// # Safety + /// + /// An exception must actually be set on this thread; the marker's whole + /// meaning depends on it. + #[inline] + pub unsafe fn assume_set(ts: &ThreadState<'_>) -> PyErrRaised { + debug_assert!( + ts.exception_set(), + "PyErrRaised::assume_set called with no exception set" + ); + PyErrRaised { _priv: PhantomData } + } +} + +impl fmt::Debug for PyErrRaised { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("PyErrRaised") + } +} + +/// Set `exc_type` with a formatted message and return the raised marker. +pub(crate) fn raise_with_type( + ts: &ThreadState<'_>, + exc_type: *mut ffi::PyObject, + msg: impl fmt::Display, +) -> PyErrRaised { + let text = msg.to_string(); + // A message containing an interior NUL can't round-trip through the C + // API; truncate at the first NUL rather than losing the exception. + let ctext = CString::new(text.clone()) + .unwrap_or_else(|e| CString::new(&text[..e.nul_position()]).expect("prefix has no NUL")); + unsafe { + sys_calls::err_set_string(ts, exc_type, ctext.as_ptr()); + PyErrRaised::assume_set(ts) + } +} + +/// Convert a C `-1`-on-error return into a `PyResult`. +#[inline] +pub(crate) fn error_on_minus_one(ts: &ThreadState<'_>, ret: std::os::raw::c_int) -> PyResult<()> { + if ret == -1 { + Err(unsafe { PyErrRaised::assume_set(ts) }) + } else { + Ok(()) + } +} + +macro_rules! builtin_exceptions { + ($($(#[$doc:meta])* $name:ident => $ffi_static:ident;)*) => { + $( + $(#[$doc])* + pub struct $name; + + impl $name { + /// Set this exception with the given message and return the + /// raised marker. + pub fn raise(ts: &ThreadState<'_>, msg: impl fmt::Display) -> PyErrRaised { + raise_with_type(ts, unsafe { ffi::$ffi_static }, msg) + } + } + )* + }; +} + +builtin_exceptions! { + /// `Exception` + PyException => PyExc_Exception; + /// `ValueError` + PyValueError => PyExc_ValueError; + /// `TypeError` + PyTypeError => PyExc_TypeError; + /// `OverflowError` + PyOverflowError => PyExc_OverflowError; + /// `BufferError` + PyBufferError => PyExc_BufferError; + /// `EOFError` + PyEOFError => PyExc_EOFError; + /// `NotImplementedError` + PyNotImplementedError => PyExc_NotImplementedError; + /// `MemoryError` + PyMemoryError => PyExc_MemoryError; + /// `RuntimeError` + PyRuntimeError => PyExc_RuntimeError; + /// `SystemError` + PySystemError => PyExc_SystemError; +} + +impl<'py> Bound<'py, PyType> { + /// Raise this exception type (e.g. a module-state-held `zlib.error`) with + /// a formatted message. + pub fn raise(&self, ts: &ThreadState<'py>, msg: impl fmt::Display) -> PyErrRaised { + raise_with_type(ts, self.as_ptr(), msg) + } +} + +impl Py { + /// Raise this exception type (e.g. a module-state-held `zlib.error`) with + /// a formatted message. + pub fn raise(&self, ts: &ThreadState<'_>, msg: impl fmt::Display) -> PyErrRaised { + raise_with_type(ts, self.as_ptr(), msg) + } +} diff --git a/Modules/cpython-api/src/instance.rs b/Modules/cpython-api/src/instance.rs new file mode 100644 index 000000000000000..4ffa6b48e3e987c --- /dev/null +++ b/Modules/cpython-api/src/instance.rs @@ -0,0 +1,238 @@ +//! Owned Python object references: [`Bound`] (attached-scope) and [`Py`] +//! (`'static`, for storage in module state). + +use std::marker::PhantomData; +use std::ptr::NonNull; + +use crate::ffi; +use crate::sys_calls; +use crate::threadstate::ThreadState; +use crate::types::PyAny; + +/// An owned reference to a Python object, valid within the attached scope +/// `'py`. +/// +/// `#[repr(transparent)]` over `NonNull`, so borrowed FFI pointers +/// (e.g. the vectorcall args array) can be lent as `&Bound` without any +/// refcount traffic. +/// +/// `!Send + !Sync`: the reference is tied to this thread's attached scope. In +/// particular it can never enter a [`ThreadState::detach`] closure. +#[repr(transparent)] +pub struct Bound<'py, T> { + ptr: NonNull, + /// `&'py ()` ties the reference to the attached scope; `*mut ()` makes it + /// `!Send + !Sync`; `fn() -> T` carries the type marker without affecting + /// auto traits or drop-check. + #[allow(clippy::type_complexity)] + _marker: PhantomData<(&'py (), *mut (), fn() -> T)>, +} + +/// An owned, `'static` reference to a Python object, for storage in module +/// state (and other places that outlive a single attached scope). +/// +/// `Send + Sync`: the *handle* may move between threads; every operation on +/// it still requires `&ThreadState`. There is deliberately no `Clone` — use +/// [`Py::clone_ref`], which requires the token. +#[repr(transparent)] +pub struct Py { + ptr: NonNull, + _marker: PhantomData T>, +} + +// SAFETY: the handle is just a pointer; all operations (including clone) +// require `&ThreadState`. Drop is attachment-checked (see below). +unsafe impl Send for Py {} +unsafe impl Sync for Py {} + +impl<'py, T> Bound<'py, T> { + /// Take ownership of a strong reference returned by an FFI call. + /// + /// # Safety + /// + /// `ptr` must be a valid strong reference (this `Bound` will decref it on + /// drop) to an object of type `T`. + #[inline] + pub unsafe fn from_owned_ptr(_ts: &ThreadState<'py>, ptr: NonNull) -> Self { + Bound { + ptr, + _marker: PhantomData, + } + } + + /// Lend a *borrowed* FFI pointer as a `&Bound` without touching the + /// refcount. + /// + /// # Safety + /// + /// `ptr` must point to a valid object of type `T` that stays alive (kept + /// by its real owner) for as long as the returned reference is used. + #[inline] + pub unsafe fn ref_from_ptr<'a>(ptr: &'a NonNull) -> &'a Bound<'py, T> { + // SAFETY: Bound is repr(transparent) over NonNull. + unsafe { &*(ptr as *const NonNull).cast::>() } + } + + /// The raw object pointer (borrowed; the `Bound` keeps its reference). + #[inline] + pub fn as_ptr(&self) -> *mut ffi::PyObject { + self.ptr.as_ptr() + } + + /// Create a new strong reference. + #[inline] + pub fn clone_ref(&self, ts: &ThreadState<'py>) -> Bound<'py, T> { + unsafe { + sys_calls::incref(ts, self.ptr.as_ptr()); + Bound::from_owned_ptr(ts, self.ptr) + } + } + + /// Detach from the `'py` scope, producing a `'static` handle. + #[inline] + pub fn unbind(self) -> Py { + let ptr = self.ptr; + std::mem::forget(self); + Py { + ptr, + _marker: PhantomData, + } + } + + /// View as a `Bound`. + #[inline] + pub fn as_any(&self) -> &Bound<'py, PyAny> { + // SAFETY: identical repr(transparent) layout; PyAny is the base view. + unsafe { &*(self as *const Bound<'py, T>).cast::>() } + } + + /// Convert into a `Bound`. + #[inline] + pub fn into_any(self) -> Bound<'py, PyAny> { + let ptr = self.ptr; + std::mem::forget(self); + Bound { + ptr, + _marker: PhantomData, + } + } + + /// Reinterpret as a different concrete type without checking. + /// + /// # Safety + /// + /// The object must actually be an instance of `U` (or a subtype). + #[inline] + pub unsafe fn cast_unchecked(self) -> Bound<'py, U> { + let ptr = self.ptr; + std::mem::forget(self); + Bound { + ptr, + _marker: PhantomData, + } + } + + /// Hand the strong reference to C (e.g. as a trampoline return value). + #[inline] + pub fn into_ptr(self) -> *mut ffi::PyObject { + let ptr = self.ptr; + std::mem::forget(self); + ptr.as_ptr() + } + + /// Explicitly decref with the token, skipping the TLS attachment check + /// that `Drop` performs. + #[inline] + pub fn drop_with(self, ts: &ThreadState<'py>) { + let ptr = self.ptr; + std::mem::forget(self); + unsafe { sys_calls::decref(ts, ptr.as_ptr()) }; + } +} + +impl Py { + /// The raw object pointer (borrowed; the `Py` keeps its reference). + #[inline] + pub fn as_ptr(&self) -> *mut ffi::PyObject { + self.ptr.as_ptr() + } + + /// Borrow as a `&Bound` for the current attached scope, without touching + /// the refcount. + #[inline] + pub fn bind<'a, 'py>(&'a self, _ts: &'a ThreadState<'py>) -> &'a Bound<'py, T> { + // SAFETY: identical repr(transparent) layout; the borrow of self keeps + // the reference alive, and the token borrow caps it to the attached + // scope. + unsafe { &*(self as *const Py).cast::>() } + } + + /// Convert into a `Bound`, re-entering the attached scope. + #[inline] + pub fn into_bound<'py>(self, _ts: &ThreadState<'py>) -> Bound<'py, T> { + let ptr = self.ptr; + std::mem::forget(self); + Bound { + ptr, + _marker: PhantomData, + } + } + + /// Create a new strong reference. + #[inline] + pub fn clone_ref(&self, ts: &ThreadState<'_>) -> Py { + unsafe { sys_calls::incref(ts, self.ptr.as_ptr()) }; + Py { + ptr: self.ptr, + _marker: PhantomData, + } + } + + /// Explicitly decref with the token, skipping the TLS attachment check + /// that `Drop` performs. + #[inline] + pub fn drop_with(self, ts: &ThreadState<'_>) { + let ptr = self.ptr; + std::mem::forget(self); + unsafe { sys_calls::decref(ts, ptr.as_ptr()) }; + } +} + +/// Shared drop policy for `Bound` and `Py`: decref if this thread is +/// attached, otherwise leak (and fail a debug assertion). +/// +/// Leaking is always sound; decref-ing while detached is not. In correct code +/// drops always happen attached — the detached case is only reachable by +/// deliberately smuggling a reference across a detach boundary or dropping a +/// `Py` on a non-Python thread, and turning those into leaks (never UB) is +/// the point of this check. See RUST_API.md §1.2. +#[inline] +fn drop_ref(ptr: NonNull) { + unsafe { + if !ffi::PyThreadState_GetUnchecked().is_null() { + // tls-fallback: Drop has nowhere to store a token borrow; this is + // the one deliberate TLS dependency (see RUST_API.md, explicit- + // tstate policy). Use `drop_with` on hot paths. + ffi::Py_DecRef(ptr.as_ptr()); + } else { + debug_assert!( + false, + "Python object reference dropped while thread state is detached; leaking" + ); + } + } +} + +impl Drop for Bound<'_, T> { + #[inline] + fn drop(&mut self) { + drop_ref(self.ptr); + } +} + +impl Drop for Py { + #[inline] + fn drop(&mut self) { + drop_ref(self.ptr); + } +} diff --git a/Modules/cpython-api/src/lib.rs b/Modules/cpython-api/src/lib.rs new file mode 100644 index 000000000000000..d94ffc2b546ef76 --- /dev/null +++ b/Modules/cpython-api/src/lib.rs @@ -0,0 +1,68 @@ +//! Safe Rust API for CPython. +//! +//! See `RUST_API.md` at the repository root for the design document. The short +//! version: +//! +//! - The thread state is passed explicitly everywhere as [`ThreadState`], +//! which wraps the actual `PyThreadState*` rather than being a zero-sized +//! token. Every operation that touches the interpreter requires +//! `&ThreadState`; [`ThreadState::detach`] takes `&mut self` so nothing can +//! touch the interpreter while detached. +//! - PyO3 vocabulary is used where the semantics match: [`Bound`], [`Py`], +//! [`PyResult`], [`types::PyBytes`], ... +//! - Errors are represented by the zero-sized [`PyErrRaised`] marker: the +//! exception itself lives in the thread state, exactly as in the C API. +//! - Modules are defined with PEP 793 (`PyModExport`) via [`export_module!`]; +//! classes with PEP 820 (`PyType_FromSlots`) via [`class::ClassDef`]. Both +//! are free-threading and subinterpreter compatible by default. + +pub use cpython_sys as ffi; + +pub mod args; +pub mod buffer; +pub mod class; +pub mod conversion; +pub mod err; +pub mod instance; +pub mod module; +pub(crate) mod sys_calls; +pub mod threadstate; +pub mod types; + +pub use buffer::PyBuffer; +pub use conversion::{FromPyObject, IntoPyObject}; +pub use err::{PyErrRaised, PyResult}; +pub use instance::{Bound, Py}; +pub use module::{ModuleState, TraverseResult, TraverseStop, Visit}; +pub use threadstate::ThreadState; +pub use types::{PyAny, PyBytes, PyModule, PyType}; + +pub use cpython_api_macros::{pyfunction, pymethods}; + +/// Commonly used items, for glob import in module implementations. +pub mod prelude { + pub use crate::buffer::PyBuffer; + pub use crate::conversion::{FromPyObject, IntoPyObject}; + pub use crate::err::{ + PyBufferError, PyEOFError, PyException, PyMemoryError, PyNotImplementedError, + PyOverflowError, PyRuntimeError, PySystemError, PyTypeError, PyValueError, + }; + pub use crate::err::{PyErrRaised, PyResult}; + pub use crate::instance::{Bound, Py}; + pub use crate::module::ModuleState; + pub use crate::threadstate::ThreadState; + pub use crate::types::{PyAny, PyBytes, PyModule, PyType}; + pub use crate::{export_module, pyfunction, pymethods}; +} + +/// Build a `&'static CStr` from a nul-terminated byte string at compile time. +/// +/// Used by macro-generated code; panics at compile time if `bytes` is not +/// nul-terminated or contains interior nul bytes. +#[doc(hidden)] +pub const fn const_cstr(bytes: &'static [u8]) -> &'static core::ffi::CStr { + match core::ffi::CStr::from_bytes_with_nul(bytes) { + Ok(c) => c, + Err(_) => panic!("invalid C string literal"), + } +} diff --git a/Modules/cpython-api/src/module.rs b/Modules/cpython-api/src/module.rs new file mode 100644 index 000000000000000..9efdb9a04d8358b --- /dev/null +++ b/Modules/cpython-api/src/module.rs @@ -0,0 +1,503 @@ +//! Module definition via PEP 793 (`PyModExport` + `PySlot` arrays). +//! +//! Modules are defined with the [`export_module!`] macro, which expands to a +//! static slot array and the `PyModExport_` entry point. Free-threading +//! (`Py_mod_gil = Py_MOD_GIL_NOT_USED`) and subinterpreter support +//! (`Py_mod_multiple_interpreters = PER_INTERPRETER_GIL_SUPPORTED`) are +//! emitted unconditionally — they are defaults of this API, not options. +//! +//! Per-module state (the [`ModuleState`] trait) is where exception types and +//! class type objects live, so modules never share objects across +//! interpreters through statics. + +use std::marker::PhantomData; +use std::mem::MaybeUninit; +use std::os::raw::{c_int, c_void}; + +use crate::err::{PyResult, PySystemError}; +use crate::ffi; +use crate::instance::{Bound, Py}; +use crate::sys_calls; +use crate::threadstate::ThreadState; +use crate::types::PyModule; + +// Slot IDs that are function-like compat macros in C (`_Py_SLOT_COMPAT_VALUE`) +// and therefore absent from the generated bindings. Values are the non- +// limited-API (3.15+) IDs from Include/slots_generated.h. +pub const PY_MOD_CREATE: u32 = 84; +pub const PY_MOD_EXEC: u32 = 85; +pub const PY_MOD_MULTIPLE_INTERPRETERS: u32 = 86; +pub const PY_MOD_GIL: u32 = 87; + +// Pointer-valued slot values (pointer-cast macros in C, absent from bindings). +pub const PY_MOD_MULTIPLE_INTERPRETERS_SUPPORTED: *mut c_void = std::ptr::without_provenance_mut(1); +pub const PY_MOD_PER_INTERPRETER_GIL_SUPPORTED: *mut c_void = std::ptr::without_provenance_mut(2); +pub const PY_MOD_GIL_NOT_USED: *mut c_void = std::ptr::without_provenance_mut(1); + +// --- PySlot construction --------------------------------------------------- + +/// `PySlot_END`. +pub const SLOT_END: ffi::PySlot = ffi::PySlot { + sl_id: 0, + sl_flags: 0, + __bindgen_anon_1: ffi::PySlot__bindgen_ty_1 { sl_reserved: 0 }, + __bindgen_anon_2: ffi::PySlot__bindgen_ty_2 { + sl_ptr: std::ptr::null_mut(), + }, +}; + +/// `PySlot_DATA`: a data-pointer slot. +pub const fn slot_data(id: u32, ptr: *mut c_void) -> ffi::PySlot { + ffi::PySlot { + sl_id: id as u16, + sl_flags: ffi::PySlot_INTPTR as u16, + __bindgen_anon_1: ffi::PySlot__bindgen_ty_1 { sl_reserved: 0 }, + __bindgen_anon_2: ffi::PySlot__bindgen_ty_2 { sl_ptr: ptr }, + } +} + +/// `PySlot_STATIC_DATA`: a data-pointer slot whose target is static. +pub const fn slot_static_data(id: u32, ptr: *mut c_void) -> ffi::PySlot { + ffi::PySlot { + sl_id: id as u16, + sl_flags: ffi::PySlot_STATIC as u16, + __bindgen_anon_1: ffi::PySlot__bindgen_ty_1 { sl_reserved: 0 }, + __bindgen_anon_2: ffi::PySlot__bindgen_ty_2 { sl_ptr: ptr }, + } +} + +/// `PySlot_FUNC`: a function-pointer slot. +pub const fn slot_func(id: u32, func: ffi::_Py_funcptr_t) -> ffi::PySlot { + ffi::PySlot { + sl_id: id as u16, + sl_flags: 0, + __bindgen_anon_1: ffi::PySlot__bindgen_ty_1 { sl_reserved: 0 }, + __bindgen_anon_2: ffi::PySlot__bindgen_ty_2 { sl_func: func }, + } +} + +/// `PySlot_SIZE`: a `Py_ssize_t` slot. +pub const fn slot_size(id: u32, size: ffi::Py_ssize_t) -> ffi::PySlot { + ffi::PySlot { + sl_id: id as u16, + sl_flags: 0, + __bindgen_anon_1: ffi::PySlot__bindgen_ty_1 { sl_reserved: 0 }, + __bindgen_anon_2: ffi::PySlot__bindgen_ty_2 { sl_size: size }, + } +} + +/// `PySlot_UINT64`: a `uint64_t` slot. +pub const fn slot_uint64(id: u32, value: u64) -> ffi::PySlot { + ffi::PySlot { + sl_id: id as u16, + sl_flags: 0, + __bindgen_anon_1: ffi::PySlot__bindgen_ty_1 { sl_reserved: 0 }, + __bindgen_anon_2: ffi::PySlot__bindgen_ty_2 { sl_uint64: value }, + } +} + +/// A static `PySlot` array. +/// +/// SAFETY of `Sync`: the array is logically immutable after const +/// construction, and every pointer stored in it targets `'static` data +/// (strings, method tables, glue functions). The interpreter only reads it. +#[repr(transparent)] +pub struct SlotArray(pub [ffi::PySlot; N]); + +unsafe impl Sync for SlotArray {} + +impl SlotArray { + /// The pointer handed to `PyModExport`/`PyType_FromSlots`. + /// + /// The cast to `*mut` matches the C signatures; the interpreter treats + /// the array as read-only. + pub fn as_ptr(&'static self) -> *mut ffi::PySlot { + self.0.as_ptr().cast_mut() + } +} + +/// A static, null-terminated `PyGetSetDef` array (emitted by `#[pymethods]`). +/// +/// SAFETY of `Sync`: logically immutable, pointers target `'static` data. +#[repr(transparent)] +pub struct GetSetDefs(pub [ffi::PyGetSetDef; N]); + +unsafe impl Sync for GetSetDefs {} + +/// A static, null-terminated `PyMethodDef` array (emitted by `#[pymethods]`; +/// module-level arrays are built by `export_module!`). +#[repr(transparent)] +pub struct MethodDefs(pub [ffi::PyMethodDef; N]); + +// PyMethodDef is Send + Sync in cpython-sys, so MethodDefs is automatically. + +/// `PY_VERSION_HEX`, reassembled from its parts (the C macro is a +/// function-like macro invocation, which bindgen cannot evaluate). +pub const PY_VERSION_HEX: u32 = (ffi::PY_MAJOR_VERSION << 24) + | (ffi::PY_MINOR_VERSION << 16) + | (ffi::PY_MICRO_VERSION << 8) + | (ffi::PY_RELEASE_LEVEL << 4) + | ffi::PY_RELEASE_SERIAL; + +/// The ABI declaration for a slots-only module built by this crate: +/// internal ABI (we link against `Py_BUILD_CORE` bindings), exact-version, +/// free-threaded or GIL to match the interpreter build. +pub const fn abi_info() -> ffi::PyABIInfo { + let ft_flag = if ffi::GIL_DISABLED { + ffi::PyABIInfo_FREETHREADED + } else { + ffi::PyABIInfo_GIL + }; + ffi::PyABIInfo { + abiinfo_major_version: 1, + abiinfo_minor_version: 0, + flags: (ffi::PyABIInfo_INTERNAL | ft_flag) as u16, + build_version: PY_VERSION_HEX, + abi_version: PY_VERSION_HEX, + } +} + +// --- typed per-module state ------------------------------------------------ + +/// GC visitor passed to [`ModuleState::traverse`]. +pub struct Visit<'a> { + visit: ffi::visitproc, + arg: *mut c_void, + _marker: PhantomData<&'a ()>, +} + +/// Nonzero return from a `visitproc`, propagated out of `traverse`. +pub struct TraverseStop(pub c_int); + +pub type TraverseResult = Result<(), TraverseStop>; + +impl Visit<'_> { + /// Report an owned object reference to the GC. + pub fn call(&self, obj: &Py) -> TraverseResult { + let Some(visit) = self.visit else { + return Ok(()); + }; + let ret = unsafe { visit(obj.as_ptr(), self.arg) }; + if ret == 0 { + Ok(()) + } else { + Err(TraverseStop(ret)) + } + } +} + +/// Typed per-module state. +/// +/// This is where a module keeps its exception types and class type objects +/// (as [`Py`] handles), mirroring the C convention (e.g. `zlibstate`). +/// Implementations holding `Py` references must implement `traverse` (and +/// usually `clear`, which requires those fields to be `Option`s so the +/// references can be dropped). +pub trait ModuleState: Sized + Send + Sync + 'static { + /// Build the state. Runs from the `Py_mod_exec` glue, before the user + /// exec function. + /// + /// Note the shared `'py`: `ThreadState` is invariant in its lifetime, so + /// the token and the module reference must name the same attached scope. + fn new<'py>(ts: &ThreadState<'py>, module: &Bound<'py, PyModule>) -> PyResult; + + /// Report owned object references to the GC. + fn traverse(&self, _visit: &Visit<'_>) -> TraverseResult { + Ok(()) + } + + /// Drop owned object references (GC cycle breaking). + fn clear(&mut self) {} +} + +/// The in-memory layout of the module state block. +/// +/// CPython zero-fills the state allocation, so `initialized == false` until +/// the exec glue writes the value; the traverse/clear/free glue must not +/// touch `value` before then (exec can fail, and `m_free` runs regardless). +#[repr(C)] +pub struct StateStorage { + initialized: bool, + value: MaybeUninit, +} + +/// `Py_mod_state_size` value for a state type. +pub const fn state_size() -> ffi::Py_ssize_t { + std::mem::size_of::>() as ffi::Py_ssize_t +} + +/// Borrow the initialized state out of a raw storage block (shared by the +/// module-pointer and defining-class access paths). +/// +/// # Safety +/// +/// `storage` must point to a live `StateStorage` block. +#[doc(hidden)] +pub unsafe fn storage_state_ref<'a, T: ModuleState>( + ts: &ThreadState<'_>, + storage: *mut StateStorage, +) -> PyResult<&'a T> { + match unsafe { storage.as_ref() } { + Some(storage) if storage.initialized => Ok(unsafe { storage.value.assume_init_ref() }), + _ => Err(PySystemError::raise(ts, "module state not initialized")), + } +} + +unsafe fn storage_of<'a, T: ModuleState>( + module: *mut ffi::PyObject, +) -> Option<&'a mut StateStorage> { + let ptr = unsafe { sys_calls::module_get_state(module) } as *mut StateStorage; + unsafe { ptr.as_mut() } +} + +/// Borrow the typed state of `module`. +/// +/// Raises `SystemError` if the module has no (initialized) state — which for +/// a module defined with [`export_module!`] means `T` doesn't match the +/// module's declared state type. +pub fn module_state<'a, T: ModuleState>( + ts: &ThreadState<'_>, + module: &'a Bound<'_, PyModule>, +) -> PyResult<&'a T> { + unsafe { state_from_module_ptr(ts, module.as_ptr()) } +} + +/// Borrow the typed state from a raw module pointer (trampoline glue). +/// +/// # Safety +/// +/// `module` must be a valid module object pointer that stays alive for `'a`, +/// and its state block must be a `StateStorage` (guaranteed when the +/// module was defined by `export_module!` with state type `T`). +#[doc(hidden)] +pub unsafe fn state_from_module_ptr<'a, T: ModuleState>( + ts: &ThreadState<'_>, + module: *mut ffi::PyObject, +) -> PyResult<&'a T> { + match unsafe { storage_of::(module) } { + Some(storage) if storage.initialized => Ok(unsafe { storage.value.assume_init_ref() }), + _ => Err(PySystemError::raise(ts, "module state not initialized")), + } +} + +/// The user exec function type: shared `'py` between token and module. +pub type ModuleExecFn = for<'py> fn(&ThreadState<'py>, &Bound<'py, PyModule>) -> PyResult<()>; + +/// `Py_mod_exec` glue: initialize the typed state, then run the user exec. +/// +/// # Safety +/// +/// Must only be called by the interpreter as the module's exec slot, with the +/// module created from an `export_module!` slot array declaring state type +/// `T`. +#[doc(hidden)] +pub unsafe fn exec_impl( + module: *mut ffi::PyObject, + user_exec: ModuleExecFn, +) -> c_int { + let ts = unsafe { ThreadState::current_unchecked() }; + let Some(ptr) = std::ptr::NonNull::new(module) else { + let _ = PySystemError::raise(&ts, "module exec called with NULL module"); + return -1; + }; + let bound: &Bound<'_, PyModule> = unsafe { Bound::ref_from_ptr(&ptr) }; + + let Some(storage) = (unsafe { storage_of::(module) }) else { + let _ = PySystemError::raise(&ts, "module has no state block"); + return -1; + }; + match T::new(&ts, bound) { + Ok(state) => { + storage.value.write(state); + storage.initialized = true; + } + Err(_raised) => return -1, + } + + match user_exec(&ts, bound) { + Ok(()) => 0, + Err(_raised) => -1, + } +} + +/// `Py_mod_state_traverse` glue. +/// +/// # Safety +/// +/// Interpreter-only entry point; see `exec_impl`. +#[doc(hidden)] +pub unsafe extern "C" fn traverse_impl( + module: *mut ffi::PyObject, + visit: ffi::visitproc, + arg: *mut c_void, +) -> c_int { + match unsafe { storage_of::(module) } { + Some(storage) if storage.initialized => { + let v = Visit { + visit, + arg, + _marker: PhantomData, + }; + match unsafe { storage.value.assume_init_ref() }.traverse(&v) { + Ok(()) => 0, + Err(TraverseStop(code)) => code, + } + } + _ => 0, + } +} + +/// `Py_mod_state_clear` glue. +/// +/// # Safety +/// +/// Interpreter-only entry point; see `exec_impl`. +#[doc(hidden)] +pub unsafe extern "C" fn clear_impl(module: *mut ffi::PyObject) -> c_int { + if let Some(storage) = unsafe { storage_of::(module) } + && storage.initialized + { + unsafe { storage.value.assume_init_mut() }.clear(); + } + 0 +} + +/// `Py_mod_state_free` glue: drop the state in place. +/// +/// Runs with an attached thread state (module deallocation), so `Py` +/// fields decref normally. +/// +/// # Safety +/// +/// Interpreter-only entry point; see `exec_impl`. +#[doc(hidden)] +pub unsafe extern "C" fn free_impl(module: *mut c_void) { + if let Some(storage) = unsafe { storage_of::(module as *mut ffi::PyObject) } + && storage.initialized + { + storage.initialized = false; + unsafe { storage.value.assume_init_drop() }; + } +} + +// --- the export macro ------------------------------------------------------ + +/// Define a PEP 793 module: static slot array + `PyModExport_` symbol. +/// +/// ```ignore +/// export_module! { +/// name: zlib, +/// doc: c"zlib compression / decompression module", +/// state: ZlibState, +/// methods: [adler32, crc32, compress, decompress], +/// exec: zlib_exec, +/// } +/// ``` +/// +/// - `name` sets both the module name and the export symbol +/// (`PyModExport_zlib`). A Rust item `PyModExport` is also emitted at the +/// call site so static builds can re-export it +/// (`pub use my_module::PyModExport;`). +/// - `state` is a type implementing [`ModuleState`]; its `new` runs before +/// `exec`. +/// - `methods` lists `#[pyfunction]` names. +/// - `exec` is `fn(&ThreadState<'_>, &Bound<'_, PyModule>) -> PyResult<()>`. +/// +/// The expansion always declares `Py_mod_gil = Py_MOD_GIL_NOT_USED` and +/// `Py_mod_multiple_interpreters = PER_INTERPRETER_GIL_SUPPORTED`: modules +/// built with this API must keep their state per-module and thread-safe. +#[macro_export] +macro_rules! export_module { + ( + name: $name:ident, + doc: $doc:literal, + state: $state:ty, + methods: [$($method:ident),* $(,)?], + exec: $exec:path $(,)? + ) => { + static __PY_MOD_METHODS: &[$crate::ffi::PyMethodDef] = &[ + $($method::DEF,)* + $crate::ffi::PyMethodDef::zeroed(), + ]; + + static __PY_MOD_ABI: $crate::ffi::PyABIInfo = $crate::module::abi_info(); + + static __PY_MOD_SLOTS: $crate::module::SlotArray<12> = $crate::module::SlotArray([ + $crate::module::slot_static_data( + $crate::ffi::Py_mod_name, + concat!(stringify!($name), "\0").as_ptr() as *mut ::std::os::raw::c_void, + ), + $crate::module::slot_static_data( + $crate::ffi::Py_mod_doc, + $doc.as_ptr() as *mut ::std::os::raw::c_void, + ), + $crate::module::slot_size( + $crate::ffi::Py_mod_state_size, + $crate::module::state_size::<$state>(), + ), + $crate::module::slot_static_data( + $crate::ffi::Py_mod_methods, + __PY_MOD_METHODS.as_ptr() as *mut ::std::os::raw::c_void, + ), + $crate::module::slot_func($crate::module::PY_MOD_EXEC, { + unsafe extern "C" fn __exec(m: *mut $crate::ffi::PyObject) -> ::std::os::raw::c_int { + unsafe { $crate::module::exec_impl::<$state>(m, $exec) } + } + // SAFETY: stored as the generic C slot function pointer type; + // the interpreter calls it with the correct signature for + // Py_mod_exec. + Some(unsafe { + ::std::mem::transmute::< + unsafe extern "C" fn(*mut $crate::ffi::PyObject) -> ::std::os::raw::c_int, + unsafe extern "C" fn(), + >(__exec) + }) + }), + $crate::module::slot_func($crate::ffi::Py_mod_state_traverse, Some(unsafe { + ::std::mem::transmute::< + unsafe extern "C" fn( + *mut $crate::ffi::PyObject, + $crate::ffi::visitproc, + *mut ::std::os::raw::c_void, + ) -> ::std::os::raw::c_int, + unsafe extern "C" fn(), + >($crate::module::traverse_impl::<$state>) + })), + $crate::module::slot_func($crate::ffi::Py_mod_state_clear, Some(unsafe { + ::std::mem::transmute::< + unsafe extern "C" fn(*mut $crate::ffi::PyObject) -> ::std::os::raw::c_int, + unsafe extern "C" fn(), + >($crate::module::clear_impl::<$state>) + })), + $crate::module::slot_func($crate::ffi::Py_mod_state_free, Some(unsafe { + ::std::mem::transmute::< + unsafe extern "C" fn(*mut ::std::os::raw::c_void), + unsafe extern "C" fn(), + >($crate::module::free_impl::<$state>) + })), + $crate::module::slot_data( + $crate::module::PY_MOD_MULTIPLE_INTERPRETERS, + $crate::module::PY_MOD_PER_INTERPRETER_GIL_SUPPORTED, + ), + $crate::module::slot_data( + $crate::module::PY_MOD_GIL, + $crate::module::PY_MOD_GIL_NOT_USED, + ), + $crate::module::slot_static_data( + $crate::ffi::Py_mod_abi, + &raw const __PY_MOD_ABI as *mut ::std::os::raw::c_void, + ), + $crate::module::SLOT_END, + ]); + + /// PEP 793 module export hook (exported as `PyModExport_`). + /// + /// # Safety + /// + /// Called by the import machinery only. + #[unsafe(export_name = concat!("PyModExport_", stringify!($name)))] + pub unsafe extern "C" fn PyModExport() -> *mut $crate::ffi::PySlot { + __PY_MOD_SLOTS.as_ptr() + } + }; +} diff --git a/Modules/cpython-api/src/sys_calls.rs b/Modules/cpython-api/src/sys_calls.rs new file mode 100644 index 000000000000000..a5f21d4195598c3 --- /dev/null +++ b/Modules/cpython-api/src/sys_calls.rs @@ -0,0 +1,273 @@ +//! The single raw-call layer. +//! +//! Every C API call made by this crate goes through one of these wrappers so +//! the explicit-tstate migration is mechanical and auditable. Each wrapper is +//! annotated: +//! +//! - `explicit-tstate`: the underlying C API takes the thread state +//! explicitly (or we read a `PyThreadState` field directly). +//! - `tls-fallback`: the underlying C API fetches the thread state from TLS. +//! These are temporary: per the design (RUST_API.md, "Explicit-tstate C API +//! policy"), each one is a candidate for a thin `_Py*` internal wrapper in +//! CPython that takes `tstate` explicitly. Holding `&ThreadState` while +//! calling them keeps them sound in the meantime (the token proves the TLS +//! state is ours). +//! +//! All functions here are `unsafe`: they trust raw pointers from the caller. +//! The `ts` parameter both proves attachment and (where possible) supplies +//! the tstate pointer. + +use std::os::raw::{c_char, c_int, c_void}; + +use crate::ffi; +use crate::threadstate::ThreadState; + +// --- refcounting ----------------------------------------------------------- + +/// explicit-attachment (tls-fallback internally on free-threaded builds, +/// where biased refcounting consults the thread id). +#[inline] +pub(crate) unsafe fn incref(_ts: &ThreadState<'_>, obj: *mut ffi::PyObject) { + unsafe { ffi::Py_IncRef(obj) } +} + +/// explicit-attachment (see `incref`). +#[inline] +pub(crate) unsafe fn decref(_ts: &ThreadState<'_>, obj: *mut ffi::PyObject) { + unsafe { ffi::Py_DecRef(obj) } +} + +// --- exceptions ------------------------------------------------------------ + +/// explicit-tstate: `_PyErr_SetString(tstate, ...)`. +#[inline] +pub(crate) unsafe fn err_set_string( + ts: &ThreadState<'_>, + exc_type: *mut ffi::PyObject, + msg: *const c_char, +) { + unsafe { ffi::_PyErr_SetString(ts.as_ptr(), exc_type, msg) } +} + +/// tls-fallback: `PyErr_NewException`. +#[inline] +pub(crate) unsafe fn err_new_exception( + _ts: &ThreadState<'_>, + qualname: *const c_char, + base: *mut ffi::PyObject, +) -> *mut ffi::PyObject { + unsafe { ffi::PyErr_NewException(qualname, base, std::ptr::null_mut()) } +} + +// --- bytes ----------------------------------------------------------------- + +/// tls-fallback: `PyBytes_FromStringAndSize` (candidate for an explicit- +/// tstate `_Py*` wrapper; allocation paths consult TLS). +#[inline] +pub(crate) unsafe fn bytes_from_string_and_size( + _ts: &ThreadState<'_>, + data: *const c_char, + len: ffi::Py_ssize_t, +) -> *mut ffi::PyObject { + unsafe { ffi::PyBytes_FromStringAndSize(data, len) } +} + +/// tls-fallback: `PyBytes_AsString`. +#[inline] +pub(crate) unsafe fn bytes_as_string( + _ts: &ThreadState<'_>, + obj: *mut ffi::PyObject, +) -> *mut c_char { + unsafe { ffi::PyBytes_AsString(obj) } +} + +// --- buffers --------------------------------------------------------------- + +/// tls-fallback: `PyObject_GetBuffer`. +#[inline] +pub(crate) unsafe fn get_buffer( + _ts: &ThreadState<'_>, + obj: *mut ffi::PyObject, + view: *mut ffi::Py_buffer, + flags: c_int, +) -> c_int { + unsafe { ffi::PyObject_GetBuffer(obj, view, flags) } +} + +// --- int / bool / str conversions ----------------------------------------- + +/// tls-fallback: `PyLong_AsLongLong`. +#[inline] +pub(crate) unsafe fn long_as_longlong(_ts: &ThreadState<'_>, obj: *mut ffi::PyObject) -> i64 { + unsafe { ffi::PyLong_AsLongLong(obj) } +} + +/// tls-fallback: `PyLong_AsUnsignedLongLong`. +#[inline] +pub(crate) unsafe fn long_as_unsigned_longlong( + _ts: &ThreadState<'_>, + obj: *mut ffi::PyObject, +) -> u64 { + unsafe { ffi::PyLong_AsUnsignedLongLong(obj) } +} + +/// tls-fallback: `PyLong_AsSsize_t`. +#[inline] +pub(crate) unsafe fn long_as_ssize_t( + _ts: &ThreadState<'_>, + obj: *mut ffi::PyObject, +) -> ffi::Py_ssize_t { + unsafe { ffi::PyLong_AsSsize_t(obj) } +} + +/// tls-fallback: `PyLong_AsSize_t`. +#[inline] +pub(crate) unsafe fn long_as_size_t(_ts: &ThreadState<'_>, obj: *mut ffi::PyObject) -> usize { + unsafe { ffi::PyLong_AsSize_t(obj) } +} + +/// tls-fallback: `PyLong_FromLongLong`. +#[inline] +pub(crate) unsafe fn long_from_longlong(_ts: &ThreadState<'_>, v: i64) -> *mut ffi::PyObject { + unsafe { ffi::PyLong_FromLongLong(v) } +} + +/// tls-fallback: `PyLong_FromUnsignedLongLong`. +#[inline] +pub(crate) unsafe fn long_from_unsigned_longlong( + _ts: &ThreadState<'_>, + v: u64, +) -> *mut ffi::PyObject { + unsafe { ffi::PyLong_FromUnsignedLongLong(v) } +} + +/// tls-fallback: `PyLong_FromSsize_t`. +#[inline] +pub(crate) unsafe fn long_from_ssize_t( + _ts: &ThreadState<'_>, + v: ffi::Py_ssize_t, +) -> *mut ffi::PyObject { + unsafe { ffi::PyLong_FromSsize_t(v) } +} + +/// tls-fallback: `PyObject_IsTrue`. +#[inline] +pub(crate) unsafe fn object_is_true(_ts: &ThreadState<'_>, obj: *mut ffi::PyObject) -> c_int { + unsafe { ffi::PyObject_IsTrue(obj) } +} + +/// tls-fallback: `PyBool_FromLong`. +#[inline] +pub(crate) unsafe fn bool_from_long( + _ts: &ThreadState<'_>, + v: std::os::raw::c_long, +) -> *mut ffi::PyObject { + unsafe { ffi::PyBool_FromLong(v) } +} + +/// tls-fallback: `PyUnicode_FromStringAndSize`. +#[inline] +pub(crate) unsafe fn unicode_from_string_and_size( + _ts: &ThreadState<'_>, + data: *const c_char, + len: ffi::Py_ssize_t, +) -> *mut ffi::PyObject { + unsafe { ffi::PyUnicode_FromStringAndSize(data, len) } +} + +/// explicit-tstate-free: pure comparison, no tstate involved. +#[inline] +pub(crate) unsafe fn unicode_eq_ascii(obj: *mut ffi::PyObject, s: *const c_char) -> c_int { + unsafe { ffi::_PyUnicode_EqualToASCIIString(obj, s) } +} + +// --- tuples / dicts (argument parsing) ------------------------------------- + +/// tls-fallback: `PyTuple_Size`. +#[inline] +pub(crate) unsafe fn tuple_size(_ts: &ThreadState<'_>, t: *mut ffi::PyObject) -> ffi::Py_ssize_t { + unsafe { ffi::PyTuple_Size(t) } +} + +/// tls-fallback: `PyTuple_GetItem` (borrowed reference). +#[inline] +pub(crate) unsafe fn tuple_get_item( + _ts: &ThreadState<'_>, + t: *mut ffi::PyObject, + i: ffi::Py_ssize_t, +) -> *mut ffi::PyObject { + unsafe { ffi::PyTuple_GetItem(t, i) } +} + +/// tls-fallback: `PyDict_Size`. +#[inline] +pub(crate) unsafe fn dict_size(_ts: &ThreadState<'_>, d: *mut ffi::PyObject) -> ffi::Py_ssize_t { + unsafe { ffi::PyDict_Size(d) } +} + +/// tls-fallback: `PyDict_GetItemStringRef` (strong reference out-param). +#[inline] +pub(crate) unsafe fn dict_get_item_string_ref( + _ts: &ThreadState<'_>, + d: *mut ffi::PyObject, + key: *const c_char, + out: *mut *mut ffi::PyObject, +) -> c_int { + unsafe { ffi::PyDict_GetItemStringRef(d, key, out) } +} + +// --- modules --------------------------------------------------------------- + +/// tls-fallback: `PyModule_AddObjectRef`. +#[inline] +pub(crate) unsafe fn module_add_object_ref( + _ts: &ThreadState<'_>, + m: *mut ffi::PyObject, + name: *const c_char, + value: *mut ffi::PyObject, +) -> c_int { + unsafe { ffi::PyModule_AddObjectRef(m, name, value) } +} + +/// tls-fallback: `PyModule_AddType`. +#[inline] +pub(crate) unsafe fn module_add_type( + _ts: &ThreadState<'_>, + m: *mut ffi::PyObject, + ty: *mut ffi::PyTypeObject, +) -> c_int { + unsafe { ffi::PyModule_AddType(m, ty) } +} + +/// tstate-free: reads the module object. +#[inline] +pub(crate) unsafe fn module_get_state(m: *mut ffi::PyObject) -> *mut c_void { + unsafe { ffi::PyModule_GetState(m) } +} + +// --- types ----------------------------------------------------------------- + +/// tls-fallback: `PyType_FromSlots`. +#[inline] +pub(crate) unsafe fn type_from_slots( + _ts: &ThreadState<'_>, + slots: *mut ffi::PySlot, +) -> *mut ffi::PyObject { + unsafe { ffi::PyType_FromSlots(slots) } +} + +/// tls-fallback: `PyType_GenericAlloc`. +#[inline] +pub(crate) unsafe fn type_generic_alloc( + _ts: &ThreadState<'_>, + tp: *mut ffi::PyTypeObject, + nitems: ffi::Py_ssize_t, +) -> *mut ffi::PyObject { + unsafe { ffi::PyType_GenericAlloc(tp, nitems) } +} + +/// tstate-free: reads the type object. +#[inline] +pub(crate) unsafe fn type_get_module_state(tp: *mut ffi::PyTypeObject) -> *mut c_void { + unsafe { ffi::PyType_GetModuleState(tp) } +} diff --git a/Modules/cpython-api/src/threadstate.rs b/Modules/cpython-api/src/threadstate.rs new file mode 100644 index 000000000000000..e2cecdcc9914931 --- /dev/null +++ b/Modules/cpython-api/src/threadstate.rs @@ -0,0 +1,118 @@ +//! The explicit thread-state token. + +use std::marker::PhantomData; +use std::ptr::NonNull; + +use crate::ffi; + +/// Proof of an attached thread state, wrapping the actual `PyThreadState*`. +/// +/// Every operation that touches the interpreter takes `&ThreadState<'py>`. +/// The wrapped pointer lets the crate call explicit-tstate C APIs directly +/// instead of re-fetching the thread state from TLS. +/// +/// Soundness properties: +/// +/// - Not `Copy`/`Clone`: [`ThreadState::detach`] takes `&mut self`, so the +/// exclusive borrow statically excludes every other use of the token while +/// the thread state is detached — including uses smuggled through `Send` +/// wrappers (any `&ThreadState`, however wrapped, is still a borrow). +/// - `!Send`/`!Sync` (via the `*mut ()` marker): the token can neither move to +/// nor be shared with another thread, and a detach closure (which must be +/// `Send`) can never capture one. +/// - The lifetime `'py` is invariant, so tokens with different attachment +/// scopes never unify. +#[repr(transparent)] +pub struct ThreadState<'py> { + ptr: NonNull, + /// `fn(&'py ()) -> &'py ()` makes `'py` invariant; `*mut ()` makes the + /// type `!Send + !Sync`. + #[allow(clippy::type_complexity)] + _marker: PhantomData<(fn(&'py ()) -> &'py (), *mut ())>, +} + +impl<'py> ThreadState<'py> { + /// Wrap a raw thread-state pointer. + /// + /// # Safety + /// + /// `ptr` must be the thread state attached to the *current* thread, and it + /// must remain attached for the whole lifetime `'py` (except inside + /// [`ThreadState::detach`], which re-establishes attachment before + /// returning). + #[inline] + pub unsafe fn from_raw(ptr: NonNull) -> ThreadState<'py> { + ThreadState { + ptr, + _marker: PhantomData, + } + } + + /// Fetch the current thread's attached thread state from TLS. + /// + /// This is the entry-point shim used by generated trampolines: extension + /// entry points are only ever invoked with an attached thread state. + /// + /// # Safety + /// + /// The current thread must have an attached thread state (true whenever + /// Python calls into extension code), and it must stay attached for `'py`. + #[doc(hidden)] + #[inline] + pub unsafe fn current_unchecked() -> ThreadState<'py> { + // tls-fallback by nature: this is the one place a trampoline has to + // consult TLS, converting the implicit attachment into the explicit + // token everything else uses. + let ptr = unsafe { ffi::PyThreadState_GetUnchecked() }; + debug_assert!(!ptr.is_null(), "no attached thread state at entry point"); + unsafe { ThreadState::from_raw(NonNull::new_unchecked(ptr)) } + } + + /// The raw `PyThreadState*`. + #[inline] + pub fn as_ptr(&self) -> *mut ffi::PyThreadState { + self.ptr.as_ptr() + } + + /// True if an exception is currently set on this thread. + /// + /// Reads the thread state's `current_exception` field directly + /// (explicit-tstate; no TLS fetch). + #[inline] + pub fn exception_set(&self) -> bool { + unsafe { !(*self.ptr.as_ptr()).current_exception.is_null() } + } + + /// Detach the thread state (releasing the GIL on GIL builds, and allowing + /// stop-the-world pauses on free-threaded builds), run `f`, then reattach. + /// + /// The closure runs without any access to the interpreter: + /// + /// - it has no token, and `F: Send` means it cannot capture + /// `&ThreadState` (`!Sync`) or any `Bound` reference (`!Send`); + /// - the `&mut self` borrow prevents any other use of this token for the + /// duration, even through `Send`-laundering wrappers. + pub fn detach(&mut self, f: F) -> R + where + F: FnOnce() -> R + Send, + R: Send, + { + struct ReattachGuard(*mut ffi::PyThreadState); + impl Drop for ReattachGuard { + fn drop(&mut self) { + // Reattach even if `f` panics, so unwinding back into + // interpreter-touching code is sound. + unsafe { ffi::PyEval_RestoreThread(self.0) }; + } + } + + let saved = unsafe { ffi::PyEval_SaveThread() }; + debug_assert_eq!( + saved, + self.ptr.as_ptr(), + "detached a different thread state than the token wraps" + ); + let _guard = ReattachGuard(saved); + f() + } +} diff --git a/Modules/cpython-api/src/types/any.rs b/Modules/cpython-api/src/types/any.rs new file mode 100644 index 000000000000000..c6933b31f32ba5b --- /dev/null +++ b/Modules/cpython-api/src/types/any.rs @@ -0,0 +1,22 @@ +//! The abstract object type. + +use crate::ffi; + +/// Any Python object. +/// +/// A Python object can be mutated by other code at any time, so there is +/// never a `&mut PyAny`; the wrapped `PyObject` already models interior +/// mutability (`cpython-sys` defines it as a transparent `UnsafeCell`). +/// +/// This type is only ever used behind [`crate::Bound`]/[`crate::Py`] (or +/// `&PyAny`); it is never instantiated by value. +#[repr(transparent)] +pub struct PyAny(ffi::PyObject); + +impl PyAny { + /// The raw object pointer. + #[inline] + pub fn as_ptr(&self) -> *mut ffi::PyObject { + self as *const PyAny as *mut ffi::PyObject + } +} diff --git a/Modules/cpython-api/src/types/bytes.rs b/Modules/cpython-api/src/types/bytes.rs new file mode 100644 index 000000000000000..33237867cb93c2e --- /dev/null +++ b/Modules/cpython-api/src/types/bytes.rs @@ -0,0 +1,61 @@ +//! The `bytes` type. + +use std::mem::MaybeUninit; +use std::os::raw::c_char; +use std::ptr::NonNull; + +use crate::err::{PyErrRaised, PyMemoryError, PyResult}; +use crate::ffi; +use crate::instance::Bound; +use crate::sys_calls; +use crate::threadstate::ThreadState; +use crate::types::any::PyAny; + +/// The Python `bytes` type marker. +#[repr(transparent)] +pub struct PyBytes(PyAny); + +impl PyBytes { + /// Create a `bytes` object as a copy of `data`. + pub fn new<'py>(ts: &ThreadState<'py>, data: &[u8]) -> PyResult> { + let ptr = unsafe { + sys_calls::bytes_from_string_and_size( + ts, + data.as_ptr() as *const c_char, + data.len() as ffi::Py_ssize_t, + ) + }; + match NonNull::new(ptr) { + Some(p) => Ok(unsafe { Bound::from_owned_ptr(ts, p) }), + None => Err(unsafe { PyErrRaised::assume_set(ts) }), + } + } + + /// Create a `bytes` object of length `len` and let `init` fill it, + /// avoiding the extra copy of building in a `Vec` first. + /// + /// `init` receives the uninitialized buffer and must fully initialize it + /// when returning `Ok(())`. + pub fn new_with<'py>( + ts: &ThreadState<'py>, + len: usize, + init: impl FnOnce(&mut [MaybeUninit]) -> PyResult<()>, + ) -> PyResult> { + if len > ffi::Py_ssize_t::MAX as usize { + return Err(PyMemoryError::raise(ts, "bytes object is too large")); + } + let ptr = unsafe { + sys_calls::bytes_from_string_and_size(ts, std::ptr::null(), len as ffi::Py_ssize_t) + }; + let Some(p) = NonNull::new(ptr) else { + return Err(unsafe { PyErrRaised::assume_set(ts) }); + }; + let bytes: Bound<'py, PyBytes> = unsafe { Bound::from_owned_ptr(ts, p) }; + let buf = unsafe { + let data = sys_calls::bytes_as_string(ts, bytes.as_ptr()) as *mut MaybeUninit; + std::slice::from_raw_parts_mut(data, len) + }; + init(buf)?; + Ok(bytes) + } +} diff --git a/Modules/cpython-api/src/types/mod.rs b/Modules/cpython-api/src/types/mod.rs new file mode 100644 index 000000000000000..0b96af6c8376603 --- /dev/null +++ b/Modules/cpython-api/src/types/mod.rs @@ -0,0 +1,11 @@ +//! Concrete Python type markers and their APIs. + +mod any; +mod bytes; +mod module; +mod type_object; + +pub use any::PyAny; +pub use bytes::PyBytes; +pub use module::PyModule; +pub use type_object::PyType; diff --git a/Modules/cpython-api/src/types/module.rs b/Modules/cpython-api/src/types/module.rs new file mode 100644 index 000000000000000..a7ac759d77fc999 --- /dev/null +++ b/Modules/cpython-api/src/types/module.rs @@ -0,0 +1,42 @@ +//! The module type. + +use std::ffi::CStr; + +use crate::conversion::IntoPyObject; +use crate::err::{PyResult, error_on_minus_one}; +use crate::ffi; +use crate::instance::Bound; +use crate::sys_calls; +use crate::threadstate::ThreadState; +use crate::types::any::PyAny; +use crate::types::type_object::PyType; + +/// The Python module type marker. +#[repr(transparent)] +pub struct PyModule(PyAny); + +impl<'py> Bound<'py, PyModule> { + /// Add an attribute to the module (constants, exception types, ...). + pub fn add( + &self, + ts: &ThreadState<'py>, + name: &CStr, + value: impl IntoPyObject<'py>, + ) -> PyResult<()> { + let value = value.into_pyobject(ts)?; + let ret = unsafe { + sys_calls::module_add_object_ref(ts, self.as_ptr(), name.as_ptr(), value.as_ptr()) + }; + // `value` drops here, releasing our reference; AddObjectRef took its + // own. + error_on_minus_one(ts, ret) + } + + /// Add a type object to the module under its (unqualified) name. + pub fn add_type(&self, ts: &ThreadState<'py>, ty: &Bound<'py, PyType>) -> PyResult<()> { + let ret = unsafe { + sys_calls::module_add_type(ts, self.as_ptr(), ty.as_ptr() as *mut ffi::PyTypeObject) + }; + error_on_minus_one(ts, ret) + } +} diff --git a/Modules/cpython-api/src/types/type_object.rs b/Modules/cpython-api/src/types/type_object.rs new file mode 100644 index 000000000000000..a29f586cc0f254d --- /dev/null +++ b/Modules/cpython-api/src/types/type_object.rs @@ -0,0 +1,37 @@ +//! The type type. + +use std::ffi::CStr; +use std::ptr::NonNull; + +use crate::err::{PyErrRaised, PyResult}; +use crate::instance::Bound; +use crate::sys_calls; +use crate::threadstate::ThreadState; +use crate::types::any::PyAny; + +/// The Python type-object marker. +#[repr(transparent)] +pub struct PyType(PyAny); + +impl PyType { + /// Create a new exception type (`PyErr_NewException`). + /// + /// `qualname` must be of the form `"module.name"` (e.g. `c"zlib.error"`). + /// With `base: None` the base class is `Exception`. + /// + /// Exception types belong in per-module state, created from the module's + /// exec function — never in statics — so that modules stay subinterpreter + /// compatible. + pub fn new_exception<'py>( + ts: &ThreadState<'py>, + qualname: &CStr, + base: Option<&Bound<'py, PyType>>, + ) -> PyResult> { + let base_ptr = base.map_or(std::ptr::null_mut(), |b| b.as_ptr()); + let ptr = unsafe { sys_calls::err_new_exception(ts, qualname.as_ptr(), base_ptr) }; + match NonNull::new(ptr) { + Some(p) => Ok(unsafe { Bound::from_owned_ptr(ts, p) }), + None => Err(unsafe { PyErrRaised::assume_set(ts) }), + } + } +} diff --git a/Modules/cpython-api/tests/define_module.rs b/Modules/cpython-api/tests/define_module.rs new file mode 100644 index 000000000000000..e22203c8dfc3430 --- /dev/null +++ b/Modules/cpython-api/tests/define_module.rs @@ -0,0 +1,304 @@ +//! Compile-and-shape test: defines a zlib-shaped module end-to-end. +//! +//! Nothing here starts an interpreter — the value of this test is that the +//! whole definition surface (proc macros, slot builders, module state, +//! class definition) compiles and const-evaluates, and that the resulting +//! static tables have the right shape. +//! +//! Gated on `has_libpython` (emitted by build.rs when the build tree +//! contains a static libpython): the test binary's static tables reference +//! interpreter symbols that the dynamic loader resolves at startup, so it +//! can only load when linked against a real libpython. +#![cfg(has_libpython)] + +use std::sync::Mutex; + +use cpython_api::class::{ClassDef, payload_offset}; +use cpython_api::prelude::*; +use cpython_api::{ffi, module}; + +// --- module state ----------------------------------------------------------- + +struct TestState { + error: Option>, + compress_type: Option>, +} + +impl ModuleState for TestState { + fn new<'py>(ts: &ThreadState<'py>, m: &Bound<'py, PyModule>) -> PyResult { + let error = PyType::new_exception(ts, c"testmod.error", None)?; + m.add(ts, c"error", error.clone_ref(ts))?; + let compress_type = COMPRESS_CLASS.create(ts, m)?; + m.add_type(ts, &compress_type)?; + Ok(TestState { + error: Some(error.unbind()), + compress_type: Some(compress_type.unbind()), + }) + } + + fn traverse(&self, visit: &cpython_api::Visit<'_>) -> cpython_api::TraverseResult { + if let Some(e) = &self.error { + visit.call(e)?; + } + if let Some(t) = &self.compress_type { + visit.call(t)?; + } + Ok(()) + } + + fn clear(&mut self) { + self.error = None; + self.compress_type = None; + } +} + +// --- module-level functions ------------------------------------------------- + +#[pyfunction(signature = (data, value = 1, /))] +fn adler32( + _ts: &ThreadState<'_>, + _state: &TestState, + data: PyBuffer<'_>, + value: u32, +) -> PyResult { + Ok(value.wrapping_add(data.len() as u32)) +} + +// `ts: &mut ThreadState` opts in to `detach` (the trampoline passes the +// token through with the declared mutability). +#[pyfunction(signature = (data, /, level = -1, wbits = 15))] +fn compress<'py>( + ts: &mut ThreadState<'py>, + state: &TestState, + data: PyBuffer<'py>, + level: i32, + wbits: i32, +) -> PyResult> { + if !(-1..=9).contains(&level) || wbits == 0 { + let error = state.error.as_ref().expect("state initialized"); + return Err(error.raise(ts, format_args!("Bad compression level {level}"))); + } + // Long-running work runs with the thread state detached; the closure can + // only capture Send data (the byte slice), never the token or a Bound. + let input = data.as_bytes(); + let compressed = ts.detach(|| input.to_vec()); + PyBytes::new(ts, &compressed) +} + +// A signature-less function: all parameters positional-or-keyword, required. +#[pyfunction] +fn crc32_combine( + _ts: &ThreadState<'_>, + _state: &TestState, + crc1: u32, + crc2: u32, + len2: i64, +) -> PyResult { + Ok(crc1 ^ crc2 ^ (len2 as u32)) +} + +// --- a class --------------------------------------------------------------- + +struct Compress { + inner: Mutex>, +} + +#[pymethods] +impl Compress { + #[pyfunction(signature = (data, /))] + fn compress<'py>( + &self, + ts: &ThreadState<'py>, + state: &TestState, + data: PyBuffer<'py>, + ) -> PyResult> { + let mut inner = self.inner.lock().unwrap(); + if inner.len() > 1 << 20 { + let error = state.error.as_ref().expect("state initialized"); + return Err(error.raise(ts, "buffer overflow")); + } + inner.extend_from_slice(data.as_bytes()); + PyBytes::new(ts, &inner) + } + + #[pyfunction(signature = (mode = 4, /))] + fn flush<'py>( + &self, + ts: &ThreadState<'py>, + _state: &TestState, + mode: i32, + ) -> PyResult> { + let _ = mode; + PyBytes::new(ts, &self.inner.lock().unwrap()) + } + + #[getter] + fn eof(&self, _ts: &ThreadState<'_>) -> bool { + false + } + + #[new] + #[pyfunction(signature = (wbits = 15, zdict = None))] + fn new(ts: &ThreadState<'_>, wbits: i32, zdict: Option>) -> PyResult { + if wbits == 0 { + return Err(PyValueError::raise(ts, "invalid wbits")); + } + Ok(Compress { + inner: Mutex::new(zdict.map(|z| z.as_bytes().to_vec()).unwrap_or_default()), + }) + } +} + +static COMPRESS_CLASS: ClassDef = ClassDef::new(c"testmod._Compress") + .methods(&COMPRESS_METHODS) + .getsets(&COMPRESS_GETSETS) + .tp_new(COMPRESS_TP_NEW); + +// --- factory function using new_instance ------------------------------------ + +#[pyfunction(signature = (level = -1))] +fn compressobj<'py>( + ts: &ThreadState<'py>, + state: &TestState, + level: i32, +) -> PyResult> { + let _ = level; + let cls = state.compress_type.as_ref().expect("state initialized"); + cpython_api::class::new_instance( + ts, + cls.bind(ts), + Compress { + inner: Mutex::new(Vec::new()), + }, + ) +} + +// --- the module ------------------------------------------------------------- + +fn testmod_exec<'py>(ts: &ThreadState<'py>, m: &Bound<'py, PyModule>) -> PyResult<()> { + m.add(ts, c"MAX_WBITS", 15)?; + m.add(ts, c"DEF_BUF_SIZE", 16384usize)?; + m.add(ts, c"ZLIB_VERSION", "1.3.1")?; + Ok(()) +} + +export_module! { + name: testmod, + doc: c"A zlib-shaped test module", + state: TestState, + methods: [adler32, compress, crc32_combine, compressobj], + exec: testmod_exec, +} + +// --- shape checks ----------------------------------------------------------- + +#[test] +fn method_table_shape() { + // 4 methods + terminator, built by export_module!. + let def = &adler32::DEF; + assert_eq!( + unsafe { std::ffi::CStr::from_ptr(def.ml_name) } + .to_str() + .unwrap(), + "adler32" + ); + assert_eq!(def.ml_flags, ffi::METH_FASTCALL | ffi::METH_KEYWORDS); + + // Module functions stay FASTCALL (state comes from the module object); + // methods are uniformly METH_METHOD (state via defining class). + assert_eq!( + compress::DEF.ml_flags, + ffi::METH_FASTCALL | ffi::METH_KEYWORDS + ); + for def in &COMPRESS_METHODS.0[..2] { + assert_eq!( + def.ml_flags, + ffi::METH_FASTCALL | ffi::METH_KEYWORDS | ffi::METH_METHOD + ); + } + // Terminator. + assert!(COMPRESS_METHODS.0[2].ml_name.is_null()); +} + +#[test] +fn spec_shape() { + assert_eq!(adler32::SPEC.fn_name, "adler32"); + assert_eq!(adler32::SPEC.params.len(), 2); + assert_eq!(adler32::SPEC.pos_only, 2); + assert!(adler32::SPEC.params[0].required); + assert!(!adler32::SPEC.params[1].required); + + assert_eq!(compress::SPEC.pos_only, 1); + assert_eq!(compress::SPEC.params.len(), 3); + + // Signature-less: all positional-or-keyword, required. + assert_eq!(crc32_combine::SPEC.pos_only, 0); + assert_eq!(crc32_combine::SPEC.params.len(), 3); + assert!(crc32_combine::SPEC.params.iter().all(|p| p.required)); +} + +#[test] +fn getset_table_shape() { + assert_eq!( + unsafe { std::ffi::CStr::from_ptr(COMPRESS_GETSETS.0[0].name) } + .to_str() + .unwrap(), + "eof" + ); + assert!(COMPRESS_GETSETS.0[0].get.is_some()); + assert!(COMPRESS_GETSETS.0[0].set.is_none()); + assert!(COMPRESS_GETSETS.0[1].name.is_null()); +} + +#[test] +fn payload_layout() { + // Payload starts after the header, aligned for the payload type. + let off = payload_offset::(); + assert!(off >= std::mem::size_of::()); + assert_eq!(off % std::mem::align_of::(), 0); +} + +#[test] +fn abi_info_shape() { + let abi = module::abi_info(); + assert_eq!(abi.abiinfo_major_version, 1); + assert_ne!(abi.flags & ffi::PyABIInfo_INTERNAL as u16, 0); + // Exactly one of GIL / FREETHREADED, matching the interpreter build. + let ft = abi.flags & (ffi::PyABIInfo_GIL | ffi::PyABIInfo_FREETHREADED) as u16; + assert!(ft == ffi::PyABIInfo_GIL as u16 || ft == ffi::PyABIInfo_FREETHREADED as u16); + assert_eq!(abi.build_version, module::PY_VERSION_HEX); +} + +#[test] +fn module_slots_shape() { + // The export hook returns the static slot array; walk it and check the + // required PEP 793 slots are present, terminated, and defaulted to + // free-threading + per-interpreter-GIL support. + let slots = unsafe { PyModExport() }; + let mut ids = Vec::new(); + let mut i = 0; + loop { + let slot = unsafe { *slots.add(i) }; + if slot.sl_id == 0 { + break; + } + ids.push(slot.sl_id as u32); + i += 1; + assert!(i < 32, "unterminated slot array"); + } + for required in [ + ffi::Py_mod_name, + ffi::Py_mod_doc, + ffi::Py_mod_state_size, + ffi::Py_mod_methods, + module::PY_MOD_EXEC, + ffi::Py_mod_state_traverse, + ffi::Py_mod_state_clear, + ffi::Py_mod_state_free, + module::PY_MOD_MULTIPLE_INTERPRETERS, + module::PY_MOD_GIL, + ffi::Py_mod_abi, + ] { + assert!(ids.contains(&required), "missing slot id {required}"); + } +} diff --git a/Modules/cpython-rust-staticlib/src/lib.rs b/Modules/cpython-rust-staticlib/src/lib.rs index a440707833f65d6..35de053efc72711 100644 --- a/Modules/cpython-rust-staticlib/src/lib.rs +++ b/Modules/cpython-rust-staticlib/src/lib.rs @@ -1 +1,9 @@ -pub use _base64::PyInit__base64; +// Re-export each Rust module's PEP 793 export hook so its object code (and +// the `PyModExport_` symbol) is retained in the static archive. +// +// NOTE: builtin/static interpreter builds currently register modules through +// `struct _inittab`, which only carries `PyInit_*`-style functions — slots- +// only (PyModExport) modules can be imported as shared extensions today, but +// wiring them into a static interpreter needs PEP 793 inittab support in the +// C import machinery first. +pub use _base64::PyModExport; diff --git a/Modules/cpython-sys/build.rs b/Modules/cpython-sys/build.rs index 03335128e9fbe55..f2e19e64e201cc9 100644 --- a/Modules/cpython-sys/build.rs +++ b/Modules/cpython-sys/build.rs @@ -265,6 +265,9 @@ fn generate_c_api_bindings( .allowlist_function("_?Py.*") .allowlist_type("_?Py.*") .allowlist_var("_?Py.*") + // Version macros (PY_VERSION_HEX etc.) don't match the case-sensitive + // Py.* patterns above but are needed for PyABIInfo construction. + .allowlist_var("PY_(MAJOR|MINOR|MICRO)_VERSION|PY_VERSION_HEX|PY_RELEASE_(LEVEL|SERIAL)") .blocklist_type("^PyMethodDef$") .blocklist_type("PyObject") .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) diff --git a/Modules/cpython-sys/src/lib.rs b/Modules/cpython-sys/src/lib.rs index 17924f0e5fb00a9..1025a35e60e7801 100644 --- a/Modules/cpython-sys/src/lib.rs +++ b/Modules/cpython-sys/src/lib.rs @@ -57,6 +57,15 @@ pub const _Py_STATIC_IMMORTAL_INITIAL_REFCNT: Py_ssize_t = #[cfg(not(target_pointer_width = "64"))] pub const _Py_STATIC_IMMORTAL_INITIAL_REFCNT: Py_ssize_t = (7u32 << 28) as Py_ssize_t; +/// Whether these bindings were generated for a free-threaded (`Py_GIL_DISABLED`) +/// build. Exposed as a const so dependent crates can branch on it without +/// re-detecting the build configuration (the `py_gil_disabled` cfg is local to +/// this crate). +#[cfg(py_gil_disabled)] +pub const GIL_DISABLED: bool = true; +#[cfg(not(py_gil_disabled))] +pub const GIL_DISABLED: bool = false; + #[repr(transparent)] pub struct PyObject(std::cell::UnsafeCell<_object>);