From 2cf6bb01b7b8ff7ca3052f38850de8dd11ca2919 Mon Sep 17 00:00:00 2001 From: Lahfir Date: Thu, 16 Apr 2026 03:21:53 -0700 Subject: [PATCH] refactor(ffi): split actions.rs into actions/ submodules Move actions.rs into actions/{conversion,resolve,execute,result}.rs. - conversion.rs: direction_from_c + key_combo_from_c + action_from_c + tests - resolve.rs: ad_resolve_element - execute.rs: ad_execute_action - result.rs: action_result_to_c + ad_free_action_result + tests actions/mod.rs declares submodules; the #[no_mangle] entries remain discoverable by the cdylib linker and cbindgen without re-exports. Drop the unused opt_string_to_c re-export from convert/mod.rs (all new modules import from crate::convert::string:: directly). No behavior or ABI change. cbindgen header symbol set unchanged; emission order shifts due to cbindgen's module-path topological sort (ordering-only diff per Unit R verification criteria). --- crates/ffi/src/actions.rs | 376 --------------------------- crates/ffi/src/actions/conversion.rs | 177 +++++++++++++ crates/ffi/src/actions/execute.rs | 46 ++++ crates/ffi/src/actions/mod.rs | 4 + crates/ffi/src/actions/resolve.rs | 58 +++++ crates/ffi/src/actions/result.rs | 109 ++++++++ crates/ffi/src/convert/mod.rs | 2 +- 7 files changed, 395 insertions(+), 377 deletions(-) delete mode 100644 crates/ffi/src/actions.rs create mode 100644 crates/ffi/src/actions/conversion.rs create mode 100644 crates/ffi/src/actions/execute.rs create mode 100644 crates/ffi/src/actions/mod.rs create mode 100644 crates/ffi/src/actions/resolve.rs create mode 100644 crates/ffi/src/actions/result.rs diff --git a/crates/ffi/src/actions.rs b/crates/ffi/src/actions.rs deleted file mode 100644 index fc823aa..0000000 --- a/crates/ffi/src/actions.rs +++ /dev/null @@ -1,376 +0,0 @@ -use crate::convert::{c_to_str, free_c_string, opt_string_to_c, string_to_c}; -use crate::error::{self, AdResult}; -use crate::types::{ - AdAction, AdActionKind, AdActionResult, AdDirection, AdElementState, AdKeyCombo, AdModifier, - AdNativeHandle, AdRefEntry, -}; -use crate::AdAdapter; -use agent_desktop_core::action::{ - Action, ActionResult as CoreActionResult, Direction, DragParams as CoreDragParams, - KeyCombo as CoreKeyCombo, Modifier, Point as CorePoint, -}; -use agent_desktop_core::adapter::NativeHandle; -use agent_desktop_core::refs::RefEntry as CoreRefEntry; -use std::ptr; - -pub(crate) fn direction_from_c(d: AdDirection) -> Direction { - match d { - AdDirection::Up => Direction::Up, - AdDirection::Down => Direction::Down, - AdDirection::Left => Direction::Left, - AdDirection::Right => Direction::Right, - } -} - -pub(crate) unsafe fn key_combo_from_c(k: &AdKeyCombo) -> Result { - let key = c_to_str(k.key) - .ok_or("key is null or invalid UTF-8")? - .to_owned(); - let mut modifiers = Vec::new(); - if !k.modifiers.is_null() && k.modifier_count > 0 { - let slice = std::slice::from_raw_parts(k.modifiers, k.modifier_count as usize); - for m in slice { - let modifier = match m { - AdModifier::Cmd => Modifier::Cmd, - AdModifier::Ctrl => Modifier::Ctrl, - AdModifier::Alt => Modifier::Alt, - AdModifier::Shift => Modifier::Shift, - }; - modifiers.push(modifier); - } - } - Ok(CoreKeyCombo { key, modifiers }) -} - -pub(crate) unsafe fn action_from_c(action: &AdAction) -> Result { - match action.kind { - AdActionKind::Click => Ok(Action::Click), - AdActionKind::DoubleClick => Ok(Action::DoubleClick), - AdActionKind::RightClick => Ok(Action::RightClick), - AdActionKind::TripleClick => Ok(Action::TripleClick), - AdActionKind::SetFocus => Ok(Action::SetFocus), - AdActionKind::Expand => Ok(Action::Expand), - AdActionKind::Collapse => Ok(Action::Collapse), - AdActionKind::Toggle => Ok(Action::Toggle), - AdActionKind::Check => Ok(Action::Check), - AdActionKind::Uncheck => Ok(Action::Uncheck), - AdActionKind::ScrollTo => Ok(Action::ScrollTo), - AdActionKind::Clear => Ok(Action::Clear), - AdActionKind::Hover => Ok(Action::Hover), - AdActionKind::SetValue => { - let text = c_to_str(action.text).ok_or("text is null or invalid UTF-8")?; - Ok(Action::SetValue(text.to_owned())) - } - AdActionKind::Select => { - let text = c_to_str(action.text).ok_or("text is null or invalid UTF-8")?; - Ok(Action::Select(text.to_owned())) - } - AdActionKind::TypeText => { - let text = c_to_str(action.text).ok_or("text is null or invalid UTF-8")?; - Ok(Action::TypeText(text.to_owned())) - } - AdActionKind::Scroll => { - let dir = direction_from_c(action.scroll.direction); - Ok(Action::Scroll(dir, action.scroll.amount)) - } - AdActionKind::PressKey => { - let combo = key_combo_from_c(&action.key)?; - Ok(Action::PressKey(combo)) - } - AdActionKind::KeyDown => { - let combo = key_combo_from_c(&action.key)?; - Ok(Action::KeyDown(combo)) - } - AdActionKind::KeyUp => { - let combo = key_combo_from_c(&action.key)?; - Ok(Action::KeyUp(combo)) - } - AdActionKind::Drag => { - let params = CoreDragParams { - from: CorePoint { - x: action.drag.from.x, - y: action.drag.from.y, - }, - to: CorePoint { - x: action.drag.to.x, - y: action.drag.to.y, - }, - duration_ms: if action.drag.duration_ms == 0 { - None - } else { - Some(action.drag.duration_ms) - }, - }; - Ok(Action::Drag(params)) - } - } -} - -pub(crate) fn action_result_to_c(r: &CoreActionResult) -> AdActionResult { - let action = string_to_c(&r.action); - let ref_id = opt_string_to_c(r.ref_id.as_deref()); - let post_state = match &r.post_state { - None => ptr::null_mut(), - Some(state) => { - let role = string_to_c(&state.role); - let value = opt_string_to_c(state.value.as_deref()); - let state_count = state.states.len() as u32; - let states = if state.states.is_empty() { - ptr::null_mut() - } else { - let ptrs: Vec<*mut std::os::raw::c_char> = - state.states.iter().map(|s| string_to_c(s)).collect(); - let mut boxed = ptrs.into_boxed_slice(); - let raw = boxed.as_mut_ptr(); - std::mem::forget(boxed); - raw - }; - let elem = Box::new(AdElementState { - role, - states, - state_count, - value, - }); - Box::into_raw(elem) - } - }; - AdActionResult { - action, - ref_id, - post_state, - } -} - -/// # Safety -/// -/// `adapter` must be a non-null pointer returned by `ad_adapter_create`. -/// `entry` must be a non-null pointer to a valid `AdRefEntry`. -/// `out` must be a non-null pointer to an `AdNativeHandle` to write the result into. -#[no_mangle] -pub unsafe extern "C" fn ad_resolve_element( - adapter: *const AdAdapter, - entry: *const AdRefEntry, - out: *mut AdNativeHandle, -) -> AdResult { - let adapter = &*adapter; - let entry = &*entry; - let role = match c_to_str(entry.role) { - Some(s) => s.to_owned(), - None => { - error::set_last_error(&agent_desktop_core::error::AdapterError::new( - agent_desktop_core::error::ErrorCode::InvalidArgs, - "role is null or invalid UTF-8", - )); - return AdResult::ErrInvalidArgs; - } - }; - let name = c_to_str(entry.name).map(|s| s.to_owned()); - let bounds_hash = if entry.has_bounds_hash { - Some(entry.bounds_hash) - } else { - None - }; - let core_entry = CoreRefEntry { - pid: entry.pid, - role, - name, - value: None, - states: vec![], - bounds: None, - bounds_hash, - available_actions: vec![], - source_app: None, - }; - match adapter.inner.resolve_element(&core_entry) { - Ok(handle) => { - (*out).ptr = handle.as_raw(); - error::clear_last_error(); - AdResult::Ok - } - Err(e) => { - error::set_last_error(&e); - error::last_error_code() - } - } -} - -/// # Safety -/// -/// `adapter` must be a non-null pointer returned by `ad_adapter_create`. -/// `handle` must be a non-null pointer to a valid `AdNativeHandle`. -/// `action` must be a non-null pointer to a valid `AdAction`. -/// `out` must be a non-null pointer to an `AdActionResult` to write the result into. -#[no_mangle] -pub unsafe extern "C" fn ad_execute_action( - adapter: *const AdAdapter, - handle: *const AdNativeHandle, - action: *const AdAction, - out: *mut AdActionResult, -) -> AdResult { - let adapter = &*adapter; - let handle_ref = &*handle; - let action_ref = &*action; - let core_action = match action_from_c(action_ref) { - Ok(a) => a, - Err(msg) => { - error::set_last_error(&agent_desktop_core::error::AdapterError::new( - agent_desktop_core::error::ErrorCode::InvalidArgs, - msg, - )); - return AdResult::ErrInvalidArgs; - } - }; - let native_handle = NativeHandle::from_ptr(handle_ref.ptr); - match adapter.inner.execute_action(&native_handle, core_action) { - Ok(result) => { - *out = action_result_to_c(&result); - error::clear_last_error(); - AdResult::Ok - } - Err(e) => { - error::set_last_error(&e); - error::last_error_code() - } - } -} - -/// # Safety -/// -/// `result` must be a pointer to an `AdActionResult` previously written by `ad_execute_action`, -/// or null. After this call all pointers inside the struct are invalid. -#[no_mangle] -pub unsafe extern "C" fn ad_free_action_result(result: *mut AdActionResult) { - if result.is_null() { - return; - } - let r = &mut *result; - free_c_string(r.action as *mut _); - free_c_string(r.ref_id as *mut _); - if !r.post_state.is_null() { - let state = &mut *r.post_state; - free_c_string(state.role as *mut _); - free_c_string(state.value as *mut _); - if !state.states.is_null() && state.state_count > 0 { - let slice = std::slice::from_raw_parts_mut(state.states, state.state_count as usize); - for ptr in slice.iter() { - free_c_string(*ptr); - } - drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut( - state.states, - state.state_count as usize, - ))); - } - drop(Box::from_raw(r.post_state)); - r.post_state = ptr::null_mut(); - } - r.action = ptr::null(); - r.ref_id = ptr::null(); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::convert::string_to_c; - use crate::types::{AdDragParams, AdPoint, AdScrollParams}; - use agent_desktop_core::action::ElementState; - - fn make_scroll_params() -> AdScrollParams { - AdScrollParams { - direction: AdDirection::Down, - amount: 3, - } - } - - fn make_key_combo() -> AdKeyCombo { - AdKeyCombo { - key: ptr::null(), - modifiers: ptr::null(), - modifier_count: 0, - } - } - - fn make_drag_params() -> AdDragParams { - AdDragParams { - from: AdPoint { x: 0.0, y: 0.0 }, - to: AdPoint { x: 0.0, y: 0.0 }, - duration_ms: 0, - } - } - - #[test] - fn test_simple_action_roundtrip() { - let action = AdAction { - kind: AdActionKind::Click, - text: ptr::null(), - scroll: make_scroll_params(), - key: make_key_combo(), - drag: make_drag_params(), - }; - let result = unsafe { action_from_c(&action) }; - assert!(result.is_ok()); - assert!(matches!(result.unwrap(), Action::Click)); - } - - #[test] - fn test_action_result_to_c_with_state() { - let core_result = CoreActionResult { - action: "click".to_owned(), - ref_id: Some("@e3".to_owned()), - post_state: Some(ElementState { - role: "button".to_owned(), - states: vec!["focused".to_owned(), "enabled".to_owned()], - value: Some("OK".to_owned()), - }), - }; - let c_result = action_result_to_c(&core_result); - unsafe { - assert_eq!(c_to_str(c_result.action), Some("click")); - assert_eq!(c_to_str(c_result.ref_id), Some("@e3")); - assert!(!c_result.post_state.is_null()); - let state = &*c_result.post_state; - assert_eq!(c_to_str(state.role), Some("button")); - assert_eq!(c_to_str(state.value), Some("OK")); - assert_eq!(state.state_count, 2); - } - let mut c_result = c_result; - unsafe { ad_free_action_result(&mut c_result) }; - } - - #[test] - fn test_free_null_action_result() { - unsafe { ad_free_action_result(ptr::null_mut()) }; - } - - #[test] - fn test_set_value_action() { - let text = string_to_c("hello world"); - let action = AdAction { - kind: AdActionKind::SetValue, - text, - scroll: make_scroll_params(), - key: make_key_combo(), - drag: make_drag_params(), - }; - let result = unsafe { action_from_c(&action) }; - assert!(result.is_ok()); - assert!(matches!(result.unwrap(), Action::SetValue(ref s) if s == "hello world")); - unsafe { free_c_string(text as *mut _) }; - } - - #[test] - fn test_scroll_action() { - let action = AdAction { - kind: AdActionKind::Scroll, - text: ptr::null(), - scroll: AdScrollParams { - direction: AdDirection::Up, - amount: 5, - }, - key: make_key_combo(), - drag: make_drag_params(), - }; - let result = unsafe { action_from_c(&action) }; - assert!(result.is_ok()); - assert!(matches!(result.unwrap(), Action::Scroll(Direction::Up, 5))); - } -} diff --git a/crates/ffi/src/actions/conversion.rs b/crates/ffi/src/actions/conversion.rs new file mode 100644 index 0000000..6115f4f --- /dev/null +++ b/crates/ffi/src/actions/conversion.rs @@ -0,0 +1,177 @@ +use crate::convert::string::c_to_str; +use crate::types::{AdAction, AdActionKind, AdDirection, AdKeyCombo, AdModifier}; +use agent_desktop_core::action::{ + Action, Direction, DragParams as CoreDragParams, KeyCombo as CoreKeyCombo, Modifier, + Point as CorePoint, +}; + +pub(crate) fn direction_from_c(d: AdDirection) -> Direction { + match d { + AdDirection::Up => Direction::Up, + AdDirection::Down => Direction::Down, + AdDirection::Left => Direction::Left, + AdDirection::Right => Direction::Right, + } +} + +pub(crate) unsafe fn key_combo_from_c(k: &AdKeyCombo) -> Result { + let key = c_to_str(k.key) + .ok_or("key is null or invalid UTF-8")? + .to_owned(); + let mut modifiers = Vec::new(); + if !k.modifiers.is_null() && k.modifier_count > 0 { + let slice = std::slice::from_raw_parts(k.modifiers, k.modifier_count as usize); + for m in slice { + let modifier = match m { + AdModifier::Cmd => Modifier::Cmd, + AdModifier::Ctrl => Modifier::Ctrl, + AdModifier::Alt => Modifier::Alt, + AdModifier::Shift => Modifier::Shift, + }; + modifiers.push(modifier); + } + } + Ok(CoreKeyCombo { key, modifiers }) +} + +pub(crate) unsafe fn action_from_c(action: &AdAction) -> Result { + match action.kind { + AdActionKind::Click => Ok(Action::Click), + AdActionKind::DoubleClick => Ok(Action::DoubleClick), + AdActionKind::RightClick => Ok(Action::RightClick), + AdActionKind::TripleClick => Ok(Action::TripleClick), + AdActionKind::SetFocus => Ok(Action::SetFocus), + AdActionKind::Expand => Ok(Action::Expand), + AdActionKind::Collapse => Ok(Action::Collapse), + AdActionKind::Toggle => Ok(Action::Toggle), + AdActionKind::Check => Ok(Action::Check), + AdActionKind::Uncheck => Ok(Action::Uncheck), + AdActionKind::ScrollTo => Ok(Action::ScrollTo), + AdActionKind::Clear => Ok(Action::Clear), + AdActionKind::Hover => Ok(Action::Hover), + AdActionKind::SetValue => { + let text = c_to_str(action.text).ok_or("text is null or invalid UTF-8")?; + Ok(Action::SetValue(text.to_owned())) + } + AdActionKind::Select => { + let text = c_to_str(action.text).ok_or("text is null or invalid UTF-8")?; + Ok(Action::Select(text.to_owned())) + } + AdActionKind::TypeText => { + let text = c_to_str(action.text).ok_or("text is null or invalid UTF-8")?; + Ok(Action::TypeText(text.to_owned())) + } + AdActionKind::Scroll => { + let dir = direction_from_c(action.scroll.direction); + Ok(Action::Scroll(dir, action.scroll.amount)) + } + AdActionKind::PressKey => { + let combo = key_combo_from_c(&action.key)?; + Ok(Action::PressKey(combo)) + } + AdActionKind::KeyDown => { + let combo = key_combo_from_c(&action.key)?; + Ok(Action::KeyDown(combo)) + } + AdActionKind::KeyUp => { + let combo = key_combo_from_c(&action.key)?; + Ok(Action::KeyUp(combo)) + } + AdActionKind::Drag => { + let params = CoreDragParams { + from: CorePoint { + x: action.drag.from.x, + y: action.drag.from.y, + }, + to: CorePoint { + x: action.drag.to.x, + y: action.drag.to.y, + }, + duration_ms: if action.drag.duration_ms == 0 { + None + } else { + Some(action.drag.duration_ms) + }, + }; + Ok(Action::Drag(params)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::convert::string::{free_c_string, string_to_c}; + use crate::types::{AdDragParams, AdPoint, AdScrollParams}; + use std::ptr; + + fn make_scroll_params() -> AdScrollParams { + AdScrollParams { + direction: AdDirection::Down, + amount: 3, + } + } + + fn make_key_combo() -> AdKeyCombo { + AdKeyCombo { + key: ptr::null(), + modifiers: ptr::null(), + modifier_count: 0, + } + } + + fn make_drag_params() -> AdDragParams { + AdDragParams { + from: AdPoint { x: 0.0, y: 0.0 }, + to: AdPoint { x: 0.0, y: 0.0 }, + duration_ms: 0, + } + } + + #[test] + fn test_simple_action_roundtrip() { + let action = AdAction { + kind: AdActionKind::Click, + text: ptr::null(), + scroll: make_scroll_params(), + key: make_key_combo(), + drag: make_drag_params(), + }; + let result = unsafe { action_from_c(&action) }; + assert!(result.is_ok()); + assert!(matches!(result.unwrap(), Action::Click)); + } + + #[test] + fn test_set_value_action() { + let text = string_to_c("hello world"); + let action = AdAction { + kind: AdActionKind::SetValue, + text, + scroll: make_scroll_params(), + key: make_key_combo(), + drag: make_drag_params(), + }; + let result = unsafe { action_from_c(&action) }; + assert!(result.is_ok()); + assert!(matches!(result.unwrap(), Action::SetValue(ref s) if s == "hello world")); + unsafe { free_c_string(text as *mut _) }; + } + + #[test] + fn test_scroll_action() { + let action = AdAction { + kind: AdActionKind::Scroll, + text: ptr::null(), + scroll: AdScrollParams { + direction: AdDirection::Up, + amount: 5, + }, + key: make_key_combo(), + drag: make_drag_params(), + }; + let result = unsafe { action_from_c(&action) }; + assert!(result.is_ok()); + assert!(matches!(result.unwrap(), Action::Scroll(Direction::Up, 5))); + } +} diff --git a/crates/ffi/src/actions/execute.rs b/crates/ffi/src/actions/execute.rs new file mode 100644 index 0000000..48edaef --- /dev/null +++ b/crates/ffi/src/actions/execute.rs @@ -0,0 +1,46 @@ +use crate::actions::conversion::action_from_c; +use crate::actions::result::action_result_to_c; +use crate::error::{self, AdResult}; +use crate::types::{AdAction, AdActionResult, AdNativeHandle}; +use crate::AdAdapter; +use agent_desktop_core::adapter::NativeHandle; + +/// # Safety +/// +/// `adapter` must be a non-null pointer returned by `ad_adapter_create`. +/// `handle` must be a non-null pointer to a valid `AdNativeHandle`. +/// `action` must be a non-null pointer to a valid `AdAction`. +/// `out` must be a non-null pointer to an `AdActionResult` to write the result into. +#[no_mangle] +pub unsafe extern "C" fn ad_execute_action( + adapter: *const AdAdapter, + handle: *const AdNativeHandle, + action: *const AdAction, + out: *mut AdActionResult, +) -> AdResult { + let adapter = &*adapter; + let handle_ref = &*handle; + let action_ref = &*action; + let core_action = match action_from_c(action_ref) { + Ok(a) => a, + Err(msg) => { + error::set_last_error(&agent_desktop_core::error::AdapterError::new( + agent_desktop_core::error::ErrorCode::InvalidArgs, + msg, + )); + return AdResult::ErrInvalidArgs; + } + }; + let native_handle = NativeHandle::from_ptr(handle_ref.ptr); + match adapter.inner.execute_action(&native_handle, core_action) { + Ok(result) => { + *out = action_result_to_c(&result); + error::clear_last_error(); + AdResult::Ok + } + Err(e) => { + error::set_last_error(&e); + error::last_error_code() + } + } +} diff --git a/crates/ffi/src/actions/mod.rs b/crates/ffi/src/actions/mod.rs new file mode 100644 index 0000000..febccf0 --- /dev/null +++ b/crates/ffi/src/actions/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod conversion; +pub(crate) mod execute; +pub(crate) mod resolve; +pub(crate) mod result; diff --git a/crates/ffi/src/actions/resolve.rs b/crates/ffi/src/actions/resolve.rs new file mode 100644 index 0000000..f4a3166 --- /dev/null +++ b/crates/ffi/src/actions/resolve.rs @@ -0,0 +1,58 @@ +use crate::convert::string::c_to_str; +use crate::error::{self, AdResult}; +use crate::types::{AdNativeHandle, AdRefEntry}; +use crate::AdAdapter; +use agent_desktop_core::refs::RefEntry as CoreRefEntry; + +/// # Safety +/// +/// `adapter` must be a non-null pointer returned by `ad_adapter_create`. +/// `entry` must be a non-null pointer to a valid `AdRefEntry`. +/// `out` must be a non-null pointer to an `AdNativeHandle` to write the result into. +#[no_mangle] +pub unsafe extern "C" fn ad_resolve_element( + adapter: *const AdAdapter, + entry: *const AdRefEntry, + out: *mut AdNativeHandle, +) -> AdResult { + let adapter = &*adapter; + let entry = &*entry; + let role = match c_to_str(entry.role) { + Some(s) => s.to_owned(), + None => { + error::set_last_error(&agent_desktop_core::error::AdapterError::new( + agent_desktop_core::error::ErrorCode::InvalidArgs, + "role is null or invalid UTF-8", + )); + return AdResult::ErrInvalidArgs; + } + }; + let name = c_to_str(entry.name).map(|s| s.to_owned()); + let bounds_hash = if entry.has_bounds_hash { + Some(entry.bounds_hash) + } else { + None + }; + let core_entry = CoreRefEntry { + pid: entry.pid, + role, + name, + value: None, + states: vec![], + bounds: None, + bounds_hash, + available_actions: vec![], + source_app: None, + }; + match adapter.inner.resolve_element(&core_entry) { + Ok(handle) => { + (*out).ptr = handle.as_raw(); + error::clear_last_error(); + AdResult::Ok + } + Err(e) => { + error::set_last_error(&e); + error::last_error_code() + } + } +} diff --git a/crates/ffi/src/actions/result.rs b/crates/ffi/src/actions/result.rs new file mode 100644 index 0000000..3b5a959 --- /dev/null +++ b/crates/ffi/src/actions/result.rs @@ -0,0 +1,109 @@ +use crate::convert::string::{free_c_string, opt_string_to_c, string_to_c}; +use crate::types::{AdActionResult, AdElementState}; +use agent_desktop_core::action::ActionResult as CoreActionResult; +use std::ptr; + +pub(crate) fn action_result_to_c(r: &CoreActionResult) -> AdActionResult { + let action = string_to_c(&r.action); + let ref_id = opt_string_to_c(r.ref_id.as_deref()); + let post_state = match &r.post_state { + None => ptr::null_mut(), + Some(state) => { + let role = string_to_c(&state.role); + let value = opt_string_to_c(state.value.as_deref()); + let state_count = state.states.len() as u32; + let states = if state.states.is_empty() { + ptr::null_mut() + } else { + let ptrs: Vec<*mut std::os::raw::c_char> = + state.states.iter().map(|s| string_to_c(s)).collect(); + let mut boxed = ptrs.into_boxed_slice(); + let raw = boxed.as_mut_ptr(); + std::mem::forget(boxed); + raw + }; + let elem = Box::new(AdElementState { + role, + states, + state_count, + value, + }); + Box::into_raw(elem) + } + }; + AdActionResult { + action, + ref_id, + post_state, + } +} + +/// # Safety +/// +/// `result` must be a pointer to an `AdActionResult` previously written by `ad_execute_action`, +/// or null. After this call all pointers inside the struct are invalid. +#[no_mangle] +pub unsafe extern "C" fn ad_free_action_result(result: *mut AdActionResult) { + if result.is_null() { + return; + } + let r = &mut *result; + free_c_string(r.action as *mut _); + free_c_string(r.ref_id as *mut _); + if !r.post_state.is_null() { + let state = &mut *r.post_state; + free_c_string(state.role as *mut _); + free_c_string(state.value as *mut _); + if !state.states.is_null() && state.state_count > 0 { + let slice = std::slice::from_raw_parts_mut(state.states, state.state_count as usize); + for ptr in slice.iter() { + free_c_string(*ptr); + } + drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut( + state.states, + state.state_count as usize, + ))); + } + drop(Box::from_raw(r.post_state)); + r.post_state = ptr::null_mut(); + } + r.action = ptr::null(); + r.ref_id = ptr::null(); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::convert::string::c_to_str; + use agent_desktop_core::action::ElementState; + + #[test] + fn test_action_result_to_c_with_state() { + let core_result = CoreActionResult { + action: "click".to_owned(), + ref_id: Some("@e3".to_owned()), + post_state: Some(ElementState { + role: "button".to_owned(), + states: vec!["focused".to_owned(), "enabled".to_owned()], + value: Some("OK".to_owned()), + }), + }; + let c_result = action_result_to_c(&core_result); + unsafe { + assert_eq!(c_to_str(c_result.action), Some("click")); + assert_eq!(c_to_str(c_result.ref_id), Some("@e3")); + assert!(!c_result.post_state.is_null()); + let state = &*c_result.post_state; + assert_eq!(c_to_str(state.role), Some("button")); + assert_eq!(c_to_str(state.value), Some("OK")); + assert_eq!(state.state_count, 2); + } + let mut c_result = c_result; + unsafe { ad_free_action_result(&mut c_result) }; + } + + #[test] + fn test_free_null_action_result() { + unsafe { ad_free_action_result(ptr::null_mut()) }; + } +} diff --git a/crates/ffi/src/convert/mod.rs b/crates/ffi/src/convert/mod.rs index 6216a8c..70b8f93 100644 --- a/crates/ffi/src/convert/mod.rs +++ b/crates/ffi/src/convert/mod.rs @@ -6,6 +6,6 @@ pub(crate) mod window; pub(crate) use app::{app_info_to_c, free_app_info_fields}; pub(crate) use rect::rect_to_c; -pub(crate) use string::{c_to_str, free_c_string, opt_string_to_c, string_to_c}; +pub(crate) use string::{c_to_str, free_c_string, string_to_c}; pub(crate) use surface::{free_surface_info_fields, surface_info_to_c}; pub(crate) use window::{free_window_info_fields, window_info_to_c};