diff --git a/Cargo.lock b/Cargo.lock index 314d1f4..c492fdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,6 +285,14 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "opc_classic_utils" +version = "0.3.0" +dependencies = [ + "windows", + "windows-core", +] + [[package]] name = "opc_comn_bindings" version = "0.3.0" @@ -301,6 +309,7 @@ version = "0.3.1" dependencies = [ "actix", "globset", + "opc_classic_utils", "opc_comn_bindings", "opc_da_bindings", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 6aba39a..9214aaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "opc_ae_bindings", + "opc_classic_utils", "opc_comn_bindings", "opc_da", "opc_da_bindings", @@ -9,11 +10,20 @@ members = [ ] [workspace.dependencies] +actix = "0.13.5" +globset = "0.4.16" +opc_classic_utils = { version = "0.3.0", path = "opc_classic_utils" } +opc_comn_bindings = { path = "opc_comn_bindings" } +opc_da_bindings = { path = "opc_da_bindings" } +tokio = { version = "1.46.1", features = ["full"] } windows = { version = "0.61.3", features = [ "Win32_Foundation", + "Win32_Graphics_Gdi", "Win32_System_Com", + "Win32_System_Com_StructuredStorage", "Win32_System_Ole", "Win32_System_Variant", + "Win32_System_WinRT", ] } windows-bindgen = "0.62.1" windows-core = "0.61.2" diff --git a/opc_classic_utils/Cargo.toml b/opc_classic_utils/Cargo.toml new file mode 100644 index 0000000..86a29d4 --- /dev/null +++ b/opc_classic_utils/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "opc_classic_utils" +version = "0.3.0" +edition = "2024" +description = "OPC Classic utilities and common functionality" +readme = "README.md" +repository = "https://github.com/Ronbb/rust_opc" +license = "MIT" +keywords = ["classic", "opc", "utilities", "utils"] + +[dependencies] +windows = { workspace = true } +windows-core = { workspace = true } \ No newline at end of file diff --git a/opc_classic_utils/README.md b/opc_classic_utils/README.md new file mode 100644 index 0000000..ebaf3f9 --- /dev/null +++ b/opc_classic_utils/README.md @@ -0,0 +1,100 @@ +# OPC Classic Utils + +OPC Classic utilities and common functionality for the rust_opc project. + +## Features + +- **Dual Memory Management Patterns**: Supports both COM memory management conventions + - **Caller-allocated**: Caller allocates, callee frees (for input parameters) + - **Callee-allocated**: Callee allocates, caller frees (for output parameters) +- Automatic memory management with `CoTaskMemFree` +- Common utility structures and traits +- Shared functionality for OPC Classic implementations + +## Memory Management Patterns + +### Caller-allocated Memory (Input Parameters) + +Use these types when **you** allocate memory that will be freed by the COM function: + +```rust +use opc_classic_utils::memory::{CallerAllocatedPtr, CallerAllocatedWString}; + +// For input parameters - you allocate, COM function frees +let input_string = CallerAllocatedWString::from_raw(your_allocated_string); +let input_data = CallerAllocatedPtr::from_raw(your_allocated_data); + +// Pass to COM function - it will free the memory +some_com_function(&input_string, &input_data); +// Memory is NOT freed by our wrapper (COM function's responsibility) +``` + +### Callee-allocated Memory (Output Parameters) + +Use these types when the **COM function** allocates memory that you must free: + +```rust +use opc_classic_utils::memory::{CalleeAllocatedPtr, CalleeAllocatedWString}; + +// For output parameters - COM function allocates, you free +let (output_string, output_data) = some_com_function(); + +// Use the returned data +println!("String: {:?}", output_string.as_ptr()); +println!("Data: {:?}", output_data.as_ptr()); + +// Memory is automatically freed when variables go out of scope +``` + +## Usage Examples + +### Basic Usage + +```rust +use opc_classic_utils::memory::{ + CallerAllocatedPtr, CalleeAllocatedPtr, + CallerAllocatedWString, CalleeAllocatedWString, +}; + +// Caller-allocated (input parameters) +let input_ptr = CallerAllocatedPtr::from_raw(some_pointer); +// Memory will NOT be freed by our wrapper + +// Callee-allocated (output parameters) +let output_ptr = CalleeAllocatedPtr::from_raw(com_returned_pointer); +// Memory will be automatically freed when dropped +``` + +### OPC Client-Server Scenario + +```rust +// Client side - preparing input parameters +let client_input = CallerAllocatedWString::from_raw(client_allocated_string); + +// Call server method +let server_output = server_method(&client_input); + +// Server output is automatically freed when it goes out of scope +// Client input is NOT freed (server's responsibility) +``` + +## Type Overview + +| Type | Purpose | Memory Management | +|------|---------|-------------------| +| `CallerAllocatedPtr` | Input parameters | Caller allocates, callee frees | +| `CalleeAllocatedPtr` | Output parameters | Callee allocates, caller frees | +| `CallerAllocatedWString` | Input strings | Caller allocates, callee frees | +| `CalleeAllocatedWString` | Output strings | Callee allocates, caller frees | + +## Benefits + +- **Prevents Memory Leaks**: Automatic cleanup for callee-allocated memory +- **Prevents Double-free**: Clear ownership semantics prevent errors +- **COM Convention Compliance**: Follows standard COM memory management rules +- **Type Safety**: Compile-time guarantees about memory ownership +- **RAII**: Resource management through Rust's ownership system + +## License + +MIT \ No newline at end of file diff --git a/opc_classic_utils/examples/array_functions.rs b/opc_classic_utils/examples/array_functions.rs new file mode 100644 index 0000000..9c669f0 --- /dev/null +++ b/opc_classic_utils/examples/array_functions.rs @@ -0,0 +1,221 @@ +//! Array functions demonstration for opc_classic_utils +//! +//! This example shows how to use the array and pointer array functions for +//! COM memory management in OPC Classic scenarios. + +use opc_classic_utils::memory::{ + CallerAllocatedArray, CallerAllocatedPtr, CallerAllocatedPtrArray, CallerAllocatedWString, +}; +use std::str::FromStr; + +#[derive(Debug, Clone, Copy, PartialEq)] +struct OPCItem { + id: i32, + value: f64, + quality: u16, +} + +fn demonstrate_array_functions() { + println!("Array Functions Demonstration"); + println!("============================"); + + println!("\n1. Creating arrays from Rust slices:"); + + // Create arrays from Rust data + let item_data = vec![ + OPCItem { + id: 1, + value: 10.5, + quality: 192, + }, + OPCItem { + id: 2, + value: 20.7, + quality: 192, + }, + OPCItem { + id: 3, + value: 30.2, + quality: 192, + }, + ]; + + let mut caller_array = CallerAllocatedArray::from_slice(&item_data).unwrap(); + println!( + " Created caller-allocated array with {} items", + caller_array.len() + ); + + // Verify the data was copied correctly + unsafe { + let slice = caller_array.as_slice().unwrap(); + assert_eq!(slice, item_data.as_slice()); + println!(" ✓ Data was correctly copied to COM memory"); + } + + println!("\n2. Allocating uninitialized arrays:"); + + // Allocate memory for later initialization + let mut uninit_array = CallerAllocatedArray::::allocate(5).unwrap(); + println!(" Allocated uninitialized array for 5 i32 values"); + + // Initialize the array + unsafe { + let mut_slice = uninit_array.as_mut_slice().unwrap(); + for (i, value) in mut_slice.iter_mut().enumerate() { + *value = ((i + 1) * 10) as i32; + } + } + println!(" Initialized array with values: [10, 20, 30, 40, 50]"); + + println!("\n3. Accessing array elements:"); + + // Test individual element access + unsafe { + assert_eq!(*caller_array.get(0).unwrap(), item_data[0]); + assert_eq!(*caller_array.get(1).unwrap(), item_data[1]); + assert_eq!(*caller_array.get(2).unwrap(), item_data[2]); + assert!(caller_array.get(3).is_none()); // Out of bounds + } + println!(" ✓ Array element access works correctly"); + + // Test mutable access + unsafe { + let mut item = *caller_array.get(1).unwrap(); + item.value = 25.0; + *caller_array.as_mut_slice().unwrap().get_mut(1).unwrap() = item; + assert_eq!(caller_array.get(1).unwrap().value, 25.0); + } + println!(" ✓ Mutable array access works correctly"); +} + +fn demonstrate_pointer_array_functions() { + println!("\n\nPointer Array Functions Demonstration"); + println!("====================================="); + + println!("\n1. Creating pointer arrays:"); + + // Create some pointers to simulate OPC item handles + let ptr1 = CallerAllocatedPtr::::from_value(&100).unwrap(); + let ptr2 = CallerAllocatedPtr::::from_value(&200).unwrap(); + let ptr3 = CallerAllocatedPtr::::from_value(&300).unwrap(); + + let ptrs = vec![ptr1.as_ptr(), ptr2.as_ptr(), ptr3.as_ptr()]; + let caller_ptr_array = CallerAllocatedPtrArray::from_ptr_slice(&ptrs).unwrap(); + println!( + " Created caller-allocated pointer array with {} pointers", + caller_ptr_array.len() + ); + + // Verify the pointers were copied correctly + unsafe { + let slice = caller_ptr_array.as_slice().unwrap(); + assert_eq!(slice, ptrs.as_slice()); + println!(" ✓ Pointers were correctly copied to COM memory"); + } + + println!("\n2. Allocating uninitialized pointer arrays:"); + + // Allocate memory for pointer array + let mut uninit_ptr_array = CallerAllocatedPtrArray::::allocate(3).unwrap(); + println!(" Allocated uninitialized pointer array for 3 pointers"); + + // Set pointers in the array + unsafe { + let test_ptr1 = std::ptr::null_mut::(); + let test_ptr2 = std::ptr::null_mut::(); + let test_ptr3 = std::ptr::null_mut::(); + + assert!(uninit_ptr_array.set(0, test_ptr1)); + assert!(uninit_ptr_array.set(1, test_ptr2)); + assert!(uninit_ptr_array.set(2, test_ptr3)); + assert!(!uninit_ptr_array.set(3, test_ptr1)); // Out of bounds + } + println!(" ✓ Pointer array set operations work correctly"); + + println!("\n3. Accessing pointer array elements:"); + + // Test individual pointer access + unsafe { + assert_eq!(caller_ptr_array.get(0).unwrap(), ptrs[0]); + assert_eq!(caller_ptr_array.get(1).unwrap(), ptrs[1]); + assert_eq!(caller_ptr_array.get(2).unwrap(), ptrs[2]); + assert!(caller_ptr_array.get(3).is_none()); // Out of bounds + } + println!(" ✓ Pointer array element access works correctly"); +} + +fn demonstrate_real_world_opc_scenario() { + println!("\n\nReal-World OPC Array Scenario"); + println!("=============================="); + + println!("\n1. Simulating OPC client preparing batch read request:"); + + // Client prepares item IDs for batch read + let item_ids = ["Item1", "Item2", "Item3", "Item4", "Item5"]; + let item_id_wstrings: Vec = item_ids + .iter() + .map(|id| CallerAllocatedWString::from_str(id).unwrap()) + .collect(); + let item_id_ptrs: Vec<*mut u16> = item_id_wstrings.iter().map(|ws| ws.as_ptr()).collect(); + + let item_id_array = CallerAllocatedPtrArray::from_ptr_slice(&item_id_ptrs).unwrap(); + println!( + " Client prepared {} item IDs for batch read", + item_id_array.len() + ); + + println!("\n2. Simulating OPC server processing batch request:"); + + // Server would process the request and return results + println!(" Server would process the batch request"); + println!(" Server would allocate memory for results"); + + println!("\n3. Simulating server returning batch results:"); + + // Server would return arrays of results + let result_values = vec![42.0, 84.0, 126.0, 168.0, 210.0]; + let result_qualities = vec![192u16, 192, 192, 192, 192]; + let result_timestamps = vec![ + 1234567890u64, + 1234567891, + 1234567892, + 1234567893, + 1234567894, + ]; + + // In a real scenario, the server would allocate these arrays + // and return pointers to the client + println!(" Server would return:"); + println!(" - {} values: {:?}", result_values.len(), result_values); + println!( + " - {} qualities: {:?}", + result_qualities.len(), + result_qualities + ); + println!( + " - {} timestamps: {:?}", + result_timestamps.len(), + result_timestamps + ); + + println!("\n4. Client would receive and process batch results:"); + + // Client would receive CalleeAllocatedArray types + // and they would be automatically freed when they go out of scope + println!(" Client would receive CalleeAllocatedArray types"); + println!(" Client would automatically free result memory"); + println!(" when the wrapper types go out of scope"); + + println!("\n ✓ Complete OPC batch operation cycle demonstrated"); +} + +fn main() { + demonstrate_array_functions(); + demonstrate_pointer_array_functions(); + demonstrate_real_world_opc_scenario(); + + println!("\n\n✅ Array functions demonstration completed!"); + println!(" These functions make COM array management much easier."); + println!(" Perfect for batch operations in OPC Classic applications."); +} diff --git a/opc_classic_utils/examples/basic_usage.rs b/opc_classic_utils/examples/basic_usage.rs new file mode 100644 index 0000000..e9fd0ba --- /dev/null +++ b/opc_classic_utils/examples/basic_usage.rs @@ -0,0 +1,76 @@ +//! Basic usage example for opc_classic_utils +//! +//! This example demonstrates how to use the memory management utilities +//! provided by opc_classic_utils, showing both caller-allocated and callee-allocated patterns. + +use opc_classic_utils::memory::{ + CalleeAllocatedPtr, CalleeAllocatedWString, CallerAllocatedPtr, CallerAllocatedWString, +}; + +fn main() { + println!("OPC Classic Utils - Memory Management Patterns"); + println!("=============================================="); + + // Example 1: Caller-allocated pointers (input parameters) + println!("\n1. Caller-allocated pointers (input parameters):"); + println!(" - Caller allocates memory"); + println!(" - Callee (COM function) is responsible for freeing"); + println!(" - Our wrapper does NOT free the memory"); + + let caller_ptr: CallerAllocatedPtr = CallerAllocatedPtr::default(); + println!( + " Created caller-allocated pointer, null: {}", + caller_ptr.is_null() + ); + + // Example 2: Callee-allocated pointers (output parameters) + println!("\n2. Callee-allocated pointers (output parameters):"); + println!(" - Callee (COM function) allocates memory"); + println!(" - Caller is responsible for freeing using CoTaskMemFree"); + println!(" - Our wrapper automatically frees the memory when dropped"); + + let callee_ptr: CalleeAllocatedPtr = CalleeAllocatedPtr::default(); + println!( + " Created callee-allocated pointer, null: {}", + callee_ptr.is_null() + ); + + // Example 3: Caller-allocated wide strings + println!("\n3. Caller-allocated wide strings:"); + let caller_wstring = CallerAllocatedWString::default(); + println!( + " Created caller-allocated wide string, null: {}", + caller_wstring.is_null() + ); + + // Example 4: Callee-allocated wide strings + println!("\n4. Callee-allocated wide strings:"); + let callee_wstring = CalleeAllocatedWString::default(); + println!( + " Created callee-allocated wide string, null: {}", + callee_wstring.is_null() + ); + + // Example 5: Demonstrating automatic cleanup for callee-allocated memory + println!("\n5. Demonstrating automatic cleanup for callee-allocated memory:"); + { + // This would normally be a pointer returned from a COM function + let _ptr = CalleeAllocatedPtr::from_raw(std::ptr::null_mut::()); + println!( + " Created callee-allocated pointer, will be automatically freed when scope ends" + ); + } // _ptr is automatically dropped and memory freed here + + // Example 6: Demonstrating no cleanup for caller-allocated memory + println!("\n6. Demonstrating no cleanup for caller-allocated memory:"); + { + // This would normally be a pointer allocated by the caller + let _ptr = CallerAllocatedPtr::from_raw(std::ptr::null_mut::()); + println!(" Created caller-allocated pointer, callee will be responsible for freeing"); + } // _ptr is dropped but memory is NOT freed (callee's responsibility) + + println!("\n7. Example completed successfully!"); + println!(" Memory management follows COM conventions:"); + println!(" - Caller-allocated: callee frees"); + println!(" - Callee-allocated: caller frees (handled automatically)"); +} diff --git a/opc_classic_utils/examples/convenience_functions.rs b/opc_classic_utils/examples/convenience_functions.rs new file mode 100644 index 0000000..f572f2f --- /dev/null +++ b/opc_classic_utils/examples/convenience_functions.rs @@ -0,0 +1,186 @@ +//! Convenience functions demonstration for opc_classic_utils +//! +//! This example shows how to use the convenience functions for +//! creating COM-allocated memory from Rust types and string conversions. + +use opc_classic_utils::memory::{CallerAllocatedPtr, CallerAllocatedWString}; + +#[derive(Debug, Clone, Copy, PartialEq)] +struct TestStruct { + id: i32, + value: f64, +} + +fn demonstrate_pointer_convenience_functions() { + println!("Pointer Convenience Functions"); + println!("============================"); + + println!("\n1. Creating pointers from values:"); + + // Create a pointer from a simple value + let int_value = 42; + let int_ptr = CallerAllocatedPtr::from_value(&int_value).unwrap(); + println!(" Created pointer from int: {:?}", int_value); + + // Create a pointer from a struct + let struct_value = TestStruct { + id: 1, + value: std::f64::consts::PI, + }; + let struct_ptr = CallerAllocatedPtr::from_value(&struct_value).unwrap(); + println!(" Created pointer from struct: {:?}", struct_value); + + // Verify the values were copied correctly + unsafe { + assert_eq!(*int_ptr.as_ptr(), 42); + assert_eq!(*struct_ptr.as_ptr(), struct_value); + } + + println!(" ✓ Values were correctly copied to COM memory"); + + println!("\n2. Allocating uninitialized memory:"); + + // Allocate memory for later initialization + let uninit_ptr = CallerAllocatedPtr::::allocate().unwrap(); + println!(" Allocated uninitialized memory for i32"); + + // Initialize the memory + unsafe { + *uninit_ptr.as_ptr() = 100; + } + println!(" Initialized memory with value: 100"); + + println!("\n3. Dereferencing pointers:"); + + let value = 123; + let mut ptr = CallerAllocatedPtr::from_value(&value).unwrap(); + + // Use as_ref for read-only access + unsafe { + let ref_value = ptr.as_ref().unwrap(); + println!(" Read value through as_ref: {}", ref_value); + } + + // Use as_mut for mutable access + unsafe { + let mut_value = ptr.as_mut().unwrap(); + *mut_value = 456; + println!(" Modified value through as_mut: {}", mut_value); + } + + // Verify the change + unsafe { + assert_eq!(*ptr.as_ptr(), 456); + } + println!(" ✓ Value was successfully modified"); +} + +fn demonstrate_string_convenience_functions() { + println!("\n\nString Convenience Functions"); + println!("============================"); + + println!("\n1. Creating wide strings from Rust strings:"); + + // From string slice + use std::str::FromStr; + let str_slice = "Hello, World!"; + let wstring1 = CallerAllocatedWString::from_str(str_slice).unwrap(); + println!(" Created from &str: '{}'", str_slice); + + // From String + let owned_string = String::from("Owned String"); + let wstring2 = CallerAllocatedWString::from_string(owned_string.clone()).unwrap(); + println!(" Created from String: '{}'", owned_string); + + // From OsStr + use std::ffi::OsStr; + let os_string = OsStr::new("OS String"); + let wstring3 = CallerAllocatedWString::from_os_str(os_string).unwrap(); + println!(" Created from OsStr: '{:?}'", os_string); + + println!("\n2. Converting wide strings back to Rust strings:"); + + // Convert back to String + unsafe { + let converted1 = wstring1.to_string().unwrap(); + println!(" Converted back to String: '{}'", converted1); + assert_eq!(converted1, str_slice); + + let converted2 = wstring2.to_string().unwrap(); + println!(" Converted back to String: '{}'", converted2); + assert_eq!(converted2, owned_string); + + let converted3 = wstring3.to_os_string().unwrap(); + println!(" Converted back to OsString: '{:?}'", converted3); + assert_eq!(converted3, os_string); + } + + println!(" ✓ All string conversions work correctly"); + + println!("\n3. Handling null strings:"); + + let null_wstring = CallerAllocatedWString::default(); + unsafe { + let result = null_wstring.to_string(); + assert!(result.is_none()); + println!(" Null string correctly returns None"); + } +} + +fn demonstrate_real_world_scenario() { + use std::str::FromStr; + + println!("\n\nReal-World OPC Scenario"); + println!("======================="); + + println!("\n1. Simulating OPC client preparing input parameters:"); + + // Client allocates memory for input parameters + let server_name = CallerAllocatedWString::from_str("OPC.Server.1").unwrap(); + let item_count = CallerAllocatedPtr::from_value(&5i32).unwrap(); + + println!(" Client allocated memory for:"); + println!(" - Server name: 'OPC.Server.1'"); + println!(" - Item count: 5"); + + println!("\n2. Simulating OPC server processing:"); + + // Server would use these parameters + unsafe { + let name = server_name.to_string().unwrap(); + let count = *item_count.as_ptr(); + println!(" Server received:"); + println!(" - Server name: '{}'", name); + println!(" - Item count: {}", count); + } + + println!("\n3. Simulating server returning output parameters:"); + + // Server would allocate memory for output + let result_string = "Operation completed successfully"; + let result_code = 0i32; + + // In a real scenario, the server would allocate this memory + // and return pointers to the client + println!(" Server would return:"); + println!(" - Result message: '{}'", result_string); + println!(" - Result code: {}", result_code); + + println!("\n4. Client would receive and process output:"); + + // Client would receive CalleeAllocatedPtr/CalleeAllocatedWString + // and they would be automatically freed when they go out of scope + println!(" Client would automatically free output memory"); + println!(" when the wrapper types go out of scope"); + + println!("\n ✓ Complete OPC memory management cycle demonstrated"); +} + +fn main() { + demonstrate_pointer_convenience_functions(); + demonstrate_string_convenience_functions(); + demonstrate_real_world_scenario(); + + println!("\n\n✅ Convenience functions demonstration completed!"); + println!(" These functions make COM memory management much easier."); +} diff --git a/opc_classic_utils/examples/memory_management_comparison.rs b/opc_classic_utils/examples/memory_management_comparison.rs new file mode 100644 index 0000000..d8c8600 --- /dev/null +++ b/opc_classic_utils/examples/memory_management_comparison.rs @@ -0,0 +1,129 @@ +//! Memory Management Comparison Example +//! +//! This example demonstrates the key differences between `CalleeAllocatedArray` and `CalleeAllocatedPtrArray` +//! to help users understand when to use each type. + +use opc_classic_utils::memory::{ + CalleeAllocatedArray, CalleeAllocatedPtrArray, CallerAllocatedArray, CallerAllocatedPtrArray, +}; +use std::ptr; + +fn main() { + println!("=== OPC Classic Memory Management Comparison ===\n"); + + // 1. Value array comparison + println!("1. Value Array Comparison:"); + compare_value_arrays(); + println!(); + + // 2. Pointer array comparison + println!("2. Pointer Array Comparison:"); + compare_pointer_arrays(); + println!(); + + // 3. Real-world usage scenarios + println!("3. Real-World Usage Scenarios:"); + demonstrate_real_world_usage(); +} + +fn compare_value_arrays() { + println!(" CalleeAllocatedArray (Callee-allocated):"); + println!(" - Manages the array container itself"); + println!(" - Does not free array elements"); + println!(" - Use case: Simple data type arrays"); + + // Simulate COM-allocated value array - use null pointer to avoid actual allocation + let array = CalleeAllocatedArray::from_raw(ptr::null_mut::(), 0); + println!(" - Created empty array (length: {})", array.len()); + // Automatically frees array container + + println!("\n CallerAllocatedArray (Caller-allocated):"); + println!(" - Caller responsible for allocation and deallocation"); + println!(" - Full control over memory lifecycle"); + println!(" - Use case: Scenarios requiring precise memory control"); + + let caller_array: CallerAllocatedArray = CallerAllocatedArray::allocate(5).unwrap(); + println!(" - Allocated array (length: {})", caller_array.len()); + // Caller responsible for deallocation +} + +fn compare_pointer_arrays() { + println!(" CalleeAllocatedPtrArray (Callee-allocated):"); + println!(" - Manages array container and each pointer element"); + println!(" - Frees array container + each pointer's memory"); + println!(" - Use case: COM interface pointer arrays"); + + // Simulate COM-allocated pointer array - use null pointer to avoid actual allocation + let ptr_array = CalleeAllocatedPtrArray::from_raw(ptr::null_mut::<*mut i32>(), 0); + println!( + " - Created empty pointer array (length: {})", + ptr_array.len() + ); + // Automatically frees array container and each pointer + + println!("\n CallerAllocatedPtrArray (Caller-allocated):"); + println!(" - Caller responsible for allocation and deallocation"); + println!(" - Full control over pointer array lifecycle"); + println!(" - Use case: Scenarios requiring precise pointer memory control"); + + let caller_ptr_array: CallerAllocatedPtrArray = + CallerAllocatedPtrArray::allocate(3).unwrap(); + println!( + " - Allocated pointer array (length: {})", + caller_ptr_array.len() + ); + // Caller responsible for deallocation +} + +fn demonstrate_real_world_usage() { + println!(" Scenario 1: Batch reading OPC item values"); + println!(" - Use CalleeAllocatedArray to receive value arrays"); + println!(" - COM server allocates memory, client frees container"); + + let value_array = CalleeAllocatedArray::from_raw(ptr::null_mut::(), 0); + println!(" - Read {} values", value_array.len()); + + println!("\n Scenario 2: Getting OPC server interface list"); + println!(" - Use CalleeAllocatedPtrArray to receive interface pointer arrays"); + println!(" - COM server allocates memory and interface pointers, client frees all resources"); + + let interface_array = CalleeAllocatedPtrArray::from_raw(ptr::null_mut::<*mut ()>(), 0); + println!(" - Retrieved {} interface pointers", interface_array.len()); + + println!("\n Scenario 3: Custom memory management"); + println!(" - Use CallerAllocatedArray for precise memory control"); + println!(" - Suitable for performance-critical scenarios"); + + let custom_array: CallerAllocatedArray = CallerAllocatedArray::allocate(4).unwrap(); + println!(" - Custom array (length: {})", custom_array.len()); + + println!("\n Best Practices Summary:"); + println!(" - Simple data: Use CalleeAllocatedArray"); + println!(" - Interface pointers: Use CalleeAllocatedPtrArray"); + println!(" - Performance optimization: Use CallerAllocatedArray/CallerAllocatedPtrArray"); + println!(" - Avoid manual memory management errors"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_memory_management_differences() { + // This test demonstrates the different memory management behaviors + + // Test CalleeAllocatedArray - should only free the container + { + let _array = CalleeAllocatedArray::::default(); + // When _array goes out of scope, only the container is freed + } + + // Test CalleeAllocatedPtrArray - should free both container and elements + { + let _ptr_array = CalleeAllocatedPtrArray::::default(); + // When _ptr_array goes out of scope, both container and elements are freed + } + + // Both should complete without memory leaks + } +} diff --git a/opc_classic_utils/examples/opc_scenarios.rs b/opc_classic_utils/examples/opc_scenarios.rs new file mode 100644 index 0000000..bd8b879 --- /dev/null +++ b/opc_classic_utils/examples/opc_scenarios.rs @@ -0,0 +1,99 @@ +//! OPC scenarios example for opc_classic_utils +//! +//! This example demonstrates how to use the memory management utilities +//! in typical OPC scenarios, showing the correct patterns for different situations. + +use opc_classic_utils::memory::{ + CalleeAllocatedPtr, CalleeAllocatedWString, CallerAllocatedPtr, CallerAllocatedWString, +}; + +/// Simulates an OPC server method that takes input parameters (caller-allocated) +/// and returns output parameters (callee-allocated) +fn simulate_opc_server_method( + input_string: &CallerAllocatedWString, // Caller allocates, server frees + input_data: &CallerAllocatedPtr, // Caller allocates, server frees +) -> ( + CalleeAllocatedWString, // Server allocates, caller frees + CalleeAllocatedPtr, // Server allocates, caller frees +) { + println!(" Server: Processing input parameters..."); + println!(" Server: Input string is null: {}", input_string.is_null()); + println!(" Server: Input data is null: {}", input_data.is_null()); + + // Simulate server allocating memory for output + println!(" Server: Allocating memory for output parameters..."); + + // In a real scenario, the server would allocate memory using COM functions + // and return pointers to that memory + ( + CalleeAllocatedWString::default(), // Simulated server-allocated string + CalleeAllocatedPtr::default(), // Simulated server-allocated data + ) +} + +/// Simulates an OPC client calling a server method +fn simulate_opc_client_call() { + println!("OPC Client-Server Memory Management Example"); + println!("==========================================="); + + println!("\n1. Client preparing input parameters (caller-allocated):"); + + // Client allocates memory for input parameters + let client_input_string = CallerAllocatedWString::from_raw(std::ptr::null_mut()); + let client_input_data = CallerAllocatedPtr::from_raw(std::ptr::null_mut()); + + println!(" Client: Allocated memory for input string and data"); + println!(" Client: These will be freed by the server"); + + println!("\n2. Client calling server method:"); + + // Call the server method + let (server_output_string, server_output_data) = + simulate_opc_server_method(&client_input_string, &client_input_data); + + println!("\n3. Client processing output parameters (callee-allocated):"); + println!( + " Client: Received output string, null: {}", + server_output_string.is_null() + ); + println!( + " Client: Received output data, null: {}", + server_output_data.is_null() + ); + println!(" Client: These will be automatically freed when they go out of scope"); + + // The server_output_string and server_output_data will be automatically + // freed when they go out of scope at the end of this function +} + +/// Demonstrates the difference between input and output parameters +fn demonstrate_parameter_differences() { + println!("\n\nOPC Parameter Type Differences"); + println!("==============================="); + + println!("\nInput Parameters (Caller-allocated):"); + println!(" - Client allocates memory"); + println!(" - Server uses the memory"); + println!(" - Server frees the memory"); + println!(" - Our wrapper does NOT free (server's responsibility)"); + + println!("\nOutput Parameters (Callee-allocated):"); + println!(" - Server allocates memory"); + println!(" - Client receives the memory"); + println!(" - Client frees the memory"); + println!(" - Our wrapper automatically frees (client's responsibility)"); + + println!("\nKey Benefits:"); + println!(" - Clear ownership semantics"); + println!(" - Prevents double-free errors"); + println!(" - Prevents memory leaks"); + println!(" - Follows COM conventions exactly"); +} + +fn main() { + simulate_opc_client_call(); + demonstrate_parameter_differences(); + + println!("\n\nExample completed successfully!"); + println!("Memory management follows OPC/COM conventions correctly."); +} diff --git a/opc_classic_utils/examples/transparent_repr_demo.rs b/opc_classic_utils/examples/transparent_repr_demo.rs new file mode 100644 index 0000000..f62fda5 --- /dev/null +++ b/opc_classic_utils/examples/transparent_repr_demo.rs @@ -0,0 +1,137 @@ +//! Demonstration of #[repr(transparent)] usage +//! +//! This example shows how the transparent representation allows +//! our wrapper types to be used directly in FFI scenarios. + +use opc_classic_utils::memory::{ + CalleeAllocatedPtr, CalleeAllocatedWString, CallerAllocatedPtr, CallerAllocatedWString, +}; + +/// Simulates a C function that takes a pointer parameter +/// In real scenarios, this would be an external C function +#[allow(dead_code)] +extern "C" fn simulate_c_function(ptr: *mut i32) -> i32 { + if ptr.is_null() { 0 } else { unsafe { *ptr } } +} + +/// Simulates a C function that returns a pointer +/// In real scenarios, this would be an external C function +#[allow(dead_code)] +extern "C" fn simulate_c_function_return() -> *mut u16 { + std::ptr::null_mut() +} + +fn demonstrate_transparent_repr() { + println!("#[repr(transparent)] Demonstration"); + println!("=================================="); + + println!("\n1. Memory layout compatibility:"); + + // Create a raw pointer + let raw_ptr = std::ptr::null_mut::(); + + // Wrap it in our transparent types + let caller_ptr = CallerAllocatedPtr::from_raw(raw_ptr); + let callee_ptr = CalleeAllocatedPtr::from_raw(raw_ptr); + + println!(" Raw pointer: {:?}", raw_ptr); + println!(" CallerAllocatedPtr: {:?}", caller_ptr.as_ptr()); + println!(" CalleeAllocatedPtr: {:?}", callee_ptr.as_ptr()); + + // All pointers should be identical due to transparent repr + assert_eq!(raw_ptr, caller_ptr.as_ptr()); + assert_eq!(raw_ptr, callee_ptr.as_ptr()); + + println!(" ✓ All pointers have identical memory layout"); + + println!("\n2. FFI compatibility demonstration:"); + + // Our wrapper types can be used directly with C functions + // because they have transparent representation + + // For input parameters (caller-allocated) + let input_ptr = CallerAllocatedPtr::from_raw(std::ptr::null_mut::()); + println!(" Input pointer created: {:?}", input_ptr.as_ptr()); + + // For output parameters (callee-allocated) + let output_ptr = CalleeAllocatedPtr::from_raw(std::ptr::null_mut::()); + println!(" Output pointer created: {:?}", output_ptr.as_ptr()); + + println!(" ✓ Wrapper types are FFI-compatible"); + + println!("\n3. Zero-cost abstraction:"); + + // Demonstrate that there's no overhead + let ptr1 = std::ptr::null_mut::(); + let _ptr2 = CallerAllocatedPtr::from_raw(ptr1); + let _ptr3 = CalleeAllocatedPtr::from_raw(ptr1); + + // All should have the same size + println!( + " Size of *mut i32: {} bytes", + std::mem::size_of::<*mut i32>() + ); + println!( + " Size of CallerAllocatedPtr: {} bytes", + std::mem::size_of::>() + ); + println!( + " Size of CalleeAllocatedPtr: {} bytes", + std::mem::size_of::>() + ); + + assert_eq!( + std::mem::size_of::<*mut i32>(), + std::mem::size_of::>() + ); + assert_eq!( + std::mem::size_of::<*mut i32>(), + std::mem::size_of::>() + ); + + println!(" ✓ Zero memory overhead"); + + println!("\n4. Wide string compatibility:"); + + let wstring_ptr = std::ptr::null_mut::(); + let _caller_wstring = CallerAllocatedWString::from_raw(wstring_ptr); + let _callee_wstring = CalleeAllocatedWString::from_raw(wstring_ptr); + + println!( + " Size of *mut u16: {} bytes", + std::mem::size_of::<*mut u16>() + ); + println!( + " Size of CallerAllocatedWString: {} bytes", + std::mem::size_of::() + ); + println!( + " Size of CalleeAllocatedWString: {} bytes", + std::mem::size_of::() + ); + + assert_eq!( + std::mem::size_of::<*mut u16>(), + std::mem::size_of::() + ); + assert_eq!( + std::mem::size_of::<*mut u16>(), + std::mem::size_of::() + ); + + println!(" ✓ Wide string types also have zero overhead"); + + println!("\n5. Benefits summary:"); + println!(" - Memory layout identical to raw pointers"); + println!(" - FFI-compatible without any conversion"); + println!(" - Zero runtime overhead"); + println!(" - Type safety and automatic memory management"); + println!(" - Perfect for COM/OPC scenarios"); +} + +fn main() { + demonstrate_transparent_repr(); + + println!("\n✅ #[repr(transparent)] demonstration completed successfully!"); + println!(" Our wrapper types provide safety without any performance cost."); +} diff --git a/opc_classic_utils/src/lib.rs b/opc_classic_utils/src/lib.rs new file mode 100644 index 0000000..38d6213 --- /dev/null +++ b/opc_classic_utils/src/lib.rs @@ -0,0 +1,8 @@ +//! OPC Classic utilities and common functionality +//! +//! This crate provides shared utilities for OPC Classic implementations, +//! including automatic memory management and common traits. + +pub mod memory; + +pub use memory::*; diff --git a/opc_classic_utils/src/memory/array.rs b/opc_classic_utils/src/memory/array.rs new file mode 100644 index 0000000..985e961 --- /dev/null +++ b/opc_classic_utils/src/memory/array.rs @@ -0,0 +1,347 @@ +use std::ptr; +use windows::Win32::System::Com::{CoTaskMemAlloc, CoTaskMemFree}; + +/// A smart pointer for COM memory arrays that the **caller allocates and callee frees** +/// +/// This is used for input array parameters where the caller allocates memory +/// and the callee (COM function) is responsible for freeing it. +/// This wrapper does NOT free the memory when dropped. +#[derive(Debug)] +pub struct CallerAllocatedArray { + ptr: *mut T, + len: usize, +} + +impl CallerAllocatedArray { + /// Creates a new `CallerAllocatedArray` from a raw pointer and length + /// + /// # Safety + /// + /// The caller must ensure that `ptr` is a valid pointer to an array of `len` elements + /// allocated by the caller and that the callee will be responsible for freeing it. + pub unsafe fn new(ptr: *mut T, len: usize) -> Self { + Self { ptr, len } + } + + /// Creates a new `CallerAllocatedArray` from a raw pointer and length, taking ownership + pub fn from_raw(ptr: *mut T, len: usize) -> Self { + Self { ptr, len } + } + + /// Allocates memory for an array using `CoTaskMemAlloc` and creates a `CallerAllocatedArray` + /// + /// This allocates memory that will be freed by the callee (COM function). + /// The caller is responsible for ensuring the callee will free this memory. + pub fn allocate(len: usize) -> Result { + if len == 0 { + return Ok(Self { + ptr: ptr::null_mut(), + len: 0, + }); + } + + let size = std::mem::size_of::().checked_mul(len).ok_or_else(|| { + windows::core::Error::new( + windows::core::HRESULT::from_win32(0x80070057), // E_INVALIDARG + "Array size overflow", + ) + })?; + + let ptr = unsafe { CoTaskMemAlloc(size) }; + if ptr.is_null() { + return Err(windows::core::Error::from_win32()); + } + Ok(unsafe { Self::new(ptr.cast(), len) }) + } + + /// Allocates memory and initializes it with a copy of the given slice + /// + /// This creates a copy of the slice in COM-allocated memory. + pub fn from_slice(slice: &[T]) -> Result + where + T: Copy, + { + if slice.is_empty() { + return Ok(Self { + ptr: ptr::null_mut(), + len: 0, + }); + } + + let array = Self::allocate(slice.len())?; + unsafe { + std::ptr::copy_nonoverlapping(slice.as_ptr(), array.as_ptr(), slice.len()); + } + Ok(array) + } + + /// Returns the raw pointer without transferring ownership + pub fn as_ptr(&self) -> *mut T { + self.ptr + } + + /// Returns the raw pointer and transfers ownership to the caller + /// + /// After calling this method, the `CallerAllocatedArray` will not manage the memory. + pub fn into_raw(mut self) -> (*mut T, usize) { + let ptr = self.ptr; + let len = self.len; + self.ptr = ptr::null_mut(); + self.len = 0; + (ptr, len) + } + + /// Returns the length of the array + pub fn len(&self) -> usize { + self.len + } + + /// Returns true if the array is empty + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Checks if the pointer is null + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Returns a slice of the array if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_slice(&self) -> Option<&[T]> { + if self.ptr.is_null() { + None + } else { + unsafe { Some(std::slice::from_raw_parts(self.ptr, self.len)) } + } + } + + /// Returns a mutable slice of the array if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_mut_slice(&mut self) -> Option<&mut [T]> { + if self.ptr.is_null() { + None + } else { + unsafe { Some(std::slice::from_raw_parts_mut(self.ptr, self.len)) } + } + } + + /// Gets an element at the given index + /// + /// # Safety + /// + /// The caller must ensure the index is within bounds and the pointer is valid. + pub unsafe fn get(&self, index: usize) -> Option<&T> { + if index >= self.len || self.ptr.is_null() { + None + } else { + unsafe { Some(&*self.ptr.add(index)) } + } + } + + /// Gets a mutable element at the given index + /// + /// # Safety + /// + /// The caller must ensure the index is within bounds and the pointer is valid. + pub unsafe fn get_mut(&mut self, index: usize) -> Option<&mut T> { + if index >= self.len || self.ptr.is_null() { + None + } else { + unsafe { Some(&mut *self.ptr.add(index)) } + } + } +} + +impl Drop for CallerAllocatedArray { + fn drop(&mut self) { + // Do NOT free the memory - the callee is responsible for this + // Just clear the pointer to prevent use-after-free + self.ptr = ptr::null_mut(); + self.len = 0; + } +} + +impl Default for CallerAllocatedArray { + fn default() -> Self { + Self { + ptr: ptr::null_mut(), + len: 0, + } + } +} + +impl Clone for CallerAllocatedArray { + /// Creates a shallow copy of the pointer. + /// + /// # Safety + /// + /// The caller must ensure that only one instance is passed to functions + /// that will free the memory, to avoid double-free errors. + fn clone(&self) -> Self { + Self { + ptr: self.ptr, + len: self.len, + } + } +} + +/// A smart pointer for COM memory arrays that the **callee allocates and caller frees** +/// +/// This is used for output array parameters where the callee (COM function) allocates memory +/// and the caller is responsible for freeing it using `CoTaskMemFree`. +/// +/// # Memory Management +/// - **Only frees the array container itself** +/// - **Does NOT free individual array elements** +/// - Use this when the callee returns an array of values (not pointers) +/// +/// # Typical Use Cases +/// - OPC server returning an array of data values +/// - COM function returning an array of structures +/// - Any scenario where you receive a contiguous array of data +/// +/// # Example +/// ```rust +/// use opc_classic_utils::memory::CalleeAllocatedArray; +/// use std::ptr; +/// +/// // Server returns: [42.0, 84.0, 126.0] as *mut f64 +/// let ptr = ptr::null_mut::(); // In real code, this would be from COM +/// let values = CalleeAllocatedArray::from_raw(ptr, 3); +/// // When values goes out of scope, only the array is freed +/// ``` +#[derive(Debug)] +pub struct CalleeAllocatedArray { + ptr: *mut T, + len: usize, +} + +impl CalleeAllocatedArray { + /// Creates a new `CalleeAllocatedArray` from a raw pointer and length + /// + /// # Safety + /// + /// The caller must ensure that `ptr` is a valid pointer to an array of `len` elements + /// allocated by the callee and that it will be freed using `CoTaskMemFree`. + pub unsafe fn new(ptr: *mut T, len: usize) -> Self { + Self { ptr, len } + } + + /// Creates a new `CalleeAllocatedArray` from a raw pointer and length, taking ownership + /// + /// This is safe when the pointer is null, as `CoTaskMemFree` handles null pointers. + pub fn from_raw(ptr: *mut T, len: usize) -> Self { + Self { ptr, len } + } + + /// Returns the raw pointer without transferring ownership + pub fn as_ptr(&self) -> *mut T { + self.ptr + } + + /// Returns the raw pointer and transfers ownership to the caller + /// + /// After calling this method, the `CalleeAllocatedArray` will not free the memory. + pub fn into_raw(mut self) -> (*mut T, usize) { + let ptr = self.ptr; + let len = self.len; + self.ptr = ptr::null_mut(); + self.len = 0; + (ptr, len) + } + + /// Returns the length of the array + pub fn len(&self) -> usize { + self.len + } + + /// Returns true if the array is empty + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Checks if the pointer is null + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Returns a slice of the array if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_slice(&self) -> Option<&[T]> { + if self.ptr.is_null() { + None + } else { + unsafe { Some(std::slice::from_raw_parts(self.ptr, self.len)) } + } + } + + /// Returns a mutable slice of the array if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_mut_slice(&mut self) -> Option<&mut [T]> { + if self.ptr.is_null() { + None + } else { + unsafe { Some(std::slice::from_raw_parts_mut(self.ptr, self.len)) } + } + } + + /// Gets an element at the given index + /// + /// # Safety + /// + /// The caller must ensure the index is within bounds and the pointer is valid. + pub unsafe fn get(&self, index: usize) -> Option<&T> { + if index >= self.len || self.ptr.is_null() { + None + } else { + unsafe { Some(&*self.ptr.add(index)) } + } + } + + /// Gets a mutable element at the given index + /// + /// # Safety + /// + /// The caller must ensure the index is within bounds and the pointer is valid. + pub unsafe fn get_mut(&mut self, index: usize) -> Option<&mut T> { + if index >= self.len || self.ptr.is_null() { + None + } else { + unsafe { Some(&mut *self.ptr.add(index)) } + } + } +} + +impl Drop for CalleeAllocatedArray { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + CoTaskMemFree(Some(self.ptr.cast())); + } + self.ptr = ptr::null_mut(); + self.len = 0; + } + } +} + +impl Default for CalleeAllocatedArray { + fn default() -> Self { + Self { + ptr: ptr::null_mut(), + len: 0, + } + } +} diff --git a/opc_classic_utils/src/memory/mod.rs b/opc_classic_utils/src/memory/mod.rs new file mode 100644 index 0000000..a6d66e6 --- /dev/null +++ b/opc_classic_utils/src/memory/mod.rs @@ -0,0 +1,22 @@ +//! Memory management utilities for OPC Classic +//! +//! This module provides automatic memory management for COM objects +//! using `CoTaskMemFree` for cleanup. +//! +//! COM memory management follows two patterns: +//! 1. Caller allocates, callee frees (e.g., input parameters) +//! 2. Callee allocates, caller frees (e.g., output parameters) + +pub mod array; +pub mod ptr; +pub mod ptr_array; +pub mod wstring; + +// Re-export all public types for convenience +pub use array::{CalleeAllocatedArray, CallerAllocatedArray}; +pub use ptr::{CalleeAllocatedPtr, CallerAllocatedPtr}; +pub use ptr_array::{CalleeAllocatedPtrArray, CallerAllocatedPtrArray}; +pub use wstring::{CalleeAllocatedWString, CallerAllocatedWString}; + +#[cfg(test)] +mod tests; diff --git a/opc_classic_utils/src/memory/ptr.rs b/opc_classic_utils/src/memory/ptr.rs new file mode 100644 index 0000000..07f2dee --- /dev/null +++ b/opc_classic_utils/src/memory/ptr.rs @@ -0,0 +1,240 @@ +use std::ptr; +use windows::Win32::System::Com::{CoTaskMemAlloc, CoTaskMemFree}; + +/// A smart pointer for COM memory that the **caller allocates and callee frees** +/// +/// This is used for input parameters where the caller allocates memory +/// and the callee (COM function) is responsible for freeing it. +/// This wrapper does NOT free the memory when dropped. +#[repr(transparent)] +#[derive(Debug)] +pub struct CallerAllocatedPtr { + ptr: *mut T, +} + +impl CallerAllocatedPtr { + /// Creates a new `CallerAllocatedPtr` from a raw pointer + /// + /// # Safety + /// + /// The caller must ensure that `ptr` is a valid pointer allocated by the caller + /// and that the callee will be responsible for freeing it. + pub unsafe fn new(ptr: *mut T) -> Self { + Self { ptr } + } + + /// Creates a new `CallerAllocatedPtr` from a raw pointer, taking ownership + pub fn from_raw(ptr: *mut T) -> Self { + Self { ptr } + } + + /// Allocates memory using `CoTaskMemAlloc` and creates a `CallerAllocatedPtr` + /// + /// This allocates memory that will be freed by the callee (COM function). + /// The caller is responsible for ensuring the callee will free this memory. + pub fn allocate() -> Result { + let ptr = unsafe { CoTaskMemAlloc(std::mem::size_of::()) }; + if ptr.is_null() { + return Err(windows::core::Error::from_win32()); + } + Ok(unsafe { Self::new(ptr.cast()) }) + } + + /// Allocates memory and initializes it with a copy of the given value + /// + /// This creates a copy of the value in COM-allocated memory. + pub fn from_value(value: &T) -> Result + where + T: Copy, + { + let ptr = Self::allocate()?; + unsafe { + *ptr.as_ptr() = *value; + } + Ok(ptr) + } + + /// Returns the raw pointer without transferring ownership + pub fn as_ptr(&self) -> *mut T { + self.ptr + } + + /// Returns the raw pointer and transfers ownership to the caller + /// + /// After calling this method, the `CallerAllocatedPtr` will not manage the memory. + pub fn into_raw(mut self) -> *mut T { + let ptr = self.ptr; + self.ptr = ptr::null_mut(); + ptr + } + + /// Checks if the pointer is null + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Dereferences the pointer if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_ref(&self) -> Option<&T> { + if self.ptr.is_null() { + None + } else { + Some(unsafe { &*self.ptr }) + } + } + + /// Mutably dereferences the pointer if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_mut(&mut self) -> Option<&mut T> { + if self.ptr.is_null() { + None + } else { + Some(unsafe { &mut *self.ptr }) + } + } +} + +impl Drop for CallerAllocatedPtr { + fn drop(&mut self) { + // Do NOT free the memory - the callee is responsible for this + // Just clear the pointer to prevent use-after-free + self.ptr = ptr::null_mut(); + } +} + +impl Default for CallerAllocatedPtr { + fn default() -> Self { + Self { + ptr: ptr::null_mut(), + } + } +} + +impl Clone for CallerAllocatedPtr { + /// Creates a shallow copy of the pointer. + /// + /// # Safety + /// + /// The caller must ensure that only one instance is passed to functions + /// that will free the memory, to avoid double-free errors. + fn clone(&self) -> Self { + Self { ptr: self.ptr } + } +} + +/// A smart pointer for COM memory that the **callee allocates and caller frees** +/// +/// This is used for output parameters where the callee (COM function) allocates memory +/// and the caller is responsible for freeing it using `CoTaskMemFree`. +#[repr(transparent)] +#[derive(Debug)] +pub struct CalleeAllocatedPtr { + ptr: *mut T, +} + +impl CalleeAllocatedPtr { + /// Creates a new `CalleeAllocatedPtr` from a raw pointer + /// + /// # Safety + /// + /// The caller must ensure that `ptr` is a valid pointer allocated by the callee + /// and that it will be freed using `CoTaskMemFree`. + pub unsafe fn new(ptr: *mut T) -> Self { + Self { ptr } + } + + /// Creates a new `CalleeAllocatedPtr` from a raw pointer, taking ownership + /// + /// This is safe when the pointer is null, as `CoTaskMemFree` handles null pointers. + pub fn from_raw(ptr: *mut T) -> Self { + Self { ptr } + } + + /// Creates a new `CalleeAllocatedPtr` from a value, allocating memory + /// + /// This allocates memory using `CoTaskMemAlloc` and copies the value into it. + pub fn from_value(value: &T) -> Result + where + T: Copy, + { + let size = std::mem::size_of::(); + let ptr = unsafe { windows::Win32::System::Com::CoTaskMemAlloc(size) }; + if ptr.is_null() { + return Err(windows::core::Error::from_win32()); + } + unsafe { + std::ptr::copy_nonoverlapping(value, ptr.cast(), 1); + } + Ok(unsafe { Self::new(ptr.cast()) }) + } + + /// Returns the raw pointer without transferring ownership + pub fn as_ptr(&self) -> *mut T { + self.ptr + } + + /// Returns the raw pointer and transfers ownership to the caller + /// + /// After calling this method, the `CalleeAllocatedPtr` will not free the memory. + pub fn into_raw(mut self) -> *mut T { + let ptr = self.ptr; + self.ptr = ptr::null_mut(); + ptr + } + + /// Checks if the pointer is null + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Dereferences the pointer if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_ref(&self) -> Option<&T> { + if self.ptr.is_null() { + None + } else { + Some(unsafe { &*self.ptr }) + } + } + + /// Mutably dereferences the pointer if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_mut(&mut self) -> Option<&mut T> { + if self.ptr.is_null() { + None + } else { + Some(unsafe { &mut *self.ptr }) + } + } +} + +impl Drop for CalleeAllocatedPtr { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + CoTaskMemFree(Some(self.ptr.cast())); + } + self.ptr = ptr::null_mut(); + } + } +} + +impl Default for CalleeAllocatedPtr { + fn default() -> Self { + Self { + ptr: ptr::null_mut(), + } + } +} diff --git a/opc_classic_utils/src/memory/ptr_array.rs b/opc_classic_utils/src/memory/ptr_array.rs new file mode 100644 index 0000000..c16b00d --- /dev/null +++ b/opc_classic_utils/src/memory/ptr_array.rs @@ -0,0 +1,366 @@ +use std::ptr; +use windows::Win32::System::Com::{CoTaskMemAlloc, CoTaskMemFree}; + +/// A smart pointer for COM memory pointer arrays that the **caller allocates and callee frees** +/// +/// This is used for input pointer array parameters where the caller allocates memory +/// and the callee (COM function) is responsible for freeing it. +/// This wrapper does NOT free the memory when dropped. +#[derive(Debug)] +pub struct CallerAllocatedPtrArray { + ptr: *mut *mut T, + len: usize, +} + +impl CallerAllocatedPtrArray { + /// Creates a new `CallerAllocatedPtrArray` from a raw pointer and length + /// + /// # Safety + /// + /// The caller must ensure that `ptr` is a valid pointer to an array of `len` pointers + /// allocated by the caller and that the callee will be responsible for freeing it. + pub unsafe fn new(ptr: *mut *mut T, len: usize) -> Self { + Self { ptr, len } + } + + /// Creates a new `CallerAllocatedPtrArray` from a raw pointer and length, taking ownership + pub fn from_raw(ptr: *mut *mut T, len: usize) -> Self { + Self { ptr, len } + } + + /// Allocates memory for a pointer array using `CoTaskMemAlloc` and creates a `CallerAllocatedPtrArray` + /// + /// This allocates memory that will be freed by the callee (COM function). + /// The caller is responsible for ensuring the callee will free this memory. + pub fn allocate(len: usize) -> Result { + if len == 0 { + return Ok(Self { + ptr: ptr::null_mut(), + len: 0, + }); + } + + let size = std::mem::size_of::<*mut T>() + .checked_mul(len) + .ok_or_else(|| { + windows::core::Error::new( + windows::core::HRESULT::from_win32(0x80070057), // E_INVALIDARG + "Pointer array size overflow", + ) + })?; + + let ptr = unsafe { CoTaskMemAlloc(size) }; + if ptr.is_null() { + return Err(windows::core::Error::from_win32()); + } + Ok(unsafe { Self::new(ptr.cast(), len) }) + } + + /// Allocates memory and initializes it with pointers from the given slice + /// + /// This creates a copy of the pointers in COM-allocated memory. + pub fn from_ptr_slice(slice: &[*mut T]) -> Result { + if slice.is_empty() { + return Ok(Self { + ptr: ptr::null_mut(), + len: 0, + }); + } + + let array = Self::allocate(slice.len())?; + unsafe { + std::ptr::copy_nonoverlapping(slice.as_ptr(), array.as_ptr(), slice.len()); + } + Ok(array) + } + + /// Returns the raw pointer without transferring ownership + pub fn as_ptr(&self) -> *mut *mut T { + self.ptr + } + + /// Returns the raw pointer and transfers ownership to the caller + /// + /// After calling this method, the `CallerAllocatedPtrArray` will not manage the memory. + pub fn into_raw(mut self) -> (*mut *mut T, usize) { + let ptr = self.ptr; + let len = self.len; + self.ptr = ptr::null_mut(); + self.len = 0; + (ptr, len) + } + + /// Returns the length of the array + pub fn len(&self) -> usize { + self.len + } + + /// Returns true if the array is empty + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Checks if the pointer is null + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Returns a slice of the pointer array if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_slice(&self) -> Option<&[*mut T]> { + if self.ptr.is_null() { + None + } else { + unsafe { Some(std::slice::from_raw_parts(self.ptr, self.len)) } + } + } + + /// Returns a mutable slice of the pointer array if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_mut_slice(&mut self) -> Option<&mut [*mut T]> { + if self.ptr.is_null() { + None + } else { + unsafe { Some(std::slice::from_raw_parts_mut(self.ptr, self.len)) } + } + } + + /// Gets a pointer at the given index + /// + /// # Safety + /// + /// The caller must ensure the index is within bounds and the pointer is valid. + pub unsafe fn get(&self, index: usize) -> Option<*mut T> { + if index >= self.len || self.ptr.is_null() { + None + } else { + unsafe { Some(*self.ptr.add(index)) } + } + } + + /// Sets a pointer at the given index + /// + /// # Safety + /// + /// The caller must ensure the index is within bounds and the pointer is valid. + pub unsafe fn set(&mut self, index: usize, value: *mut T) -> bool { + if index >= self.len || self.ptr.is_null() { + false + } else { + unsafe { + *self.ptr.add(index) = value; + } + true + } + } +} + +impl Drop for CallerAllocatedPtrArray { + fn drop(&mut self) { + // Do NOT free the memory - the callee is responsible for this + // Just clear the pointer to prevent use-after-free + self.ptr = ptr::null_mut(); + self.len = 0; + } +} + +impl Default for CallerAllocatedPtrArray { + fn default() -> Self { + Self { + ptr: ptr::null_mut(), + len: 0, + } + } +} + +impl Clone for CallerAllocatedPtrArray { + /// Creates a shallow copy of the pointer. + /// + /// # Safety + /// + /// The caller must ensure that only one instance is passed to functions + /// that will free the memory, to avoid double-free errors. + fn clone(&self) -> Self { + Self { + ptr: self.ptr, + len: self.len, + } + } +} + +/// A smart pointer for COM memory pointer arrays that the **callee allocates and caller frees** +/// +/// This is used for output pointer array parameters where the callee (COM function) allocates memory +/// and the caller is responsible for freeing it using `CoTaskMemFree`. +/// +/// # Memory Management +/// - **Frees the array container itself** +/// - **ALSO frees each individual pointer in the array** +/// - Use this when the callee returns an array of pointers to allocated memory +/// +/// # Typical Use Cases +/// - OPC server returning an array of string pointers +/// - COM function returning an array of object pointers +/// - Any scenario where you receive an array of pointers to allocated memory +/// +/// # Example +/// ```rust +/// use opc_classic_utils::memory::CalleeAllocatedPtrArray; +/// use std::ptr; +/// +/// // Server returns: [ptr1, ptr2, ptr3] where each ptr points to allocated memory +/// let ptr = ptr::null_mut::<*mut u16>(); // In real code, this would be from COM +/// let string_ptrs = CalleeAllocatedPtrArray::from_raw(ptr, 3); +/// // When string_ptrs goes out of scope: +/// // 1. Each pointer in the array is freed +/// // 2. The array itself is freed +/// ``` +/// +/// # ⚠️ Important Difference from CalleeAllocatedArray +/// - `CalleeAllocatedArray`: Only frees the array container +/// - `CalleeAllocatedPtrArray`: Frees both the array AND each pointer in it +#[derive(Debug)] +pub struct CalleeAllocatedPtrArray { + ptr: *mut *mut T, + len: usize, +} + +impl CalleeAllocatedPtrArray { + /// Creates a new `CalleeAllocatedPtrArray` from a raw pointer and length + /// + /// # Safety + /// + /// The caller must ensure that `ptr` is a valid pointer to an array of `len` pointers + /// allocated by the callee and that it will be freed using `CoTaskMemFree`. + pub unsafe fn new(ptr: *mut *mut T, len: usize) -> Self { + Self { ptr, len } + } + + /// Creates a new `CalleeAllocatedPtrArray` from a raw pointer and length, taking ownership + /// + /// This is safe when the pointer is null, as `CoTaskMemFree` handles null pointers. + pub fn from_raw(ptr: *mut *mut T, len: usize) -> Self { + Self { ptr, len } + } + + /// Returns the raw pointer without transferring ownership + pub fn as_ptr(&self) -> *mut *mut T { + self.ptr + } + + /// Returns the raw pointer and transfers ownership to the caller + /// + /// After calling this method, the `CalleeAllocatedPtrArray` will not free the memory. + pub fn into_raw(mut self) -> (*mut *mut T, usize) { + let ptr = self.ptr; + let len = self.len; + self.ptr = ptr::null_mut(); + self.len = 0; + (ptr, len) + } + + /// Returns the length of the array + pub fn len(&self) -> usize { + self.len + } + + /// Returns true if the array is empty + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Checks if the pointer is null + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Returns a slice of the pointer array if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_slice(&self) -> Option<&[*mut T]> { + if self.ptr.is_null() { + None + } else { + unsafe { Some(std::slice::from_raw_parts(self.ptr, self.len)) } + } + } + + /// Returns a mutable slice of the pointer array if it's not null + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to initialized data. + pub unsafe fn as_mut_slice(&mut self) -> Option<&mut [*mut T]> { + if self.ptr.is_null() { + None + } else { + unsafe { Some(std::slice::from_raw_parts_mut(self.ptr, self.len)) } + } + } + + /// Gets a pointer at the given index + /// + /// # Safety + /// + /// The caller must ensure the index is within bounds and the pointer is valid. + pub unsafe fn get(&self, index: usize) -> Option<*mut T> { + if index >= self.len || self.ptr.is_null() { + None + } else { + unsafe { Some(*self.ptr.add(index)) } + } + } + + /// Sets a pointer at the given index + /// + /// # Safety + /// + /// The caller must ensure the index is within bounds and the pointer is valid. + pub unsafe fn set(&mut self, index: usize, value: *mut T) -> bool { + if index >= self.len || self.ptr.is_null() { + false + } else { + unsafe { + *self.ptr.add(index) = value; + } + true + } + } +} + +impl Drop for CalleeAllocatedPtrArray { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + // First, free each individual pointer in the array + for i in 0..self.len { + let element_ptr = *self.ptr.add(i); + if !element_ptr.is_null() { + CoTaskMemFree(Some(element_ptr.cast())); + } + } + // Then free the array itself + CoTaskMemFree(Some(self.ptr.cast())); + } + self.ptr = ptr::null_mut(); + self.len = 0; + } + } +} + +impl Default for CalleeAllocatedPtrArray { + fn default() -> Self { + Self { + ptr: ptr::null_mut(), + len: 0, + } + } +} diff --git a/opc_classic_utils/src/memory/tests.rs b/opc_classic_utils/src/memory/tests.rs new file mode 100644 index 0000000..0aeef95 --- /dev/null +++ b/opc_classic_utils/src/memory/tests.rs @@ -0,0 +1,374 @@ +use super::*; +use std::str::FromStr; + +#[test] +fn test_caller_allocated_ptr_null() { + let ptr = CallerAllocatedPtr::::default(); + assert!(ptr.is_null()); +} + +#[test] +fn test_callee_allocated_ptr_null() { + let ptr = CalleeAllocatedPtr::::default(); + assert!(ptr.is_null()); +} + +#[test] +fn test_caller_allocated_wstring_null() { + let wstring = CallerAllocatedWString::default(); + assert!(wstring.is_null()); +} + +#[test] +fn test_callee_allocated_wstring_null() { + let wstring = CalleeAllocatedWString::default(); + assert!(wstring.is_null()); +} + +#[test] +fn test_caller_allocated_ptr_no_free() { + // This test verifies that CallerAllocatedPtr doesn't free memory + // In a real scenario, this would be memory allocated by the caller + let _ptr = CallerAllocatedPtr::from_raw(std::ptr::null_mut::()); + // When _ptr goes out of scope, it should NOT call CoTaskMemFree +} + +#[test] +fn test_callee_allocated_ptr_frees() { + // This test verifies that CalleeAllocatedPtr frees memory + // In a real scenario, this would be memory allocated by the callee + let _ptr = CalleeAllocatedPtr::from_raw(std::ptr::null_mut::()); + // When _ptr goes out of scope, it should call CoTaskMemFree +} + +#[test] +fn test_transparent_repr() { + // Test that transparent repr works correctly + let ptr = std::ptr::null_mut::(); + + // CallerAllocatedPtr should have the same memory layout as *mut i32 + let caller_ptr = CallerAllocatedPtr::from_raw(ptr); + assert_eq!(caller_ptr.as_ptr(), ptr); + + // CalleeAllocatedPtr should have the same memory layout as *mut i32 + let callee_ptr = CalleeAllocatedPtr::from_raw(ptr); + assert_eq!(callee_ptr.as_ptr(), ptr); + + // WString types should have the same memory layout as *mut u16 + let wstring_ptr = std::ptr::null_mut::(); + let caller_wstring = CallerAllocatedWString::from_raw(wstring_ptr); + assert_eq!(caller_wstring.as_ptr(), wstring_ptr); + + let callee_wstring = CalleeAllocatedWString::from_raw(wstring_ptr); + assert_eq!(callee_wstring.as_ptr(), wstring_ptr); +} + +#[test] +fn test_caller_allocated_ptr_allocate() { + // Test allocation of caller-allocated pointer + let ptr = CallerAllocatedPtr::::allocate().unwrap(); + assert!(!ptr.is_null()); + // Memory will be freed by callee, not by our wrapper +} + +#[test] +fn test_caller_allocated_ptr_from_value() { + // Test creating pointer from value + let value = 42i32; + let ptr = CallerAllocatedPtr::from_value(&value).unwrap(); + assert!(!ptr.is_null()); + + // Verify the value was copied correctly + unsafe { + assert_eq!(*ptr.as_ptr(), 42); + } +} + +#[test] +fn test_caller_allocated_wstring_from_str() { + // Test creating wide string from Rust string + use std::str::FromStr; + let test_string = "Hello, World!"; + let wstring = CallerAllocatedWString::from_str(test_string).unwrap(); + assert!(!wstring.is_null()); + + // Verify the string was converted correctly + unsafe { + let converted = wstring.to_string().unwrap(); + assert_eq!(converted, test_string); + } +} + +#[test] +fn test_caller_allocated_wstring_from_string() { + // Test creating wide string from String + let test_string = String::from("Test String"); + let wstring = CallerAllocatedWString::from_string(test_string.clone()).unwrap(); + assert!(!wstring.is_null()); + + // Verify the string was converted correctly + unsafe { + let converted = wstring.to_string().unwrap(); + assert_eq!(converted, test_string); + } +} + +#[test] +fn test_caller_allocated_wstring_from_os_str() { + // Test creating wide string from OsStr + let test_string = std::ffi::OsStr::new("OS String Test"); + let wstring = CallerAllocatedWString::from_os_str(test_string).unwrap(); + assert!(!wstring.is_null()); + + // Verify the string was converted correctly + unsafe { + let converted = wstring.to_os_string().unwrap(); + assert_eq!(converted, test_string); + } +} + +#[test] +fn test_pointer_dereference() { + // Test dereferencing pointers + let value = 123i32; + let mut caller_ptr = CallerAllocatedPtr::from_value(&value).unwrap(); + // Don't create a CalleeAllocatedPtr from CallerAllocatedPtr - it would cause double-free + + // Test as_ref + unsafe { + assert_eq!(caller_ptr.as_ref().unwrap(), &123); + } + + // Test as_mut + unsafe { + *caller_ptr.as_mut().unwrap() = 456; + assert_eq!(*caller_ptr.as_ptr(), 456); + } +} + +#[test] +fn test_callee_allocated_ptr_dereference() { + // Test dereferencing CalleeAllocatedPtr with separate memory + let value = 789i32; + let callee_ptr = CalleeAllocatedPtr::from_value(&value).unwrap(); + + // Test as_ref + unsafe { + assert_eq!(callee_ptr.as_ref().unwrap(), &789); + } +} + +#[test] +fn test_null_pointer_dereference() { + // Test dereferencing null pointers + let caller_ptr = CallerAllocatedPtr::::default(); + let callee_ptr = CalleeAllocatedPtr::::default(); + + unsafe { + assert!(caller_ptr.as_ref().is_none()); + assert!(callee_ptr.as_ref().is_none()); + } +} + +#[test] +fn test_wstring_null_conversion() { + // Test converting null wide strings + let caller_wstring = CallerAllocatedWString::default(); + let callee_wstring = CalleeAllocatedWString::default(); + + unsafe { + assert!(caller_wstring.to_string().is_none()); + assert!(callee_wstring.to_string().is_none()); + assert!(caller_wstring.to_os_string().is_none()); + assert!(callee_wstring.to_os_string().is_none()); + } +} + +#[test] +fn test_from_str_trait() { + // Test the FromStr trait implementation + let test_string = "Test FromStr Trait"; + let wstring = CallerAllocatedWString::from_str(test_string).unwrap(); + assert!(!wstring.is_null()); + + // Verify the string was converted correctly + unsafe { + let converted = wstring.to_string().unwrap(); + assert_eq!(converted, test_string); + } +} + +#[test] +fn test_caller_allocated_array_null() { + let array = CallerAllocatedArray::::default(); + assert!(array.is_null()); + assert!(array.is_empty()); + assert_eq!(array.len(), 0); +} + +#[test] +fn test_callee_allocated_array_null() { + let array = CalleeAllocatedArray::::default(); + assert!(array.is_null()); + assert!(array.is_empty()); + assert_eq!(array.len(), 0); +} + +#[test] +fn test_caller_allocated_array_allocate() { + let array = CallerAllocatedArray::::allocate(5).unwrap(); + assert!(!array.is_null()); + assert!(!array.is_empty()); + assert_eq!(array.len(), 5); +} + +#[test] +fn test_caller_allocated_array_from_slice() { + let data = vec![1, 2, 3, 4, 5]; + let array = CallerAllocatedArray::from_slice(&data).unwrap(); + assert!(!array.is_null()); + assert_eq!(array.len(), 5); + + // Verify the data was copied correctly + unsafe { + let slice = array.as_slice().unwrap(); + assert_eq!(slice, data.as_slice()); + } +} + +#[test] +fn test_caller_allocated_array_access() { + let data = vec![10, 20, 30]; + let mut array = CallerAllocatedArray::from_slice(&data).unwrap(); + + // Test get + unsafe { + assert_eq!(*array.get(0).unwrap(), 10); + assert_eq!(*array.get(1).unwrap(), 20); + assert_eq!(*array.get(2).unwrap(), 30); + assert!(array.get(3).is_none()); // Out of bounds + } + + // Test get_mut + unsafe { + *array.get_mut(1).unwrap() = 25; + assert_eq!(*array.get(1).unwrap(), 25); + } + + // Test as_slice and as_mut_slice + unsafe { + let slice = array.as_slice().unwrap(); + assert_eq!(slice, &[10, 25, 30]); + + let mut_slice = array.as_mut_slice().unwrap(); + mut_slice[2] = 35; + assert_eq!(*array.get(2).unwrap(), 35); + } +} + +#[test] +fn test_callee_allocated_array_frees_container() { + // This test verifies that CalleeAllocatedArray frees the container memory + // In a real scenario, this would be memory allocated by the callee + let _array: CalleeAllocatedArray = CalleeAllocatedArray::from_raw(std::ptr::null_mut(), 0); + // When _array goes out of scope, it should call CoTaskMemFree on the container +} + +#[test] +fn test_caller_allocated_ptr_array_null() { + let array = CallerAllocatedPtrArray::::default(); + assert!(array.is_null()); + assert!(array.is_empty()); + assert_eq!(array.len(), 0); +} + +#[test] +fn test_callee_allocated_ptr_array_null() { + let array = CalleeAllocatedPtrArray::::default(); + assert!(array.is_null()); + assert!(array.is_empty()); + assert_eq!(array.len(), 0); +} + +#[test] +fn test_caller_allocated_ptr_array_allocate() { + let array = CallerAllocatedPtrArray::::allocate(3).unwrap(); + assert!(!array.is_null()); + assert!(!array.is_empty()); + assert_eq!(array.len(), 3); +} + +#[test] +fn test_caller_allocated_ptr_array_from_slice() { + let ptrs = vec![ + std::ptr::null_mut::(), + std::ptr::null_mut::(), + std::ptr::null_mut::(), + ]; + let array = CallerAllocatedPtrArray::from_ptr_slice(&ptrs).unwrap(); + assert!(!array.is_null()); + assert_eq!(array.len(), 3); + + // Verify the pointers were copied correctly + unsafe { + let slice = array.as_slice().unwrap(); + assert_eq!(slice, ptrs.as_slice()); + } +} + +#[test] +fn test_caller_allocated_ptr_array_access() { + let mut array = CallerAllocatedPtrArray::::allocate(2).unwrap(); + + // Test get and set + unsafe { + // Newly allocated memory contains uninitialized values, not necessarily null + let _ptr0 = array.get(0).unwrap(); + let _ptr1 = array.get(1).unwrap(); + + // Set to null and verify + let test_ptr = std::ptr::null_mut::(); + assert!(array.set(0, test_ptr)); + assert_eq!(array.get(0).unwrap(), test_ptr); + assert!(array.get(0).unwrap().is_null()); + + // Test out of bounds + assert!(!array.set(2, test_ptr)); // Out of bounds + } +} + +#[test] +fn test_callee_allocated_ptr_array_frees_all() { + // This test verifies that CalleeAllocatedPtrArray frees both container and elements + // In a real scenario, this would be memory allocated by the callee + let _array: CalleeAllocatedPtrArray = + CalleeAllocatedPtrArray::from_raw(std::ptr::null_mut(), 0); + // When _array goes out of scope, it should call CoTaskMemFree on both container and elements +} + +#[test] +fn test_array_transparent_repr() { + // Test that transparent repr works correctly for arrays + let ptr = std::ptr::null_mut::(); + let len = 5; + + // CallerAllocatedArray should have the same memory layout as (*mut i32, usize) + let caller_array = CallerAllocatedArray::from_raw(ptr, len); + assert_eq!(caller_array.as_ptr(), ptr); + assert_eq!(caller_array.len(), len); + + // CalleeAllocatedArray should have the same memory layout as (*mut i32, usize) + let callee_array = CalleeAllocatedArray::from_raw(ptr, len); + assert_eq!(callee_array.as_ptr(), ptr); + assert_eq!(callee_array.len(), len); + + // Pointer arrays should have the same memory layout as (*mut *mut i32, usize) + let ptr_array = std::ptr::null_mut::<*mut i32>(); + let caller_ptr_array = CallerAllocatedPtrArray::from_raw(ptr_array, len); + assert_eq!(caller_ptr_array.as_ptr(), ptr_array); + assert_eq!(caller_ptr_array.len(), len); + + let callee_ptr_array = CalleeAllocatedPtrArray::from_raw(ptr_array, len); + assert_eq!(callee_ptr_array.as_ptr(), ptr_array); + assert_eq!(callee_ptr_array.len(), len); +} diff --git a/opc_classic_utils/src/memory/wstring.rs b/opc_classic_utils/src/memory/wstring.rs new file mode 100644 index 0000000..5d919b3 --- /dev/null +++ b/opc_classic_utils/src/memory/wstring.rs @@ -0,0 +1,302 @@ +use std::ffi::{OsStr, OsString}; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::ptr; +use windows::Win32::System::Com::{CoTaskMemAlloc, CoTaskMemFree}; +use windows::core::PCWSTR; + +/// A smart pointer for wide string pointers that the **caller allocates and callee frees** +/// +/// This is used for input string parameters where the caller allocates memory +/// and the callee (COM function) is responsible for freeing it. +/// This wrapper does NOT free the memory when dropped. +#[repr(transparent)] +#[derive(Debug)] +pub struct CallerAllocatedWString { + ptr: *mut u16, +} + +impl CallerAllocatedWString { + /// Creates a new `CallerAllocatedWString` from a raw wide string pointer + /// + /// # Safety + /// + /// The caller must ensure that `ptr` is a valid wide string pointer + /// allocated by the caller and that the callee will be responsible for freeing it. + pub unsafe fn new(ptr: *mut u16) -> Self { + Self { ptr } + } + + /// Creates a new `CallerAllocatedWString` from a raw pointer, taking ownership + pub fn from_raw(ptr: *mut u16) -> Self { + Self { ptr } + } + + /// Creates a new `CallerAllocatedWString` from a `PCWSTR` + pub fn from_pcwstr(pcwstr: PCWSTR) -> Self { + Self { + ptr: pcwstr.as_ptr() as *mut u16, + } + } + + /// Allocates memory using `CoTaskMemAlloc` and creates a `CallerAllocatedWString` + /// + /// This allocates memory for a wide string that will be freed by the callee. + pub fn allocate(len: usize) -> Result { + let size = (len + 1) * std::mem::size_of::(); // +1 for null terminator + let ptr = unsafe { CoTaskMemAlloc(size) }; + if ptr.is_null() { + return Err(windows::core::Error::from_win32()); + } + Ok(unsafe { Self::new(ptr.cast()) }) + } + + /// Creates a `CallerAllocatedWString` from a Rust string + pub fn from_string(s: String) -> Result { + use std::str::FromStr; + Self::from_str(&s) + } + + /// Creates a `CallerAllocatedWString` from an `OsStr` + pub fn from_os_str(os_str: &OsStr) -> Result { + let wide_string: Vec = os_str.encode_wide().chain(std::iter::once(0)).collect(); + let len = wide_string.len() - 1; // Exclude null terminator for allocation + + let ptr = Self::allocate(len)?; + unsafe { + std::ptr::copy_nonoverlapping(wide_string.as_ptr(), ptr.as_ptr(), wide_string.len()); + } + Ok(ptr) + } + + /// Converts the wide string to a Rust string slice + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to a null-terminated wide string. + pub unsafe fn to_string(&self) -> Option { + if self.ptr.is_null() { + return None; + } + + let mut len = 0; + while unsafe { *self.ptr.add(len) } != 0 { + len += 1; + } + + let slice = unsafe { std::slice::from_raw_parts(self.ptr, len) }; + let os_string = OsString::from_wide(slice); + Some(os_string.to_string_lossy().into_owned()) + } + + /// Converts the wide string to an `OsString` + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to a null-terminated wide string. + pub unsafe fn to_os_string(&self) -> Option { + if self.ptr.is_null() { + return None; + } + + let mut len = 0; + while unsafe { *self.ptr.add(len) } != 0 { + len += 1; + } + + let slice = unsafe { std::slice::from_raw_parts(self.ptr, len) }; + Some(OsString::from_wide(slice)) + } + + /// Returns the raw pointer without transferring ownership + pub fn as_ptr(&self) -> *mut u16 { + self.ptr + } + + /// Returns the raw pointer and transfers ownership to the caller + pub fn into_raw(mut self) -> *mut u16 { + let ptr = self.ptr; + self.ptr = ptr::null_mut(); + ptr + } + + /// Checks if the pointer is null + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Converts to a `PCWSTR` for use with Windows APIs + pub fn as_pcwstr(&self) -> PCWSTR { + PCWSTR(self.ptr) + } +} + +impl Drop for CallerAllocatedWString { + fn drop(&mut self) { + // Do NOT free the memory - the callee is responsible for this + self.ptr = ptr::null_mut(); + } +} + +impl Default for CallerAllocatedWString { + fn default() -> Self { + Self { + ptr: ptr::null_mut(), + } + } +} + +impl Clone for CallerAllocatedWString { + /// Creates a shallow copy of the pointer. + /// + /// # Safety + /// + /// The caller must ensure that only one instance is passed to functions + /// that will free the memory, to avoid double-free errors. + fn clone(&self) -> Self { + Self { ptr: self.ptr } + } +} + +impl std::str::FromStr for CallerAllocatedWString { + type Err = windows::core::Error; + + fn from_str(s: &str) -> Result { + let wide_string: Vec = OsStr::new(s) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + let len = wide_string.len() - 1; // Exclude null terminator for allocation + + let ptr = Self::allocate(len)?; + unsafe { + std::ptr::copy_nonoverlapping(wide_string.as_ptr(), ptr.as_ptr(), wide_string.len()); + } + Ok(ptr) + } +} + +/// A smart pointer for wide string pointers that the **callee allocates and caller frees** +/// +/// This is used for output string parameters where the callee allocates memory +/// and the caller is responsible for freeing it using `CoTaskMemFree`. +#[repr(transparent)] +#[derive(Debug)] +pub struct CalleeAllocatedWString { + ptr: *mut u16, +} + +impl CalleeAllocatedWString { + /// Creates a new `CalleeAllocatedWString` from a raw wide string pointer + /// + /// # Safety + /// + /// The caller must ensure that `ptr` is a valid wide string pointer + /// allocated by the callee and that it will be freed using `CoTaskMemFree`. + pub unsafe fn new(ptr: *mut u16) -> Self { + Self { ptr } + } + + /// Creates a new `CalleeAllocatedWString` from a raw pointer, taking ownership + pub fn from_raw(ptr: *mut u16) -> Self { + Self { ptr } + } + + /// Creates a new `CalleeAllocatedWString` from a `PCWSTR` + pub fn from_pcwstr(pcwstr: PCWSTR) -> Self { + Self { + ptr: pcwstr.as_ptr() as *mut u16, + } + } + + /// Converts the wide string to a Rust string slice + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to a null-terminated wide string. + pub unsafe fn to_string(&self) -> Option { + if self.ptr.is_null() { + return None; + } + + let mut len = 0; + while unsafe { *self.ptr.add(len) } != 0 { + len += 1; + } + + let slice = unsafe { std::slice::from_raw_parts(self.ptr, len) }; + let os_string = OsString::from_wide(slice); + Some(os_string.to_string_lossy().into_owned()) + } + + /// Converts the wide string to an `OsString` + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to a null-terminated wide string. + pub unsafe fn to_os_string(&self) -> Option { + if self.ptr.is_null() { + return None; + } + + let mut len = 0; + while unsafe { *self.ptr.add(len) } != 0 { + len += 1; + } + + let slice = unsafe { std::slice::from_raw_parts(self.ptr, len) }; + Some(OsString::from_wide(slice)) + } + + /// Returns the raw pointer without transferring ownership + pub fn as_ptr(&self) -> *mut u16 { + self.ptr + } + + /// Returns the raw pointer and transfers ownership to the caller + pub fn into_raw(mut self) -> *mut u16 { + let ptr = self.ptr; + self.ptr = ptr::null_mut(); + ptr + } + + /// Checks if the pointer is null + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Converts to a `PCWSTR` for use with Windows APIs + pub fn as_pcwstr(&self) -> PCWSTR { + PCWSTR(self.ptr) + } +} + +impl Drop for CalleeAllocatedWString { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + CoTaskMemFree(Some(self.ptr.cast())); + } + self.ptr = ptr::null_mut(); + } + } +} + +impl Default for CalleeAllocatedWString { + fn default() -> Self { + Self { + ptr: ptr::null_mut(), + } + } +} + +impl Clone for CalleeAllocatedWString { + /// Creates a shallow copy of the pointer. + /// + /// # Safety + /// + /// This creates two instances that will both attempt to free the same memory + /// when dropped, potentially causing double-free errors. Use with extreme caution. + fn clone(&self) -> Self { + Self { ptr: self.ptr } + } +} diff --git a/opc_da/Cargo.toml b/opc_da/Cargo.toml index e32de6a..c2de177 100644 --- a/opc_da/Cargo.toml +++ b/opc_da/Cargo.toml @@ -12,21 +12,14 @@ default-target = "x86_64-pc-windows-msvc" targets = [] [dependencies] -actix = "0.13.5" -globset = "0.4.16" -opc_comn_bindings = { version = "0.3.0", path = "../opc_comn_bindings" } -opc_da_bindings = { version = "0.3.1", path = "../opc_da_bindings" } -tokio = { version = "1.46.1", features = ["full", "rt-multi-thread"] } -windows = { version = "0.61.3", features = [ - "Win32_Foundation", - "Win32_Graphics_Gdi", - "Win32_System_Com", - "Win32_System_Com_StructuredStorage", - "Win32_System_Ole", - "Win32_System_Variant", - "Win32_System_WinRT", -] } -windows-core = "0.61.2" +actix = { workspace = true } +globset = { workspace = true } +opc_classic_utils = { workspace = true } +opc_comn_bindings = { workspace = true } +opc_da_bindings = { workspace = true } +tokio = { workspace = true } +windows = { workspace = true } +windows-core = { workspace = true } [features] default = ["unstable_client", "unstable_server"]