diff --git a/Cargo.lock b/Cargo.lock index 288c339..342951e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,17 @@ dependencies = [ "windows-core", ] +[[package]] +name = "opc_comn" +version = "0.3.1" +dependencies = [ + "opc_classic_utils", + "opc_comn_bindings", + "thiserror", + "windows", + "windows-core", +] + [[package]] name = "opc_comn_bindings" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 7889214..3c98cdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "opc_ae_bindings", "opc_classic_utils", + "opc_comn", "opc_comn_bindings", "opc_da", "opc_da_bindings", @@ -11,6 +12,7 @@ members = [ [workspace.dependencies] opc_classic_utils = { path = "opc_classic_utils" } +opc_comn = { path = "opc_comn" } opc_comn_bindings = { path = "opc_comn_bindings" } opc_da_bindings = { path = "opc_da_bindings" } opc_hda_bindings = { path = "opc_hda_bindings" } diff --git a/opc_classic_utils/README.md b/opc_classic_utils/README.md index ebaf3f9..2e13ef9 100644 --- a/opc_classic_utils/README.md +++ b/opc_classic_utils/README.md @@ -87,6 +87,41 @@ let server_output = server_method(&client_input); | `CallerAllocatedWString` | Input strings | Caller allocates, callee frees | | `CalleeAllocatedWString` | Output strings | Callee allocates, caller frees | +## Convenience Macros + +The library provides several convenience macros to simplify common operations: + +### `write_caller_allocated_ptr!` +Writes a value to a caller-allocated pointer with proper error handling: + +```rust +use opc_classic_utils::write_caller_allocated_ptr; + +let mut count: u32 = 0; +let count_ptr = &mut count as *mut u32; +write_caller_allocated_ptr!(count_ptr, 42u32)?; +``` + +### `write_caller_allocated_array!` +Writes an array to a caller-allocated pointer: + +```rust +use opc_classic_utils::write_caller_allocated_array; + +let mut array_ptr: *mut u32 = std::ptr::null_mut(); +let data = vec![1u32, 2u32, 3u32]; +write_caller_allocated_array!(&mut array_ptr, &data)?; +``` + +### `alloc_callee_wstring!` +Allocates a callee-allocated wide string from a Rust string: + +```rust +use opc_classic_utils::alloc_callee_wstring; + +let error_string_ptr = alloc_callee_wstring!("Error message")?; +``` + ## Benefits - **Prevents Memory Leaks**: Automatic cleanup for callee-allocated memory diff --git a/opc_classic_utils/examples/convenience_functions.rs b/opc_classic_utils/examples/convenience_functions.rs index f572f2f..c527f45 100644 --- a/opc_classic_utils/examples/convenience_functions.rs +++ b/opc_classic_utils/examples/convenience_functions.rs @@ -20,7 +20,7 @@ fn demonstrate_pointer_convenience_functions() { // 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); + println!(" Created pointer from int: {int_value:?}"); // Create a pointer from a struct let struct_value = TestStruct { @@ -28,7 +28,7 @@ fn demonstrate_pointer_convenience_functions() { value: std::f64::consts::PI, }; let struct_ptr = CallerAllocatedPtr::from_value(&struct_value).unwrap(); - println!(" Created pointer from struct: {:?}", struct_value); + println!(" Created pointer from struct: {struct_value:?}"); // Verify the values were copied correctly unsafe { @@ -58,14 +58,14 @@ fn demonstrate_pointer_convenience_functions() { // Use as_ref for read-only access unsafe { let ref_value = ptr.as_ref().unwrap(); - println!(" Read value through as_ref: {}", ref_value); + 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); + println!(" Modified value through as_mut: {mut_value}"); } // Verify the change @@ -85,34 +85,25 @@ fn demonstrate_string_convenience_functions() { 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); + println!(" Created from &str: '{str_slice}'"); // 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); + let wstring2 = 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); + let converted1 = wstring1.to_string_lossy().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); + let converted2 = wstring2.to_os_string().unwrap(); + println!(" Converted back to OsString: '{converted2:?}'"); + assert_eq!(converted2, os_string); } println!(" ✓ All string conversions work correctly"); @@ -121,7 +112,7 @@ fn demonstrate_string_convenience_functions() { let null_wstring = CallerAllocatedWString::default(); unsafe { - let result = null_wstring.to_string(); + let result = null_wstring.to_string_lossy(); assert!(result.is_none()); println!(" Null string correctly returns None"); } @@ -147,11 +138,11 @@ fn demonstrate_real_world_scenario() { // Server would use these parameters unsafe { - let name = server_name.to_string().unwrap(); + let name = server_name.to_string_lossy().unwrap(); let count = *item_count.as_ptr(); println!(" Server received:"); - println!(" - Server name: '{}'", name); - println!(" - Item count: {}", count); + println!(" - Server name: '{name}'"); + println!(" - Item count: {count}"); } println!("\n3. Simulating server returning output parameters:"); @@ -163,8 +154,8 @@ fn demonstrate_real_world_scenario() { // 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!(" - Result message: '{result_string}'"); + println!(" - Result code: {result_code}"); println!("\n4. Client would receive and process output:"); diff --git a/opc_classic_utils/examples/macro_usage.rs b/opc_classic_utils/examples/macro_usage.rs new file mode 100644 index 0000000..50c8b8d --- /dev/null +++ b/opc_classic_utils/examples/macro_usage.rs @@ -0,0 +1,114 @@ +//! Example demonstrating the usage of convenience macros for OPC Classic memory management +//! +//! This example shows how to use the new macros: +//! - `write_caller_allocated_ptr!` - for writing to caller-allocated pointers +//! - `write_caller_allocated_array!` - for writing to caller-allocated arrays +//! - `alloc_callee_wstring!` - for allocating callee-allocated wide strings + +use opc_classic_utils::{ + alloc_callee_wstring, write_caller_allocated_array, write_caller_allocated_ptr, +}; + +fn main() -> windows::core::Result<()> { + println!("=== OPC Classic Utils Macro Usage Examples ===\n"); + + // Example 1: Using write_caller_allocated_ptr! macro + println!("1. Using write_caller_allocated_ptr! macro:"); + { + let mut count: u32 = 0; + let count_ptr = &mut count as *mut u32; + + // Using the macro to write to a caller-allocated pointer + write_caller_allocated_ptr!(count_ptr, 42u32)?; + println!(" Written value: {count}"); + } + + // Example 2: Using write_caller_allocated_array! macro + println!("\n2. Using write_caller_allocated_array! macro:"); + { + let mut array_ptr: *mut u32 = std::ptr::null_mut(); + + // Using the macro to write an array to a caller-allocated pointer + let data = vec![1u32, 2u32, 3u32, 4u32, 5u32]; + write_caller_allocated_array!(&mut array_ptr, &data)?; + + println!(" Array pointer: {array_ptr:?}"); + println!(" Array is not null: {}", !array_ptr.is_null()); + } + + // Example 3: Using alloc_callee_wstring! macro + println!("\n3. Using alloc_callee_wstring! macro:"); + { + let error_message = "This is an error message"; + + // Using the macro to allocate a callee-allocated wide string + let wide_string_ptr = alloc_callee_wstring!(error_message)?; + + println!(" Wide string pointer: {wide_string_ptr:?}"); + } + + // Example 4: Simulating OPC Common interface usage + println!("\n4. Simulating OPC Common interface usage:"); + { + // Simulate QueryAvailableLocaleIDs method + let mut count: u32 = 0; + let mut locale_ids_ptr: *mut u32 = std::ptr::null_mut(); + + // Simulate available locale IDs + let available_locale_ids = vec![0x0409u32, 0x0410u32, 0x0411u32]; // EN-US, IT, JA + + // Write count using macro + write_caller_allocated_ptr!(&mut count, available_locale_ids.len() as u32)?; + + // Write array using macro + write_caller_allocated_array!(&mut locale_ids_ptr, &available_locale_ids)?; + + println!(" Available locale count: {count}"); + println!(" Locale IDs pointer: {locale_ids_ptr:?}"); + } + + // Example 5: Error handling with macros + println!("\n5. Error handling with macros:"); + { + // Simulate getting an error string from an HRESULT + let hresult = windows::Win32::Foundation::E_POINTER; + let error_string_ptr = alloc_callee_wstring!("Pointer is invalid")?; + + println!(" HRESULT: {hresult:?}"); + println!(" Error string pointer: {error_string_ptr:?}"); + } + + println!("\n=== All examples completed successfully! ==="); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_write_caller_allocated_ptr_macro() { + let mut value: u32 = 0; + let ptr = &mut value as *mut u32; + + let result = write_caller_allocated_ptr!(ptr, 123u32); + assert!(result.is_ok()); + assert_eq!(value, 123); + } + + #[test] + fn test_write_caller_allocated_array_macro() { + let mut array_ptr: *mut u32 = std::ptr::null_mut(); + let data = vec![1u32, 2u32, 3u32]; + + let result = write_caller_allocated_array!(&mut array_ptr, &data); + assert!(result.is_ok()); + assert!(!array_ptr.is_null()); + } + + #[test] + fn test_alloc_callee_wstring_macro() { + let result = alloc_callee_wstring!("test string"); + assert!(result.is_ok()); + } +} diff --git a/opc_classic_utils/examples/transparent_repr_demo.rs b/opc_classic_utils/examples/transparent_repr_demo.rs index f62fda5..c6d1a24 100644 --- a/opc_classic_utils/examples/transparent_repr_demo.rs +++ b/opc_classic_utils/examples/transparent_repr_demo.rs @@ -34,9 +34,9 @@ fn demonstrate_transparent_repr() { 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()); + println!(" Raw pointer: {raw_ptr:?}"); + println!(" CallerAllocatedPtr: {caller_ptr:?}"); + println!(" CalleeAllocatedPtr: {callee_ptr:?}"); // All pointers should be identical due to transparent repr assert_eq!(raw_ptr, caller_ptr.as_ptr()); @@ -51,11 +51,11 @@ fn demonstrate_transparent_repr() { // For input parameters (caller-allocated) let input_ptr = CallerAllocatedPtr::from_raw(std::ptr::null_mut::()); - println!(" Input pointer created: {:?}", input_ptr.as_ptr()); + println!(" Input pointer created: {input_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!(" Output pointer created: {output_ptr:?}"); println!(" ✓ Wrapper types are FFI-compatible"); diff --git a/opc_classic_utils/src/memory/array.rs b/opc_classic_utils/src/memory/array.rs index 985e961..4ed4a0f 100644 --- a/opc_classic_utils/src/memory/array.rs +++ b/opc_classic_utils/src/memory/array.rs @@ -83,11 +83,9 @@ impl CallerAllocatedArray { /// 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) { + pub fn into_raw(self) -> (*mut T, usize) { let ptr = self.ptr; let len = self.len; - self.ptr = ptr::null_mut(); - self.len = 0; (ptr, len) } @@ -162,9 +160,10 @@ impl CallerAllocatedArray { 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(); + // Just clear the length to prevent use-after-free + // Keep the pointer intact for the callee to use self.len = 0; + // Note: We don't clear self.ptr because the callee needs it } } @@ -249,11 +248,9 @@ impl CalleeAllocatedArray { /// 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) { + pub fn into_raw(self) -> (*mut T, usize) { let ptr = self.ptr; let len = self.len; - self.ptr = ptr::null_mut(); - self.len = 0; (ptr, len) } @@ -345,3 +342,135 @@ impl Default for CalleeAllocatedArray { } } } + +/// Writes an array to a caller-allocated pointer using COM memory management +/// +/// This macro simplifies writing arrays to caller-allocated pointers by creating +/// a `CallerAllocatedArray` from a slice and writing its pointer to the target. +/// +/// # Arguments +/// +/// * `$ptr` - A raw pointer (`*mut *mut T`) that points to caller-allocated memory +/// * `$value` - A slice (`&[T]`) containing the array data to write +/// +/// # Returns +/// +/// Returns `Result<(), windows::core::Error>`: +/// * `Ok(())` - Array was successfully written +/// * `Err(...)` - Memory allocation failed or pointer is invalid +/// +/// # Safety +/// +/// The caller must ensure that: +/// * `$ptr` is a valid pointer to caller-allocated memory +/// * The memory pointed to by `$ptr` is properly initialized +/// * The callee (COM function) will be responsible for freeing the array memory +/// * The array elements are `Copy` types +/// +/// # Memory Management +/// +/// This macro: +/// 1. Allocates memory using `CoTaskMemAlloc` for the array data +/// 2. Copies the slice data into the allocated memory +/// 3. Writes the pointer to the allocated memory to `$ptr` +/// 4. The callee is responsible for freeing the memory using `CoTaskMemFree` +/// +/// # Example +/// +/// ```rust +/// use opc_classic_utils::write_caller_allocated_array; +/// +/// let mut array_ptr: *mut u32 = std::ptr::null_mut(); +/// let data = vec![1u32, 2u32, 3u32, 4u32, 5u32]; +/// +/// // Write the array to the caller-allocated pointer +/// write_caller_allocated_array!(&mut array_ptr, &data)?; +/// +/// // The array_ptr now points to COM-allocated memory containing the data +/// // The callee (COM function) will be responsible for freeing this memory +/// # Ok::<(), windows::core::Error>(()) +/// ``` +/// +/// # Typical Use Cases +/// +/// * Passing arrays to COM function calls +/// * OPC Classic API array parameter passing +/// * Setting up caller-allocated array output parameters +#[macro_export] +macro_rules! write_caller_allocated_array { + ($ptr:expr, $value:expr) => { + opc_classic_utils::write_caller_allocated_ptr!( + $ptr, + opc_classic_utils::CallerAllocatedArray::from_slice($value)?.as_ptr() + ) + }; +} + +/// Copies data directly to a caller-allocated array +/// +/// This macro simplifies copying data directly to a caller-allocated array +/// without allocating new memory. It's used when the caller has already +/// allocated the array and we just need to copy data into it. +/// +/// # Arguments +/// +/// * `$dst` - A raw pointer (`*mut T`) to the destination array +/// * `$src` - A slice (`&[T]`) containing the source data +/// +/// # Returns +/// +/// Returns `Result<(), windows::core::Error>`: +/// * `Ok(())` - Data was successfully copied +/// * `Err(E_INVALIDARG)` - The destination pointer is null +/// +/// # Safety +/// +/// The caller must ensure that: +/// * `$dst` is a valid pointer to caller-allocated memory +/// * The destination array has sufficient space for the source data +/// * The array elements are `Copy` types +/// +/// # Example +/// +/// ```rust +/// use opc_classic_utils::copy_to_caller_array; +/// +/// let mut array: [u32; 5] = [0; 5]; +/// let data = vec![1u32, 2u32, 3u32, 4u32, 5u32]; +/// +/// // Copy data directly to the caller-allocated array +/// copy_to_caller_array!(array.as_mut_ptr(), &data)?; +/// assert_eq!(array, [1, 2, 3, 4, 5]); +/// # Ok::<(), windows::core::Error>(()) +/// ``` +/// +/// # Typical Use Cases +/// +/// * Copying data to caller-allocated output parameters +/// * OPC Classic API array output parameters +/// * Direct memory copying without allocation +#[macro_export] +macro_rules! copy_to_caller_array { + ($dst:expr, $src:expr) => { + unsafe { + let src = $src; + let mut dst_ptr = opc_classic_utils::CallerAllocatedArray::allocate(src.len())?; + if dst_ptr.is_null() { + return Err(windows::core::Error::new( + windows::Win32::Foundation::E_INVALIDARG, + "Destination pointer is null", + )); + } + *$dst = dst_ptr.as_ptr(); + if let Some(dst_slice) = dst_ptr.as_mut_slice() { + dst_slice.copy_from_slice(src); + Ok(()) + } else { + Err(windows::core::Error::new( + windows::Win32::Foundation::E_INVALIDARG, + "Failed to get mutable slice", + )) + } + } + }; +} diff --git a/opc_classic_utils/src/memory/ptr.rs b/opc_classic_utils/src/memory/ptr.rs index 07f2dee..44cf4e0 100644 --- a/opc_classic_utils/src/memory/ptr.rs +++ b/opc_classic_utils/src/memory/ptr.rs @@ -62,10 +62,8 @@ impl CallerAllocatedPtr { /// 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 + pub fn into_raw(self) -> *mut T { + self.ptr } /// Checks if the pointer is null @@ -103,8 +101,8 @@ impl CallerAllocatedPtr { 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(); + // Keep the pointer intact for the callee to use + // Note: We don't clear self.ptr because the callee needs it } } @@ -159,10 +157,7 @@ impl CalleeAllocatedPtr { /// 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, - { + pub fn from_value(value: &T) -> Result { let size = std::mem::size_of::(); let ptr = unsafe { windows::Win32::System::Com::CoTaskMemAlloc(size) }; if ptr.is_null() { @@ -182,10 +177,8 @@ impl CalleeAllocatedPtr { /// 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 + pub fn into_raw(self) -> *mut T { + self.ptr } /// Checks if the pointer is null @@ -238,3 +231,58 @@ impl Default for CalleeAllocatedPtr { } } } + +/// Writes a value to a caller-allocated pointer using COM memory management +/// +/// This macro simplifies writing values to caller-allocated pointers by wrapping +/// the raw pointer in a `CallerAllocatedPtr` and safely writing the value. +/// +/// # Arguments +/// +/// * `$ptr` - A raw pointer (`*mut T`) that points to caller-allocated memory +/// * `$value` - The value to write to the pointer +/// +/// # Returns +/// +/// Returns `Result<(), windows::core::Error>`: +/// * `Ok(())` - Value was successfully written +/// * `Err(E_INVALIDARG)` - The pointer is null or invalid +/// +/// # Safety +/// +/// The caller must ensure that: +/// * `$ptr` is a valid pointer to caller-allocated memory +/// * The memory pointed to by `$ptr` is properly initialized +/// * The callee (COM function) will be responsible for freeing the memory +/// +/// # Example +/// +/// ```rust +/// use opc_classic_utils::write_caller_allocated_ptr; +/// +/// let mut count: u32 = 0; +/// let count_ptr = &mut count as *mut u32; +/// +/// // Write a value to the caller-allocated pointer +/// write_caller_allocated_ptr!(count_ptr, 42u32)?; +/// assert_eq!(count, 42); +/// # Ok::<(), windows::core::Error>(()) +/// ``` +/// +/// # Typical Use Cases +/// +/// * Writing input parameters to COM function calls +/// * Setting up caller-allocated output parameters +/// * OPC Classic API parameter passing +#[macro_export] +macro_rules! write_caller_allocated_ptr { + ($ptr:expr, $value:expr) => { + unsafe { + let mut ptr = opc_classic_utils::CallerAllocatedPtr::from_raw($ptr); + let value = $value; + ptr.as_mut() + .ok_or(windows::Win32::Foundation::E_INVALIDARG) + .map(|ptr| *ptr = value) + } + }; +} diff --git a/opc_classic_utils/src/memory/ptr_array.rs b/opc_classic_utils/src/memory/ptr_array.rs index c16b00d..6496bdf 100644 --- a/opc_classic_utils/src/memory/ptr_array.rs +++ b/opc_classic_utils/src/memory/ptr_array.rs @@ -1,6 +1,8 @@ use std::ptr; use windows::Win32::System::Com::{CoTaskMemAlloc, CoTaskMemFree}; +use crate::CalleeAllocatedPtr; + /// 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 @@ -82,11 +84,9 @@ impl CallerAllocatedPtrArray { /// 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) { + pub fn into_raw(self) -> (*mut *mut T, usize) { let ptr = self.ptr; let len = self.len; - self.ptr = ptr::null_mut(); - self.len = 0; (ptr, len) } @@ -165,8 +165,6 @@ 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; } } @@ -249,6 +247,56 @@ impl CalleeAllocatedPtrArray { Self { ptr, len } } + /// Creates a new `CalleeAllocatedPtrArray` from a slice + pub fn from_slice(slice: &[T]) -> Result { + if slice.is_empty() { + return Ok(Self { + ptr: ptr::null_mut(), + len: 0, + }); + } + let array = Self::allocate(slice.len())?; + + // convert items to CalleeAllocatedPtr write to array + for (i, item) in slice.iter().enumerate() { + let item_ptr = CalleeAllocatedPtr::from_value(item)?; + if item_ptr.is_null() { + return Err(windows::core::Error::from_win32()); + } + unsafe { *array.ptr.add(i) = item_ptr.as_ptr() }; + } + + Ok(array) + } + + /// Allocates memory for a pointer array using `CoTaskMemAlloc` and creates a `CalleeAllocatedPtrArray` + /// + /// This allocates memory that will be freed by the caller (COM function). + /// The callee is responsible for ensuring the caller 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) }) + } + /// Returns the raw pointer without transferring ownership pub fn as_ptr(&self) -> *mut *mut T { self.ptr @@ -257,11 +305,9 @@ impl CalleeAllocatedPtrArray { /// 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) { + pub fn into_raw(self) -> (*mut *mut T, usize) { let ptr = self.ptr; let len = self.len; - self.ptr = ptr::null_mut(); - self.len = 0; (ptr, len) } diff --git a/opc_classic_utils/src/memory/tests.rs b/opc_classic_utils/src/memory/tests.rs index 0aeef95..d485dce 100644 --- a/opc_classic_utils/src/memory/tests.rs +++ b/opc_classic_utils/src/memory/tests.rs @@ -94,21 +94,7 @@ fn test_caller_allocated_wstring_from_str() { // 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(); + let converted = wstring.to_string_lossy().unwrap(); assert_eq!(converted, test_string); } } @@ -177,8 +163,8 @@ fn test_wstring_null_conversion() { let callee_wstring = CalleeAllocatedWString::default(); unsafe { - assert!(caller_wstring.to_string().is_none()); - assert!(callee_wstring.to_string().is_none()); + assert!(caller_wstring.to_string_lossy().is_none()); + assert!(callee_wstring.to_string_lossy().is_none()); assert!(caller_wstring.to_os_string().is_none()); assert!(callee_wstring.to_os_string().is_none()); } @@ -193,7 +179,7 @@ fn test_from_str_trait() { // Verify the string was converted correctly unsafe { - let converted = wstring.to_string().unwrap(); + let converted = wstring.to_string_lossy().unwrap(); assert_eq!(converted, test_string); } } diff --git a/opc_classic_utils/src/memory/wstring.rs b/opc_classic_utils/src/memory/wstring.rs index 5d919b3..d561b63 100644 --- a/opc_classic_utils/src/memory/wstring.rs +++ b/opc_classic_utils/src/memory/wstring.rs @@ -2,7 +2,7 @@ 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; +use windows::core::{PCWSTR, PWSTR}; /// A smart pointer for wide string pointers that the **caller allocates and callee frees** /// @@ -38,6 +38,13 @@ impl CallerAllocatedWString { } } + /// Creates a new `CallerAllocatedWString` from a `PWSTR` + pub fn from_pwstr(pwstr: PWSTR) -> Self { + Self { + ptr: pwstr.as_ptr(), + } + } + /// Allocates memory using `CoTaskMemAlloc` and creates a `CallerAllocatedWString` /// /// This allocates memory for a wide string that will be freed by the callee. @@ -50,12 +57,6 @@ impl CallerAllocatedWString { 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(); @@ -73,7 +74,7 @@ impl CallerAllocatedWString { /// # Safety /// /// The caller must ensure the pointer is valid and points to a null-terminated wide string. - pub unsafe fn to_string(&self) -> Option { + pub unsafe fn to_string_lossy(&self) -> Option { if self.ptr.is_null() { return None; } @@ -113,10 +114,8 @@ impl CallerAllocatedWString { } /// 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 + pub fn into_raw(self) -> *mut u16 { + self.ptr } /// Checks if the pointer is null @@ -128,12 +127,18 @@ impl CallerAllocatedWString { pub fn as_pcwstr(&self) -> PCWSTR { PCWSTR(self.ptr) } + + /// Converts to a `PWSTR` for use with Windows APIs + pub fn as_pwstr(&self) -> PWSTR { + PWSTR(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(); + // Keep the pointer intact for the callee to use + // Note: We don't clear self.ptr because the callee needs it } } @@ -175,6 +180,14 @@ impl std::str::FromStr for CallerAllocatedWString { } } +impl TryFrom for CallerAllocatedWString { + type Error = windows::core::Error; + + fn try_from(value: PCWSTR) -> Result { + Ok(Self::from_pcwstr(value)) + } +} + /// 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 @@ -208,20 +221,30 @@ impl CalleeAllocatedWString { } } + /// Creates a `CalleeAllocatedWString` from a Rust string + pub fn from_string(s: String) -> Result { + use std::str::FromStr; + Self::from_str(&s) + } + + /// Null + pub fn null() -> Self { + Self { + ptr: ptr::null_mut(), + } + } + /// 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 { + pub unsafe fn to_string_lossy(&self) -> Option { if self.ptr.is_null() { return None; } - let mut len = 0; - while unsafe { *self.ptr.add(len) } != 0 { - len += 1; - } + let len = unsafe { self.as_pcwstr().len() }; let slice = unsafe { std::slice::from_raw_parts(self.ptr, len) }; let os_string = OsString::from_wide(slice); @@ -238,25 +261,37 @@ impl CalleeAllocatedWString { return None; } - let mut len = 0; - while unsafe { *self.ptr.add(len) } != 0 { - len += 1; - } + let len = unsafe { self.as_pcwstr().len() }; let slice = unsafe { std::slice::from_raw_parts(self.ptr, len) }; Some(OsString::from_wide(slice)) } + /// Converts the wide string to a Rust string + /// + /// # Safety + /// + /// The caller must ensure the pointer is valid and points to a null-terminated wide string. + pub unsafe fn to_string(&self) -> windows::core::Result> { + if self.ptr.is_null() { + return Ok(None); + } + + let len = unsafe { self.as_pcwstr().len() }; + + let slice = unsafe { std::slice::from_raw_parts(self.ptr, len) }; + let string = String::from_utf16(slice)?; + Ok(Some(string)) + } + /// 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 + pub fn into_raw(self) -> *mut u16 { + self.ptr } /// Checks if the pointer is null @@ -268,6 +303,21 @@ impl CalleeAllocatedWString { pub fn as_pcwstr(&self) -> PCWSTR { PCWSTR(self.ptr) } + + /// Converts to a `PCWSTR` for use with Windows APIs + pub fn as_pcwstr_mut_ptr(&mut self) -> *mut PCWSTR { + &mut self.ptr as *mut *mut u16 as *mut PCWSTR + } + + /// Converts to a `PWSTR` for use with Windows APIs + pub fn as_pwstr(&self) -> PWSTR { + PWSTR(self.ptr) + } + + /// Converts to a `PWSTR` for use with Windows APIs + pub fn as_pwstr_mut_ptr(&mut self) -> *mut PWSTR { + &mut self.ptr as *mut *mut u16 as *mut PWSTR + } } impl Drop for CalleeAllocatedWString { @@ -300,3 +350,81 @@ impl Clone for CalleeAllocatedWString { Self { ptr: self.ptr } } } + +impl std::str::FromStr for CalleeAllocatedWString { + 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 = unsafe { CoTaskMemAlloc(len * std::mem::size_of::()) }; + if ptr.is_null() { + return Err(windows::core::Error::from_win32()); + } + unsafe { + std::ptr::copy_nonoverlapping(wide_string.as_ptr(), ptr.cast(), wide_string.len()); + } + Ok(unsafe { Self::new(ptr.cast()) }) + } +} + +/// Allocates a callee-allocated wide string from a Rust string +/// +/// This macro simplifies creating callee-allocated wide strings by converting +/// a Rust string to a `CalleeAllocatedWString` and returning its `PWSTR` representation. +/// +/// # Arguments +/// +/// * `$s` - A string expression (`String`, `&str`, or any type that can be converted to `String`) +/// +/// # Returns +/// +/// Returns `Result`: +/// * `Ok(PWSTR)` - Successfully allocated wide string pointer +/// * `Err(...)` - Memory allocation failed +/// +/// # Memory Management +/// +/// This macro: +/// 1. Converts the input string to a wide string (UTF-16) +/// 2. Allocates memory using `CoTaskMemAlloc` for the wide string +/// 3. Copies the wide string data into the allocated memory +/// 4. Returns a `PWSTR` pointing to the allocated memory +/// 5. The caller is responsible for freeing the memory using `CoTaskMemFree` +/// +/// # Example +/// +/// ```rust +/// use opc_classic_utils::alloc_callee_wstring; +/// use windows_core::PWSTR; +/// +/// // Allocate a wide string from a Rust string +/// let wide_string: PWSTR = alloc_callee_wstring!("Hello, World!")?; +/// +/// // Use the wide string with Windows APIs +/// // The caller is responsible for freeing the memory when done +/// unsafe { +/// // Use wide_string with Windows API calls +/// // ... +/// // Free the memory when done +/// windows::Win32::System::Com::CoTaskMemFree(Some(wide_string.0.cast())); +/// } +/// # Ok::<(), windows::core::Error>(()) +/// ``` +/// +/// # Typical Use Cases +/// +/// * Creating wide strings for Windows API calls +/// * OPC Classic API string parameter passing +/// * Converting Rust strings to COM-allocated wide strings +/// * Setting up callee-allocated string output parameters +#[macro_export] +macro_rules! alloc_callee_wstring { + ($s:expr) => {{ + use std::str::FromStr; + opc_classic_utils::CalleeAllocatedWString::from_str(&$s).map(|s| s.as_pwstr()) + }}; +} diff --git a/opc_comn/.gitignore b/opc_comn/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/opc_comn/.gitignore @@ -0,0 +1 @@ +/target diff --git a/opc_comn/Cargo.toml b/opc_comn/Cargo.toml new file mode 100644 index 0000000..c7bfbce --- /dev/null +++ b/opc_comn/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "opc_comn" +version = "0.3.1" +edition = "2024" +description = "OPC Common" +repository = "https://github.com/Ronbb/rust_opc" +license = "MIT" +keywords = ["comn", "opc", "opccomn"] + +[package.metadata.docs.rs] +default-target = "x86_64-pc-windows-msvc" +targets = [] + +[dependencies] +opc_classic_utils = { workspace = true } +opc_comn_bindings = { workspace = true } +thiserror = { workspace = true } +windows = { workspace = true } +windows-core = { workspace = true } diff --git a/opc_comn/README.md b/opc_comn/README.md new file mode 100644 index 0000000..f493a3c --- /dev/null +++ b/opc_comn/README.md @@ -0,0 +1,9 @@ +# OPC Comn + +Please see docs on [docs.rs](https://docs.rs/opc_comn/). + +## **UNSTABLE** + +**Warning: This library is still under development and may not be fully functional. Use at your own risk.** + +**Note: The API is unstable, tests are incomplete, and there may be unsafe memory operations.** diff --git a/opc_comn/src/client/mod.rs b/opc_comn/src/client/mod.rs new file mode 100644 index 0000000..b73a025 --- /dev/null +++ b/opc_comn/src/client/mod.rs @@ -0,0 +1,2 @@ +pub mod opc_server_list; +pub mod utils; diff --git a/opc_comn/src/client/opc_server_list.rs b/opc_comn/src/client/opc_server_list.rs new file mode 100644 index 0000000..4aa60cd --- /dev/null +++ b/opc_comn/src/client/opc_server_list.rs @@ -0,0 +1,118 @@ +use std::{mem::ManuallyDrop, str::FromStr as _}; + +use windows_core::Interface as _; + +#[derive(Default)] +pub struct CreateOpcServerListOptions<'a> { + pub class_id: Option, + pub context: Option, + pub outer: Option<&'a windows_core::IUnknown>, + pub server_info: Option<&'a windows::Win32::System::Com::COSERVERINFO>, +} + +pub struct OpcServerList { + pub inner: opc_comn_bindings::IOPCServerList, +} + +impl OpcServerList { + pub fn create_opc_server_list( + options: Option, + ) -> windows_core::Result { + let options = options.unwrap_or_default(); + + let class_id = match options.class_id { + Some(class_id) => class_id, + None => { + super::utils::get_class_id_from_program_id(windows_core::w!("OPC.ServerList.1"))? + } + }; + + let context = options + .context + .unwrap_or(windows::Win32::System::Com::CLSCTX_ALL); + + let outer = options.outer; + + let server_list: opc_comn_bindings::IOPCServerList = unsafe { + match options.server_info { + Some(info) => { + let mut results = [windows::Win32::System::Com::MULTI_QI { + pIID: &opc_comn_bindings::IOPCServerList::IID, + ..Default::default() + }]; + + windows::Win32::System::Com::CoCreateInstanceEx( + &class_id, + outer, + context, + Some(info), + &mut results, + )?; + + let result = results.into_iter().next().unwrap(); + + if result.hr.is_err() { + return Err(result.hr.into()); + } + + ManuallyDrop::into_inner(result.pItf) + .ok_or(windows::Win32::Foundation::E_POINTER)? + .cast::()? + } + None => windows::Win32::System::Com::CoCreateInstance(&class_id, outer, context)?, + } + }; + + Ok(OpcServerList { inner: server_list }) + } +} + +pub struct ClassDetails { + pub program_id: Option, + pub user_type: Option, +} + +impl OpcServerList { + pub fn enum_classes_of_categories( + &self, + implemented_categories: &[windows_core::GUID], + required_categories: &[windows_core::GUID], + ) -> windows_core::Result { + unsafe { + self.inner + .EnumClassesOfCategories(implemented_categories, required_categories) + } + } + + pub fn get_class_details( + &self, + clsid: &windows_core::GUID, + ) -> windows_core::Result { + let mut program_id = opc_classic_utils::CalleeAllocatedWString::null(); + let mut user_type = opc_classic_utils::CalleeAllocatedWString::null(); + + unsafe { + self.inner.GetClassDetails( + clsid, + program_id.as_pwstr_mut_ptr(), + user_type.as_pwstr_mut_ptr(), + )?; + + Ok(ClassDetails { + program_id: program_id.to_string()?, + user_type: user_type.to_string()?, + }) + } + } + + pub fn class_id_from_program_id( + &self, + program_id: &str, + ) -> windows_core::Result { + unsafe { + self.inner.CLSIDFromProgID( + opc_classic_utils::CallerAllocatedWString::from_str(program_id)?.as_pcwstr(), + ) + } + } +} diff --git a/opc_comn/src/client/utils.rs b/opc_comn/src/client/utils.rs new file mode 100644 index 0000000..3fb713a --- /dev/null +++ b/opc_comn/src/client/utils.rs @@ -0,0 +1,9 @@ +pub fn get_class_id_from_program_id< + T: TryInto, +>( + program_id: T, +) -> windows_core::Result { + let program_id: opc_classic_utils::CallerAllocatedWString = program_id.try_into()?; + let id = unsafe { windows::Win32::System::Com::CLSIDFromProgID(program_id.as_pcwstr())? }; + Ok(id) +} diff --git a/opc_comn/src/lib.rs b/opc_comn/src/lib.rs new file mode 100644 index 0000000..c07f47e --- /dev/null +++ b/opc_comn/src/lib.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod server; diff --git a/opc_comn/src/server/mod.rs b/opc_comn/src/server/mod.rs new file mode 100644 index 0000000..6ae14f6 --- /dev/null +++ b/opc_comn/src/server/mod.rs @@ -0,0 +1,2 @@ +pub mod opc_server_base; +pub mod traits; diff --git a/opc_comn/src/server/opc_server_base.rs b/opc_comn/src/server/opc_server_base.rs new file mode 100644 index 0000000..d1529dd --- /dev/null +++ b/opc_comn/src/server/opc_server_base.rs @@ -0,0 +1,57 @@ +#[windows::core::implement(opc_comn_bindings::IOPCCommon, opc_comn_bindings::IOPCShutdown)] +pub struct OpcServerBase(T) +where + T: 'static + super::traits::opc_common::OpcCommon + super::traits::opc_shutdown::OpcShutdown; + +impl + opc_comn_bindings::IOPCCommon_Impl for OpcServerBase_Impl +{ + fn SetLocaleID(&self, dwlcid: u32) -> windows_core::Result<()> { + self.0.set_locale_id(dwlcid) + } + + fn GetLocaleID(&self) -> windows_core::Result { + self.0.get_locale_id() + } + + fn QueryAvailableLocaleIDs( + &self, + pdwcount: *mut u32, + pdwlcid: *mut *mut u32, + ) -> windows_core::Result<()> { + let locale_ids = self.0.query_available_locale_ids()?; + opc_classic_utils::write_caller_allocated_ptr!(pdwcount, locale_ids.len().try_into()?)?; + opc_classic_utils::write_caller_allocated_array!(pdwlcid, locale_ids.as_slice())?; + + Ok(()) + } + + fn GetErrorString( + &self, + dwerror: windows_core::HRESULT, + ) -> windows_core::Result { + opc_classic_utils::alloc_callee_wstring!(self.0.get_error_string(dwerror)?) + } + + fn SetClientName(&self, szname: &windows_core::PCWSTR) -> windows_core::Result<()> { + self.0.set_client_name(unsafe { + opc_classic_utils::CallerAllocatedWString::from_pcwstr(*szname) + .to_string_lossy() + .ok_or(windows::Win32::Foundation::E_POINTER) + }?) + } +} + +impl + opc_comn_bindings::IOPCShutdown_Impl for OpcServerBase_Impl +{ + fn ShutdownRequest(&self, szreason: &windows_core::PCWSTR) -> windows_core::Result<()> { + let reason = unsafe { + opc_classic_utils::CallerAllocatedWString::from_pcwstr(*szreason) + .to_string_lossy() + .ok_or(windows::Win32::Foundation::E_POINTER) + }?; + + self.0.shutdown_request(reason) + } +} diff --git a/opc_comn/src/server/traits/mod.rs b/opc_comn/src/server/traits/mod.rs new file mode 100644 index 0000000..9539b14 --- /dev/null +++ b/opc_comn/src/server/traits/mod.rs @@ -0,0 +1,2 @@ +pub mod opc_common; +pub mod opc_shutdown; diff --git a/opc_comn/src/server/traits/opc_common.rs b/opc_comn/src/server/traits/opc_common.rs new file mode 100644 index 0000000..7569879 --- /dev/null +++ b/opc_comn/src/server/traits/opc_common.rs @@ -0,0 +1,7 @@ +pub trait OpcCommon { + fn set_locale_id(&self, dwlcid: u32) -> windows_core::Result<()>; + fn get_locale_id(&self) -> windows_core::Result; + fn query_available_locale_ids(&self) -> windows_core::Result>; + fn get_error_string(&self, dwerror: windows_core::HRESULT) -> windows_core::Result; + fn set_client_name(&self, szname: String) -> windows_core::Result<()>; +} diff --git a/opc_comn/src/server/traits/opc_shutdown.rs b/opc_comn/src/server/traits/opc_shutdown.rs new file mode 100644 index 0000000..7beaae8 --- /dev/null +++ b/opc_comn/src/server/traits/opc_shutdown.rs @@ -0,0 +1,3 @@ +pub trait OpcShutdown { + fn shutdown_request(&self, reason: String) -> windows_core::Result<()>; +} diff --git a/opc_da/Cargo.toml b/opc_da/Cargo.toml index b8b9a87..7263212 100644 --- a/opc_da/Cargo.toml +++ b/opc_da/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" description = "OPC Data Access" repository = "https://github.com/Ronbb/rust_opc" license = "MIT" -keywords = ["da", "opc"] +keywords = ["da", "opc", "opcda"] [package.metadata.docs.rs] default-target = "x86_64-pc-windows-msvc" diff --git a/opc_da/src/lib.rs b/opc_da/src/lib.rs index 8b13789..bca356e 100644 --- a/opc_da/src/lib.rs +++ b/opc_da/src/lib.rs @@ -1 +1,24 @@ +mod opc_async_io; +mod opc_browse; +mod opc_data_callback; +mod opc_group; +mod opc_item; +mod opc_server; +mod opc_sync_io; +pub use opc_async_io::{ + OPCAsyncIO, + traits::{ + OPCAsyncIO as OPCAsyncIOTrait, OPCAsyncIO2 as OPCAsyncIO2Trait, + OPCAsyncIO3 as OPCAsyncIO3Trait, + }, +}; +pub use opc_browse::{OPCBrowse, traits::OPCBrowse as OPCBrowseTrait}; +pub use opc_data_callback::{OPCDataCallback, traits::OPCDataCallback as OPCDataCallbackTrait}; +pub use opc_group::{OPCGroup, traits::OPCGroup as OPCGroupTrait}; +pub use opc_item::{OPCItem, traits::OPCItem as OPCItemTrait}; +pub use opc_server::{OPCServer, traits::OPCServer as OPCServerTrait}; +pub use opc_sync_io::{ + OPCSyncIO, + traits::{OPCSyncIO as OPCSyncIOTrait, OPCSyncIO2 as OPCSyncIO2Trait}, +}; diff --git a/opc_da/src/opc_async_io.rs b/opc_da/src/opc_async_io.rs new file mode 100644 index 0000000..2310192 --- /dev/null +++ b/opc_da/src/opc_async_io.rs @@ -0,0 +1,337 @@ +use windows_core::*; +use opc_da_bindings::*; +use opc_classic_utils::*; + +#[windows::core::implement(IOPCAsyncIO)] +pub struct OPCAsyncIO(T) +where + T: 'static + traits::OPCAsyncIO; + +impl IOPCAsyncIO_Impl for OPCAsyncIO_Impl { + fn Read( + &self, + dwconnection: u32, + dwsource: tagOPCDATASOURCE, + dwcount: u32, + phserver: *const u32, + ptransactionid: *mut u32, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { + std::slice::from_raw_parts(phserver, dwcount as usize) + }; + + let (transaction_id, errors) = self.0.read(dwconnection, dwsource, server_handles.to_vec())?; + + write_caller_allocated_ptr!(ptransactionid, transaction_id)?; + + // Allocate and write errors + if !pperrors.is_null() { + let size = std::mem::size_of::() * errors.len(); + 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( + errors.as_ptr(), + ptr.cast(), + errors.len() + ); + *pperrors = ptr.cast(); + } + } + + Ok(()) + } + + fn Write( + &self, + dwconnection: u32, + dwcount: u32, + phserver: *const u32, + pitemvalues: *const windows::Win32::System::Variant::VARIANT, + ptransactionid: *mut u32, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { + std::slice::from_raw_parts(phserver, dwcount as usize) + }; + let values = unsafe { + std::slice::from_raw_parts(pitemvalues, dwcount as usize) + }; + + let (transaction_id, errors) = self.0.write(dwconnection, server_handles.to_vec(), values.to_vec())?; + + write_caller_allocated_ptr!(ptransactionid, transaction_id)?; + + // Allocate and write errors + if !pperrors.is_null() { + let size = std::mem::size_of::() * errors.len(); + 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( + errors.as_ptr(), + ptr.cast(), + errors.len() + ); + *pperrors = ptr.cast(); + } + } + + Ok(()) + } + + fn Refresh( + &self, + dwconnection: u32, + dwsource: tagOPCDATASOURCE, + ) -> Result { + self.0.refresh(dwconnection, dwsource) + } + + fn Cancel(&self, dwtransactionid: u32) -> Result<()> { + self.0.cancel(dwtransactionid) + } +} + + + +#[windows::core::implement(IOPCAsyncIO2, IOPCAsyncIO3)] +pub struct OPCAsyncIO3(T) +where + T: 'static + traits::OPCAsyncIO3; + +impl IOPCAsyncIO2_Impl for OPCAsyncIO3_Impl { + fn Read( + &self, + dwcount: u32, + phserver: *const u32, + dwtransactionid: u32, + pdwcancelid: *mut u32, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { + std::slice::from_raw_parts(phserver, dwcount as usize) + }; + + let (cancel_id, errors) = traits::OPCAsyncIO2::read(&self.0, dwcount, server_handles.to_vec(), dwtransactionid)?; + + write_caller_allocated_ptr!(pdwcancelid, cancel_id)?; + + // Allocate and write errors + if !pperrors.is_null() { + let size = std::mem::size_of::() * errors.len(); + 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( + errors.as_ptr(), + ptr.cast(), + errors.len() + ); + *pperrors = ptr.cast(); + } + } + + Ok(()) + } + + fn Write( + &self, + dwcount: u32, + phserver: *const u32, + pitemvalues: *const windows::Win32::System::Variant::VARIANT, + dwtransactionid: u32, + pdwcancelid: *mut u32, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { + std::slice::from_raw_parts(phserver, dwcount as usize) + }; + let values = unsafe { + std::slice::from_raw_parts(pitemvalues, dwcount as usize) + }; + + let (cancel_id, errors) = traits::OPCAsyncIO2::write(&self.0, dwcount, server_handles.to_vec(), values.to_vec(), dwtransactionid)?; + + write_caller_allocated_ptr!(pdwcancelid, cancel_id)?; + + // Allocate and write errors + if !pperrors.is_null() { + let size = std::mem::size_of::() * errors.len(); + 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( + errors.as_ptr(), + ptr.cast(), + errors.len() + ); + *pperrors = ptr.cast(); + } + } + + Ok(()) + } + + fn Refresh2( + &self, + dwsource: tagOPCDATASOURCE, + dwtransactionid: u32, + ) -> Result { + self.0.refresh2(dwsource, dwtransactionid) + } + + fn Cancel2(&self, dwcancelid: u32) -> Result<()> { + self.0.cancel2(dwcancelid) + } + + fn SetEnable(&self, benable: BOOL) -> Result<()> { + self.0.set_enable(benable.as_bool()) + } + + fn GetEnable(&self) -> Result { + let enabled = self.0.get_enable()?; + Ok(BOOL::from(enabled)) + } +} + +impl IOPCAsyncIO3_Impl for OPCAsyncIO3_Impl { + fn ReadMaxAge( + &self, + dwcount: u32, + phserver: *const u32, + pdwmaxage: *const u32, + dwtransactionid: u32, + pdwcancelid: *mut u32, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { + std::slice::from_raw_parts(phserver, dwcount as usize) + }; + let max_ages = unsafe { + std::slice::from_raw_parts(pdwmaxage, dwcount as usize) + }; + + let (cancel_id, errors) = self.0.read_max_age(dwcount, server_handles.to_vec(), max_ages.to_vec(), dwtransactionid)?; + + write_caller_allocated_ptr!(pdwcancelid, cancel_id)?; + + // Allocate and write errors + if !pperrors.is_null() { + let size = std::mem::size_of::() * errors.len(); + 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( + errors.as_ptr(), + ptr.cast(), + errors.len() + ); + *pperrors = ptr.cast(); + } + } + + Ok(()) + } + + fn WriteVQT( + &self, + dwcount: u32, + phserver: *const u32, + pitemvqt: *const tagOPCITEMVQT, + dwtransactionid: u32, + pdwcancelid: *mut u32, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { + std::slice::from_raw_parts(phserver, dwcount as usize) + }; + let item_vqt = unsafe { + std::slice::from_raw_parts(pitemvqt, dwcount as usize) + }; + + let (cancel_id, errors) = self.0.write_vqt(dwcount, server_handles.to_vec(), item_vqt.to_vec(), dwtransactionid)?; + + write_caller_allocated_ptr!(pdwcancelid, cancel_id)?; + + // Allocate and write errors + if !pperrors.is_null() { + let size = std::mem::size_of::() * errors.len(); + 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( + errors.as_ptr(), + ptr.cast(), + errors.len() + ); + *pperrors = ptr.cast(); + } + } + + Ok(()) + } + + fn RefreshMaxAge( + &self, + dwmaxage: u32, + dwtransactionid: u32, + ) -> Result { + self.0.refresh_max_age(dwmaxage, dwtransactionid) + } +} + +pub mod traits { + use super::*; + + pub trait OPCAsyncIO { + fn read(&self, connection: u32, source: tagOPCDATASOURCE, server_handles: Vec) -> Result<(u32, Vec)>; + + fn write(&self, connection: u32, server_handles: Vec, values: Vec) -> Result<(u32, Vec)>; + + fn refresh(&self, connection: u32, source: tagOPCDATASOURCE) -> Result; + + fn cancel(&self, transaction_id: u32) -> Result<()>; + } + + pub trait OPCAsyncIO2: OPCAsyncIO { + fn read(&self, count: u32, server_handles: Vec, transaction_id: u32) -> Result<(u32, Vec)>; + + fn write(&self, count: u32, server_handles: Vec, values: Vec, transaction_id: u32) -> Result<(u32, Vec)>; + + fn refresh2(&self, source: tagOPCDATASOURCE, transaction_id: u32) -> Result; + + fn cancel2(&self, cancel_id: u32) -> Result<()>; + + fn set_enable(&self, enable: bool) -> Result<()>; + + fn get_enable(&self) -> Result; + } + + pub trait OPCAsyncIO3: OPCAsyncIO2 { + fn read_max_age(&self, count: u32, server_handles: Vec, max_ages: Vec, transaction_id: u32) -> Result<(u32, Vec)>; + + fn write_vqt(&self, count: u32, server_handles: Vec, item_vqt: Vec, transaction_id: u32) -> Result<(u32, Vec)>; + + fn refresh_max_age(&self, max_age: u32, transaction_id: u32) -> Result; + } +} \ No newline at end of file diff --git a/opc_da/src/opc_browse.rs b/opc_da/src/opc_browse.rs new file mode 100644 index 0000000..028741a --- /dev/null +++ b/opc_da/src/opc_browse.rs @@ -0,0 +1,129 @@ +use windows_core::*; +use opc_da_bindings::*; +use opc_classic_utils::*; + +#[windows::core::implement(IOPCBrowse)] +pub struct OPCBrowse(T) +where + T: 'static + traits::OPCBrowse; + +impl IOPCBrowse_Impl for OPCBrowse_Impl { + fn GetProperties( + &self, + dwitemcount: u32, + pszitemids: *const PCWSTR, + breturnpropertyvalues: BOOL, + dwpropertycount: u32, + pdwpropertyids: *const u32, + ppitemproperties: *mut *mut tagOPCITEMPROPERTIES, + ) -> Result<()> { + let item_ids = unsafe { + std::slice::from_raw_parts(pszitemids, dwitemcount as usize) + }; + let property_ids = unsafe { + std::slice::from_raw_parts(pdwpropertyids, dwpropertycount as usize) + }; + + let item_ids: Vec = item_ids.iter().map(|pcwstr| { + unsafe { + CallerAllocatedWString::from_pcwstr(*pcwstr) + .to_string_lossy() + .unwrap_or_default() + } + }).collect(); + + let properties = self.0.get_properties(item_ids, breturnpropertyvalues.as_bool(), property_ids.to_vec())?; + + // Allocate and write properties + if !ppitemproperties.is_null() { + let properties_array = CallerAllocatedArray::from_slice(&properties)?; + unsafe { *ppitemproperties = properties_array.as_ptr() }; + } + + Ok(()) + } + + fn Browse( + &self, + szitemid: &PCWSTR, + pszcontinuationpoint: *mut PWSTR, + dwmaxelementsreturned: u32, + dwbrowsefilter: tagOPCBROWSEFILTER, + szelementnamefilter: &PCWSTR, + szvendorfilter: &PCWSTR, + breturnallproperties: BOOL, + breturnpropertyvalues: BOOL, + dwpropertycount: u32, + pdwpropertyids: *const u32, + pbmoreelements: *mut BOOL, + pdwcount: *mut u32, + ppbrowseelements: *mut *mut tagOPCBROWSEELEMENT, + ) -> Result<()> { + let item_id = unsafe { + CallerAllocatedWString::from_pcwstr(*szitemid) + .to_string_lossy() + .unwrap_or_default() + }; + let element_name_filter = unsafe { + CallerAllocatedWString::from_pcwstr(*szelementnamefilter) + .to_string_lossy() + .unwrap_or_default() + }; + let vendor_filter = unsafe { + CallerAllocatedWString::from_pcwstr(*szvendorfilter) + .to_string_lossy() + .unwrap_or_default() + }; + let property_ids = unsafe { + std::slice::from_raw_parts(pdwpropertyids, dwpropertycount as usize) + }; + + let (more_elements, count, elements) = self.0.browse( + item_id, + dwmaxelementsreturned, + dwbrowsefilter, + element_name_filter, + vendor_filter, + breturnallproperties.as_bool(), + breturnpropertyvalues.as_bool(), + property_ids.to_vec(), + )?; + + // Write output parameters + if !pszcontinuationpoint.is_null() { + unsafe { *pszcontinuationpoint = PWSTR::null() }; + } + if !pbmoreelements.is_null() { + unsafe { *pbmoreelements = BOOL::from(more_elements) }; + } + if !pdwcount.is_null() { + unsafe { *pdwcount = count }; + } + if !ppbrowseelements.is_null() { + let elements_array = CallerAllocatedArray::from_slice(&elements)?; + unsafe { *ppbrowseelements = elements_array.as_ptr() }; + } + + Ok(()) + } +} + +pub mod traits { + use super::*; + + pub trait OPCBrowse { + fn get_properties(&self, item_ids: Vec, return_property_values: bool, property_ids: Vec) -> Result>; + + fn browse( + &self, + item_id: String, + max_elements_returned: u32, + browse_filter: tagOPCBROWSEFILTER, + element_name_filter: String, + vendor_filter: String, + return_all_properties: bool, + return_property_values: bool, + property_ids: Vec, + ) -> Result<(bool, u32, Vec)>; + } +} \ No newline at end of file diff --git a/opc_da/src/opc_data_callback.rs b/opc_da/src/opc_data_callback.rs new file mode 100644 index 0000000..e1f90d8 --- /dev/null +++ b/opc_da/src/opc_data_callback.rs @@ -0,0 +1,141 @@ +use opc_da_bindings::*; +use windows_core::*; + +#[windows::core::implement(IOPCDataCallback)] +pub struct OPCDataCallback(T) +where + T: 'static + traits::OPCDataCallback; + +impl IOPCDataCallback_Impl for OPCDataCallback_Impl { + fn OnDataChange( + &self, + dwtransid: u32, + hgroup: u32, + hrquality: HRESULT, + hrerror: HRESULT, + dwcount: u32, + phclientitems: *const u32, + pvvalues: *const windows::Win32::System::Variant::VARIANT, + pwqualities: *const u16, + pfttimestamps: *const windows::Win32::Foundation::FILETIME, + perrors: *const HRESULT, + ) -> Result<()> { + let client_items = unsafe { std::slice::from_raw_parts(phclientitems, dwcount as usize) }; + let values = unsafe { std::slice::from_raw_parts(pvvalues, dwcount as usize) }; + let qualities = unsafe { std::slice::from_raw_parts(pwqualities, dwcount as usize) }; + let timestamps = unsafe { std::slice::from_raw_parts(pfttimestamps, dwcount as usize) }; + let errors = unsafe { std::slice::from_raw_parts(perrors, dwcount as usize) }; + + self.0.on_data_change( + dwtransid, + hgroup, + hrquality, + hrerror, + client_items.to_vec(), + values.to_vec(), + qualities.to_vec(), + timestamps.to_vec(), + errors.to_vec(), + ) + } + + fn OnReadComplete( + &self, + dwtransid: u32, + hgroup: u32, + hrquality: HRESULT, + hrerror: HRESULT, + dwcount: u32, + phclientitems: *const u32, + pvvalues: *const windows::Win32::System::Variant::VARIANT, + pwqualities: *const u16, + pfttimestamps: *const windows::Win32::Foundation::FILETIME, + perrors: *const HRESULT, + ) -> Result<()> { + let client_items = unsafe { std::slice::from_raw_parts(phclientitems, dwcount as usize) }; + let values = unsafe { std::slice::from_raw_parts(pvvalues, dwcount as usize) }; + let qualities = unsafe { std::slice::from_raw_parts(pwqualities, dwcount as usize) }; + let timestamps = unsafe { std::slice::from_raw_parts(pfttimestamps, dwcount as usize) }; + let errors = unsafe { std::slice::from_raw_parts(perrors, dwcount as usize) }; + + self.0.on_read_complete( + dwtransid, + hgroup, + hrquality, + hrerror, + client_items.to_vec(), + values.to_vec(), + qualities.to_vec(), + timestamps.to_vec(), + errors.to_vec(), + ) + } + + fn OnWriteComplete( + &self, + dwtransid: u32, + hgroup: u32, + hrmastererr: HRESULT, + dwcount: u32, + pclienthandles: *const u32, + perrors: *const HRESULT, + ) -> Result<()> { + let client_items = unsafe { std::slice::from_raw_parts(pclienthandles, dwcount as usize) }; + let errors = unsafe { std::slice::from_raw_parts(perrors, dwcount as usize) }; + + self.0.on_write_complete( + dwtransid, + hgroup, + hrmastererr, + client_items.to_vec(), + errors.to_vec(), + ) + } + + fn OnCancelComplete(&self, dwtransid: u32, hgroup: u32) -> Result<()> { + self.0.on_cancel_complete(dwtransid, hgroup) + } +} + +pub mod traits { + use super::*; + + pub trait OPCDataCallback { + fn on_data_change( + &self, + transaction_id: u32, + group: u32, + quality: HRESULT, + error: HRESULT, + client_items: Vec, + values: Vec, + qualities: Vec, + timestamps: Vec, + errors: Vec, + ) -> Result<()>; + + fn on_read_complete( + &self, + transaction_id: u32, + group: u32, + quality: HRESULT, + error: HRESULT, + client_items: Vec, + values: Vec, + qualities: Vec, + timestamps: Vec, + errors: Vec, + ) -> Result<()>; + + fn on_write_complete( + &self, + transaction_id: u32, + group: u32, + master_error: HRESULT, + client_items: Vec, + errors: Vec, + ) -> Result<()>; + + fn on_cancel_complete(&self, transaction_id: u32, group: u32) -> Result<()>; + } +} diff --git a/opc_da/src/opc_group.rs b/opc_da/src/opc_group.rs new file mode 100644 index 0000000..8e8cd2e --- /dev/null +++ b/opc_da/src/opc_group.rs @@ -0,0 +1,203 @@ +use opc_classic_utils::*; +use opc_da_bindings::*; +use windows_core::*; + +#[windows::core::implement(IOPCGroupStateMgt, IOPCGroupStateMgt2)] +pub struct OPCGroup(T) +where + T: 'static + traits::OPCGroup + traits::OPCGroupStateMgt2; + +impl IOPCGroupStateMgt_Impl for OPCGroup_Impl { + fn GetState( + &self, + pupdateRate: *mut u32, + pactive: *mut BOOL, + ppname: *mut PWSTR, + ptimebias: *mut i32, + ppercentdeadband: *mut f32, + plcid: *mut u32, + phclientgroup: *mut u32, + phservergroup: *mut u32, + ) -> Result<()> { + let state = self.0.get_state()?; + + write_caller_allocated_ptr!(pupdateRate, state.update_rate)?; + write_caller_allocated_ptr!(pactive, BOOL::from(state.active))?; + write_caller_allocated_ptr!(ptimebias, state.time_bias)?; + write_caller_allocated_ptr!(ppercentdeadband, state.percent_deadband)?; + write_caller_allocated_ptr!(plcid, state.locale_id)?; + write_caller_allocated_ptr!(phclientgroup, state.client_group)?; + write_caller_allocated_ptr!(phservergroup, state.server_group)?; + + // Handle name separately as it's a string + if !ppname.is_null() { + let name_ptr = alloc_callee_wstring!(state.name)?; + unsafe { *ppname = name_ptr }; + } + + Ok(()) + } + + fn SetState( + &self, + prequestedupdaterate: *const u32, + previsedupdaterate: *mut u32, + pactive: *const BOOL, + ptimebias: *const i32, + ppercentdeadband: *const f32, + plcid: *const u32, + phclientgroup: *const u32, + ) -> Result<()> { + let requested_update_rate = if !prequestedupdaterate.is_null() { + Some(unsafe { *prequestedupdaterate }) + } else { + None + }; + + let active = if !pactive.is_null() { + Some(unsafe { (*pactive).as_bool() }) + } else { + None + }; + + let time_bias = if !ptimebias.is_null() { + Some(unsafe { *ptimebias }) + } else { + None + }; + + let percent_deadband = if !ppercentdeadband.is_null() { + Some(unsafe { *ppercentdeadband }) + } else { + None + }; + + let locale_id = if !plcid.is_null() { + Some(unsafe { *plcid }) + } else { + None + }; + + let client_group = if !phclientgroup.is_null() { + Some(unsafe { *phclientgroup }) + } else { + None + }; + + let ( + revised_update_rate, + new_active, + new_time_bias, + new_percent_deadband, + new_locale_id, + new_client_group, + ) = self.0.set_state( + requested_update_rate, + active, + time_bias, + percent_deadband, + locale_id, + client_group, + )?; + + write_caller_allocated_ptr!(previsedupdaterate, revised_update_rate)?; + + Ok(()) + } + + fn SetName(&self, szname: &PCWSTR) -> Result<()> { + let name = unsafe { + CallerAllocatedWString::from_pcwstr(*szname) + .to_string_lossy() + .ok_or(windows::Win32::Foundation::E_POINTER) + }?; + + self.0.set_name(name) + } + + fn CloneGroup(&self, szname: &PCWSTR, riid: *const GUID) -> Result { + let name = unsafe { + CallerAllocatedWString::from_pcwstr(*szname) + .to_string_lossy() + .ok_or(windows::Win32::Foundation::E_POINTER) + }?; + + self.0.clone_group(name, riid) + } +} + +impl IOPCGroupStateMgt2_Impl for OPCGroup_Impl { + fn SetKeepAlive(&self, dwkeepalivetime: u32) -> Result { + self.0.set_keep_alive(dwkeepalivetime) + } + + fn GetKeepAlive(&self) -> Result { + self.0.get_keep_alive() + } +} + +pub mod traits { + use super::*; + + #[derive(Debug, Clone)] + pub struct GroupState { + pub update_rate: u32, + pub active: bool, + pub name: String, + pub time_bias: i32, + pub percent_deadband: f32, + pub locale_id: u32, + pub client_group: u32, + pub server_group: u32, + } + + #[derive(Debug, Clone)] + pub struct GroupState2 { + pub update_rate: u32, + pub active: bool, + pub name: String, + pub time_bias: i32, + pub percent_deadband: f32, + pub locale_id: u32, + pub client_group: u32, + pub server_group: u32, + pub keep_alive: u32, + } + + pub trait OPCGroup { + fn get_state(&self) -> Result; + + fn set_state( + &self, + requested_update_rate: Option, + active: Option, + time_bias: Option, + percent_deadband: Option, + locale_id: Option, + client_group: Option, + ) -> Result<(u32, bool, i32, f32, u32, u32)>; + + fn set_name(&self, name: String) -> Result<()>; + + fn clone_group(&self, name: String, riid: *const GUID) -> Result; + } + + pub trait OPCGroupStateMgt2: OPCGroup { + fn get_state2(&self) -> Result; + + fn set_state2( + &self, + requested_update_rate: Option, + active: Option, + time_bias: Option, + percent_deadband: Option, + locale_id: Option, + client_group: Option, + keep_alive: Option, + ) -> Result<(u32, bool, i32, f32, u32, u32, u32)>; + + fn set_keep_alive(&self, keep_alive_time: u32) -> Result; + + fn get_keep_alive(&self) -> Result; + } +} diff --git a/opc_da/src/opc_item.rs b/opc_da/src/opc_item.rs new file mode 100644 index 0000000..e888e8b --- /dev/null +++ b/opc_da/src/opc_item.rs @@ -0,0 +1,239 @@ +use opc_classic_utils::*; +use opc_da_bindings::*; +use windows_core::*; + +#[windows::core::implement(IOPCItemMgt)] +pub struct OPCItem(T) +where + T: 'static + traits::OPCItem; + +impl IOPCItemMgt_Impl for OPCItem_Impl { + fn AddItems( + &self, + dwcount: u32, + pitemarray: *const tagOPCITEMDEF, + ppaddresults: *mut *mut tagOPCITEMRESULT, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let items = unsafe { std::slice::from_raw_parts(pitemarray, dwcount as usize) }; + + let item_defs: Vec = items + .iter() + .map(|item| traits::ItemDefinition { + access_path: unsafe { + CallerAllocatedWString::from_pwstr(item.szAccessPath) + .to_string_lossy() + .unwrap_or_default() + }, + item_id: unsafe { + CallerAllocatedWString::from_pwstr(item.szItemID) + .to_string_lossy() + .unwrap_or_default() + }, + active: item.bActive.as_bool(), + client_handle: item.hClient, + blob_size: item.dwBlobSize, + blob: if !item.pBlob.is_null() { + unsafe { + std::slice::from_raw_parts(item.pBlob, item.dwBlobSize as usize).to_vec() + } + } else { + Vec::new() + }, + requested_data_type: item.vtRequestedDataType, + }) + .collect(); + + let (results, errors) = self.0.add_items(item_defs)?; + + // Allocate and write results + copy_to_caller_array!(ppaddresults, &results)?; + + // Allocate and write errors + copy_to_caller_array!(pperrors, &errors)?; + + Ok(()) + } + + fn ValidateItems( + &self, + dwcount: u32, + pitemarray: *const tagOPCITEMDEF, + bblobupdate: BOOL, + ppvalidationresults: *mut *mut tagOPCITEMRESULT, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let items = unsafe { std::slice::from_raw_parts(pitemarray, dwcount as usize) }; + + let item_defs: Vec = items + .iter() + .map(|item| traits::ItemDefinition { + access_path: unsafe { + if item.szAccessPath.is_null() { + String::new() + } else { + CallerAllocatedWString::from_pcwstr(PCWSTR(item.szAccessPath.0)) + .to_string_lossy() + .unwrap_or_default() + } + }, + item_id: unsafe { + if item.szItemID.is_null() { + String::new() + } else { + CallerAllocatedWString::from_pcwstr(PCWSTR(item.szItemID.0)) + .to_string_lossy() + .unwrap_or_default() + } + }, + active: item.bActive.as_bool(), + client_handle: item.hClient, + blob_size: item.dwBlobSize, + blob: if !item.pBlob.is_null() { + unsafe { + std::slice::from_raw_parts(item.pBlob, item.dwBlobSize as usize).to_vec() + } + } else { + Vec::new() + }, + requested_data_type: item.vtRequestedDataType, + }) + .collect(); + + let (results, errors) = self.0.validate_items(item_defs, bblobupdate.as_bool())?; + + // Allocate and write results + copy_to_caller_array!(ppvalidationresults, &results)?; + + // Allocate and write errors + copy_to_caller_array!(pperrors, &errors)?; + + Ok(()) + } + + fn RemoveItems( + &self, + dwcount: u32, + phserver: *const u32, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { std::slice::from_raw_parts(phserver, dwcount as usize) }; + + let errors = self.0.remove_items(server_handles.to_vec())?; + + // Allocate and write errors + copy_to_caller_array!(pperrors, &errors)?; + + Ok(()) + } + + fn SetActiveState( + &self, + dwcount: u32, + phserver: *const u32, + bactive: BOOL, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { std::slice::from_raw_parts(phserver, dwcount as usize) }; + + let errors = self + .0 + .set_active_state(server_handles.to_vec(), bactive.as_bool())?; + + // Allocate and write errors + copy_to_caller_array!(pperrors, &errors)?; + + Ok(()) + } + + fn SetClientHandles( + &self, + dwcount: u32, + phserver: *const u32, + phclient: *const u32, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { std::slice::from_raw_parts(phserver, dwcount as usize) }; + let client_handles = unsafe { std::slice::from_raw_parts(phclient, dwcount as usize) }; + + let errors = self + .0 + .set_client_handles(server_handles.to_vec(), client_handles.to_vec())?; + + // Allocate and write errors + copy_to_caller_array!(pperrors, &errors)?; + + Ok(()) + } + + fn SetDatatypes( + &self, + dwcount: u32, + phserver: *const u32, + pvrequesteddatatypes: *const u16, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { std::slice::from_raw_parts(phserver, dwcount as usize) }; + let requested_datatypes = + unsafe { std::slice::from_raw_parts(pvrequesteddatatypes, dwcount as usize) }; + + let errors = self + .0 + .set_datatypes(server_handles.to_vec(), requested_datatypes.to_vec())?; + + // Allocate and write errors + copy_to_caller_array!(pperrors, &errors)?; + + Ok(()) + } + + fn CreateEnumerator(&self, riid: *const GUID) -> Result { + self.0.create_enumerator(riid) + } +} + +pub mod traits { + use super::*; + + #[derive(Debug, Clone)] + pub struct ItemDefinition { + pub access_path: String, + pub item_id: String, + pub active: bool, + pub client_handle: u32, + pub blob_size: u32, + pub blob: Vec, + pub requested_data_type: u16, + } + + pub trait OPCItem { + fn add_items( + &self, + items: Vec, + ) -> Result<(Vec, Vec)>; + + fn validate_items( + &self, + items: Vec, + blob_update: bool, + ) -> Result<(Vec, Vec)>; + + fn remove_items(&self, server_handles: Vec) -> Result>; + + fn set_active_state(&self, server_handles: Vec, active: bool) -> Result>; + + fn set_client_handles( + &self, + server_handles: Vec, + client_handles: Vec, + ) -> Result>; + + fn set_datatypes( + &self, + server_handles: Vec, + requested_datatypes: Vec, + ) -> Result>; + + fn create_enumerator(&self, riid: *const GUID) -> Result; + } +} diff --git a/opc_da/src/opc_server.rs b/opc_da/src/opc_server.rs new file mode 100644 index 0000000..5deac3a --- /dev/null +++ b/opc_da/src/opc_server.rs @@ -0,0 +1,123 @@ +use opc_classic_utils::*; +use opc_da_bindings::*; +use windows_core::*; + +#[windows::core::implement(IOPCServer)] +pub struct OPCServer(T) +where + T: 'static + traits::OPCServer; + +impl IOPCServer_Impl for OPCServer_Impl { + fn AddGroup( + &self, + szname: &PCWSTR, + bactive: BOOL, + dwrequestedupdaterate: u32, + hclientgroup: u32, + ptimebias: *const i32, + ppercentdeadband: *const f32, + dwlcid: u32, + phservergroup: *mut u32, + previsedupdaterate: *mut u32, + riid: *const GUID, + ppunk: OutRef<'_, IUnknown>, + ) -> Result<()> { + let name = unsafe { + CallerAllocatedWString::from_pcwstr(*szname) + .to_string_lossy() + .ok_or(windows::Win32::Foundation::E_POINTER) + }?; + + let time_bias = if !ptimebias.is_null() { + Some(unsafe { *ptimebias }) + } else { + None + }; + + let percent_deadband = if !ppercentdeadband.is_null() { + Some(unsafe { *ppercentdeadband }) + } else { + None + }; + + let (server_group, revised_update_rate, group_interface) = self.0.add_group( + name, + bactive.as_bool(), + dwrequestedupdaterate, + hclientgroup, + time_bias, + percent_deadband, + dwlcid, + )?; + + write_caller_allocated_ptr!(phservergroup, server_group)?; + write_caller_allocated_ptr!(previsedupdaterate, revised_update_rate)?; + ppunk.write(Some(group_interface))?; + + Ok(()) + } + + fn GetErrorString(&self, dwerror: HRESULT, dwlocale: u32) -> Result { + alloc_callee_wstring!(self.0.get_error_string(dwerror, dwlocale)?) + } + + fn GetGroupByName(&self, szname: &PCWSTR, riid: *const GUID) -> Result { + let name = unsafe { + CallerAllocatedWString::from_pcwstr(*szname) + .to_string_lossy() + .ok_or(windows::Win32::Foundation::E_POINTER) + }?; + + self.0.get_group_by_name(name, riid) + } + + fn GetStatus(&self) -> Result<*mut tagOPCSERVERSTATUS> { + let status = self.0.get_status()?; + let ptr = CalleeAllocatedPtr::from_value(&status)?; + Ok(ptr.into_raw()) + } + + fn RemoveGroup(&self, hservergroup: u32, bforce: BOOL) -> Result<()> { + self.0.remove_group(hservergroup, bforce.as_bool()) + } + + fn CreateGroupEnumerator( + &self, + dwscope: tagOPCENUMSCOPE, + riid: *const GUID, + ) -> Result { + self.0.create_group_enumerator(dwscope, riid) + } +} + +pub mod traits { + use super::*; + use opc_da_bindings::tagOPCSERVERSTATUS; + + pub trait OPCServer { + fn add_group( + &self, + name: String, + active: bool, + requested_update_rate: u32, + client_group: u32, + time_bias: Option, + percent_deadband: Option, + locale_id: u32, + ) -> Result<(u32, u32, IUnknown)>; + + fn get_error_string(&self, error: HRESULT, locale: u32) -> Result; + + fn get_group_by_name(&self, name: String, riid: *const GUID) -> Result; + + fn get_status(&self) -> Result; + + fn remove_group(&self, server_group: u32, force: bool) -> Result<()>; + + fn create_group_enumerator( + &self, + scope: tagOPCENUMSCOPE, + riid: *const GUID, + ) -> Result; + } +} diff --git a/opc_da/src/opc_sync_io.rs b/opc_da/src/opc_sync_io.rs new file mode 100644 index 0000000..9b2ecdf --- /dev/null +++ b/opc_da/src/opc_sync_io.rs @@ -0,0 +1,230 @@ +use opc_classic_utils::*; +use opc_da_bindings::*; +use windows_core::*; + +#[windows::core::implement(IOPCSyncIO, IOPCSyncIO2)] +pub struct OPCSyncIO(T) +where + T: 'static + traits::OPCSyncIO + traits::OPCSyncIO2; + +impl IOPCSyncIO_Impl for OPCSyncIO_Impl { + fn Read( + &self, + dwsource: tagOPCDATASOURCE, + dwcount: u32, + phserver: *const u32, + ppitemvalues: *mut *mut tagOPCITEMSTATE, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { std::slice::from_raw_parts(phserver, dwcount as usize) }; + + let (item_states, errors) = self.0.read(dwsource, server_handles.to_vec())?; + + // Allocate and write item states + if !ppitemvalues.is_null() { + let size = std::mem::size_of::() * item_states.len(); + 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(item_states.as_ptr(), ptr.cast(), item_states.len()); + *ppitemvalues = ptr.cast(); + } + } + + // Allocate and write errors + if !pperrors.is_null() { + let size = std::mem::size_of::() * errors.len(); + 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(errors.as_ptr(), ptr.cast(), errors.len()); + *pperrors = ptr.cast(); + } + } + + Ok(()) + } + + fn Write( + &self, + dwcount: u32, + phserver: *const u32, + pitemvalues: *const windows::Win32::System::Variant::VARIANT, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { std::slice::from_raw_parts(phserver, dwcount as usize) }; + let values = unsafe { std::slice::from_raw_parts(pitemvalues, dwcount as usize) }; + + let errors = self.0.write(server_handles.to_vec(), values.to_vec())?; + + // Allocate and write errors + if !pperrors.is_null() { + let size = std::mem::size_of::() * errors.len(); + 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(errors.as_ptr(), ptr.cast(), errors.len()); + *pperrors = ptr.cast(); + } + } + + Ok(()) + } +} + +impl IOPCSyncIO2_Impl for OPCSyncIO_Impl { + fn ReadMaxAge( + &self, + dwcount: u32, + phserver: *const u32, + pdwmaxage: *const u32, + ppvvalues: *mut *mut windows::Win32::System::Variant::VARIANT, + ppwqualities: *mut *mut u16, + ppfttimestamps: *mut *mut windows::Win32::Foundation::FILETIME, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { std::slice::from_raw_parts(phserver, dwcount as usize) }; + let max_ages = unsafe { std::slice::from_raw_parts(pdwmaxage, dwcount as usize) }; + + let (values, qualities, timestamps, errors) = self + .0 + .read_max_age(server_handles.to_vec(), max_ages.to_vec())?; + + // Allocate and write values + if !ppvvalues.is_null() { + let size = + std::mem::size_of::() * values.len(); + 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(values.as_ptr(), ptr.cast(), values.len()); + *ppvvalues = ptr.cast(); + } + } + + // Allocate and write qualities + if !ppwqualities.is_null() { + let size = std::mem::size_of::() * qualities.len(); + 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(qualities.as_ptr(), ptr.cast(), qualities.len()); + *ppwqualities = ptr.cast(); + } + } + + // Allocate and write timestamps + if !ppfttimestamps.is_null() { + let size = + std::mem::size_of::() * timestamps.len(); + 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(timestamps.as_ptr(), ptr.cast(), timestamps.len()); + *ppfttimestamps = ptr.cast(); + } + } + + // Allocate and write errors + if !pperrors.is_null() { + let size = std::mem::size_of::() * errors.len(); + 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(errors.as_ptr(), ptr.cast(), errors.len()); + *pperrors = ptr.cast(); + } + } + + Ok(()) + } + + fn WriteVQT( + &self, + dwcount: u32, + phserver: *const u32, + pitemvqt: *const tagOPCITEMVQT, + pperrors: *mut *mut HRESULT, + ) -> Result<()> { + let server_handles = unsafe { std::slice::from_raw_parts(phserver, dwcount as usize) }; + let item_vqt = unsafe { std::slice::from_raw_parts(pitemvqt, dwcount as usize) }; + + let errors = self + .0 + .write_vqt(server_handles.to_vec(), item_vqt.to_vec())?; + + // Allocate and write errors + if !pperrors.is_null() { + let size = std::mem::size_of::() * errors.len(); + 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(errors.as_ptr(), ptr.cast(), errors.len()); + *pperrors = ptr.cast(); + } + } + + Ok(()) + } +} + +pub mod traits { + use super::*; + + pub trait OPCSyncIO { + fn read( + &self, + source: tagOPCDATASOURCE, + server_handles: Vec, + ) -> Result<(Vec, Vec)>; + + fn write( + &self, + server_handles: Vec, + values: Vec, + ) -> Result>; + } + + pub trait OPCSyncIO2: OPCSyncIO { + fn read_max_age( + &self, + server_handles: Vec, + max_ages: Vec, + ) -> Result<( + Vec, + Vec, + Vec, + Vec, + )>; + + fn write_vqt( + &self, + server_handles: Vec, + item_vqt: Vec, + ) -> Result>; + } +}