diff --git a/crates/host_env/src/ctypes.rs b/crates/host_env/src/ctypes.rs index 9bfd41c2818..2dfc8cbe29d 100644 --- a/crates/host_env/src/ctypes.rs +++ b/crates/host_env/src/ctypes.rs @@ -24,7 +24,7 @@ use libffi::middle::Type; ))] use libffi::{ low, - middle::{Arg, Cif, Closure, CodePtr}, + middle::{Arg, Cif, Closure, CodePtr, Ret}, }; #[cfg(any(unix, windows))] use libloading::Library; @@ -652,21 +652,6 @@ pub enum FfiValue { Pointer(usize), } -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub enum CallResult { - Void, - Pointer(usize), - Value(low::ffi_arg), -} - #[cfg(all( any( target_os = "linux", @@ -1537,78 +1522,6 @@ pub fn ffi_type_from_code(ty: &str) -> Option { } } -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub fn ffi_type_from_tag(tag: u8) -> Type { - match tag { - b'c' | b'b' => Type::i8(), - b'B' | b'?' => Type::u8(), - b'h' | b'v' => Type::i16(), - b'H' => Type::u16(), - b'i' => Type::i32(), - b'I' => Type::u32(), - b'l' => { - if core::mem::size_of::() == 8 { - Type::i64() - } else { - Type::i32() - } - } - b'L' => { - if core::mem::size_of::() == 8 { - Type::u64() - } else { - Type::u32() - } - } - b'q' => Type::i64(), - b'Q' => Type::u64(), - b'f' => Type::f32(), - b'd' | b'g' => Type::f64(), - b'u' => { - if core::mem::size_of::() == 2 { - Type::u16() - } else { - Type::u32() - } - } - _ => Type::pointer(), - } -} - -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub fn ffi_type_from_format(fmt: &str) -> Type { - match fmt.trim_start_matches(['<', '>', '!', '@', '=']) { - "b" => Type::i8(), - "B" => Type::u8(), - "h" => Type::i16(), - "H" => Type::u16(), - "i" | "l" => Type::i32(), - "I" | "L" => Type::u32(), - "q" => Type::i64(), - "Q" => Type::u64(), - "f" => Type::f32(), - "d" => Type::f64(), - "P" | "z" | "Z" | "O" => Type::pointer(), - _ => Type::u8(), - } -} - #[cfg(all( any( target_os = "linux", @@ -1687,119 +1600,6 @@ pub fn ffi_void_type() -> Type { Type::void() } -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub fn ffi_type_for_return_size(size: usize) -> Type { - if size <= 4 { - Type::i32() - } else if size <= 8 { - Type::i64() - } else { - Type::pointer() - } -} - -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CTypeParamKind { - Structure, - Union, - Array, - Pointer, - Simple, -} - -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub fn ffi_type_for_layout( - kind: CTypeParamKind, - ffi_field_types: &[Type], - size: usize, - length: usize, - format: Option<&str>, -) -> Type { - const MAX_FFI_STRUCT_SIZE: usize = 1024 * 1024; - - match kind { - CTypeParamKind::Structure | CTypeParamKind::Union => { - if !ffi_field_types.is_empty() { - Type::structure(ffi_field_types.iter().cloned()) - } else if size <= MAX_FFI_STRUCT_SIZE { - ffi_byte_struct(size) - } else { - ffi_pointer_type() - } - } - CTypeParamKind::Array => { - if size > MAX_FFI_STRUCT_SIZE || length > MAX_FFI_STRUCT_SIZE { - ffi_pointer_type() - } else if let Some(fmt) = format { - ffi_repeat_type(ffi_type_from_format(fmt), length) - } else { - ffi_byte_struct(size) - } - } - CTypeParamKind::Pointer => ffi_pointer_type(), - CTypeParamKind::Simple => { - if let Some(fmt) = format { - ffi_type_from_format(fmt) - } else { - Type::u8() - } - } - } -} - -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub fn callproc( - code_ptr: CodePtr, - ffi_arg_types: Vec, - ffi_return_type: Type, - ffi_args: &[Arg<'_>], - restype_is_none: bool, - is_pointer_return: bool, -) -> CallResult { - let cif = Cif::new(ffi_arg_types, ffi_return_type); - if restype_is_none { - unsafe { cif.call::<()>(code_ptr, ffi_args) }; - CallResult::Void - } else if is_pointer_return { - CallResult::Pointer(unsafe { cif.call::(code_ptr, ffi_args) }) - } else { - CallResult::Value(unsafe { cif.call::(code_ptr, ffi_args) }) - } -} - #[cfg(all( any( target_os = "linux", @@ -2052,6 +1852,230 @@ impl Drop for CallbackThunk { } } +/// Type codes whose value is a pointer (drives pointer-return decoding and +/// TYPEFLAG_ISPOINTER). +pub fn simple_type_is_pointer(code: &str) -> bool { + matches!(code, "z" | "Z" | "P" | "s" | "X" | "O") +} + +/// All valid ctypes simple type codes on this platform. +// +// TODO: the vm's `SIMPLE_TYPE_CHARS` const (crates/vm/src/stdlib/_ctypes/simple.rs) +// should adopt this as the single source of truth. +pub fn simple_type_chars() -> &'static str { + #[cfg(windows)] + { + // spell-checker: disable-next-line + "cbBhHiIlLdfuzZqQPXOv?g" + } + #[cfg(not(windows))] + { + // spell-checker: disable-next-line + "cbBhHiIlLdfuzZqQPOv?g" + } +} + +/// Recursive layout of a ctypes type: memory shape independent of any object +/// model, used to lower by-value aggregate arguments and aggregate returns for +/// [`call`]. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CTypeLayout { + /// Simple type identified by its ctypes code ('i', 'd', 'P', 'u', ...). + Simple(char), + /// Any pointer-kind field (`POINTER(T)`, `c_void_p`/`z`/`Z`, function pointer). + Pointer, + /// Struct with per-field layouts in declaration order; `size` is the total + /// size including trailing padding. + Struct { fields: Vec, size: usize }, + /// Union, lowered to a size-matched byte struct (libffi has no union kind, so + /// register classification of float-only unions is approximate). + Union { fields: Vec, size: usize }, + /// Fixed-length array; only meaningful nested inside an aggregate. + Array { + element: Box, + length: usize, + size: usize, + }, + /// No field information available: a size-matched byte struct fallback. + Opaque { size: usize }, +} + +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +impl CTypeLayout { + /// Total size in bytes. + pub fn size(&self) -> usize { + match self { + Self::Simple(code) => { + let mut buf = [0u8; 4]; + simple_type_size(code.encode_utf8(&mut buf)).unwrap_or(0) + } + Self::Pointer => POINTER_SIZE, + Self::Struct { size, .. } + | Self::Union { size, .. } + | Self::Array { size, .. } + | Self::Opaque { size } => *size, + } + } + + /// Lower to a libffi type. `Err` if a simple code is unrecognized. + fn to_ffi_type(&self) -> Result { + match self { + Self::Simple(code) => { + let mut buf = [0u8; 4]; + let code = code.encode_utf8(&mut buf); + ffi_type_from_code(code).ok_or_else(|| CallError::UnknownTypeCode(code.to_string())) + } + Self::Pointer => Ok(ffi_pointer_type()), + Self::Struct { fields, .. } => { + let mut ffi_fields = Vec::with_capacity(fields.len()); + for field in fields { + ffi_fields.push(field.to_ffi_type()?); + } + Ok(Type::structure(ffi_fields)) + } + Self::Array { + element, length, .. + } => Ok(ffi_repeat_type(element.to_ffi_type()?, *length)), + Self::Union { size, .. } | Self::Opaque { size } => Ok(ffi_byte_struct(*size)), + } + } +} + +/// One argument of a foreign call. Buffers are borrowed and must outlive the +/// call; keeping their owners alive is the caller's responsibility. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug, Clone, Copy)] +pub enum CallArg<'a> { + /// A value typed by a ctypes simple type code, as its raw native-endian + /// buffer (at least `simple_type_size(code)` bytes relevant). + Typed { code: &'a str, buffer: &'a [u8] }, + /// Untyped Python int (ConvParam default: C int). + Int(i32), + /// Untyped Python float (ConvParam default: C double). + Double(f64), + /// Address-valued argument (pointer decay, byref, bytes/str copies, NULL = 0). + Pointer(usize), + /// By-value aggregate: layout plus its raw bytes (`buffer.len() >= layout.size()`). + Aggregate { + layout: &'a CTypeLayout, + buffer: &'a [u8], + }, +} + +/// Return-type selector for [`call`]. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug, Clone, Copy)] +pub enum CallRet<'a> { + /// restype is None: the call returns void. + Void, + /// A ctypes simple type code. Pointer-kind codes (`simple_type_is_pointer`) + /// yield [`CallValue::Pointer`]; everything else [`CallValue::Scalar`]. + Code(&'a str), + /// A pointer-typed return without a driving code (`POINTER(T)`, function + /// pointer). + Pointer, + /// A by-value aggregate return. + Aggregate(&'a CTypeLayout), +} + +/// Per-call error-swapping options. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug, Clone, Copy, Default)] +pub struct CallOptions { + /// Swap the ctypes-local errno around the raw call (unix; ignored on windows). + pub use_errno: bool, + /// Swap the ctypes-local last error around the raw call (windows; ignored + /// elsewhere). + pub use_last_error: bool, +} + +/// Result of a foreign call. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug)] +pub enum CallValue { + /// Void return. + Void, + /// Raw return-register image, native endian (register-sized: 8 bytes on + /// 64-bit). Decode with [`decode_type_code`]. + Scalar(Vec), + /// Pointer-valued return. + Pointer(usize), + /// Exactly `layout.size()` bytes of a returned aggregate. + Aggregate(Vec), +} + +/// Errors from [`call`]. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallError { + NullFunctionPointer, + UnknownTypeCode(String), + /// An aggregate argument's buffer was shorter than its layout size. + BufferTooSmall { + expected: usize, + got: usize, + }, +} + +/// Perform a foreign call: handles scalar, pointer, and by-value aggregate +/// arguments, and void / scalar / pointer / aggregate returns. #[cfg(all( any( target_os = "linux", @@ -2061,18 +2085,156 @@ impl Drop for CallbackThunk { ), not(any(target_env = "musl", target_env = "sgx")) ))] -pub fn call_result_bytes(raw_result: &CallResult) -> Option<(Vec, usize)> { - match raw_result { - CallResult::Void => None, - CallResult::Pointer(ptr) => { - let bytes = ptr.to_ne_bytes(); - Some((bytes.to_vec(), core::mem::size_of::())) +pub fn call( + addr: usize, + args: &[CallArg<'_>], + ret: CallRet<'_>, + options: CallOptions, +) -> Result { + enum Lowered<'a> { + Scalar(FfiValue), + Aggregate(&'a [u8]), + } + + let code_ptr = code_ptr_from_addr(addr).ok_or(CallError::NullFunctionPointer)?; + + // Pass 1: argument types + owned scalar values / borrowed aggregate buffers. + let mut ffi_arg_types: Vec = Vec::with_capacity(args.len()); + let mut lowered: Vec> = Vec::with_capacity(args.len()); + for arg in args { + match arg { + CallArg::Typed { code, buffer } => { + let ty = ffi_type_from_code(code) + .ok_or_else(|| CallError::UnknownTypeCode((*code).to_string()))?; + ffi_arg_types.push(ty); + lowered.push(Lowered::Scalar(ffi_value_from_type_code(code, buffer))); + } + CallArg::Int(value) => { + ffi_arg_types.push(ffi_i32_type()); + lowered.push(Lowered::Scalar(FfiValue::I32(*value))); + } + CallArg::Double(value) => { + ffi_arg_types.push(ffi_f64_type()); + lowered.push(Lowered::Scalar(FfiValue::F64(*value))); + } + CallArg::Pointer(value) => { + ffi_arg_types.push(ffi_pointer_type()); + lowered.push(Lowered::Scalar(FfiValue::Pointer(*value))); + } + CallArg::Aggregate { layout, buffer } => { + let expected = layout.size(); + if buffer.len() < expected { + return Err(CallError::BufferTooSmall { + expected, + got: buffer.len(), + }); + } + ffi_arg_types.push(layout.to_ffi_type()?); + lowered.push(Lowered::Aggregate(buffer)); + } } - CallResult::Value(val) => { - let bytes = val.to_ne_bytes(); - Some((bytes.to_vec(), core::mem::size_of_val(val))) + } + + let ffi_return_type = match ret { + CallRet::Void => ffi_void_type(), + CallRet::Code(code) => { + ffi_type_from_code(code).ok_or_else(|| CallError::UnknownTypeCode(code.to_string()))? } + CallRet::Pointer => ffi_pointer_type(), + CallRet::Aggregate(layout) => layout.to_ffi_type()?, + }; + + // Pass 2: borrow the completed `lowered` as libffi Args. No reallocation can + // now invalidate the scalar borrows; aggregate Args point at caller buffers. + let ffi_args: Vec> = lowered + .iter() + .map(|arg| match arg { + Lowered::Scalar(value) => ffi_arg_from_value(value), + // `.first()` avoids indexing an empty buffer (a zero-sized by-value + // aggregate); libffi reads nothing for a zero-size type. + Lowered::Aggregate(buffer) => Arg::new(buffer.first().unwrap_or(&0u8)), + }) + .collect(); + + let cif = Cif::new(ffi_arg_types, ffi_return_type); + + // Allocate the aggregate return buffer outside the error-swap window so no + // allocation runs between the raw call and the errno/last-error capture. + // libffi requires this buffer be at least `ffi_arg`-sized and suitably + // aligned; a `u64` slice guarantees both. + let mut aggregate_buffer: Vec = match ret { + CallRet::Aggregate(layout) => vec![0u64; core::cmp::max(layout.size(), 8).div_ceil(8)], + _ => Vec::new(), + }; + + enum RawResult { + Void, + Pointer(usize), + Scalar(u64), + Aggregate, } + + let mut invoke = || -> RawResult { + match ret { + CallRet::Void => { + unsafe { cif.call::<()>(code_ptr, &ffi_args) }; + RawResult::Void + } + CallRet::Code(code) if simple_type_is_pointer(code) => { + RawResult::Pointer(unsafe { cif.call::(code_ptr, &ffi_args) }) + } + CallRet::Code(_) => { + // Capture a full register (`u64`), not `low::ffi_arg`: the + // `libffi_sys` binding types `ffi_arg` as `c_ulong`, which is + // 4 bytes under LLP64 (Windows x64) and would truncate 8-byte + // returns (`q`/`Q`/`d`). `decode_type_code` reads the leading + // bytes the type code needs. + RawResult::Scalar(unsafe { cif.call::(code_ptr, &ffi_args) }) + } + CallRet::Pointer => { + RawResult::Pointer(unsafe { cif.call::(code_ptr, &ffi_args) }) + } + CallRet::Aggregate(_) => { + unsafe { + cif.call_return_into(code_ptr, &ffi_args, Ret::new(&mut aggregate_buffer[..])); + } + RawResult::Aggregate + } + } + }; + + #[cfg(not(windows))] + let raw = if options.use_errno { + with_swapped_errno(invoke) + } else { + invoke() + }; + + #[cfg(windows)] + let raw = if options.use_last_error { + with_swapped_last_error(invoke) + } else { + invoke() + }; + + let result = match raw { + RawResult::Void => CallValue::Void, + RawResult::Pointer(ptr) => CallValue::Pointer(ptr), + RawResult::Scalar(value) => CallValue::Scalar(value.to_ne_bytes().to_vec()), + RawResult::Aggregate => { + let size = match ret { + CallRet::Aggregate(layout) => layout.size(), + _ => 0, + }; + let bytes: Vec = aggregate_buffer + .iter() + .flat_map(|word| word.to_ne_bytes()) + .collect(); + CallValue::Aggregate(bytes[..size].to_vec()) + } + }; + + Ok(result) } /// # Safety @@ -2718,3 +2880,690 @@ pub fn dlsym_checked(_handle: usize, symbol_name: &CStr) -> Result<*mut c_void, symbol_name.to_string_lossy() )) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn simple_type_is_pointer_classifies_codes() { + assert!(simple_type_is_pointer("z")); + assert!(simple_type_is_pointer("Z")); + assert!(simple_type_is_pointer("P")); + assert!(simple_type_is_pointer("O")); + assert!(!simple_type_is_pointer("i")); + assert!(!simple_type_is_pointer("d")); + assert!(!simple_type_is_pointer("")); + } + + #[test] + fn simple_type_chars_contains_expected_codes() { + let chars = simple_type_chars(); + assert!(chars.contains('i')); + assert!(chars.contains('d')); + assert!(chars.contains('P')); + // junk / non-code characters are excluded + assert!(!chars.contains('@')); + assert!(!chars.contains(' ')); + assert!(!chars.contains('1')); + } + + #[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) + ))] + mod call_tests { + use super::*; + + extern "C" fn abs_i32(x: i32) -> i32 { + x.abs() + } + + extern "C" fn add_i32(a: i32, b: i32) -> i32 { + a + b + } + + extern "C" fn sqrt_f64(x: f64) -> f64 { + x.sqrt() + } + + extern "C" fn noop() {} + + #[repr(C)] + struct PairI32 { + a: i32, + b: i32, + } + extern "C" fn sum_pair(p: PairI32) -> i32 { + p.a + p.b + } + extern "C" fn ret_pair() -> PairI32 { + PairI32 { a: 10, b: 20 } + } + + #[repr(C)] + struct PairF32 { + x: f32, + y: f32, + } + extern "C" fn sum_pair_f32(p: PairF32) -> f32 { + p.x + p.y + } + + #[repr(C)] + struct Inner { + a: i32, + b: i32, + } + #[repr(C)] + struct Outer { + inner: Inner, + c: i32, + } + extern "C" fn sum_outer(o: Outer) -> i32 { + o.inner.a + o.inner.b + o.c + } + + #[repr(C)] + struct ArrStruct { + arr: [i32; 3], + tag: i32, + } + extern "C" fn sum_arr_struct(s: ArrStruct) -> i32 { + s.arr[0] + s.arr[1] + s.arr[2] + s.tag + } + + #[repr(C)] + struct Big { + a: i64, + b: i64, + c: i64, + } + extern "C" fn sum_big(v: Big) -> i64 { + v.a + v.b + v.c + } + extern "C" fn ret_big() -> Big { + Big { a: 1, b: 2, c: 3 } + } + + #[allow(dead_code)] + #[repr(C)] + struct S3 { + a: u8, + b: u8, + c: u8, + } + extern "C" fn ret_s3() -> S3 { + S3 { a: 1, b: 2, c: 3 } + } + + #[allow(dead_code)] + #[repr(C)] + struct S5 { + a: u8, + b: u8, + c: u8, + d: u8, + e: u8, + } + extern "C" fn ret_s5() -> S5 { + S5 { + a: 1, + b: 2, + c: 3, + d: 4, + e: 5, + } + } + + #[allow(dead_code)] + #[repr(C)] + struct S12 { + a: i32, + b: i32, + c: i32, + } + extern "C" fn ret_s12() -> S12 { + S12 { + a: 100, + b: 200, + c: 300, + } + } + + fn addr_of(f: extern "C" fn() -> ()) -> usize { + f as *const () as usize + } + + fn scalar_bytes(value: &CallValue) -> &[u8] { + match value { + CallValue::Scalar(bytes) => bytes, + other => panic!("expected Scalar, got {other:?}"), + } + } + + fn aggregate_bytes(value: &CallValue) -> &[u8] { + match value { + CallValue::Aggregate(bytes) => bytes, + other => panic!("expected Aggregate, got {other:?}"), + } + } + + fn i32_bytes(values: &[i32]) -> Vec { + values.iter().flat_map(|v| v.to_ne_bytes()).collect() + } + + // --- scalar parity ----------------------------------------------------- + + #[test] + fn calls_f64_scalar() { + let addr = sqrt_f64 as *const () as usize; + let result = call( + addr, + &[CallArg::Double(2.0)], + CallRet::Code("d"), + CallOptions::default(), + ) + .unwrap(); + match decode_type_code("d", scalar_bytes(&result)) { + DecodedValue::Float(v) => { + assert!((v - core::f64::consts::SQRT_2).abs() < 1e-12) + } + _ => panic!("expected Float return"), + } + } + + #[test] + fn typed_scalar_arg_from_buffer() { + let addr = abs_i32 as *const () as usize; + let buffer = (-5i32).to_ne_bytes(); + let result = call( + addr, + &[CallArg::Typed { + code: "i", + buffer: &buffer, + }], + CallRet::Code("i"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(5) + )); + } + + #[test] + fn typed_two_scalar_args() { + let addr = add_i32 as *const () as usize; + let a = 2i32.to_ne_bytes(); + let b = 3i32.to_ne_bytes(); + let result = call( + addr, + &[ + CallArg::Typed { + code: "i", + buffer: &a, + }, + CallArg::Typed { + code: "i", + buffer: &b, + }, + ], + CallRet::Code("i"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(5) + )); + } + + #[test] + fn void_return_is_void() { + let result = call(addr_of(noop), &[], CallRet::Void, CallOptions::default()).unwrap(); + assert!(matches!(result, CallValue::Void)); + } + + #[test] + fn every_simple_code_is_accepted() { + for code in simple_type_chars().chars() { + let code = code.to_string(); + assert!( + ffi_type_from_code(&code).is_some(), + "code {code:?} not accepted by call's arg/return lowering" + ); + } + } + + #[test] + fn scalar_lowering_helpers_agree_where_carrier_matches() { + // For codes whose ffi carrier type matches their ctypes signedness the + // two lowering helpers agree. + let buffer = 0x1122_3344_5566_7788u64.to_ne_bytes(); + for code in ["b", "B", "h", "H", "i", "I", "q", "Q", "d", "f"] { + let by_code = ffi_value_from_type_code(code, &buffer); + let by_type = + ffi_value_from_type(&buffer, ffi_type_from_code(code).unwrap()).unwrap(); + assert_eq!( + format!("{by_code:?}"), + format!("{by_type:?}"), + "code {code}" + ); + } + } + + #[test] + fn scalar_lowering_helpers_diverge_for_signed_char() { + // `ffi_value_from_type` classifies purely by libffi Type identity, while + // `ffi_value_from_type_code` carries ctypes signedness: for 'c' (u8 + // carrier, signed value) the two intentionally differ. `call` uses only + // the code-based helper. + let buffer = [200u8]; + assert!(matches!( + ffi_value_from_type_code("c", &buffer), + FfiValue::I8(-56) + )); + assert!(matches!( + ffi_value_from_type(&buffer, ffi_type_from_code("c").unwrap()), + Some(FfiValue::U8(200)) + )); + } + + // --- by-value aggregate arguments ------------------------------------- + + #[test] + fn passes_struct_by_value() { + let addr = sum_pair as *const () as usize; + let layout = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('i')], + size: core::mem::size_of::(), + }; + let buffer = i32_bytes(&[3, 4]); + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("i"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(7) + )); + } + + #[test] + fn passes_nested_struct_by_value() { + let addr = sum_outer as *const () as usize; + let inner = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('i')], + size: core::mem::size_of::(), + }; + let layout = CTypeLayout::Struct { + fields: vec![inner, CTypeLayout::Simple('i')], + size: core::mem::size_of::(), + }; + let buffer = i32_bytes(&[5, 6, 7]); + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("i"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(18) + )); + } + + #[test] + fn passes_array_in_struct_by_value() { + let addr = sum_arr_struct as *const () as usize; + let layout = CTypeLayout::Struct { + fields: vec![ + CTypeLayout::Array { + element: Box::new(CTypeLayout::Simple('i')), + length: 3, + size: 12, + }, + CTypeLayout::Simple('i'), + ], + size: core::mem::size_of::(), + }; + let buffer = i32_bytes(&[1, 2, 3, 4]); + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("i"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(10) + )); + } + + #[test] + fn passes_float_pair_struct_by_value() { + let addr = sum_pair_f32 as *const () as usize; + let layout = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('f'), CTypeLayout::Simple('f')], + size: core::mem::size_of::(), + }; + let mut buffer = Vec::new(); + buffer.extend_from_slice(&1.5f32.to_ne_bytes()); + buffer.extend_from_slice(&2.25f32.to_ne_bytes()); + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("f"), + CallOptions::default(), + ) + .unwrap(); + match decode_type_code("f", scalar_bytes(&result)) { + DecodedValue::Float(v) => assert!((v - 3.75).abs() < 1e-6), + _ => panic!("expected Float return"), + } + } + + #[test] + fn passes_large_struct_by_value() { + let addr = sum_big as *const () as usize; + let layout = CTypeLayout::Struct { + fields: vec![ + CTypeLayout::Simple('q'), + CTypeLayout::Simple('q'), + CTypeLayout::Simple('q'), + ], + size: core::mem::size_of::(), + }; + let mut buffer = Vec::new(); + for v in [11i64, 22, 33] { + buffer.extend_from_slice(&v.to_ne_bytes()); + } + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("q"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("q", scalar_bytes(&result)), + DecodedValue::Signed(66) + )); + } + + // --- by-value aggregate returns --------------------------------------- + + #[test] + fn returns_small_struct_by_value() { + let layout = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('i')], + size: core::mem::size_of::(), + }; + let result = call( + ret_pair as *const () as usize, + &[], + CallRet::Aggregate(&layout), + CallOptions::default(), + ) + .unwrap(); + assert_eq!(aggregate_bytes(&result), i32_bytes(&[10, 20]).as_slice()); + } + + #[test] + fn returns_odd_size_structs_by_value() { + let s3 = CTypeLayout::Struct { + fields: vec![ + CTypeLayout::Simple('B'), + CTypeLayout::Simple('B'), + CTypeLayout::Simple('B'), + ], + size: 3, + }; + let result = call( + ret_s3 as *const () as usize, + &[], + CallRet::Aggregate(&s3), + CallOptions::default(), + ) + .unwrap(); + assert_eq!(aggregate_bytes(&result), &[1u8, 2, 3]); + + let s5 = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('B'); 5], + size: 5, + }; + let result = call( + ret_s5 as *const () as usize, + &[], + CallRet::Aggregate(&s5), + CallOptions::default(), + ) + .unwrap(); + assert_eq!(aggregate_bytes(&result), &[1u8, 2, 3, 4, 5]); + + let s12 = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'); 3], + size: 12, + }; + let result = call( + ret_s12 as *const () as usize, + &[], + CallRet::Aggregate(&s12), + CallOptions::default(), + ) + .unwrap(); + assert_eq!( + aggregate_bytes(&result), + i32_bytes(&[100, 200, 300]).as_slice() + ); + } + + #[test] + fn returns_large_struct_by_value() { + let layout = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('q'); 3], + size: core::mem::size_of::(), + }; + let result = call( + ret_big as *const () as usize, + &[], + CallRet::Aggregate(&layout), + CallOptions::default(), + ) + .unwrap(); + let mut expected = Vec::new(); + for v in [1i64, 2, 3] { + expected.extend_from_slice(&v.to_ne_bytes()); + } + assert_eq!(aggregate_bytes(&result), expected.as_slice()); + } + + // --- pointers, layout, unions, errors --------------------------------- + + #[test] + fn pointer_return_round_trips_address() { + extern "C" fn echo_ptr(p: usize) -> usize { + p + } + let addr = echo_ptr as *const () as usize; + let sentinel = 0xDEAD_BEEFusize; + let result = call( + addr, + &[CallArg::Pointer(sentinel)], + CallRet::Code("P"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!(result, CallValue::Pointer(p) if p == sentinel)); + let result = call( + addr, + &[CallArg::Pointer(sentinel)], + CallRet::Pointer, + CallOptions::default(), + ) + .unwrap(); + assert!(matches!(result, CallValue::Pointer(p) if p == sentinel)); + } + + #[test] + fn layout_size_matches_repr_c() { + assert_eq!(CTypeLayout::Simple('i').size(), core::mem::size_of::()); + assert_eq!(CTypeLayout::Pointer.size(), core::mem::size_of::()); + assert_eq!( + CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('i')], + size: 8, + } + .size(), + 8 + ); + assert_eq!(CTypeLayout::Opaque { size: 5 }.size(), 5); + assert_eq!( + CTypeLayout::Array { + element: Box::new(CTypeLayout::Simple('i')), + length: 3, + size: 12, + } + .size(), + 12 + ); + } + + #[test] + fn union_layout_reports_size_and_lowers() { + let layout = CTypeLayout::Union { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('d')], + size: 8, + }; + assert_eq!(layout.size(), 8); + assert!(layout.to_ffi_type().is_ok()); + } + + #[test] + fn null_addr_is_error() { + let result = call(0, &[], CallRet::Void, CallOptions::default()); + assert_eq!(result.err(), Some(CallError::NullFunctionPointer)); + } + + #[test] + fn unknown_arg_code_is_error() { + let addr = noop as *const () as usize; + let result = call( + addr, + &[CallArg::Typed { + code: "@", + buffer: &[], + }], + CallRet::Void, + CallOptions::default(), + ); + assert_eq!( + result.err(), + Some(CallError::UnknownTypeCode("@".to_string())) + ); + } + + #[test] + fn unknown_return_code_is_error() { + let addr = noop as *const () as usize; + let result = call(addr, &[], CallRet::Code("@"), CallOptions::default()); + assert_eq!( + result.err(), + Some(CallError::UnknownTypeCode("@".to_string())) + ); + } + + #[test] + fn short_aggregate_buffer_is_error() { + let addr = sum_pair as *const () as usize; + let layout = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('i')], + size: 8, + }; + let buffer = [0u8; 4]; + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("i"), + CallOptions::default(), + ); + assert_eq!( + result.err(), + Some(CallError::BufferTooSmall { + expected: 8, + got: 4, + }) + ); + } + + // EINVAL: a valid errno value on all unix targets, so it round-trips + // through crate::os::set_errno/get_errno. + #[cfg(not(windows))] + const ERRNO_MARKER: i32 = 22; + + #[cfg(not(windows))] + extern "C" fn write_errno_marker() -> i32 { + crate::os::set_errno(ERRNO_MARKER); + 7 + } + + #[cfg(not(windows))] + #[test] + fn errno_swap_window_captures_and_restores() { + // Distinguish the real platform errno from the ctypes-local one. + crate::os::set_errno(11); + super::super::CTYPES_LOCAL_ERRNO.with(|e| e.set(99)); + let result = call( + write_errno_marker as *const () as usize, + &[], + CallRet::Code("i"), + CallOptions { + use_errno: true, + use_last_error: false, + }, + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(7) + )); + // The function's errno write landed in the ctypes-local slot... + assert_eq!( + super::super::CTYPES_LOCAL_ERRNO.with(|e| e.get()), + ERRNO_MARKER + ); + // ...and the real errno was restored to its pre-call value. + assert_eq!(crate::os::get_errno(), 11); + } + } +} diff --git a/crates/vm/src/stdlib/_ctypes.rs b/crates/vm/src/stdlib/_ctypes.rs index 6370bc42b3d..adf047ec750 100644 --- a/crates/vm/src/stdlib/_ctypes.rs +++ b/crates/vm/src/stdlib/_ctypes.rs @@ -16,7 +16,7 @@ use crate::{ }; pub(super) use array::PyCArray; -pub(super) use base::{FfiArgValue, PyCData, PyCField, StgInfo, StgInfoFlags}; +pub(super) use base::{CArgValue, PyCData, PyCField, StgInfo, StgInfoFlags}; pub(super) use pointer::PyCPointer; pub(super) use simple::{PyCSimple, PyCSimpleType}; pub(super) use structure::PyCStructure; @@ -107,10 +107,13 @@ pub(crate) mod _ctypes { pub(crate) struct CArgObject { /// Type tag ('P', 'V', 'i', 'd', etc.) pub tag: u8, - /// The actual FFI value (mirrors union value) - pub value: super::FfiArgValue, + /// The actual foreign-call value (mirrors union value) + pub value: super::CArgValue, /// Reference to original object (for memory safety) pub obj: PyObjectRef, + /// Owner keeping a `Pointer` value's target memory alive (e.g. a + /// null-terminated buffer copy created by `from_param`), if any. + pub keep: Option, /// Size for struct/union ('V' tag) #[allow(dead_code)] pub size: usize, @@ -126,72 +129,68 @@ pub(crate) mod _ctypes { impl Representable for CArgObject { // PyCArg_repr - use tag and value fields directly fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { - use super::base::FfiArgValue; + use rustpython_host_env::ctypes::{FfiValue, ffi_value_from_type_code}; let tag_char = zelf.tag as char; + // Reconstruct the scalar the value lowers to, so the formatting + // matches the value passed to the foreign call exactly. + let ffi_val = match &zelf.value { + super::CArgValue::Typed { code, bytes } => { + let mut buf = [0u8; 4]; + ffi_value_from_type_code(code.encode_utf8(&mut buf), bytes) + } + super::CArgValue::Int(v) => FfiValue::I32(*v), + super::CArgValue::Double(v) => FfiValue::F64(*v), + super::CArgValue::Pointer(v) => FfiValue::Pointer(*v), + // 'V' aggregates format via the object-address default arm below. + super::CArgValue::Aggregate { .. } => FfiValue::Pointer(0), + }; + // Format value based on tag match zelf.tag { b'b' | b'h' | b'i' | b'l' | b'q' => { // Signed integers - let n = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::I8(v)) => { - v as i64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::I16(v)) => { - v as i64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::I32(v)) => { - v as i64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::I64(v)) => v, + let n = match ffi_val { + FfiValue::I8(v) => v as i64, + FfiValue::I16(v) => v as i64, + FfiValue::I32(v) => v as i64, + FfiValue::I64(v) => v, _ => 0, }; Ok(format!("")) } b'B' | b'H' | b'I' | b'L' | b'Q' => { // Unsigned integers - let n = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::U8(v)) => { - v as u64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::U16(v)) => { - v as u64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::U32(v)) => { - v as u64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::U64(v)) => v, + let n = match ffi_val { + FfiValue::U8(v) => v as u64, + FfiValue::U16(v) => v as u64, + FfiValue::U32(v) => v as u64, + FfiValue::U64(v) => v, _ => 0, }; Ok(format!("")) } b'f' => { - let v = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::F32(v)) => { - v as f64 - } + let v = match ffi_val { + FfiValue::F32(v) => v as f64, _ => 0.0, }; Ok(format!("")) } b'd' | b'g' => { - let v = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::F64(v)) => v, - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::F32(v)) => { - v as f64 - } + let v = match ffi_val { + FfiValue::F64(v) => v, + FfiValue::F32(v) => v as f64, _ => 0.0, }; Ok(format!("")) } b'c' => { // c_char - single byte - let byte = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::I8(v)) => { - v as u8 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::U8(v)) => v, + let byte = match ffi_val { + FfiValue::I8(v) => v as u8, + FfiValue::U8(v) => v, _ => 0, }; if is_literal_char(byte) { @@ -200,11 +199,10 @@ pub(crate) mod _ctypes { Ok(format!("")) } } - b'z' | b'Z' | b'P' | b'V' => { + b'z' | b'Z' | b'P' => { // Pointer types - let ptr = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::Pointer(v)) => v, - FfiArgValue::OwnedPointer(v, _) => v, + let ptr = match ffi_val { + FfiValue::Pointer(v) => v, _ => 0, }; if ptr == 0 { @@ -600,7 +598,7 @@ pub(crate) mod _ctypes { offset: OptionalArg, vm: &VirtualMachine, ) -> PyResult { - use super::FfiArgValue; + use super::CArgValue; // Check if obj is a ctypes instance if !obj.fast_isinstance(PyCData::static_type()) @@ -628,8 +626,9 @@ pub(crate) mod _ctypes { // Create CArgObject to hold the reference Ok(CArgObject { tag: b'P', - value: FfiArgValue::pointer(ptr_val), + value: CArgValue::pointer(ptr_val), obj, + keep: None, size: 0, offset: offset_val, } diff --git a/crates/vm/src/stdlib/_ctypes/base.rs b/crates/vm/src/stdlib/_ctypes/base.rs index 62856c4cef8..1cc84750cb5 100644 --- a/crates/vm/src/stdlib/_ctypes/base.rs +++ b/crates/vm/src/stdlib/_ctypes/base.rs @@ -18,8 +18,8 @@ use num_traits::{Signed, ToPrimitive}; use rustpython_common::lock::PyRwLock; use rustpython_common::wtf8::Wtf8; use rustpython_host_env::ctypes::{ - CTypeParamKind, FfiArg, FfiType, FfiValue, char_array_assignment_bytes, char_array_field_value, - ffi_arg_from_value, ffi_type_for_layout, wchar_array_field_value, write_cow_bytes_at_offset, + CTypeLayout, char_array_assignment_bytes, char_array_field_value, wchar_array_field_value, + write_cow_bytes_at_offset, }; // StgInfo - Storage information for ctypes types @@ -99,8 +99,9 @@ pub struct StgInfo { // Byte order (for _swappedbytes_) pub big_endian: bool, // true if big endian, false if little endian - // FFI field types for structure/union passing (inherited from base class) - pub ffi_field_types: Vec, + // Call layouts of the struct/union fields, in declaration order (inherited + // from base class). Drives by-value aggregate passing. + pub field_layouts: Vec, // Cached pointer type (non-inheritable via descriptor) pub pointer_type: Option, @@ -127,7 +128,7 @@ impl core::fmt::Debug for StgInfo { .field("shape", &self.shape) .field("paramfunc", &self.paramfunc) .field("big_endian", &self.big_endian) - .field("ffi_field_types", &self.ffi_field_types.len()) + .field("field_layouts", &self.field_layouts.len()) .finish() } } @@ -147,7 +148,7 @@ impl Default for StgInfo { shape: Vec::new(), paramfunc: ParamFunc::None, big_endian: cfg!(target_endian = "big"), // native endian by default - ffi_field_types: Vec::new(), + field_layouts: Vec::new(), pointer_type: None, } } @@ -168,7 +169,7 @@ impl StgInfo { shape: Vec::new(), paramfunc: ParamFunc::None, big_endian: cfg!(target_endian = "big"), // native endian by default - ffi_field_types: Vec::new(), + field_layouts: Vec::new(), pointer_type: None, } } @@ -220,30 +221,11 @@ impl StgInfo { shape, paramfunc: ParamFunc::Array, big_endian: cfg!(target_endian = "big"), // native endian by default - ffi_field_types: Vec::new(), + field_layouts: Vec::new(), pointer_type: None, } } - /// Get libffi type for this StgInfo - /// Note: For very large types, returns pointer type to avoid overflow - pub fn to_ffi_type(&self) -> FfiType { - let kind = match self.paramfunc { - ParamFunc::Structure => CTypeParamKind::Structure, - ParamFunc::Union => CTypeParamKind::Union, - ParamFunc::Array => CTypeParamKind::Array, - ParamFunc::Pointer => CTypeParamKind::Pointer, - _ => CTypeParamKind::Simple, - }; - ffi_type_for_layout( - kind, - &self.ffi_field_types, - self.size, - self.length, - self.format.as_deref(), - ) - } - /// Check if this type is finalized (cannot set _fields_ again) pub fn is_final(&self) -> bool { self.flags.contains(StgInfoFlags::DICTFLAG_FINAL) @@ -255,6 +237,44 @@ impl StgInfo { } } +/// Build the host_env call layout for a ctypes type from its already-borrowed +/// `StgInfo`. Aggregate layouts come straight from the type's `field_layouts` +/// (built incrementally from the base class, so struct inheritance is +/// reflected); array elements recurse into the element type; simple types read +/// their `_type_` code. The caller passes the borrowed `stg` so this never +/// re-locks `ty`'s own type data. +pub(super) fn type_layout(ty: &Py, stg: &StgInfo, vm: &VirtualMachine) -> CTypeLayout { + match stg.paramfunc { + ParamFunc::Structure => CTypeLayout::Struct { + fields: stg.field_layouts.clone(), + size: stg.size, + }, + ParamFunc::Union => CTypeLayout::Union { + fields: stg.field_layouts.clone(), + size: stg.size, + }, + ParamFunc::Array => { + let element = stg + .element_type + .as_ref() + .and_then(|et| et.stg_info_opt().map(|et_stg| type_layout(et, &et_stg, vm))) + .unwrap_or(CTypeLayout::Opaque { + size: stg.element_size, + }); + CTypeLayout::Array { + element: Box::new(element), + length: stg.length, + size: stg.size, + } + } + ParamFunc::Pointer => CTypeLayout::Pointer, + ParamFunc::Simple | ParamFunc::None => ty + .type_code(vm) + .and_then(|code| code.chars().next()) + .map_or(CTypeLayout::Opaque { size: stg.size }, CTypeLayout::Simple), + } +} + /// __pointer_type__ getter for ctypes metaclasses. /// Reads from StgInfo.pointer_type (non-inheritable). pub(super) fn pointer_type_get(zelf: &Py, vm: &VirtualMachine) -> PyResult { @@ -1828,12 +1848,12 @@ fn simple_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult // Read value from buffer: memcpy(&parg->value, self->b_ptr, self->b_size) let buffer = simple.0.buffer.read(); - let ffi_value = buffer_to_ffi_value(&type_code, &buffer); Ok(CArgObject { tag, - value: ffi_value, + value: CArgValue::typed(tag as char, &buffer), obj: obj.to_owned(), + keep: None, size: 0, offset: 0, }) @@ -1853,8 +1873,9 @@ fn array_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult Ok(CArgObject { tag: b'P', - value: FfiArgValue::pointer(ptr_val), + value: CArgValue::pointer(ptr_val), obj: obj.to_owned(), + keep: None, size: 0, offset: 0, }) @@ -1873,8 +1894,9 @@ fn pointer_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult PyResult CArgObject { - // Get buffer pointer - // For large structs (> sizeof(void*)), we'd need to allocate and copy. - // For now, just point to buffer directly and keep obj reference for memory safety. - let buffer = if let Some(cdata) = obj.downcast_ref::() { - cdata.buffer.read() + // Snapshot the instance bytes and pass the aggregate by value. The layout + // is built here from the already-borrowed `stg_info` to avoid re-locking. + let (bytes, size) = if let Some(cdata) = obj.downcast_ref::() { + let buffer = cdata.buffer.read(); + (buffer.to_vec(), buffer.len()) } else { - return CArgObject { - tag: b'V', - value: FfiArgValue::pointer(0), - obj: obj.to_owned(), - size: stg_info.size, - offset: 0, - }; + (Vec::new(), stg_info.size) }; - let ptr_val = buffer.as_ptr() as usize; - let size = buffer.len(); + let layout = if matches!(stg_info.paramfunc, ParamFunc::Union) { + CTypeLayout::Union { + fields: stg_info.field_layouts.clone(), + size: stg_info.size, + } + } else { + CTypeLayout::Struct { + fields: stg_info.field_layouts.clone(), + size: stg_info.size, + } + }; CArgObject { tag: b'V', - value: FfiArgValue::pointer(ptr_val), + value: CArgValue::aggregate(layout, bytes), obj: obj.to_owned(), + keep: None, size, offset: 0, } } -// FfiArgValue - Owned FFI argument value +// CArgValue - Owned foreign-call argument value -/// Owned FFI argument value. Keeps the value alive for the duration of the FFI call. +/// A foreign-call argument in a form the unified `call` entry point accepts: a +/// simple-typed scalar (its ctypes code plus a native-endian bytes snapshot), +/// an untyped int/float, or an address. Any object whose memory an address +/// refers to is kept alive by the enclosing `Argument`/`CArgObject`, not here. #[derive(Debug, Clone)] -pub enum FfiArgValue { - Scalar(FfiValue), - /// Pointer with owned data. The PyObjectRef keeps the pointed data alive. - OwnedPointer(usize, #[allow(dead_code)] PyObjectRef), +pub enum CArgValue { + /// A value typed by its ctypes simple-type code, snapshotted as its bytes. + Typed { code: char, bytes: Vec }, + /// Untyped Python int (ConvParam default: C int). + Int(i32), + /// Untyped Python float (ConvParam default: C double). + Double(f64), + /// Address-valued argument (pointer decay, byref, buffer copies, NULL = 0). + Pointer(usize), + /// By-value aggregate: its call layout plus a snapshot of its bytes. + Aggregate { layout: CTypeLayout, bytes: Vec }, } -impl FfiArgValue { +impl CArgValue { pub fn pointer(value: usize) -> Self { - Self::Scalar(FfiValue::Pointer(value)) + Self::Pointer(value) } - /// Create an Arg reference to this owned value - pub fn as_arg(&self) -> FfiArg<'_> { - match self { - Self::Scalar(value) => ffi_arg_from_value(value), - Self::OwnedPointer(v, _) => rustpython_host_env::ctypes::ffi_arg( - rustpython_host_env::ctypes::FfiArgRef::Pointer(v), - ), + /// Snapshot a simple-typed value from its code and buffer bytes. + pub(super) fn typed(code: char, buffer: &[u8]) -> Self { + Self::Typed { + code, + bytes: buffer.to_vec(), } } -} -/// Convert buffer bytes to FfiArgValue based on type code -pub(super) fn buffer_to_ffi_value(type_code: &str, buffer: &[u8]) -> FfiArgValue { - FfiArgValue::Scalar(rustpython_host_env::ctypes::ffi_value_from_type_code( - type_code, buffer, - )) + /// Snapshot an aggregate value from its call layout and buffer bytes. + pub(super) fn aggregate(layout: CTypeLayout, bytes: Vec) -> Self { + Self::Aggregate { layout, bytes } + } + + /// Lower to a [`CallArg`], borrowing `code_buf` for the code's `&str`. + pub(super) fn as_call_arg<'a>( + &'a self, + code_buf: &'a mut [u8; 4], + ) -> rustpython_host_env::ctypes::CallArg<'a> { + use rustpython_host_env::ctypes::CallArg; + match self { + Self::Typed { code, bytes } => CallArg::Typed { + code: code.encode_utf8(code_buf), + buffer: bytes, + }, + Self::Int(value) => CallArg::Int(*value), + Self::Double(value) => CallArg::Double(*value), + Self::Pointer(value) => CallArg::Pointer(*value), + Self::Aggregate { layout, bytes } => CallArg::Aggregate { + layout, + buffer: bytes, + }, + } + } } /// Convert bytes to appropriate Python object based on ctypes type diff --git a/crates/vm/src/stdlib/_ctypes/function.rs b/crates/vm/src/stdlib/_ctypes/function.rs index 2cf3eda13e1..86b4ff59b3f 100644 --- a/crates/vm/src/stdlib/_ctypes/function.rs +++ b/crates/vm/src/stdlib/_ctypes/function.rs @@ -3,13 +3,13 @@ use super::{ _ctypes::CArgObject, - PyCArray, PyCData, PyCPointer, PyCStructure, StgInfo, - base::{CDATA_BUFFER_METHODS, FfiArgValue, ParamFunc, StgInfoFlags}, + PyCArray, PyCData, PyCPointer, PyCStructure, PyCUnion, StgInfo, + base::{CArgValue, CDATA_BUFFER_METHODS, ParamFunc, StgInfoFlags}, simple::PyCSimple, }; use crate::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyBytes, PyDict, PyNone, PyStr, PyTuple, PyType, PyTypeRef}, + builtins::{PyBytes, PyDict, PyStr, PyTuple, PyType, PyTypeRef}, class::StaticType, function::FuncArgs, protocol::{BufferDescriptor, PyBuffer, PyNumberMethods}, @@ -25,11 +25,10 @@ use rustpython_common::lock::PyRwLock; #[cfg(windows)] use rustpython_host_env::ctypes::ComMethodError; use rustpython_host_env::ctypes::{ - CallResult as RawResult, FfiCif, FfiCodePtr, FfiType, FfiValue, RawMemoryView, - RawMemoryViewError, StringAtError, ffi_f64_type, ffi_i32_type, ffi_pointer_type, - ffi_type_for_return_size, ffi_type_from_code, ffi_type_from_tag, ffi_void_type, - has_pointer_width, null_code_ptr, offset_address, pointer_bytes, pointer_format, pointer_size, - write_pointer_to_buffer_at, write_prefix_limited, + CTypeLayout, CallError, CallOptions, CallRet, CallValue, FfiCif, FfiCodePtr, FfiType, + RawMemoryView, RawMemoryViewError, StringAtError, call, ffi_pointer_type, ffi_type_from_code, + ffi_void_type, has_pointer_width, offset_address, pointer_bytes, pointer_format, pointer_size, + simple_type_is_pointer, write_pointer_to_buffer_at, write_prefix_limited, }; // Internal function addresses for special ctypes functions @@ -42,7 +41,7 @@ pub(super) const INTERNAL_MEMORYVIEW_AT_ADDR: usize = 4; /// Convert any object to a pointer value for c_void_p arguments /// Follows ConvParam logic for pointer types -fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult { +fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult { // 0. CArgObject (from byref()) -> buffer address + offset if let Some(carg) = value.downcast_ref::() { // Get buffer address from the underlying object @@ -55,29 +54,29 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult NULL if value.is(&vm.ctx.none) { - return Ok(FfiArgValue::pointer(0)); + return Ok(CArgValue::pointer(0)); } // 2. PyCArray -> buffer address (PyCArrayType_paramfunc) if let Some(array) = value.downcast_ref::() { let addr = array.0.buffer.read().as_ptr() as usize; - return Ok(FfiArgValue::pointer(addr)); + return Ok(CArgValue::pointer(addr)); } // 3. PyCPointer -> stored pointer value if let Some(ptr) = value.downcast_ref::() { - return Ok(FfiArgValue::pointer(ptr.get_ptr_value())); + return Ok(CArgValue::pointer(ptr.get_ptr_value())); } // 4. PyCStructure -> buffer address if let Some(struct_obj) = value.downcast_ref::() { let addr = struct_obj.0.buffer.read().as_ptr() as usize; - return Ok(FfiArgValue::pointer(addr)); + return Ok(CArgValue::pointer(addr)); } // 5. PyCSimple (c_void_p, c_char_p, etc.) -> value from buffer @@ -85,14 +84,14 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult buffer address (PyBytes_AsString) if let Some(bytes) = value.downcast_ref::() { let addr = bytes.as_bytes().as_ptr() as usize; - return Ok(FfiArgValue::pointer(addr)); + return Ok(CArgValue::pointer(addr)); } // 7. Integer -> direct value (PyLong_AsVoidPtr behavior) @@ -101,10 +100,10 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult PyResult PyResult { - // 1. CArgObject (from byref() or paramfunc) -> use stored type and value + // 1. CArgObject (from byref() or paramfunc) -> use stored value if let Some(carg) = value.downcast_ref::() { - let ffi_type = ffi_type_from_tag(carg.tag); return Ok(Argument { - ffi_type, - keep: None, + keep: carg.keep.clone(), value: carg.value.clone(), }); } @@ -137,18 +134,15 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { // 2. None -> NULL pointer if value.is(&vm.ctx.none) { return Ok(Argument { - ffi_type: ffi_pointer_type(), keep: None, - value: FfiArgValue::pointer(0), + value: CArgValue::pointer(0), }); } // 3. ctypes objects -> use paramfunc if let Ok(carg) = super::base::call_paramfunc(value, vm) { - let ffi_type = ffi_type_from_tag(carg.tag); return Ok(Argument { - ffi_type, - keep: None, + keep: carg.keep, value: carg.value, }); } @@ -159,9 +153,8 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { let keep = vm.ctx.new_bytes(wide_bytes); let addr = keep.as_bytes().as_ptr() as usize; return Ok(Argument { - ffi_type: ffi_pointer_type(), keep: Some(keep.into()), - value: FfiArgValue::pointer(addr), + value: CArgValue::pointer(addr), }); } @@ -172,9 +165,8 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { let keep = vm.ctx.new_bytes(buffer); let addr = keep.as_bytes().as_ptr() as usize; return Ok(Argument { - ffi_type: ffi_pointer_type(), keep: Some(keep.into()), - value: FfiArgValue::pointer(addr), + value: CArgValue::pointer(addr), }); } @@ -182,18 +174,16 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { if let Ok(int_val) = value.try_int(vm) { let val = int_val.as_bigint().to_i32().unwrap_or(0); return Ok(Argument { - ffi_type: ffi_i32_type(), keep: None, - value: FfiArgValue::Scalar(FfiValue::I32(val)), + value: CArgValue::Int(val), }); } // 11. Python float -> f64 if let Ok(float_val) = value.try_float(vm) { return Ok(Argument { - ffi_type: ffi_f64_type(), keep: None, - value: FfiArgValue::Scalar(FfiValue::F64(float_val.to_f64())), + value: CArgValue::Double(float_val.to_f64()), }); } @@ -209,47 +199,47 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { } trait ArgumentType { - fn to_ffi_type(&self, vm: &VirtualMachine) -> PyResult; - fn convert_object(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult; + /// Convert an argument for this type into a foreign-call value plus an + /// optional owner keeping any referenced memory alive. + fn convert_object( + &self, + value: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<(CArgValue, Option)>; } impl ArgumentType for PyTypeRef { - fn to_ffi_type(&self, vm: &VirtualMachine) -> PyResult { - use super::pointer::PyCPointer; - use super::structure::PyCStructure; - - // CArgObject (from byref()) should be treated as pointer - if self.fast_issubclass(CArgObject::static_type()) { - return Ok(ffi_pointer_type()); - } - - // Pointer types (POINTER(T)) are always pointer FFI type - // Check if type is a subclass of _Pointer (PyCPointer) - if self.fast_issubclass(PyCPointer::static_type()) { - return Ok(ffi_pointer_type()); - } - - // Structure types are passed as pointers - if self.fast_issubclass(PyCStructure::static_type()) { - return Ok(ffi_pointer_type()); - } + fn convert_object( + &self, + value: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<(CArgValue, Option)> { + // Validate the argument type up front (mirrors the pre-conversion + // check): pointer-like ctypes types are always acceptable; a simple + // type must carry a known _type_ code; anything else is unsupported. + let type_code = if self.fast_issubclass(CArgObject::static_type()) + || self.fast_issubclass(PyCPointer::static_type()) + || self.fast_issubclass(PyCStructure::static_type()) + || self.fast_issubclass(PyCUnion::static_type()) + { + None + } else { + // Use get_attr to traverse MRO (for subclasses like MyInt(c_int)) + let typ = self + .as_object() + .get_attr(vm.ctx.intern_str("_type_"), vm) + .ok() + .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?; + let typ = typ + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("Unsupported argument type"))? + .to_string(); + if ffi_type_from_code(&typ).is_none() { + return Err(vm.new_type_error(format!("Unsupported argument type: {typ}"))); + } + Some(typ) + }; - // Use get_attr to traverse MRO (for subclasses like MyInt(c_int)) - let typ = self - .as_object() - .get_attr(vm.ctx.intern_str("_type_"), vm) - .ok() - .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?; - let typ = typ - .downcast_ref::() - .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?; - let typ = typ.to_string(); - let typ = typ.as_str(); - ffi_type_from_code(typ) - .ok_or_else(|| vm.new_type_error(format!("Unsupported argument type: {typ}"))) - } - - fn convert_object(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult { // Call from_param first to convert the value // converter = PyTuple_GET_ITEM(argtypes, i); // v = PyObject_CallOneArg(converter, arg); @@ -259,88 +249,62 @@ impl ArgumentType for PyTypeRef { let converted = from_param.call((value,), vm)?; // Then pass the converted value to ConvParam logic - // CArgObject (from from_param) -> use stored value directly + // CArgObject (from from_param) -> use stored value and keepalive directly if let Some(carg) = converted.downcast_ref::() { - return Ok(carg.value.clone()); + return Ok((carg.value.clone(), carg.keep.clone())); } // None -> NULL pointer if vm.is_none(&converted) { - return Ok(FfiArgValue::pointer(0)); + return Ok((CArgValue::pointer(0), None)); } // For pointer types (POINTER(T)), we need to pass the pointer VALUE stored in buffer if self.fast_issubclass(PyCPointer::static_type()) { if let Some(pointer) = converted.downcast_ref::() { - return Ok(FfiArgValue::pointer(pointer.get_ptr_value())); + return Ok((CArgValue::pointer(pointer.get_ptr_value()), None)); } - return convert_to_pointer(&converted, vm); + return Ok((convert_to_pointer(&converted, vm)?, None)); } - // For structure types, convert to pointer to structure - if self.fast_issubclass(PyCStructure::static_type()) { - return convert_to_pointer(&converted, vm); + // For structure/union types, pass the aggregate by value: snapshot the + // instance bytes and build its call layout from the argtype. A byref() + // result is a CArgObject and was already handled above (stays a pointer). + if self.fast_issubclass(PyCStructure::static_type()) + || self.fast_issubclass(PyCUnion::static_type()) + { + if let Some(cdata) = converted.downcast_ref::() { + let bytes = cdata.buffer.read().to_vec(); + let layout = self.stg_info_opt().map_or_else( + || CTypeLayout::Opaque { size: bytes.len() }, + |stg| super::base::type_layout(self, &stg, vm), + ); + // Keep the converted instance alive through the call: the + // snapshot may embed pointers into buffers its keep-alive set + // owns, which must outlive the foreign call. + return Ok((CArgValue::aggregate(layout, bytes), Some(converted.clone()))); + } + return Ok((convert_to_pointer(&converted, vm)?, None)); } - // Get the type code for this argument type - let type_code = self - .as_object() - .get_attr(vm.ctx.intern_str("_type_"), vm) - .ok() - .and_then(|t| t.downcast_ref::().map(|s| s.to_string())); - // For pointer types (c_void_p, c_char_p, c_wchar_p), handle as pointer if matches!(type_code.as_deref(), Some("P" | "z" | "Z")) { - return convert_to_pointer(&converted, vm); + return Ok((convert_to_pointer(&converted, vm)?, None)); } // PyCSimple (already a ctypes instance from from_param) if let Ok(simple) = converted.downcast::() { - let typ = ArgumentType::to_ffi_type(self, vm)?; - let ffi_value = simple - .to_ffi_value(typ, vm) + let code = type_code + .as_deref() + .and_then(|s| s.chars().next()) .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?; - return Ok(ffi_value); + return Ok((simple.to_carg_value(code), None)); } Err(vm.new_type_error("Unsupported argument type")) } } -trait ReturnType { - fn to_ffi_type(&self, vm: &VirtualMachine) -> Option; -} - -impl ReturnType for PyTypeRef { - fn to_ffi_type(&self, vm: &VirtualMachine) -> Option { - // Try to get _type_ attribute first (for ctypes types like c_void_p) - if let Ok(type_attr) = self.as_object().get_attr(vm.ctx.intern_str("_type_"), vm) - && let Some(s) = type_attr.downcast_ref::() - && let Some(ffi_type) = s.to_str().and_then(ffi_type_from_code) - { - return Some(ffi_type); - } - - // Check for Structure/Array types (have StgInfo but no _type_) - // _ctypes_get_ffi_type: returns appropriately sized type for struct returns - if let Some(stg_info) = self.stg_info_opt() { - let size = stg_info.size; - // Small structs can be returned in registers - // Match can_return_struct_as_int/can_return_struct_as_sint64 - return Some(ffi_type_for_return_size(size)); - } - - // Fallback to class name - ffi_type_from_code(self.name().to_string().as_str()) - } -} - -impl ReturnType for PyNone { - fn to_ffi_type(&self, _vm: &VirtualMachine) -> Option { - ffi_type_from_code("void") - } -} - // PyCFuncPtrType - Metaclass for function pointer types // PyCFuncPtrType_init @@ -676,12 +640,6 @@ impl PyCFuncPtr { rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer) } - /// Get CodePtr from buffer for FFI calls - fn get_code_ptr(&self) -> Option { - let addr = self.get_func_ptr(); - rustpython_host_env::ctypes::code_ptr_from_addr(addr) - } - /// Create buffer with function pointer address fn make_ptr_buffer(addr: usize) -> Vec { pointer_bytes(addr) @@ -961,13 +919,86 @@ fn handle_internal_func(addr: usize, args: &FuncArgs, vm: &VirtualMachine) -> Op None } +/// How the foreign call's return value is retrieved (mirrors `CallRet`). +enum RetSpec { + /// restype is None: void return. + Void, + /// Pointer-valued return (a `TYPEFLAG_ISPOINTER` restype, or an oversized + /// by-value struct approximated as a pointer-sized register). + Pointer, + /// A scalar retrieved as the given ctypes simple-type code. + Code(char), + /// A by-value aggregate (struct/union) return with the given call layout. + Aggregate(CTypeLayout), +} + /// Call information extracted from PyCFuncPtr (argtypes, restype, etc.) struct CallInfo { explicit_arg_types: Option>, restype_obj: Option, + ret: RetSpec, +} + +/// Determine how to retrieve the return value from restype, reproducing the +/// prior `ffi_return_type` + `is_pointer_return` dispatch. +fn compute_ret_spec( restype_is_none: bool, - ffi_return_type: FfiType, - is_pointer_return: bool, + restype_obj: Option<&PyObjectRef>, + vm: &VirtualMachine, +) -> RetSpec { + if restype_is_none { + return RetSpec::Void; + } + let Some(restype_type) = restype_obj.and_then(|t| t.clone().downcast::().ok()) else { + return RetSpec::Code('i'); + }; + + // Pointer return via TYPEFLAG_ISPOINTER (c_void_p, c_char_p, c_wchar_p, POINTER(T)) + if restype_type + .stg_info_opt() + .is_some_and(|info| info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER)) + { + return RetSpec::Pointer; + } + + // Simple type via its _type_ code (traversing MRO) + if let Ok(type_attr) = restype_type + .as_object() + .get_attr(vm.ctx.intern_str("_type_"), vm) + && let Some(s) = type_attr.downcast_ref::() + && let Some(code) = s.to_str() + && ffi_type_from_code(code).is_some() + { + return if simple_type_is_pointer(code) { + RetSpec::Pointer + } else { + RetSpec::Code(code.chars().next().unwrap_or('i')) + }; + } + + // Structure/Union (StgInfo, no _type_): returned by value as an aggregate. + // The layout is built from the held guard to avoid re-locking the type. + if let Some(stg_info) = restype_type.stg_info_opt() { + return match stg_info.paramfunc { + ParamFunc::Structure | ParamFunc::Union => { + RetSpec::Aggregate(super::base::type_layout(&restype_type, &stg_info, vm)) + } + // Any other aggregate-ish StgInfo without a code: size-approximated + // register return, as before. + _ => { + let size = stg_info.size; + if size <= 4 { + RetSpec::Code('i') + } else if size <= 8 { + RetSpec::Code('q') + } else { + RetSpec::Pointer + } + } + }; + } + + RetSpec::Code('i') } /// Extract call information (argtypes, restype) from PyCFuncPtr @@ -1012,33 +1043,12 @@ fn extract_call_info(zelf: &Py, vm: &VirtualMachine) -> PyResult().ok()) - .and_then(|t| ReturnType::to_ffi_type(&t, vm)) - .unwrap_or_else(ffi_i32_type) - }; - - // Check if return type is a pointer type via TYPEFLAG_ISPOINTER - // This handles c_void_p, c_char_p, c_wchar_p, and POINTER(T) types - let is_pointer_return = restype_obj - .as_ref() - .and_then(|t| t.clone().downcast::().ok()) - .and_then(|t| { - t.stg_info_opt() - .map(|info| info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER)) - }) - .unwrap_or(false); + let ret = compute_ret_spec(restype_is_none, restype_obj.as_ref(), vm); Ok(CallInfo { explicit_arg_types, restype_obj, - restype_is_none, - ffi_return_type, - is_pointer_return, + ret, }) } @@ -1135,8 +1145,7 @@ fn resolve_com_method( /// Single argument for FFI call // struct argument struct Argument { - ffi_type: FfiType, - value: FfiArgValue, + value: CArgValue, #[allow(dead_code)] keep: Option, // Object to keep alive during call } @@ -1198,13 +1207,8 @@ fn build_callargs_simple( let arg_type = arg_types .get(n) .ok_or_else(|| vm.new_type_error("argument amount mismatch"))?; - let ffi_type = ArgumentType::to_ffi_type(arg_type, vm)?; - let value = arg_type.convert_object(arg.clone(), vm)?; - Ok(Argument { - ffi_type, - keep: None, - value, - }) + let (value, keep) = arg_type.convert_object(arg.clone(), vm)?; + Ok(Argument { value, keep }) }) .collect::>>()?; Ok((arguments, Vec::new())) @@ -1241,17 +1245,14 @@ fn build_callargs_with_paramflags( let is_out = (*direction & 2) != 0; // OUT flag let is_in = (*direction & 1) != 0 || *direction == 0; // IN flag or default - let ffi_type = ArgumentType::to_ffi_type(arg_type, vm)?; - if is_out && !is_in { // Pure OUT parameter: create buffer, don't consume caller arg let buffer = create_out_buffer(arg_type, vm)?; let addr = get_buffer_addr(&buffer) .ok_or_else(|| vm.new_type_error("Cannot create OUT buffer for this type"))?; arguments.push(Argument { - ffi_type, keep: None, - value: FfiArgValue::pointer(addr), + value: CArgValue::pointer(addr), }); out_buffers.push((param_idx, buffer)); } else { @@ -1269,12 +1270,8 @@ fn build_callargs_with_paramflags( // IN|OUT: track for return out_buffers.push((param_idx, arg.clone())); } - let value = arg_type.convert_object(arg, vm)?; - arguments.push(Argument { - ffi_type, - keep: None, - value, - }); + let (value, keep) = arg_type.convert_object(arg, vm)?; + arguments.push(Argument { value, keep }); } } @@ -1307,13 +1304,8 @@ fn build_callargs( let arg_type = arg_types .get(n) .ok_or_else(|| vm.new_type_error("argument amount mismatch"))?; - let ffi_type = ArgumentType::to_ffi_type(arg_type, vm)?; - let value = arg_type.convert_object(arg.clone(), vm)?; - arguments.push(Argument { - ffi_type, - keep: None, - value, - }); + let (value, keep) = arg_type.convert_object(arg.clone(), vm)?; + arguments.push(Argument { value, keep }); } Ok((arguments, Vec::new())) } else { @@ -1322,22 +1314,31 @@ fn build_callargs( } } -/// Execute FFI call +/// Execute the foreign call through the unified `call` entry point. fn ctypes_callproc( - code_ptr: FfiCodePtr, + addr: usize, arguments: &[Argument], - call_info: &CallInfo, -) -> RawResult { - let ffi_arg_types: Vec = arguments.iter().map(|a| a.ffi_type.clone()).collect(); - let ffi_args: Vec<_> = arguments.iter().map(|a| a.value.as_arg()).collect(); - rustpython_host_env::ctypes::callproc( - code_ptr, - ffi_arg_types, - call_info.ffi_return_type.clone(), - &ffi_args, - call_info.restype_is_none, - call_info.is_pointer_return, - ) + ret: &RetSpec, + options: CallOptions, +) -> Result { + // Encode each simple-type code into its own buffer so the `&str` borrowed + // by `CallArg::Typed` outlives the call. + let mut code_bufs = vec![[0u8; 4]; arguments.len()]; + let call_args: Vec<_> = arguments + .iter() + .zip(code_bufs.iter_mut()) + .map(|(arg, code_buf)| arg.value.as_call_arg(code_buf)) + .collect(); + + let mut ret_code_buf = [0u8; 4]; + let call_ret = match ret { + RetSpec::Void => CallRet::Void, + RetSpec::Pointer => CallRet::Pointer, + RetSpec::Code(code) => CallRet::Code(code.encode_utf8(&mut ret_code_buf)), + RetSpec::Aggregate(layout) => CallRet::Aggregate(layout), + }; + + call(addr, &call_args, call_ret, options) } /// Check and handle HRESULT errors (Windows) @@ -1375,26 +1376,38 @@ fn check_hresult(hresult: i32, zelf: &Py, vm: &VirtualMachine) -> Py } } -/// Convert raw FFI result to Python object +/// Convert the foreign-call result to a Python object // = GetResult fn convert_raw_result( - raw_result: &mut RawResult, + result: &CallValue, call_info: &CallInfo, vm: &VirtualMachine, ) -> Option { - // Get result as bytes for type conversion - let (result_bytes, result_size) = rustpython_host_env::ctypes::call_result_bytes(raw_result)?; + // Result register image as bytes + size (None for void): pointer/scalar + // returns are pointer/register sized. + let (result_bytes, result_size) = match result { + CallValue::Void => return None, + CallValue::Pointer(ptr) => (ptr.to_ne_bytes().to_vec(), size_of::()), + CallValue::Scalar(bytes) | CallValue::Aggregate(bytes) => (bytes.clone(), bytes.len()), + }; + + // Integer view of the return register, for the fallback branches below. + let result_word: usize = match result { + CallValue::Pointer(ptr) => *ptr, + CallValue::Scalar(bytes) | CallValue::Aggregate(bytes) => { + let mut word = [0u8; size_of::()]; + let n = bytes.len().min(word.len()); + word[..n].copy_from_slice(&bytes[..n]); + usize::from_ne_bytes(word) + } + CallValue::Void => 0, + }; // 1. No restype → return as int let restype = match &call_info.restype_obj { None => { // Default: return as int - let val = match raw_result { - RawResult::Pointer(p) => *p as isize, - RawResult::Value(v) => *v as isize, - RawResult::Void => return None, - }; - return Some(vm.ctx.new_int(val).into()); + return Some(vm.ctx.new_int(result_word as isize).into()); } Some(r) => r, }; @@ -1409,12 +1422,7 @@ fn convert_raw_result( Ok(t) => t, Err(_) => { // Not a type, call it with int result - let val = match raw_result { - RawResult::Pointer(p) => *p as isize, - RawResult::Value(v) => *v as isize, - RawResult::Void => return None, - }; - return restype.call((val,), vm).ok(); + return restype.call((result_word as isize,), vm).ok(); } }; @@ -1423,15 +1431,19 @@ fn convert_raw_result( // No StgInfo → call restype with int if stg_info.is_none() { - let val = match raw_result { - RawResult::Pointer(p) => *p as isize, - RawResult::Value(v) => *v as isize, - RawResult::Void => return None, - }; - return restype_type.as_object().call((val,), vm).ok(); + return restype_type + .as_object() + .call((result_word as isize,), vm) + .ok(); } let info = stg_info.unwrap(); + // Extract what's needed and release the read guard before constructing any + // instance below: instance construction write-locks the type's StgInfo (to + // finalize it), which would self-deadlock against a held read guard. + let is_pointer_type = info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER); + let has_proto = info.proto.is_some(); + drop(info); // py_object: interpret return value as PyObject* and materialize it. if let Ok(type_attr) = restype_type @@ -1440,12 +1452,7 @@ fn convert_raw_result( && let Some(type_str) = type_attr.downcast_ref::() && type_str.to_str() == Some("O") { - let ptr = match raw_result { - RawResult::Pointer(p) => *p, - RawResult::Value(v) => *v as usize, - RawResult::Void => 0, - }; - let ptr = NonNull::new(ptr as *mut PyObject).or_else(|| { + let ptr = NonNull::new(result_word as *mut PyObject).or_else(|| { vm.set_exception(Some(vm.new_value_error("PyObject is NULL"))); None })?; @@ -1469,9 +1476,9 @@ fn convert_raw_result( // This handles POINTER(T), Structure, Array, etc. // Special handling for POINTER(T) types - set pointer value directly - if info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER) - && info.proto.is_some() - && let RawResult::Pointer(ptr) = raw_result + if is_pointer_type + && has_proto + && let CallValue::Pointer(ptr) = result && let Ok(instance) = restype_type.as_object().call((), vm) { if let Some(pointer) = instance.downcast_ref::() { @@ -1516,7 +1523,7 @@ fn extract_out_values( /// Build final result (main function) fn build_result( - mut raw_result: RawResult, + call_result: CallValue, call_info: &CallInfo, out_buffers: OutBuffers, zelf: &Py, @@ -1525,19 +1532,22 @@ fn build_result( ) -> PyResult { // Check HRESULT on Windows #[cfg(windows)] - if let RawResult::Value(val) = raw_result { + if let CallValue::Scalar(bytes) = &call_result { let is_hresult = call_info .restype_obj .as_ref() .and_then(|t| t.clone().downcast::().ok()) .is_some_and(|t| t.name().to_string() == "HRESULT"); if is_hresult { - check_hresult(val as i32, zelf, vm)?; + let mut word = [0u8; size_of::()]; + let n = bytes.len().min(word.len()); + word[..n].copy_from_slice(&bytes[..n]); + check_hresult(usize::from_ne_bytes(word) as i32, zelf, vm)?; } } - // Convert raw result to Python object - let mut result = convert_raw_result(&mut raw_result, call_info, vm); + // Convert the foreign-call result to a Python object + let mut result = convert_raw_result(&call_result, call_info, vm); // Apply errcheck if set if let Some(errcheck) = zelf.errcheck.read().as_ref() { @@ -1583,44 +1593,34 @@ impl Callable for PyCFuncPtr { let (arguments, out_buffers) = build_callargs(&args, &call_info, paramflags.as_ref(), is_com_method, vm)?; - // 6. Get code pointer - let code_ptr = match func_ptr.or_else(|| zelf.get_code_ptr()) { - Some(cp) => cp, - None => { - debug_assert!(false, "NULL function pointer"); - // In release mode, this will crash - null_code_ptr() - } + // 6. Function address (usize); the unified `call` rejects a NULL address. + let addr = match func_ptr { + Some(cp) => cp.0 as usize, + None => zelf.get_func_ptr(), }; - // 7. Get flags to check for use_last_error/use_errno + // 7. Errno / last-error swap options from flags let flags = Self::_flags_(zelf, vm); - - // 8. Call the function (with use_last_error/use_errno handling) - #[cfg(not(windows))] - let raw_result = { - if flags & super::base::StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0 { - rustpython_host_env::ctypes::with_swapped_errno(|| { - ctypes_callproc(code_ptr, &arguments, &call_info) - }) - } else { - ctypes_callproc(code_ptr, &arguments, &call_info) - } + let options = CallOptions { + use_errno: flags & super::base::StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0, + use_last_error: flags & super::base::StgInfoFlags::FUNCFLAG_USE_LASTERROR.bits() != 0, }; - #[cfg(windows)] - let raw_result = { - if flags & super::base::StgInfoFlags::FUNCFLAG_USE_LASTERROR.bits() != 0 { - rustpython_host_env::ctypes::with_swapped_last_error(|| { - ctypes_callproc(code_ptr, &arguments, &call_info) - }) - } else { - ctypes_callproc(code_ptr, &arguments, &call_info) - } - }; + // 8. Call the function through the unified entry point. + let call_result = ctypes_callproc(addr, &arguments, &call_info.ret, options).map_err( + |err| match err { + CallError::NullFunctionPointer => vm.new_value_error("NULL function pointer"), + CallError::UnknownTypeCode(code) => { + vm.new_type_error(format!("Unsupported argument type: {code}")) + } + CallError::BufferTooSmall { expected, got } => vm.new_value_error(format!( + "argument buffer too small: expected {expected}, got {got}" + )), + }, + )?; // 9. Build result - build_result(raw_result, &call_info, out_buffers, zelf, &args, vm) + build_result(call_result, &call_info, out_buffers, zelf, &args, vm) } } diff --git a/crates/vm/src/stdlib/_ctypes/simple.rs b/crates/vm/src/stdlib/_ctypes/simple.rs index 122c23cc25c..c947e56010a 100644 --- a/crates/vm/src/stdlib/_ctypes/simple.rs +++ b/crates/vm/src/stdlib/_ctypes/simple.rs @@ -1,8 +1,7 @@ use super::_ctypes::CArgObject; use super::array::PyCArray; use super::base::{ - CDATA_BUFFER_METHODS, FfiArgValue, PyCData, StgInfo, StgInfoFlags, buffer_to_ffi_value, - bytes_to_pyobject, + CArgValue, CDATA_BUFFER_METHODS, PyCData, StgInfo, StgInfoFlags, bytes_to_pyobject, }; use super::function::PyCFuncPtr; use super::pointer::PyCPointer; @@ -263,11 +262,11 @@ impl PyCSimpleType { let simple_obj: PyObjectRef = simple.into_ref_with_type(vm, cls.clone())?.into(); // from_param returns CArgObject, not the simple type itself let tag = type_str.as_bytes().first().copied().unwrap_or(b'?'); - let ffi_value = buffer_to_ffi_value(type_str, &buffer_bytes); Ok(CArgObject { tag, - value: ffi_value, + value: CArgValue::typed(tag as char, &buffer_bytes), obj: simple_obj, + keep: None, size: 0, offset: 0, } @@ -319,8 +318,9 @@ impl PyCSimpleType { let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm); return Ok(CArgObject { tag: b'z', - value: FfiArgValue::OwnedPointer(ptr, kept_alive), + value: CArgValue::pointer(ptr), obj: value.clone(), + keep: Some(kept_alive), size: 0, offset: 0, } @@ -344,8 +344,9 @@ impl PyCSimpleType { let (holder, ptr) = super::base::str_to_wchar_bytes(s.as_wtf8(), vm); return Ok(CArgObject { tag: b'Z', - value: FfiArgValue::OwnedPointer(ptr, holder), + value: CArgValue::pointer(ptr), obj: value.clone(), + keep: Some(holder), size: 0, offset: 0, } @@ -373,8 +374,9 @@ impl PyCSimpleType { let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm); return Ok(CArgObject { tag: b'z', - value: FfiArgValue::OwnedPointer(ptr, kept_alive), + value: CArgValue::pointer(ptr), obj: value.clone(), + keep: Some(kept_alive), size: 0, offset: 0, } @@ -385,8 +387,9 @@ impl PyCSimpleType { let (holder, ptr) = super::base::str_to_wchar_bytes(s.as_wtf8(), vm); return Ok(CArgObject { tag: b'Z', - value: FfiArgValue::OwnedPointer(ptr, holder), + value: CArgValue::pointer(ptr), obj: value.clone(), + keep: Some(holder), size: 0, offset: 0, } @@ -412,8 +415,9 @@ impl PyCSimpleType { }; return Ok(CArgObject { tag: b'P', - value: FfiArgValue::pointer(ptr_val), + value: CArgValue::pointer(ptr_val), obj: value.clone(), + keep: None, size: 0, offset: 0, } @@ -429,8 +433,9 @@ impl PyCSimpleType { }; return Ok(CArgObject { tag: b'Z', - value: FfiArgValue::pointer(ptr_val), + value: CArgValue::pointer(ptr_val), obj: value.clone(), + keep: None, size: 0, offset: 0, } @@ -442,8 +447,9 @@ impl PyCSimpleType { Some("O") => { return Ok(CArgObject { tag: b'O', - value: FfiArgValue::pointer(value.get_id()), + value: CArgValue::pointer(value.get_id()), obj: value, + keep: None, size: 0, offset: 0, } @@ -1255,17 +1261,10 @@ impl PyCSimple { } impl PyCSimple { - /// Extract the value from this ctypes object as an owned FfiArgValue. - /// The value must be kept alive until after the FFI call completes. - pub(crate) fn to_ffi_value( - &self, - ty: rustpython_host_env::ctypes::FfiType, - _vm: &VirtualMachine, - ) -> Option { + /// Snapshot this object's buffer as a simple-typed foreign-call value. + pub(crate) fn to_carg_value(&self, code: char) -> CArgValue { let buffer = self.0.buffer.read(); - Some(FfiArgValue::Scalar( - rustpython_host_env::ctypes::ffi_value_from_type(&buffer, ty)?, - )) + CArgValue::typed(code, &buffer) } } diff --git a/crates/vm/src/stdlib/_ctypes/structure.rs b/crates/vm/src/stdlib/_ctypes/structure.rs index 96321cd7d55..39bb8a57413 100644 --- a/crates/vm/src/stdlib/_ctypes/structure.rs +++ b/crates/vm/src/stdlib/_ctypes/structure.rs @@ -269,14 +269,14 @@ impl PyCStructType { // Determine byte order for format string let big_endian = super::base::is_big_endian(is_swapped); - // Initialize offset, alignment, type flags, and ffi_field_types from base class + // Initialize offset, alignment, type flags, and field_layouts from base class let ( mut offset, mut max_align, mut has_pointer, mut has_union, mut has_bitfield, - mut ffi_field_types, + mut field_layouts, ) = { let bases = cls.bases.read(); if let Some(base) = bases.first() @@ -288,7 +288,7 @@ impl PyCStructType { baseinfo.flags.contains(StgInfoFlags::TYPEFLAG_HASPOINTER), baseinfo.flags.contains(StgInfoFlags::TYPEFLAG_HASUNION), baseinfo.flags.contains(StgInfoFlags::TYPEFLAG_HASBITFIELD), - baseinfo.ffi_field_types.clone(), + baseinfo.field_layouts.clone(), ) } else { (0, forced_alignment, false, false, false, Vec::new()) @@ -366,8 +366,8 @@ impl PyCStructType { if field_stg.flags.contains(StgInfoFlags::TYPEFLAG_HASBITFIELD) { has_bitfield = true; } - // Collect FFI type for this field - ffi_field_types.push(field_stg.to_ffi_type()); + // Collect the call layout for this field + field_layouts.push(super::base::type_layout(type_obj, &field_stg, vm)); } // Mark field type as finalized (using type as field finalizes it) @@ -552,8 +552,8 @@ impl PyCStructType { stg_info.paramfunc = super::base::ParamFunc::Structure; // Set byte order: swap if _swappedbytes_ is defined stg_info.big_endian = super::base::is_big_endian(is_swapped); - // Store FFI field types for structure passing - stg_info.ffi_field_types = ffi_field_types; + // Store field call layouts for by-value structure passing + stg_info.field_layouts = field_layouts; super::base::set_or_init_stginfo(cls, stg_info); // Process _anonymous_ fields diff --git a/crates/vm/src/stdlib/_ctypes/union.rs b/crates/vm/src/stdlib/_ctypes/union.rs index e0b4900cbd5..8d37178d587 100644 --- a/crates/vm/src/stdlib/_ctypes/union.rs +++ b/crates/vm/src/stdlib/_ctypes/union.rs @@ -184,9 +184,9 @@ impl PyCUnionType { let forced_alignment = super::base::get_usize_attr(cls.as_object(), "_align_", 1, vm)?.max(1); - // Initialize size, alignment, type flags, and ffi_field_types from base class + // Initialize size, alignment, type flags, and field_layouts from base class // Note: Union fields always start at offset 0, but we inherit base size/align - let (mut max_size, mut max_align, mut has_pointer, mut has_bitfield, mut ffi_field_types) = { + let (mut max_size, mut max_align, mut has_pointer, mut has_bitfield, mut field_layouts) = { let bases = cls.bases.read(); if let Some(base) = bases.first() && let Some(baseinfo) = base.stg_info_opt() @@ -196,7 +196,7 @@ impl PyCUnionType { core::cmp::max(baseinfo.align, forced_alignment), baseinfo.flags.contains(StgInfoFlags::TYPEFLAG_HASPOINTER), baseinfo.flags.contains(StgInfoFlags::TYPEFLAG_HASBITFIELD), - baseinfo.ffi_field_types.clone(), + baseinfo.field_layouts.clone(), ) } else { (0, forced_alignment, false, false, Vec::new()) @@ -256,8 +256,8 @@ impl PyCUnionType { if field_stg.flags.contains(StgInfoFlags::TYPEFLAG_HASBITFIELD) { has_bitfield = true; } - // Collect FFI type for this field - ffi_field_types.push(field_stg.to_ffi_type()); + // Collect the call layout for this field + field_layouts.push(super::base::type_layout(type_obj, &field_stg, vm)); } // Mark field type as finalized (using type as field finalizes it) @@ -345,8 +345,8 @@ impl PyCUnionType { stg_info.paramfunc = super::base::ParamFunc::Union; // Set byte order: swap if _swappedbytes_ is defined stg_info.big_endian = super::base::is_big_endian(is_swapped); - // Store FFI field types for union passing - stg_info.ffi_field_types = ffi_field_types; + // Store field call layouts for by-value union passing + stg_info.field_layouts = field_layouts; super::base::set_or_init_stginfo(cls, stg_info); // Process _anonymous_ fields diff --git a/extra_tests/snippets/stdlib_ctypes_byvalue.py b/extra_tests/snippets/stdlib_ctypes_byvalue.py new file mode 100644 index 00000000000..73d4b334506 --- /dev/null +++ b/extra_tests/snippets/stdlib_ctypes_byvalue.py @@ -0,0 +1,106 @@ +# ctypes by-value aggregate arguments and returns over the live FFI path. +# +# Exercises passing structs/unions BY VALUE to foreign functions and returning +# structs BY VALUE through the unified host_env `call` entry point: +# - div(7, 3) / div(-7, 3): return div_t{quot, rem} by value (8-byte int +# struct, register-returned on SysV/AArch64), +# - imaxdiv(7, 3): return imaxdiv_t{quot, rem} by value (16-byte two-long +# struct, two-register return on SysV), +# - inet_ntoa(struct in_addr): take a 4-byte struct by value, with argtypes, +# without argtypes (direct-instance paramfunc path), and via a Union. +# +# Runs on little-endian linux/macOS; skipped on Windows (see below). Prints +# "OK"; a failed assertion aborts with a non-zero status. + +import ctypes +import sys +from ctypes import ( + CDLL, + Structure, + Union, + c_char, + c_char_p, + c_int, + c_int64, + c_uint32, + sizeof, +) + +if sys.platform == "win32": + # The C library is not reachable as CDLL(None) on Windows; by-value + # aggregate calls are covered there by test_ctypes. Keep output identical. + print("OK") + sys.exit(0) + + +libc = CDLL(None) + + +# 1. struct RETURN by value: div(7, 3) -> div_t{quot=2, rem=1} +class div_t(Structure): + _fields_ = [("quot", c_int), ("rem", c_int)] + + +assert sizeof(div_t) == 8, sizeof(div_t) +libc.div.argtypes = [c_int, c_int] +libc.div.restype = div_t + +r = libc.div(7, 3) +assert isinstance(r, div_t) +assert (r.quot, r.rem) == (2, 1), (r.quot, r.rem) + +# C division truncates toward zero. +r = libc.div(-7, 3) +assert (r.quot, r.rem) == (-2, -1), (r.quot, r.rem) + +# struct RETURN by value with NO argtypes on the arguments (ints via ConvParam) +libc.div.argtypes = None +r = libc.div(17, 5) +assert (r.quot, r.rem) == (3, 2), (r.quot, r.rem) + + +# 2. larger struct RETURN by value: imaxdiv(7, 3) -> imaxdiv_t{quot=2, rem=1} +class imaxdiv_t(Structure): + _fields_ = [("quot", c_int64), ("rem", c_int64)] + + +assert sizeof(imaxdiv_t) == 16, sizeof(imaxdiv_t) +libc.imaxdiv.argtypes = [c_int64, c_int64] +libc.imaxdiv.restype = imaxdiv_t + +r = libc.imaxdiv(7, 3) +assert (r.quot, r.rem) == (2, 1), (r.quot, r.rem) +r = libc.imaxdiv(-9, 4) +assert (r.quot, r.rem) == (-2, -1), (r.quot, r.rem) + + +# 3. struct ARGUMENT by value: inet_ntoa(struct in_addr) -> b"1.2.3.4" +class in_addr(Structure): + _fields_ = [("s_addr", c_uint32)] + + +assert sizeof(in_addr) == 4, sizeof(in_addr) +# `s_addr` holds the four address bytes in memory (network) order; a host-endian +# int whose bytes are [1, 2, 3, 4] yields the dotted string "1.2.3.4". +addr_value = int.from_bytes(bytes([1, 2, 3, 4]), sys.byteorder) + +libc.inet_ntoa.argtypes = [in_addr] +libc.inet_ntoa.restype = c_char_p +assert libc.inet_ntoa(in_addr(addr_value)) == b"1.2.3.4" + +# struct ARGUMENT by value with NO argtypes (direct-instance paramfunc path) +libc.inet_ntoa.argtypes = None +assert libc.inet_ntoa(in_addr(addr_value)) == b"1.2.3.4" + + +# 4. union ARGUMENT by value: a union laid out like in_addr, passed by value. +class in_addr_u(Union): + _fields_ = [("s_addr", c_uint32), ("bytes", c_char * 4)] + + +assert sizeof(in_addr_u) == 4, sizeof(in_addr_u) +libc.inet_ntoa.argtypes = [in_addr_u] +libc.inet_ntoa.restype = c_char_p +assert libc.inet_ntoa(in_addr_u(addr_value)) == b"1.2.3.4" + +print("OK") diff --git a/extra_tests/snippets/stdlib_ctypes_calls.py b/extra_tests/snippets/stdlib_ctypes_calls.py new file mode 100644 index 00000000000..1de29931429 --- /dev/null +++ b/extra_tests/snippets/stdlib_ctypes_calls.py @@ -0,0 +1,64 @@ +# Exercises the migrated _ctypes foreign-call path (routed through the unified +# host_env `call` entry point): scalar int/double arguments and returns, +# pointer (c_char_p / c_void_p) returns, and a use_errno round-trip. +# +# Prints "OK" and exits 0; any failed assertion aborts. Output is identical +# under CPython and RustPython on the same platform. +import ctypes +import errno +import sys +from ctypes import ( + CDLL, + c_char_p, + c_double, + c_int, + c_long, + c_size_t, + c_void_p, + get_errno, + set_errno, +) + +if sys.platform == "win32": + # The C library is not reachable as CDLL(None) on Windows; the migrated + # path is covered there by test_ctypes. Keep output identical regardless. + print("OK") + sys.exit(0) + +libc = CDLL(None, use_errno=True) + +# 1. scalar int argument + int return: abs(-5) == 5 +libc.abs.argtypes = [c_int] +libc.abs.restype = c_int +assert libc.abs(-5) == 5, libc.abs(-5) + +# 2. pointer argument (bytes -> char*) + size_t return: strlen(b"hello") == 5 +libc.strlen.argtypes = [c_char_p] +libc.strlen.restype = c_size_t +assert libc.strlen(b"hello") == 5, libc.strlen(b"hello") + +# 3. double argument + double return: sqrt(2.0) +libc.sqrt.argtypes = [c_double] +libc.sqrt.restype = c_double +root = libc.sqrt(2.0) +assert abs(root - 2.0**0.5) < 1e-12, root + +# 4. c_char_p return: strchr(b"abcdef", 'c') -> b"cdef" +libc.strchr.argtypes = [c_char_p, c_int] +libc.strchr.restype = c_char_p +assert libc.strchr(b"abcdef", ord("c")) == b"cdef", libc.strchr(b"abcdef", ord("c")) + +# 5. c_void_p return: the same call yields a non-null integer address +libc.strchr.restype = c_void_p +addr = libc.strchr(b"abcdef", ord("c")) +assert isinstance(addr, int) and addr != 0, addr + +# 6. use_errno round-trip: strtol overflow sets errno == ERANGE, captured into +# the ctypes-private errno by the call's errno swap. +libc.strtol.argtypes = [c_char_p, c_void_p, c_int] +libc.strtol.restype = c_long +set_errno(0) +libc.strtol(b"9" * 40, None, 10) +assert get_errno() == errno.ERANGE, (get_errno(), errno.ERANGE) + +print("OK")