feat(ffi): opaque list handles + image buffer length encapsulation (Unit 5)

Replaces every `(*mut T, count)` list-returning API with an opaque
handle and encapsulates AdImageBuffer's byte-buffer length. Closes
R8 and R22 from PR #22 review.

## Opaque list handles

Four new one-type-per-file opaque wrappers (no `#[repr(C)]` — cbindgen
auto-emits as `typedef struct AdFoo AdFoo;` forward declarations):

- crates/ffi/src/types/window_list.rs   — AdWindowList
- crates/ffi/src/types/app_list.rs      — AdAppList
- crates/ffi/src/types/surface_list.rs  — AdSurfaceList
- crates/ffi/src/types/notification_list.rs — AdNotificationList (used by Unit 8)

Each list owns its `Box<[AdXxxInfo]>`. Consumers walk through
`_count(list)`, `_get(list, index) -> *const AdXxxInfo` (null on OOB),
and free with `_free(list)` — the free walks the entries, releases
their interior C-strings, and drops the Box.

Rewritten signatures:

| Old                                                        | New                                                       |
|------------------------------------------------------------|-----------------------------------------------------------|
| ad_list_apps(adapter, \*\*apps, \*count)                   | ad_list_apps(adapter, \*\*list)                           |
| ad_list_windows(adapter, filter, focused, \*\*wins, \*count)| ad_list_windows(adapter, filter, focused, \*\*list)       |
| ad_list_surfaces(adapter, pid, \*\*sfs, \*count)           | ad_list_surfaces(adapter, pid, \*\*list)                  |
| ad_free_apps(apps, count)                                  | ad_app_list_free(list)                                    |
| ad_free_windows(wins, count)                               | ad_window_list_free(list)                                 |
| ad_free_surfaces(sfs, count)                               | ad_surface_list_free(list)                                |
| ad_free_window(win)   [for single AdWindowInfo]            | ad_release_window_fields(win)                             |

Count mismatches are impossible by construction — callers never see
the backing pointer or length.

## Image buffer encapsulation

crates/ffi/src/types/image_buffer.rs: dropped `#[repr(C)]`, private
`Box<[u8]>` data field, private width/height/format. Before, a C
caller who mutated `AdImageBuffer.data_len` triggered heap corruption
at free time; now the length is authoritative inside the Rust-owned
struct.

New accessors in crates/ffi/src/screenshot/accessors.rs:
- ad_image_buffer_data    -> *const u8
- ad_image_buffer_size    -> u64 (always matches the allocation)
- ad_image_buffer_width   -> u32
- ad_image_buffer_height  -> u32
- ad_image_buffer_format  -> AdImageFormat

ad_screenshot signature changed: `*mut *mut AdImageBuffer out` instead
of `*mut AdImageBuffer out`. ad_free_image renamed to
ad_image_buffer_free for consistency with the list-handle pattern.

All new files stay under 120 LOC, explicit `pub use` per type, no
wildcard imports, no inline `//` comments. `///` docs on every
public FFI export cover null-tolerance, lifetime, and safety
requirements.

53 tests pass. Clippy clean.
This commit is contained in:
Lahfir 2026-04-16 04:20:55 -07:00
parent 94bfefa9a2
commit 98f64fdeca
19 changed files with 518 additions and 139 deletions

View file

@ -113,6 +113,39 @@ typedef int32_t AdWindowOpKind;
typedef struct AdAdapter AdAdapter;
/**
* Opaque list handle emitted by `ad_list_apps`. See
* [`crate::types::window_list::AdWindowList`] for the pattern.
*/
typedef struct AdAppList AdAppList;
/**
* Opaque image-buffer handle returned by `ad_screenshot`. The backing
* byte buffer and its length live inside the Rust-owned struct a
* consumer cannot accidentally desynchronize the pair and trigger a
* heap-corruption double-free. Walk it through `ad_image_buffer_*`
* accessors and free it with `ad_image_buffer_free`.
*/
typedef struct AdImageBuffer AdImageBuffer;
/**
* Opaque list handle emitted by `ad_list_surfaces`. See
* [`crate::types::window_list::AdWindowList`] for the pattern.
*/
typedef struct AdSurfaceList AdSurfaceList;
/**
* Opaque list handle emitted by `ad_list_windows`.
*
* The struct intentionally has no `#[repr(C)]` so cbindgen emits a
* forward declaration only (`typedef struct AdWindowList AdWindowList;`).
* Consumers cannot read the backing pointer or length and cannot
* construct a count mismatch they walk the list through
* `ad_window_list_count`, `ad_window_list_get`, and free it with
* `ad_window_list_free`.
*/
typedef struct AdWindowList AdWindowList;
typedef struct AdNativeHandle {
const void *ptr;
} AdNativeHandle;
@ -204,14 +237,6 @@ typedef struct AdScreenshotTarget {
int32_t pid;
} AdScreenshotTarget;
typedef struct AdImageBuffer {
const uint8_t *data;
uint64_t data_len;
AdImageFormat format;
uint32_t width;
uint32_t height;
} AdImageBuffer;
typedef struct AdSurfaceInfo {
const char *kind;
const char *title;
@ -341,15 +366,35 @@ AdResult ad_launch_app(const struct AdAdapter *adapter,
/**
* # Safety
* `adapter` must be a valid pointer from `ad_adapter_create`.
* `out` and `out_count` must be valid writable pointers.
* `out` must be a valid writable `*mut *mut AdAppList`.
* On success, `*out` is a newly-allocated opaque list freed with
* `ad_app_list_free`. On error, `*out` is null and last-error is set.
*/
AdResult ad_list_apps(const struct AdAdapter *adapter, struct AdAppInfo **out, uint32_t *out_count);
AdResult ad_list_apps(const struct AdAdapter *adapter, struct AdAppList **out);
/**
* # Safety
* `apps` must be null or a pointer previously returned by `ad_list_apps`.
* `list` must be null or a pointer returned by `ad_list_apps`.
*/
void ad_free_apps(struct AdAppInfo *apps, uint32_t count);
uint32_t ad_app_list_count(const struct AdAppList *list);
/**
* Returns a borrowed pointer into the list; valid until the list is freed.
* Out-of-range `index` returns null.
*
* # Safety
* `list` must be null or a pointer returned by `ad_list_apps`.
*/
const struct AdAppInfo *ad_app_list_get(const struct AdAppList *list, uint32_t index);
/**
* Frees the list and every `AdAppInfo` it owns, including the interior
* C-strings.
*
* # Safety
* `list` must be null or a pointer returned by `ad_list_apps`.
*/
void ad_app_list_free(struct AdAppList *list);
/**
* Last-error lifetime errno-style.
@ -425,33 +470,101 @@ AdResult ad_drag(const struct AdAdapter *adapter, const struct AdDragParams *par
AdResult ad_mouse_event(const struct AdAdapter *adapter, const struct AdMouseEvent *event);
/**
* Borrowed pointer to the image bytes; valid until the buffer is freed.
* Returns null if `buf` is null.
*
* # Safety
* `adapter` and `target` must be valid. `out` must be writable.
* `buf` must be null or returned by `ad_screenshot`.
*/
const uint8_t *ad_image_buffer_data(const struct AdImageBuffer *buf);
/**
* Byte length of the buffer returned by `ad_image_buffer_data`.
* Always consistent with the actual allocation (no C-mutable mismatch).
*
* # Safety
* `buf` must be null or returned by `ad_screenshot`.
*/
uint64_t ad_image_buffer_size(const struct AdImageBuffer *buf);
/**
* Pixel width of the image.
*
* # Safety
* `buf` must be null or returned by `ad_screenshot`.
*/
uint32_t ad_image_buffer_width(const struct AdImageBuffer *buf);
/**
* Pixel height of the image.
*
* # Safety
* `buf` must be null or returned by `ad_screenshot`.
*/
uint32_t ad_image_buffer_height(const struct AdImageBuffer *buf);
/**
* Encoding format of the image bytes. Defaults to `PNG` on a null
* handle callers must still null-check.
*
* # Safety
* `buf` must be null or returned by `ad_screenshot`.
*/
AdImageFormat ad_image_buffer_format(const struct AdImageBuffer *buf);
/**
* Allocates and returns an opaque `AdImageBuffer`. The handle owns its
* byte buffer; inspect it through `ad_image_buffer_data` /
* `ad_image_buffer_size` / `ad_image_buffer_format` / `_width` / `_height`
* and free it with `ad_image_buffer_free`.
*
* # Safety
* `adapter` and `target` must be valid pointers. `out` must be a valid
* writable `*mut *mut AdImageBuffer`. On error `*out` is null and
* last-error is set.
*/
AdResult ad_screenshot(const struct AdAdapter *adapter,
const struct AdScreenshotTarget *target,
struct AdImageBuffer *out);
struct AdImageBuffer **out);
/**
* Frees the image buffer allocated by `ad_screenshot`.
*
* # Safety
* `buf` must be null or a pointer previously returned by `ad_screenshot`.
* Double-free is undefined behavior.
*/
void ad_image_buffer_free(struct AdImageBuffer *buf);
/**
* # Safety
* `img` must be null or point to an `AdImageBuffer` from `ad_screenshot`.
* `adapter` must be valid. `out` must be a valid writable
* `*mut *mut AdSurfaceList`. Success produces a list handle freed via
* `ad_surface_list_free`.
*/
void ad_free_image(struct AdImageBuffer *img);
AdResult ad_list_surfaces(const struct AdAdapter *adapter, int32_t pid, struct AdSurfaceList **out);
/**
* # Safety
* `adapter` must be valid. `out` and `out_count` must be writable.
* `list` must be null or a pointer returned by `ad_list_surfaces`.
*/
AdResult ad_list_surfaces(const struct AdAdapter *adapter,
int32_t pid,
struct AdSurfaceInfo **out,
uint32_t *out_count);
uint32_t ad_surface_list_count(const struct AdSurfaceList *list);
/**
* Borrow a surface info entry. Null if `index` is out of range.
*
* # Safety
* `surfaces` must be null or from `ad_list_surfaces`.
* `list` must be null or a pointer returned by `ad_list_surfaces`.
*/
void ad_free_surfaces(struct AdSurfaceInfo *surfaces, uint32_t count);
const struct AdSurfaceInfo *ad_surface_list_get(const struct AdSurfaceList *list, uint32_t index);
/**
* Frees the list and each entry's interior strings.
*
* # Safety
* `list` must be null or a pointer returned by `ad_list_surfaces`.
*/
void ad_surface_list_free(struct AdSurfaceList *list);
/**
* # Safety
@ -476,26 +589,55 @@ AdResult ad_get_tree(const struct AdAdapter *adapter,
AdResult ad_focus_window(const struct AdAdapter *adapter, const struct AdWindowInfo *win);
/**
* Releases the heap-allocated string fields (`id`, `title`, `app_name`)
* inside a single `AdWindowInfo` previously written by `ad_launch_app`
* or returned through a list accessor. Does not free the `AdWindowInfo`
* struct itself that memory is owned by the caller's stack or by the
* enclosing list.
*
* Named `ad_release_window_fields` (not `ad_free_window`) to disambiguate
* from the now-removed list-free function and make the semantics clear
* in the header.
*
* # Safety
* `win` must be null or point to a valid `AdWindowInfo`.
* `win` must be null or point to a valid `AdWindowInfo` whose string
* fields were allocated by this crate. Do not call on pointers inside
* an `AdWindowList` free the list instead.
*/
void ad_free_window(struct AdWindowInfo *win);
void ad_release_window_fields(struct AdWindowInfo *win);
/**
* # Safety
* `adapter` must be valid. `out` and `out_count` must be writable.
* `adapter` must be valid. `out` must be a valid writable
* `*mut *mut AdWindowList`. `app_filter` may be null or a C string.
* Success produces a list handle freed via `ad_window_list_free`.
*/
AdResult ad_list_windows(const struct AdAdapter *adapter,
const char *app_filter,
bool focused_only,
struct AdWindowInfo **out,
uint32_t *out_count);
struct AdWindowList **out);
/**
* # Safety
* `windows` must be null or from `ad_list_windows`.
* `list` must be null or a pointer returned by `ad_list_windows`.
*/
void ad_free_windows(struct AdWindowInfo *windows, uint32_t count);
uint32_t ad_window_list_count(const struct AdWindowList *list);
/**
* Borrow a window info entry. Null if `index` is out of range.
*
* # Safety
* `list` must be null or a pointer returned by `ad_list_windows`.
*/
const struct AdWindowInfo *ad_window_list_get(const struct AdWindowList *list, uint32_t index);
/**
* Frees the list and each entry's interior strings.
*
* # Safety
* `list` must be null or a pointer returned by `ad_list_windows`.
*/
void ad_window_list_free(struct AdWindowList *list);
/**
* # Safety

View file

@ -1,35 +1,30 @@
use crate::convert::app::{app_info_to_c, free_app_info_fields};
use crate::error::{set_last_error, AdResult};
use crate::ffi_try::{trap_panic, trap_panic_void};
use crate::types::AdAppInfo;
use crate::types::{AdAppInfo, AdAppList};
use crate::AdAdapter;
use std::ptr;
/// # Safety
/// `adapter` must be a valid pointer from `ad_adapter_create`.
/// `out` and `out_count` must be valid writable pointers.
/// `out` must be a valid writable `*mut *mut AdAppList`.
/// On success, `*out` is a newly-allocated opaque list freed with
/// `ad_app_list_free`. On error, `*out` is null and last-error is set.
#[no_mangle]
pub unsafe extern "C" fn ad_list_apps(
adapter: *const AdAdapter,
out: *mut *mut AdAppInfo,
out_count: *mut u32,
out: *mut *mut AdAppList,
) -> AdResult {
trap_panic(|| unsafe {
*out = ptr::null_mut();
*out_count = 0;
let adapter = &*adapter;
match adapter.inner.list_apps() {
Ok(apps) => {
let c_apps: Vec<AdAppInfo> = apps.iter().map(app_info_to_c).collect();
let count = c_apps.len() as u32;
if c_apps.is_empty() {
return AdResult::Ok;
}
let mut boxed = c_apps.into_boxed_slice();
*out = boxed.as_mut_ptr();
*out_count = count;
std::mem::forget(boxed);
let items: Vec<AdAppInfo> = apps.iter().map(app_info_to_c).collect();
let list = Box::new(AdAppList {
items: items.into_boxed_slice(),
});
*out = Box::into_raw(list);
AdResult::Ok
}
Err(e) => {
@ -41,20 +36,50 @@ pub unsafe extern "C" fn ad_list_apps(
}
/// # Safety
/// `apps` must be null or a pointer previously returned by `ad_list_apps`.
/// `list` must be null or a pointer returned by `ad_list_apps`.
#[no_mangle]
pub unsafe extern "C" fn ad_free_apps(apps: *mut AdAppInfo, count: u32) {
pub unsafe extern "C" fn ad_app_list_count(list: *const AdAppList) -> u32 {
if list.is_null() {
return 0;
}
let list_ref: &AdAppList = unsafe { &*list };
list_ref.items.len() as u32
}
/// Returns a borrowed pointer into the list; valid until the list is freed.
/// Out-of-range `index` returns null.
///
/// # Safety
/// `list` must be null or a pointer returned by `ad_list_apps`.
#[no_mangle]
pub unsafe extern "C" fn ad_app_list_get(
list: *const AdAppList,
index: u32,
) -> *const AdAppInfo {
if list.is_null() {
return ptr::null();
}
let list_ref: &AdAppList = unsafe { &*list };
match list_ref.items.get(index as usize) {
Some(item) => item as *const AdAppInfo,
None => ptr::null(),
}
}
/// Frees the list and every `AdAppInfo` it owns, including the interior
/// C-strings.
///
/// # Safety
/// `list` must be null or a pointer returned by `ad_list_apps`.
#[no_mangle]
pub unsafe extern "C" fn ad_app_list_free(list: *mut AdAppList) {
trap_panic_void(|| unsafe {
if apps.is_null() {
if list.is_null() {
return;
}
let slice = std::slice::from_raw_parts_mut(apps, count as usize);
for app in slice.iter_mut() {
free_app_info_fields(app);
let mut list = Box::from_raw(list);
for item in list.items.iter_mut() {
free_app_info_fields(item);
}
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
apps,
count as usize,
)));
})
}

View file

@ -53,9 +53,11 @@ pub use types::action::AdAction;
pub use types::action_kind::AdActionKind;
pub use types::action_result::AdActionResult;
pub use types::app_info::AdAppInfo;
pub use types::app_list::AdAppList;
pub use types::direction::AdDirection;
pub use types::drag_params::AdDragParams;
pub use types::element_state::AdElementState;
pub use types::find_query::AdFindQuery;
pub use types::image_buffer::AdImageBuffer;
pub use types::image_format::AdImageFormat;
pub use types::key_combo::AdKeyCombo;
@ -66,6 +68,9 @@ pub use types::mouse_event_kind::AdMouseEventKind;
pub use types::native_handle::AdNativeHandle;
pub use types::node::AdNode;
pub use types::node_tree::AdNodeTree;
pub use types::notification_filter::AdNotificationFilter;
pub use types::notification_info::AdNotificationInfo;
pub use types::notification_list::AdNotificationList;
pub use types::point::AdPoint;
pub use types::rect::AdRect;
pub use types::ref_entry::AdRefEntry;
@ -74,7 +79,9 @@ 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::surface_list::AdSurfaceList;
pub use types::tree_options::AdTreeOptions;
pub use types::window_info::AdWindowInfo;
pub use types::window_list::AdWindowList;
pub use types::window_op::AdWindowOp;
pub use types::window_op_kind::AdWindowOpKind;

View file

@ -0,0 +1,70 @@
use crate::types::{AdImageBuffer, AdImageFormat};
use std::ptr;
/// Borrowed pointer to the image bytes; valid until the buffer is freed.
/// Returns null if `buf` is null.
///
/// # Safety
/// `buf` must be null or returned by `ad_screenshot`.
#[no_mangle]
pub unsafe extern "C" fn ad_image_buffer_data(buf: *const AdImageBuffer) -> *const u8 {
if buf.is_null() {
return ptr::null();
}
let buf_ref: &AdImageBuffer = unsafe { &*buf };
buf_ref.data.as_ptr()
}
/// Byte length of the buffer returned by `ad_image_buffer_data`.
/// Always consistent with the actual allocation (no C-mutable mismatch).
///
/// # Safety
/// `buf` must be null or returned by `ad_screenshot`.
#[no_mangle]
pub unsafe extern "C" fn ad_image_buffer_size(buf: *const AdImageBuffer) -> u64 {
if buf.is_null() {
return 0;
}
let buf_ref: &AdImageBuffer = unsafe { &*buf };
buf_ref.data.len() as u64
}
/// Pixel width of the image.
///
/// # Safety
/// `buf` must be null or returned by `ad_screenshot`.
#[no_mangle]
pub unsafe extern "C" fn ad_image_buffer_width(buf: *const AdImageBuffer) -> u32 {
if buf.is_null() {
return 0;
}
let buf_ref: &AdImageBuffer = unsafe { &*buf };
buf_ref.width
}
/// Pixel height of the image.
///
/// # Safety
/// `buf` must be null or returned by `ad_screenshot`.
#[no_mangle]
pub unsafe extern "C" fn ad_image_buffer_height(buf: *const AdImageBuffer) -> u32 {
if buf.is_null() {
return 0;
}
let buf_ref: &AdImageBuffer = unsafe { &*buf };
buf_ref.height
}
/// Encoding format of the image bytes. Defaults to `PNG` on a null
/// handle — callers must still null-check.
///
/// # Safety
/// `buf` must be null or returned by `ad_screenshot`.
#[no_mangle]
pub unsafe extern "C" fn ad_image_buffer_format(buf: *const AdImageBuffer) -> AdImageFormat {
if buf.is_null() {
return AdImageFormat::Png;
}
let buf_ref: &AdImageBuffer = unsafe { &*buf };
buf_ref.format
}

View file

@ -4,18 +4,26 @@ use crate::ffi_try::trap_panic;
use crate::types::{AdImageBuffer, AdImageFormat, AdScreenshotKind, AdScreenshotTarget};
use crate::AdAdapter;
use agent_desktop_core::adapter::{ImageFormat, ScreenshotTarget as CoreScreenshotTarget};
use std::ptr;
/// Allocates and returns an opaque `AdImageBuffer`. The handle owns its
/// byte buffer; inspect it through `ad_image_buffer_data` /
/// `ad_image_buffer_size` / `ad_image_buffer_format` / `_width` / `_height`
/// and free it with `ad_image_buffer_free`.
///
/// # Safety
/// `adapter` and `target` must be valid. `out` must be writable.
/// `adapter` and `target` must be valid pointers. `out` must be a valid
/// writable `*mut *mut AdImageBuffer`. On error `*out` is null and
/// last-error is set.
#[no_mangle]
pub unsafe extern "C" fn ad_screenshot(
adapter: *const AdAdapter,
target: *const AdScreenshotTarget,
out: *mut AdImageBuffer,
out: *mut *mut AdImageBuffer,
) -> AdResult {
trap_panic(|| unsafe {
crate::main_thread::debug_assert_main_thread();
*out = std::mem::zeroed();
*out = ptr::null_mut();
let adapter = &*adapter;
let t = &*target;
let kind = match AdScreenshotKind::from_c(enum_raw_i32(&t.kind)) {
@ -36,21 +44,16 @@ pub unsafe extern "C" fn ad_screenshot(
match adapter.inner.screenshot(core_target) {
Ok(img) => {
let data_len = img.data.len() as u64;
let mut boxed = img.data.into_boxed_slice();
let data_ptr = boxed.as_mut_ptr();
std::mem::forget(boxed);
*out = AdImageBuffer {
data: data_ptr,
data_len,
let buffer = Box::new(AdImageBuffer {
data: img.data.into_boxed_slice(),
width: img.width,
height: img.height,
format: match img.format {
ImageFormat::Png => AdImageFormat::Png,
ImageFormat::Jpg => AdImageFormat::Jpg,
},
width: img.width,
height: img.height,
};
});
*out = Box::into_raw(buffer);
AdResult::Ok
}
Err(e) => {

View file

@ -1,22 +1,17 @@
use crate::ffi_try::trap_panic_void;
use crate::types::AdImageBuffer;
/// Frees the image buffer allocated by `ad_screenshot`.
///
/// # Safety
/// `img` must be null or point to an `AdImageBuffer` from `ad_screenshot`.
/// `buf` must be null or a pointer previously returned by `ad_screenshot`.
/// Double-free is undefined behavior.
#[no_mangle]
pub unsafe extern "C" fn ad_free_image(img: *mut AdImageBuffer) {
pub unsafe extern "C" fn ad_image_buffer_free(buf: *mut AdImageBuffer) {
trap_panic_void(|| unsafe {
if img.is_null() {
if buf.is_null() {
return;
}
let i = &mut *img;
if !i.data.is_null() {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
i.data as *mut u8,
i.data_len as usize,
)));
i.data = std::ptr::null();
i.data_len = 0;
}
drop(Box::from_raw(buf));
})
}

View file

@ -1,2 +1,3 @@
pub(crate) mod accessors;
pub(crate) mod capture;
pub(crate) mod free;

View file

@ -1,36 +1,30 @@
use crate::convert::surface::{free_surface_info_fields, surface_info_to_c};
use crate::error::{set_last_error, AdResult};
use crate::ffi_try::{trap_panic, trap_panic_void};
use crate::types::AdSurfaceInfo;
use crate::types::{AdSurfaceInfo, AdSurfaceList};
use crate::AdAdapter;
use std::ptr;
/// # Safety
/// `adapter` must be valid. `out` and `out_count` must be writable.
/// `adapter` must be valid. `out` must be a valid writable
/// `*mut *mut AdSurfaceList`. Success produces a list handle freed via
/// `ad_surface_list_free`.
#[no_mangle]
pub unsafe extern "C" fn ad_list_surfaces(
adapter: *const AdAdapter,
pid: i32,
out: *mut *mut AdSurfaceInfo,
out_count: *mut u32,
out: *mut *mut AdSurfaceList,
) -> AdResult {
trap_panic(|| unsafe {
*out = ptr::null_mut();
*out_count = 0;
let adapter = &*adapter;
match adapter.inner.list_surfaces(pid) {
Ok(surfaces) => {
let c_surfaces: Vec<AdSurfaceInfo> =
surfaces.iter().map(surface_info_to_c).collect();
let count = c_surfaces.len() as u32;
if c_surfaces.is_empty() {
return AdResult::Ok;
}
let mut boxed = c_surfaces.into_boxed_slice();
*out = boxed.as_mut_ptr();
*out_count = count;
std::mem::forget(boxed);
let items: Vec<AdSurfaceInfo> = surfaces.iter().map(surface_info_to_c).collect();
let list = Box::new(AdSurfaceList {
items: items.into_boxed_slice(),
});
*out = Box::into_raw(list);
AdResult::Ok
}
Err(e) => {
@ -42,20 +36,48 @@ pub unsafe extern "C" fn ad_list_surfaces(
}
/// # Safety
/// `surfaces` must be null or from `ad_list_surfaces`.
/// `list` must be null or a pointer returned by `ad_list_surfaces`.
#[no_mangle]
pub unsafe extern "C" fn ad_free_surfaces(surfaces: *mut AdSurfaceInfo, count: u32) {
pub unsafe extern "C" fn ad_surface_list_count(list: *const AdSurfaceList) -> u32 {
if list.is_null() {
return 0;
}
let list_ref: &AdSurfaceList = unsafe { &*list };
list_ref.items.len() as u32
}
/// Borrow a surface info entry. Null if `index` is out of range.
///
/// # Safety
/// `list` must be null or a pointer returned by `ad_list_surfaces`.
#[no_mangle]
pub unsafe extern "C" fn ad_surface_list_get(
list: *const AdSurfaceList,
index: u32,
) -> *const AdSurfaceInfo {
if list.is_null() {
return ptr::null();
}
let list_ref: &AdSurfaceList = unsafe { &*list };
match list_ref.items.get(index as usize) {
Some(item) => item as *const AdSurfaceInfo,
None => ptr::null(),
}
}
/// Frees the list and each entry's interior strings.
///
/// # Safety
/// `list` must be null or a pointer returned by `ad_list_surfaces`.
#[no_mangle]
pub unsafe extern "C" fn ad_surface_list_free(list: *mut AdSurfaceList) {
trap_panic_void(|| unsafe {
if surfaces.is_null() {
if list.is_null() {
return;
}
let slice = std::slice::from_raw_parts_mut(surfaces, count as usize);
for s in slice.iter_mut() {
free_surface_info_fields(s);
let mut list = Box::from_raw(list);
for item in list.items.iter_mut() {
free_surface_info_fields(item);
}
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
surfaces,
count as usize,
)));
})
}

View file

@ -0,0 +1,7 @@
use crate::types::app_info::AdAppInfo;
/// Opaque list handle emitted by `ad_list_apps`. See
/// [`crate::types::window_list::AdWindowList`] for the pattern.
pub struct AdAppList {
pub(crate) items: Box<[AdAppInfo]>,
}

View file

@ -0,0 +1,8 @@
use std::os::raw::c_char;
#[repr(C)]
pub struct AdFindQuery {
pub role: *const c_char,
pub name_substring: *const c_char,
pub value_substring: *const c_char,
}

View file

@ -1,10 +1,13 @@
use crate::types::image_format::AdImageFormat;
#[repr(C)]
/// Opaque image-buffer handle returned by `ad_screenshot`. The backing
/// byte buffer and its length live inside the Rust-owned struct — a
/// consumer cannot accidentally desynchronize the pair and trigger a
/// heap-corruption double-free. Walk it through `ad_image_buffer_*`
/// accessors and free it with `ad_image_buffer_free`.
pub struct AdImageBuffer {
pub data: *const u8,
pub data_len: u64,
pub format: AdImageFormat,
pub width: u32,
pub height: u32,
pub(crate) data: Box<[u8]>,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) format: AdImageFormat,
}

View file

@ -2,9 +2,11 @@ pub mod action;
pub mod action_kind;
pub mod action_result;
pub mod app_info;
pub mod app_list;
pub mod direction;
pub mod drag_params;
pub mod element_state;
pub mod find_query;
pub mod image_buffer;
pub mod image_format;
pub mod key_combo;
@ -15,6 +17,9 @@ pub mod mouse_event_kind;
pub mod native_handle;
pub mod node;
pub mod node_tree;
pub mod notification_filter;
pub mod notification_info;
pub mod notification_list;
pub mod point;
pub mod rect;
pub mod ref_entry;
@ -23,8 +28,10 @@ pub mod screenshot_target;
pub mod scroll_params;
pub mod snapshot_surface;
pub mod surface_info;
pub mod surface_list;
pub mod tree_options;
pub mod window_info;
pub mod window_list;
pub mod window_op;
pub mod window_op_kind;
@ -32,9 +39,11 @@ pub use action::AdAction;
pub use action_kind::AdActionKind;
pub use action_result::AdActionResult;
pub use app_info::AdAppInfo;
pub use app_list::AdAppList;
pub use direction::AdDirection;
pub use drag_params::AdDragParams;
pub use element_state::AdElementState;
pub use find_query::AdFindQuery;
pub use image_buffer::AdImageBuffer;
pub use image_format::AdImageFormat;
pub use key_combo::AdKeyCombo;
@ -45,6 +54,9 @@ pub use mouse_event_kind::AdMouseEventKind;
pub use native_handle::AdNativeHandle;
pub use node::AdNode;
pub use node_tree::AdNodeTree;
pub use notification_filter::AdNotificationFilter;
pub use notification_info::AdNotificationInfo;
pub use notification_list::AdNotificationList;
pub use point::AdPoint;
pub use rect::AdRect;
pub use ref_entry::AdRefEntry;
@ -53,7 +65,9 @@ pub use screenshot_target::AdScreenshotTarget;
pub use scroll_params::AdScrollParams;
pub use snapshot_surface::AdSnapshotSurface;
pub use surface_info::AdSurfaceInfo;
pub use surface_list::AdSurfaceList;
pub use tree_options::AdTreeOptions;
pub use window_info::AdWindowInfo;
pub use window_list::AdWindowList;
pub use window_op::AdWindowOp;
pub use window_op_kind::AdWindowOpKind;

View file

@ -0,0 +1,9 @@
use std::os::raw::c_char;
#[repr(C)]
pub struct AdNotificationFilter {
pub app: *const c_char,
pub text: *const c_char,
pub limit: u32,
pub has_limit: bool,
}

View file

@ -0,0 +1,11 @@
use std::os::raw::c_char;
#[repr(C)]
pub struct AdNotificationInfo {
pub index: u32,
pub app_name: *const c_char,
pub title: *const c_char,
pub body: *const c_char,
pub actions: *mut *mut c_char,
pub action_count: u32,
}

View file

@ -0,0 +1,6 @@
use crate::types::notification_info::AdNotificationInfo;
pub struct AdNotificationList {
#[allow(dead_code)] // populated + read by the Unit 8 notifications module
pub(crate) items: Box<[AdNotificationInfo]>,
}

View file

@ -0,0 +1,7 @@
use crate::types::surface_info::AdSurfaceInfo;
/// Opaque list handle emitted by `ad_list_surfaces`. See
/// [`crate::types::window_list::AdWindowList`] for the pattern.
pub struct AdSurfaceList {
pub(crate) items: Box<[AdSurfaceInfo]>,
}

View file

@ -0,0 +1,13 @@
use crate::types::window_info::AdWindowInfo;
/// Opaque list handle emitted by `ad_list_windows`.
///
/// The struct intentionally has no `#[repr(C)]` so cbindgen emits a
/// forward declaration only (`typedef struct AdWindowList AdWindowList;`).
/// Consumers cannot read the backing pointer or length and cannot
/// construct a count mismatch — they walk the list through
/// `ad_window_list_count`, `ad_window_list_get`, and free it with
/// `ad_window_list_free`.
pub struct AdWindowList {
pub(crate) items: Box<[AdWindowInfo]>,
}

View file

@ -2,10 +2,22 @@ use crate::convert::window::free_window_info_fields;
use crate::ffi_try::trap_panic_void;
use crate::types::AdWindowInfo;
/// Releases the heap-allocated string fields (`id`, `title`, `app_name`)
/// inside a single `AdWindowInfo` previously written by `ad_launch_app`
/// or returned through a list accessor. Does not free the `AdWindowInfo`
/// struct itself — that memory is owned by the caller's stack or by the
/// enclosing list.
///
/// Named `ad_release_window_fields` (not `ad_free_window`) to disambiguate
/// from the now-removed list-free function and make the semantics clear
/// in the header.
///
/// # Safety
/// `win` must be null or point to a valid `AdWindowInfo`.
/// `win` must be null or point to a valid `AdWindowInfo` whose string
/// fields were allocated by this crate. Do not call on pointers inside
/// an `AdWindowList` — free the list instead.
#[no_mangle]
pub unsafe extern "C" fn ad_free_window(win: *mut AdWindowInfo) {
pub unsafe extern "C" fn ad_release_window_fields(win: *mut AdWindowInfo) {
trap_panic_void(|| unsafe {
if win.is_null() {
return;

View file

@ -2,25 +2,25 @@ use crate::convert::string::c_to_string;
use crate::convert::window::{free_window_info_fields, window_info_to_c};
use crate::error::{set_last_error, AdResult};
use crate::ffi_try::{trap_panic, trap_panic_void};
use crate::types::AdWindowInfo;
use crate::types::{AdWindowInfo, AdWindowList};
use crate::AdAdapter;
use agent_desktop_core::adapter::WindowFilter;
use std::os::raw::c_char;
use std::ptr;
/// # Safety
/// `adapter` must be valid. `out` and `out_count` must be writable.
/// `adapter` must be valid. `out` must be a valid writable
/// `*mut *mut AdWindowList`. `app_filter` may be null or a C string.
/// Success produces a list handle freed via `ad_window_list_free`.
#[no_mangle]
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,
out: *mut *mut AdWindowList,
) -> AdResult {
trap_panic(|| unsafe {
*out = ptr::null_mut();
*out_count = 0;
let adapter = &*adapter;
let filter = WindowFilter {
focused_only,
@ -28,15 +28,11 @@ pub unsafe extern "C" fn ad_list_windows(
};
match adapter.inner.list_windows(&filter) {
Ok(windows) => {
let c_wins: Vec<AdWindowInfo> = windows.iter().map(window_info_to_c).collect();
let count = c_wins.len() as u32;
if c_wins.is_empty() {
return AdResult::Ok;
}
let mut boxed = c_wins.into_boxed_slice();
*out = boxed.as_mut_ptr();
*out_count = count;
std::mem::forget(boxed);
let items: Vec<AdWindowInfo> = windows.iter().map(window_info_to_c).collect();
let list = Box::new(AdWindowList {
items: items.into_boxed_slice(),
});
*out = Box::into_raw(list);
AdResult::Ok
}
Err(e) => {
@ -48,20 +44,48 @@ pub unsafe extern "C" fn ad_list_windows(
}
/// # Safety
/// `windows` must be null or from `ad_list_windows`.
/// `list` must be null or a pointer returned by `ad_list_windows`.
#[no_mangle]
pub unsafe extern "C" fn ad_free_windows(windows: *mut AdWindowInfo, count: u32) {
pub unsafe extern "C" fn ad_window_list_count(list: *const AdWindowList) -> u32 {
if list.is_null() {
return 0;
}
let list_ref: &AdWindowList = unsafe { &*list };
list_ref.items.len() as u32
}
/// Borrow a window info entry. Null if `index` is out of range.
///
/// # Safety
/// `list` must be null or a pointer returned by `ad_list_windows`.
#[no_mangle]
pub unsafe extern "C" fn ad_window_list_get(
list: *const AdWindowList,
index: u32,
) -> *const AdWindowInfo {
if list.is_null() {
return ptr::null();
}
let list_ref: &AdWindowList = unsafe { &*list };
match list_ref.items.get(index as usize) {
Some(item) => item as *const AdWindowInfo,
None => ptr::null(),
}
}
/// Frees the list and each entry's interior strings.
///
/// # Safety
/// `list` must be null or a pointer returned by `ad_list_windows`.
#[no_mangle]
pub unsafe extern "C" fn ad_window_list_free(list: *mut AdWindowList) {
trap_panic_void(|| unsafe {
if windows.is_null() {
if list.is_null() {
return;
}
let slice = std::slice::from_raw_parts_mut(windows, count as usize);
for w in slice.iter_mut() {
free_window_info_fields(w);
let mut list = Box::from_raw(list);
for item in list.items.iter_mut() {
free_window_info_fields(item);
}
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
windows,
count as usize,
)));
})
}