Repository navigation
Reuse fallible ctypes scalar conversion #9654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -608,6 +608,65 @@ pub enum SimpleStorageValue { | |
| Zero, | ||
| } | ||
|
|
||
| // An allocation is owned only until published into ctypes storage. Like CPython, | ||
| // committed BSTRs are freed by replacement, not by CData destruction: callers | ||
| // can transfer them to COM or copy their pointers into other ctypes objects. | ||
| #[cfg(windows)] | ||
| pub struct BStr { | ||
| bytes: [u8; core::mem::size_of::<usize>()], | ||
| committed: bool, | ||
| } | ||
|
|
||
| #[cfg(windows)] | ||
| impl BStr { | ||
| pub fn new(units: Option<&[u16]>) -> Option<Self> { | ||
| let address = match units { | ||
| Some(units) => crate::windows::sys_alloc_string_len(units)? as usize, | ||
| None => 0, | ||
| }; | ||
| Some(Self { | ||
| bytes: address.to_ne_bytes(), | ||
| committed: false, | ||
| }) | ||
| } | ||
|
|
||
| pub fn bytes(&self) -> &[u8] { | ||
| &self.bytes | ||
| } | ||
|
|
||
| pub fn commit(&mut self) { | ||
| self.committed = true; | ||
| } | ||
|
|
||
| /// # Safety | ||
| /// The old slot must contain NULL or a valid, live COM BSTR. | ||
| pub unsafe fn replace(&mut self, previous: &[u8]) { | ||
| let address = read_pointer_from_buffer(previous); | ||
| crate::windows::sys_free_string(address as *const u16); | ||
| self.commit(); | ||
|
Comment on lines
+643
to
+646
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| } | ||
|
|
||
| #[cfg(windows)] | ||
| impl Drop for BStr { | ||
| fn drop(&mut self) { | ||
| if !self.committed { | ||
| crate::windows::sys_free_string(read_pointer_from_buffer(&self.bytes) as *const u16); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(windows)] | ||
| unsafe fn decode_bstr(address: usize) -> DecodedValue { | ||
| if address == 0 { | ||
| return DecodedValue::None; | ||
| } | ||
| let pointer = address as *const u16; | ||
| let len = crate::windows::sys_string_len(pointer); | ||
| let units = unsafe { core::slice::from_raw_parts(pointer, len) }; | ||
| DecodedValue::WideString(Wtf8Buf::from_wide(units)) | ||
| } | ||
|
|
||
| pub enum DecodedValue { | ||
| Bytes(Vec<u8>), | ||
| Signed(i64), | ||
|
|
@@ -616,6 +675,8 @@ pub enum DecodedValue { | |
| Bool(bool), | ||
| Pointer(usize), | ||
| String(String), | ||
| #[cfg(windows)] | ||
| WideString(Wtf8Buf), | ||
| None, | ||
| } | ||
|
|
||
|
|
@@ -843,6 +904,8 @@ pub fn read_array_element( | |
| } | ||
| } | ||
| } | ||
| #[cfg(windows)] | ||
| Some("X") => unsafe { decode_bstr(read_pointer_from_buffer(rest)) }, | ||
| Some("f") => DecodedValue::Float( | ||
| rest.first_chunk::<4>() | ||
| .copied() | ||
|
|
@@ -1276,6 +1339,8 @@ pub fn decode_type_code(type_code: &str, bytes: &[u8]) -> DecodedValue { | |
| None => DecodedValue::None, | ||
| } | ||
| }, | ||
| #[cfg(windows)] | ||
| "X" => unsafe { decode_bstr(read_pointer_from_buffer(bytes)) }, | ||
| "P" => DecodedValue::Pointer(read_pointer_from_buffer(bytes)), | ||
| "u" => { | ||
| let val = if bytes.len() >= core::mem::size_of::<WChar>() { | ||
|
|
@@ -1300,6 +1365,8 @@ pub fn decode_type_code(type_code: &str, bytes: &[u8]) -> DecodedValue { | |
| /// `type_code`. | ||
| pub unsafe fn callback_arg_value(type_code: Option<&str>, ptr: *const c_void) -> DecodedValue { | ||
| match type_code { | ||
| #[cfg(windows)] | ||
| Some("X") => unsafe { decode_bstr(core::ptr::read_unaligned(ptr.cast::<usize>())) }, | ||
| Some("b") => DecodedValue::Signed(unsafe { *(ptr as *const i8) as i64 }), | ||
| Some("B") => DecodedValue::Unsigned(unsafe { *(ptr as *const u8) as u64 }), | ||
| Some("c") => DecodedValue::Bytes(vec![unsafe { *(ptr as *const u8) }]), | ||
|
|
@@ -1398,7 +1465,7 @@ pub unsafe fn write_callback_result( | |
| (Some("Q"), CallbackResultValue::Unsigned(v)) => unsafe { *(result as *mut u64) = v }, | ||
| (Some("f"), CallbackResultValue::Float(v)) => unsafe { *(result as *mut f32) = v as f32 }, | ||
| (Some("d"), CallbackResultValue::Float(v)) => unsafe { *(result as *mut f64) = v }, | ||
| (Some("P" | "z" | "Z"), CallbackResultValue::Pointer(v)) => unsafe { | ||
| (Some("P" | "z" | "Z" | "X"), CallbackResultValue::Pointer(v)) => unsafe { | ||
| *(result as *mut usize) = v | ||
| }, | ||
| (Some("?"), CallbackResultValue::Bool(v)) => unsafe { *(result as *mut u8) = u8::from(v) }, | ||
|
|
@@ -1449,7 +1516,7 @@ pub fn ffi_value_from_type_code(type_code: &str, buffer: &[u8]) -> FfiValue { | |
| .copied() | ||
| .map_or(0.0, f64::from_ne_bytes), | ||
| ), | ||
| "z" | "Z" | "P" | "O" => FfiValue::Pointer(read_pointer_from_buffer(buffer)), | ||
| "z" | "Z" | "P" | "X" | "O" => FfiValue::Pointer(read_pointer_from_buffer(buffer)), | ||
| "?" => FfiValue::U8(u8::from(buffer.first().is_some_and(|&b| b != 0))), | ||
| "u" => FfiValue::U32(buffer.first_chunk().copied().map_or(0, u32::from_ne_bytes)), | ||
| _ => FfiValue::Pointer(0), | ||
|
|
@@ -2529,7 +2596,7 @@ pub unsafe fn read_value_at_address( | |
| Some("d" | "g") => { | ||
| AddressValue::Float(unsafe { core::ptr::read_unaligned(ptr as *const f64) }) | ||
| } | ||
| Some("P" | "z" | "Z") => { | ||
| Some("P" | "z" | "Z" | "X") => { | ||
| AddressValue::Pointer(unsafe { core::ptr::read_unaligned(ptr as *const usize) }) | ||
| } | ||
| _ => AddressValue::Bytes(unsafe { bytes_at(ptr, size) }), | ||
|
|
@@ -3043,6 +3110,59 @@ pub fn dlsym_checked(_handle: usize, symbol_name: &CStr) -> Result<*mut c_void, | |
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[cfg(all( | ||
| any( | ||
| target_os = "linux", | ||
| target_os = "macos", | ||
| target_os = "windows", | ||
| target_os = "android" | ||
| ), | ||
| not(any(target_env = "musl", target_env = "sgx")) | ||
| ))] | ||
| #[test] | ||
| fn bstr_foreign_argument_preserves_pointer() { | ||
| let address = usize::MAX; | ||
| assert!(matches!( | ||
| ffi_value_from_type_code("X", &address.to_ne_bytes()), | ||
| FfiValue::Pointer(value) if value == address | ||
| )); | ||
| } | ||
|
|
||
| #[cfg(windows)] | ||
| #[test] | ||
| fn bstr_roundtrip_and_replacement() { | ||
| // Embedded NUL, a surrogate pair, and an unpaired surrogate must survive. | ||
| let units = [0x61, 0, 0xd83d, 0xde00, 0xd800]; | ||
| let mut slot = 0usize.to_ne_bytes(); | ||
| let mut value = BStr::new(Some(&units)).expect("SysAllocStringLen"); | ||
| unsafe { | ||
| value.replace(&slot); | ||
| } | ||
| slot.copy_from_slice(value.bytes()); | ||
| drop(value); | ||
| match decode_type_code("X", &slot) { | ||
| DecodedValue::WideString(value) => { | ||
| assert_eq!(value.encode_wide().collect::<Vec<_>>(), units) | ||
| } | ||
| _ => panic!("expected BSTR string"), | ||
| } | ||
| let mut empty = BStr::new(Some(&[])).expect("empty BSTR"); | ||
| unsafe { | ||
| empty.replace(&slot); | ||
| } | ||
| slot.copy_from_slice(empty.bytes()); | ||
| drop(empty); | ||
| assert!( | ||
| matches!(decode_type_code("X", &slot), DecodedValue::WideString(value) if value.is_empty()) | ||
| ); | ||
| let mut null = BStr::new(None).unwrap(); | ||
| unsafe { | ||
| null.replace(&slot); | ||
| } | ||
| slot.copy_from_slice(null.bytes()); | ||
| assert!(matches!(decode_type_code("X", &slot), DecodedValue::None)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn bstr_storage_preserves_pointer_width() { | ||
| for address in [0, usize::MAX] { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1979,6 +1979,23 @@ fn ffi_to_python( | |
| } | ||
|
|
||
| let type_code = ty.type_code(vm); | ||
| #[cfg(windows)] | ||
| if type_code.as_deref() == Some("X") && is_simple_subclass(ty, vm) { | ||
| // CPython constructs non-direct simple subclasses without arguments, | ||
| // then copies the slot. Decoding and reconstructing a BSTR would allocate | ||
| // a different COM string and lose the caller's pointer identity. | ||
| let rustpython_host_env::ctypes::DecodedValue::Pointer(address) = | ||
| (unsafe { rustpython_host_env::ctypes::callback_arg_value_at(Some("P"), args, index) }) | ||
| else { | ||
| return Err(vm.new_type_error("invalid BSTR callback argument")); | ||
| }; | ||
| let instance = ty.as_object().call((), vm)?; | ||
| let cdata = instance | ||
| .downcast_ref::<PyCData>() | ||
| .ok_or_else(|| vm.new_type_error("BSTR constructor returned a non-ctypes object"))?; | ||
| cdata.write_bytes_at_offset(0, &address.to_ne_bytes()); | ||
| return Ok(instance); | ||
| } | ||
| let raw_value: PyObjectRef = match unsafe { | ||
| rustpython_host_env::ctypes::callback_arg_value_at(type_code.as_deref(), args, index) | ||
| } { | ||
|
|
@@ -1989,6 +2006,10 @@ fn ffi_to_python( | |
| rustpython_host_env::ctypes::DecodedValue::Bool(value) => vm.ctx.new_bool(value).into(), | ||
| rustpython_host_env::ctypes::DecodedValue::Pointer(value) => vm.ctx.new_int(value).into(), | ||
| rustpython_host_env::ctypes::DecodedValue::String(value) => vm.ctx.new_str(value).into(), | ||
| #[cfg(windows)] | ||
| rustpython_host_env::ctypes::DecodedValue::WideString(value) => { | ||
| vm.ctx.new_str(value).into() | ||
| } | ||
| rustpython_host_env::ctypes::DecodedValue::None => vm.ctx.none(), | ||
| }; | ||
|
|
||
|
|
@@ -2044,6 +2065,31 @@ fn python_to_ffi(obj: PyResult, ty: &Py<PyType>, result: *mut c_void, vm: &Virtu | |
| } | ||
| } | ||
| } | ||
| #[cfg(windows)] | ||
| Some("X") => { | ||
| match super::simple::assignment_value("X", &obj, false, vm) { | ||
| Ok((mut bytes, _, _)) => { | ||
| let address = rustpython_host_env::ctypes::read_pointer_from_buffer(&bytes); | ||
| // libffi's result slot is fresh storage, not a previous BSTR. | ||
| bytes.commit(); | ||
| unsafe { | ||
| rustpython_host_env::ctypes::write_callback_result( | ||
| Some("X"), | ||
| result, | ||
| rustpython_host_env::ctypes::CallbackResultValue::Pointer(address), | ||
| ); | ||
| } | ||
| } | ||
| Err(exc) => vm.run_unraisable( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Windows, when a BSTR callback returns an unsupported value (or BSTR allocation fails), this arm reports the exception but never writes to Useful? React with 👍 / 👎. |
||
| exc, | ||
| Some( | ||
| "Exception ignored while converting result of ctypes callback function" | ||
| .to_owned(), | ||
| ), | ||
| obj, | ||
| ), | ||
| } | ||
| } | ||
| Some("P" | "z" | "Z") => { | ||
| if let Ok(i) = obj.try_int(vm) { | ||
| unsafe { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
rg -n 'BStr::new|\.commit\(|sys_free_string|CArgValue::typed|impl Drop for PyCData|impl Drop for CArgObject' crates/vm/src/stdlib/_ctypes crates/host_env/srcRepository: RustPython/RustPython
Length of output: 1732
Script executed:
Repository: RustPython/RustPython
Length of output: 7019
Script executed:
Repository: RustPython/RustPython
Length of output: 4243
Free committed BSTRs when no ctypes value owns them.
Committed BSTRs are freed only when a later assignment replaces them. Discarded temporary
Xscalars andX.from_param(str)arguments never free their BSTR, because no owner remains. Track the allocation on the owning value and free it on drop, unless ownership moves to a foreign call.