mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-08-05 05:30:21 +00:00
feat(ffi): ABI surface completion — AdSnapshotSurface, focused_only, release_handle (Unit 6)
Lands the last set of ABI-shape changes before the header is published. Any addition after publication would be a breaking change for every consumer linked against the old layout. Core / platform additions: - crates/core/src/adapter.rs: new `PlatformAdapter::release_handle` trait method with `not_supported()` default. macOS implementations must `CFRelease` the underlying AXUIElementRef to balance the CFRetain that happened during resolve. Windows/Linux inherit the default unchanged. - crates/macos/src/adapter.rs: macOS impl calls `core_foundation::base::CFRelease(raw)` when the raw pointer is non-null. FFI surface additions: - crates/ffi/src/types/snapshot_surface.rs: new AdSnapshotSurface enum (Window=0, Focused, Menu, Menubar, Sheet, Popover, Alert) mirroring the 7-variant core SnapshotSurface. - crates/ffi/src/types/tree_options.rs: new `surface: AdSnapshotSurface` field. Previous hard-coded SnapshotSurface::Window is replaced by consumer-selected surface with enum-validated read at entry. - crates/ffi/src/windows/list.rs: ad_list_windows gains `focused_only: bool` between app_filter and out — maps directly to WindowFilter::focused_only. Consumers can now call `ad_focused_window`- style queries without a second ABI round. - crates/ffi/src/actions/native_handle.rs: new `ad_free_handle(adapter, handle)` exported. Null-tolerant on both sides; Windows/Linux `ActionNotSupported` is translated to `Ok` so C consumers can call the release path uniformly across platforms. Validation: - AdSnapshotSurface added to enum_validation.rs — ad_get_tree now rejects invalid surface discriminants with `AD_RESULT_ERR_INVALID_ARGS` rather than matching on UB. Closes R5, R9, R10 from PR #22 review. 52 lib tests pass, 1 integration test passes. Clippy clean.
This commit is contained in:
parent
2c13fbefaa
commit
00d2474498
13 changed files with 164 additions and 8 deletions
|
|
@ -141,6 +141,15 @@ pub trait PlatformAdapter: Send + Sync {
|
|||
Err(AdapterError::not_supported("resolve_element"))
|
||||
}
|
||||
|
||||
/// Releases a platform-specific element handle returned from
|
||||
/// `resolve_element`. macOS implementations must `CFRelease` the
|
||||
/// underlying `AXUIElementRef` to balance the `CFRetain` that
|
||||
/// happened during resolve. Windows/Linux consumers can leave this
|
||||
/// as the default `not_supported` no-op.
|
||||
fn release_handle(&self, _handle: &NativeHandle) -> Result<(), AdapterError> {
|
||||
Err(AdapterError::not_supported("release_handle"))
|
||||
}
|
||||
|
||||
fn check_permissions(&self) -> PermissionStatus {
|
||||
PermissionStatus::Denied {
|
||||
suggestion: "Platform adapter not available".into(),
|
||||
|
|
|
|||
|
|
@ -46,10 +46,7 @@ fn main() {
|
|||
|
||||
let include_dir = Path::new(&crate_dir).join("include");
|
||||
if let Err(err) = std::fs::create_dir_all(&include_dir) {
|
||||
panic!(
|
||||
"failed to create include dir at {:?}: {}",
|
||||
include_dir, err
|
||||
);
|
||||
panic!("failed to create include dir at {:?}: {}", include_dir, err);
|
||||
}
|
||||
|
||||
let committed_header = include_dir.join("agent_desktop.h");
|
||||
|
|
|
|||
|
|
@ -91,6 +91,17 @@ enum AdScreenshotKind {
|
|||
};
|
||||
typedef int32_t AdScreenshotKind;
|
||||
|
||||
enum AdSnapshotSurface {
|
||||
AD_SNAPSHOT_SURFACE_WINDOW = 0,
|
||||
AD_SNAPSHOT_SURFACE_FOCUSED = 1,
|
||||
AD_SNAPSHOT_SURFACE_MENU = 2,
|
||||
AD_SNAPSHOT_SURFACE_MENUBAR = 3,
|
||||
AD_SNAPSHOT_SURFACE_SHEET = 4,
|
||||
AD_SNAPSHOT_SURFACE_POPOVER = 5,
|
||||
AD_SNAPSHOT_SURFACE_ALERT = 6,
|
||||
};
|
||||
typedef int32_t AdSnapshotSurface;
|
||||
|
||||
enum AdWindowOpKind {
|
||||
AD_WINDOW_OP_KIND_RESIZE = 0,
|
||||
AD_WINDOW_OP_KIND_MOVE = 1,
|
||||
|
|
@ -233,6 +244,7 @@ typedef struct AdTreeOptions {
|
|||
bool include_bounds;
|
||||
bool interactive_only;
|
||||
bool compact;
|
||||
AdSnapshotSurface surface;
|
||||
} AdTreeOptions;
|
||||
|
||||
typedef struct AdWindowOp {
|
||||
|
|
@ -256,6 +268,24 @@ AdResult ad_execute_action(const struct AdAdapter *adapter,
|
|||
const struct AdAction *action,
|
||||
struct AdActionResult *out);
|
||||
|
||||
/**
|
||||
* Releases a handle previously returned by `ad_resolve_element`.
|
||||
*
|
||||
* On macOS this calls `CFRelease` on the underlying `AXUIElementRef`,
|
||||
* balancing the `CFRetain` that happened during `ad_resolve_element`.
|
||||
* On Windows/Linux the call is a no-op that returns `AD_RESULT_OK`
|
||||
* (platform adapters inherit the default `not_supported` impl, which
|
||||
* the FFI surface rewrites to `Ok` here so callers can apply the same
|
||||
* release pattern everywhere).
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* `adapter` must be a non-null pointer returned by `ad_adapter_create`.
|
||||
* `handle` must be null or a pointer previously populated by
|
||||
* `ad_resolve_element`. Double-free is undefined behavior.
|
||||
*/
|
||||
AdResult ad_free_handle(const struct AdAdapter *adapter, const struct AdNativeHandle *handle);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
*
|
||||
|
|
@ -457,6 +487,7 @@ void ad_free_window(struct AdWindowInfo *win);
|
|||
*/
|
||||
AdResult ad_list_windows(const struct AdAdapter *adapter,
|
||||
const char *app_filter,
|
||||
bool focused_only,
|
||||
struct AdWindowInfo **out,
|
||||
uint32_t *out_count);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub(crate) mod conversion;
|
||||
pub(crate) mod execute;
|
||||
pub(crate) mod native_handle;
|
||||
pub(crate) mod resolve;
|
||||
pub(crate) mod result;
|
||||
|
|
|
|||
58
crates/ffi/src/actions/native_handle.rs
Normal file
58
crates/ffi/src/actions/native_handle.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use crate::error::{set_last_error, AdResult};
|
||||
use crate::ffi_try::trap_panic;
|
||||
use crate::types::AdNativeHandle;
|
||||
use crate::AdAdapter;
|
||||
use agent_desktop_core::adapter::NativeHandle;
|
||||
|
||||
/// Releases a handle previously returned by `ad_resolve_element`.
|
||||
///
|
||||
/// On macOS this calls `CFRelease` on the underlying `AXUIElementRef`,
|
||||
/// balancing the `CFRetain` that happened during `ad_resolve_element`.
|
||||
/// On Windows/Linux the call is a no-op that returns `AD_RESULT_OK`
|
||||
/// (platform adapters inherit the default `not_supported` impl, which
|
||||
/// the FFI surface rewrites to `Ok` here so callers can apply the same
|
||||
/// release pattern everywhere).
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `adapter` must be a non-null pointer returned by `ad_adapter_create`.
|
||||
/// `handle` must be null or a pointer previously populated by
|
||||
/// `ad_resolve_element`. Double-free is undefined behavior.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn ad_free_handle(
|
||||
adapter: *const AdAdapter,
|
||||
handle: *const AdNativeHandle,
|
||||
) -> AdResult {
|
||||
trap_panic(|| unsafe {
|
||||
if adapter.is_null() {
|
||||
set_last_error(&agent_desktop_core::error::AdapterError::new(
|
||||
agent_desktop_core::error::ErrorCode::InvalidArgs,
|
||||
"adapter is null",
|
||||
));
|
||||
return AdResult::ErrInvalidArgs;
|
||||
}
|
||||
if handle.is_null() {
|
||||
return AdResult::Ok;
|
||||
}
|
||||
let adapter = &*adapter;
|
||||
let raw = (*handle).ptr;
|
||||
if raw.is_null() {
|
||||
return AdResult::Ok;
|
||||
}
|
||||
let native = NativeHandle::from_ptr(raw);
|
||||
match adapter.inner.release_handle(&native) {
|
||||
Ok(()) => AdResult::Ok,
|
||||
Err(e) => {
|
||||
// Not-supported on Windows/Linux is a no-op by contract.
|
||||
if matches!(
|
||||
e.code,
|
||||
agent_desktop_core::error::ErrorCode::ActionNotSupported
|
||||
) {
|
||||
return AdResult::Ok;
|
||||
}
|
||||
set_last_error(&e);
|
||||
crate::error::last_error_code()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
use crate::types::{
|
||||
AdActionKind, AdDirection, AdImageFormat, AdModifier, AdMouseButton, AdMouseEventKind,
|
||||
AdScreenshotKind, AdWindowOpKind,
|
||||
AdScreenshotKind, AdSnapshotSurface, AdWindowOpKind,
|
||||
};
|
||||
|
||||
/// Reads the raw `i32` discriminant out of an `#[repr(i32)]` enum field
|
||||
|
|
@ -100,6 +100,13 @@ try_from_c_enum! {
|
|||
}
|
||||
}
|
||||
|
||||
try_from_c_enum! {
|
||||
AdSnapshotSurface {
|
||||
Window = 0, Focused = 1, Menu = 2, Menubar = 3,
|
||||
Sheet = 4, Popover = 5, Alert = 6,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ pub use types::ref_entry::AdRefEntry;
|
|||
pub use types::screenshot_kind::AdScreenshotKind;
|
||||
pub use types::screenshot_target::AdScreenshotTarget;
|
||||
pub use types::scroll_params::AdScrollParams;
|
||||
pub use types::snapshot_surface::AdSnapshotSurface;
|
||||
pub use types::surface_info::AdSurfaceInfo;
|
||||
pub use types::tree_options::AdTreeOptions;
|
||||
pub use types::window_info::AdWindowInfo;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,24 @@
|
|||
use crate::enum_validation::enum_raw_i32;
|
||||
use crate::error::{set_last_error, AdResult};
|
||||
use crate::ffi_try::trap_panic;
|
||||
use crate::tree::flatten::flatten_tree;
|
||||
use crate::types::{AdNodeTree, AdTreeOptions, AdWindowInfo};
|
||||
use crate::types::{AdNodeTree, AdSnapshotSurface, AdTreeOptions, AdWindowInfo};
|
||||
use crate::AdAdapter;
|
||||
use agent_desktop_core::adapter::SnapshotSurface;
|
||||
use std::ptr;
|
||||
|
||||
fn core_surface(s: AdSnapshotSurface) -> SnapshotSurface {
|
||||
match s {
|
||||
AdSnapshotSurface::Window => SnapshotSurface::Window,
|
||||
AdSnapshotSurface::Focused => SnapshotSurface::Focused,
|
||||
AdSnapshotSurface::Menu => SnapshotSurface::Menu,
|
||||
AdSnapshotSurface::Menubar => SnapshotSurface::Menubar,
|
||||
AdSnapshotSurface::Sheet => SnapshotSurface::Sheet,
|
||||
AdSnapshotSurface::Popover => SnapshotSurface::Popover,
|
||||
AdSnapshotSurface::Alert => SnapshotSurface::Alert,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// All pointers must be valid. `out` must be writable.
|
||||
#[no_mangle]
|
||||
|
|
@ -30,12 +44,22 @@ pub unsafe extern "C" fn ad_get_tree(
|
|||
return crate::error::last_error_code();
|
||||
}
|
||||
};
|
||||
let surface = match AdSnapshotSurface::from_c(enum_raw_i32(&opts_ref.surface)) {
|
||||
Some(s) => core_surface(s),
|
||||
None => {
|
||||
set_last_error(&agent_desktop_core::error::AdapterError::new(
|
||||
agent_desktop_core::error::ErrorCode::InvalidArgs,
|
||||
"invalid snapshot surface discriminant",
|
||||
));
|
||||
return AdResult::ErrInvalidArgs;
|
||||
}
|
||||
};
|
||||
let core_opts = agent_desktop_core::adapter::TreeOptions {
|
||||
max_depth: opts_ref.max_depth,
|
||||
include_bounds: opts_ref.include_bounds,
|
||||
interactive_only: opts_ref.interactive_only,
|
||||
compact: opts_ref.compact,
|
||||
surface: agent_desktop_core::adapter::SnapshotSurface::Window,
|
||||
surface,
|
||||
};
|
||||
|
||||
match adapter.inner.get_tree(&core_win, &core_opts) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub mod ref_entry;
|
|||
pub mod screenshot_kind;
|
||||
pub mod screenshot_target;
|
||||
pub mod scroll_params;
|
||||
pub mod snapshot_surface;
|
||||
pub mod surface_info;
|
||||
pub mod tree_options;
|
||||
pub mod window_info;
|
||||
|
|
@ -50,6 +51,7 @@ pub use ref_entry::AdRefEntry;
|
|||
pub use screenshot_kind::AdScreenshotKind;
|
||||
pub use screenshot_target::AdScreenshotTarget;
|
||||
pub use scroll_params::AdScrollParams;
|
||||
pub use snapshot_surface::AdSnapshotSurface;
|
||||
pub use surface_info::AdSurfaceInfo;
|
||||
pub use tree_options::AdTreeOptions;
|
||||
pub use window_info::AdWindowInfo;
|
||||
|
|
|
|||
11
crates/ffi/src/types/snapshot_surface.rs
Normal file
11
crates/ffi/src/types/snapshot_surface.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#[repr(i32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AdSnapshotSurface {
|
||||
Window = 0,
|
||||
Focused = 1,
|
||||
Menu = 2,
|
||||
Menubar = 3,
|
||||
Sheet = 4,
|
||||
Popover = 5,
|
||||
Alert = 6,
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
use crate::types::snapshot_surface::AdSnapshotSurface;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AdTreeOptions {
|
||||
pub max_depth: u8,
|
||||
pub include_bounds: bool,
|
||||
pub interactive_only: bool,
|
||||
pub compact: bool,
|
||||
pub surface: AdSnapshotSurface,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use std::ptr;
|
|||
pub unsafe extern "C" fn ad_list_windows(
|
||||
adapter: *const AdAdapter,
|
||||
app_filter: *const c_char,
|
||||
focused_only: bool,
|
||||
out: *mut *mut AdWindowInfo,
|
||||
out_count: *mut u32,
|
||||
) -> AdResult {
|
||||
|
|
@ -22,7 +23,7 @@ pub unsafe extern "C" fn ad_list_windows(
|
|||
*out_count = 0;
|
||||
let adapter = &*adapter;
|
||||
let filter = WindowFilter {
|
||||
focused_only: false,
|
||||
focused_only,
|
||||
app: c_to_string(app_filter),
|
||||
};
|
||||
match adapter.inner.list_windows(&filter) {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,17 @@ impl PlatformAdapter for MacOSAdapter {
|
|||
crate::tree::resolve::resolve_element_impl(entry)
|
||||
}
|
||||
|
||||
fn release_handle(&self, handle: &NativeHandle) -> Result<(), AdapterError> {
|
||||
let raw = handle.as_raw();
|
||||
if raw.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
unsafe {
|
||||
core_foundation::base::CFRelease(raw as core_foundation::base::CFTypeRef);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_windows(&self, filter: &WindowFilter) -> Result<Vec<WindowInfo>, AdapterError> {
|
||||
list_windows_impl(filter)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue