From 0c35d4ffedfce32d6d6f556643c1c9f31c65b6ad Mon Sep 17 00:00:00 2001 From: neo Date: Fri, 24 Apr 2026 23:52:52 -0400 Subject: [PATCH 1/2] feat(ffi): add DVT bindings for energy_monitor, graphics, notifications --- ffi/Cargo.toml | 4 + ffi/src/dvt/energy_monitor.rs | 207 ++++++++++++++++++++++ ffi/src/dvt/graphics.rs | 160 +++++++++++++++++ ffi/src/dvt/mod.rs | 5 + ffi/src/dvt/notifications.rs | 168 ++++++++++++++++++ idevice/src/services/dvt/notifications.rs | 12 +- 6 files changed, 550 insertions(+), 6 deletions(-) create mode 100644 ffi/src/dvt/energy_monitor.rs create mode 100644 ffi/src/dvt/graphics.rs create mode 100644 ffi/src/dvt/notifications.rs diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index f01cc1a..7edf9c5 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -38,6 +38,8 @@ dvt = ["idevice/dvt"] device_info = ["idevice/device_info"] application_listing = ["idevice/application_listing"] condition_inducer = ["idevice/condition_inducer"] +energy_monitor = ["idevice/energy_monitor"] +graphics = ["idevice/graphics"] network_monitor = ["idevice/network_monitor"] sysmontap = ["idevice/sysmontap"] heartbeat = ["idevice/heartbeat"] @@ -80,6 +82,8 @@ full = [ "device_info", "application_listing", "condition_inducer", + "energy_monitor", + "graphics", "network_monitor", "sysmontap", "heartbeat", diff --git a/ffi/src/dvt/energy_monitor.rs b/ffi/src/dvt/energy_monitor.rs new file mode 100644 index 0000000..ec74e57 --- /dev/null +++ b/ffi/src/dvt/energy_monitor.rs @@ -0,0 +1,207 @@ +// Jackson Coxson + +use std::ptr::null_mut; + +use idevice::{ + ReadWrite, + dvt::energy_monitor::{EnergyMonitorClient, EnergySample}, +}; + +use crate::{IdeviceFfiError, dvt::remote_server::RemoteServerHandle, ffi_err, run_sync}; + +pub struct EnergyMonitorHandle<'a>(pub EnergyMonitorClient<'a, Box>); + +/// A parsed per-PID energy sample +#[repr(C)] +pub struct IdeviceEnergySample { + pub pid: u32, + pub timestamp: i64, + pub total_energy: f64, + pub cpu_energy: f64, + pub gpu_energy: f64, + pub networking_energy: f64, + pub display_energy: f64, + pub location_energy: f64, + pub appstate_energy: f64, +} + +/// Creates a new EnergyMonitorClient from a RemoteServerClient +/// +/// # Safety +/// `server` must be a valid pointer to a handle allocated by this library +/// `handle` must be a valid pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn energy_monitor_new( + server: *mut RemoteServerHandle, + handle: *mut *mut EnergyMonitorHandle<'static>, +) -> *mut IdeviceFfiError { + if server.is_null() || handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let server = unsafe { &mut (*server).0 }; + let res = run_sync(async move { EnergyMonitorClient::new(server).await }); + + match res { + Ok(client) => { + let boxed = Box::new(EnergyMonitorHandle(client)); + unsafe { *handle = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Frees an EnergyMonitorClient handle +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn energy_monitor_free(handle: *mut EnergyMonitorHandle<'static>) { + if !handle.is_null() { + let _ = unsafe { Box::from_raw(handle) }; + } +} + +/// Starts energy sampling for the given PIDs. +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library. +/// If `pids` is non-null it must point to at least `pids_count` readable `u32` values. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn energy_monitor_start_sampling( + handle: *mut EnergyMonitorHandle<'static>, + pids: *const u32, + pids_count: usize, +) -> *mut IdeviceFfiError { + if handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let pids_vec: Vec = if pids.is_null() || pids_count == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(pids, pids_count) }.to_vec() + }; + + let client = unsafe { &mut (*handle).0 }; + let res = run_sync(async move { client.start_sampling(&pids_vec).await }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Stops energy sampling for the given PIDs. +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library. +/// If `pids` is non-null it must point to at least `pids_count` readable `u32` values. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn energy_monitor_stop_sampling( + handle: *mut EnergyMonitorHandle<'static>, + pids: *const u32, + pids_count: usize, +) -> *mut IdeviceFfiError { + if handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let pids_vec: Vec = if pids.is_null() || pids_count == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(pids, pids_count) }.to_vec() + }; + + let client = unsafe { &mut (*handle).0 }; + let res = run_sync(async move { client.stop_sampling(&pids_vec).await }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Requests a one-shot energy sample and parses the response. +/// +/// # Arguments +/// * [`handle`] - The EnergyMonitorClient handle +/// * [`pids`] - Pointer to an array of u32 PIDs to sample +/// * [`pids_count`] - Number of elements in `pids` +/// * [`samples_out`] - On success, set to a heap-allocated array of IdeviceEnergySample +/// * [`samples_count_out`] - On success, set to the number of samples +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// All output pointers must be valid and non-null. Free the array with +/// `energy_monitor_samples_free`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn energy_monitor_sample_attributes( + handle: *mut EnergyMonitorHandle<'static>, + pids: *const u32, + pids_count: usize, + samples_out: *mut *mut IdeviceEnergySample, + samples_count_out: *mut usize, +) -> *mut IdeviceFfiError { + if handle.is_null() || samples_out.is_null() || samples_count_out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let pids_vec: Vec = if pids.is_null() || pids_count == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(pids, pids_count) }.to_vec() + }; + + let client = unsafe { &mut (*handle).0 }; + let bytes = match run_sync(async move { client.sample_attributes(&pids_vec).await }) { + Ok(b) => b, + Err(e) => return ffi_err!(e), + }; + + let samples = match EnergySample::from_bytes(&bytes) { + Ok(v) => v, + Err(e) => return ffi_err!(e), + }; + + let mut c_samples: Box<[IdeviceEnergySample]> = samples + .into_iter() + .map(|s| IdeviceEnergySample { + pid: s.pid, + timestamp: s.timestamp, + total_energy: s.total_energy, + cpu_energy: s.cpu_energy, + gpu_energy: s.gpu_energy, + networking_energy: s.networking_energy, + display_energy: s.display_energy, + location_energy: s.location_energy, + appstate_energy: s.appstate_energy, + }) + .collect::>() + .into_boxed_slice(); + + unsafe { + *samples_out = c_samples.as_mut_ptr(); + *samples_count_out = c_samples.len(); + } + std::mem::forget(c_samples); + null_mut() +} + +/// Frees an array of IdeviceEnergySample allocated by `energy_monitor_sample_attributes`. +/// +/// # Safety +/// `samples` must be a pointer returned by this library with the matching `count`, or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn energy_monitor_samples_free( + samples: *mut IdeviceEnergySample, + count: usize, +) { + if samples.is_null() { + return; + } + let _ = unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(samples, count)) }; +} diff --git a/ffi/src/dvt/graphics.rs b/ffi/src/dvt/graphics.rs new file mode 100644 index 0000000..d5aa36e --- /dev/null +++ b/ffi/src/dvt/graphics.rs @@ -0,0 +1,160 @@ +// Jackson Coxson + +use std::{ffi::CString, ptr::null_mut}; + +use idevice::{ReadWrite, dvt::graphics::GraphicsClient}; + +use crate::{IdeviceFfiError, dvt::remote_server::RemoteServerHandle, ffi_err, run_sync}; + +pub struct GraphicsHandle<'a>(pub GraphicsClient<'a, Box>); + +/// A graphics sample from tddhe GPU instruments channel +#[repr(C)] +pub struct IdeviceGraphicsSample { + pub timestamp: u64, + pub fps: f64, + pub alloc_system_memory: u64, + pub in_use_system_memory: u64, + pub in_use_system_memory_driver: u64, + pub gpu_bundle_name: *mut std::ffi::c_char, + pub recovery_count: u64, +} + +/// Frees an IdeviceGraphicsSample and its heap-allocated string field +/// +/// # Safety +/// `sample` must be a valid pointer allocated by this library or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn graphics_sample_free(sample: *mut IdeviceGraphicsSample) { + if sample.is_null() { + return; + } + let s = unsafe { Box::from_raw(sample) }; + if !s.gpu_bundle_name.is_null() { + let _ = unsafe { CString::from_raw(s.gpu_bundle_name) }; + } +} + +/// Creates a new GraphicsClient from a RemoteServerClient +/// +/// # Safety +/// `server` must be a valid pointer to a handle allocated by this library +/// `handle` must be a valid pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn graphics_new( + server: *mut RemoteServerHandle, + handle: *mut *mut GraphicsHandle<'static>, +) -> *mut IdeviceFfiError { + if server.is_null() || handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let server = unsafe { &mut (*server).0 }; + let res = run_sync(async move { GraphicsClient::new(server).await }); + + match res { + Ok(client) => { + let boxed = Box::new(GraphicsHandle(client)); + unsafe { *handle = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Frees a GraphicsClient handle +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn graphics_free(handle: *mut GraphicsHandle<'static>) { + if !handle.is_null() { + let _ = unsafe { Box::from_raw(handle) }; + } +} + +/// Starts graphics sampling at the given interval. Consumes the device's initial reply internally. +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn graphics_start_sampling( + handle: *mut GraphicsHandle<'static>, + interval: f64, +) -> *mut IdeviceFfiError { + if handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let client = unsafe { &mut (*handle).0 }; + let res = run_sync(async move { client.start_sampling(interval).await }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Stops graphics sampling. +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn graphics_stop_sampling( + handle: *mut GraphicsHandle<'static>, +) -> *mut IdeviceFfiError { + if handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let client = unsafe { &mut (*handle).0 }; + let res = run_sync(async move { client.stop_sampling().await }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Reads the next graphics data frame pushed by the device. Blocks until a frame arrives. +/// +/// # Arguments +/// * [`handle`] - The GraphicsClient handle +/// * [`sample_out`] - On success, set to a heap-allocated IdeviceGraphicsSample +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// All pointers must be valid and non-null. Free the sample with `graphics_sample_free`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn graphics_next_sample( + handle: *mut GraphicsHandle<'static>, + sample_out: *mut *mut IdeviceGraphicsSample, +) -> *mut IdeviceFfiError { + if handle.is_null() || sample_out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let client = unsafe { &mut (*handle).0 }; + let res = run_sync(async move { client.sample().await }); + + match res { + Ok(sample) => { + let c_sample = IdeviceGraphicsSample { + timestamp: sample.timestamp, + fps: sample.fps, + alloc_system_memory: sample.alloc_system_memory, + in_use_system_memory: sample.in_use_system_memory, + in_use_system_memory_driver: sample.in_use_system_memory_driver, + gpu_bundle_name: CString::new(sample.gpu_bundle_name) + .unwrap_or_default() + .into_raw(), + recovery_count: sample.recovery_count, + }; + unsafe { *sample_out = Box::into_raw(Box::new(c_sample)) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} diff --git a/ffi/src/dvt/mod.rs b/ffi/src/dvt/mod.rs index b7b388e..0d37743 100644 --- a/ffi/src/dvt/mod.rs +++ b/ffi/src/dvt/mod.rs @@ -3,6 +3,7 @@ #[cfg(feature = "location_simulation")] pub mod location_simulation; +pub mod notifications; pub mod process_control; pub mod remote_server; pub mod screenshot; @@ -13,6 +14,10 @@ pub mod application_listing; pub mod condition_inducer; #[cfg(feature = "device_info")] pub mod device_info; +#[cfg(feature = "energy_monitor")] +pub mod energy_monitor; +#[cfg(feature = "graphics")] +pub mod graphics; #[cfg(feature = "network_monitor")] pub mod network_monitor; #[cfg(feature = "sysmontap")] diff --git a/ffi/src/dvt/notifications.rs b/ffi/src/dvt/notifications.rs new file mode 100644 index 0000000..9d9de67 --- /dev/null +++ b/ffi/src/dvt/notifications.rs @@ -0,0 +1,168 @@ +// Jackson Coxson + +use std::{ffi::CString, ptr::null_mut}; + +use idevice::{ReadWrite, dvt::notifications::NotificationsClient}; + +use crate::{IdeviceFfiError, dvt::remote_server::RemoteServerHandle, ffi_err, run_sync}; + +pub struct NotificationsHandle<'a>(pub NotificationsClient<'a, Box>); + +/// A notification from the mobile notifications instruments channel +#[repr(C)] +pub struct IdeviceNotificationInfo { + pub notification_type: *mut std::ffi::c_char, + pub mach_absolute_time: i64, + pub exec_name: *mut std::ffi::c_char, + pub app_name: *mut std::ffi::c_char, + pub pid: u32, + pub state_description: *mut std::ffi::c_char, +} + +/// Frees an IdeviceNotificationInfo and its heap-allocated string fields +/// +/// # Safety +/// `info` must be a valid pointer allocated by this library or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notifications_info_free(info: *mut IdeviceNotificationInfo) { + if info.is_null() { + return; + } + let n = unsafe { Box::from_raw(info) }; + if !n.notification_type.is_null() { + let _ = unsafe { CString::from_raw(n.notification_type) }; + } + if !n.exec_name.is_null() { + let _ = unsafe { CString::from_raw(n.exec_name) }; + } + if !n.app_name.is_null() { + let _ = unsafe { CString::from_raw(n.app_name) }; + } + if !n.state_description.is_null() { + let _ = unsafe { CString::from_raw(n.state_description) }; + } +} + +/// Creates a new NotificationsClient from a RemoteServerClient +/// +/// # Safety +/// `server` must be a valid pointer to a handle allocated by this library +/// `handle` must be a valid pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notifications_new( + server: *mut RemoteServerHandle, + handle: *mut *mut NotificationsHandle<'static>, +) -> *mut IdeviceFfiError { + if server.is_null() || handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let server = unsafe { &mut (*server).0 }; + let res = run_sync(async move { NotificationsClient::new(server).await }); + + match res { + Ok(client) => { + let boxed = Box::new(NotificationsHandle(client)); + unsafe { *handle = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Frees a NotificationsClient handle +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notifications_free(handle: *mut NotificationsHandle<'static>) { + if !handle.is_null() { + let _ = unsafe { Box::from_raw(handle) }; + } +} + +/// Enables application state and memory notifications on the device. +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notifications_start( + handle: *mut NotificationsHandle<'static>, +) -> *mut IdeviceFfiError { + if handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let client = unsafe { &mut (*handle).0 }; + let res = run_sync(async move { client.start_notifications().await }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Disables application state and memory notifications on the device. +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notifications_stop( + handle: *mut NotificationsHandle<'static>, +) -> *mut IdeviceFfiError { + if handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let client = unsafe { &mut (*handle).0 }; + let res = run_sync(async move { client.stop_notifications().await }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Reads the next notification pushed by the device. Blocks until a notification arrives. +/// +/// # Arguments +/// * [`handle`] - The NotificationsClient handle +/// * [`info_out`] - On success, set to a heap-allocated IdeviceNotificationInfo +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// All pointers must be valid and non-null. Free the info with `notifications_info_free`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notifications_get_next( + handle: *mut NotificationsHandle<'static>, + info_out: *mut *mut IdeviceNotificationInfo, +) -> *mut IdeviceFfiError { + if handle.is_null() || info_out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let client = unsafe { &mut (*handle).0 }; + let res = run_sync(async move { client.get_notification().await }); + + match res { + Ok(info) => { + let c_info = IdeviceNotificationInfo { + notification_type: CString::new(info.notification_type) + .unwrap_or_default() + .into_raw(), + mach_absolute_time: info.mach_absolute_time, + exec_name: CString::new(info.exec_name).unwrap_or_default().into_raw(), + app_name: CString::new(info.app_name).unwrap_or_default().into_raw(), + pid: info.pid, + state_description: CString::new(info.state_description) + .unwrap_or_default() + .into_raw(), + }; + unsafe { *info_out = Box::into_raw(Box::new(c_info)) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} diff --git a/idevice/src/services/dvt/notifications.rs b/idevice/src/services/dvt/notifications.rs index fee7b73..761c180 100644 --- a/idevice/src/services/dvt/notifications.rs +++ b/idevice/src/services/dvt/notifications.rs @@ -15,12 +15,12 @@ use tracing::warn; #[derive(Debug)] pub struct NotificationInfo { - notification_type: String, - mach_absolute_time: i64, - exec_name: String, - app_name: String, - pid: u32, - state_description: String, + pub notification_type: String, + pub mach_absolute_time: i64, + pub exec_name: String, + pub app_name: String, + pub pid: u32, + pub state_description: String, } #[derive(Debug)] From 33563a4d05e658862595b95e5e5d6c71e74aaf6b Mon Sep 17 00:00:00 2001 From: neo Date: Sat, 25 Apr 2026 06:52:21 -0400 Subject: [PATCH 2/2] chore: cargo clippy --- idevice/src/xpc/http2/mod.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/idevice/src/xpc/http2/mod.rs b/idevice/src/xpc/http2/mod.rs index d93d441..aeffc78 100644 --- a/idevice/src/xpc/http2/mod.rs +++ b/idevice/src/xpc/http2/mod.rs @@ -94,18 +94,16 @@ impl Http2Client { let frame = frame::Frame::next(&mut self.inner).await?; // debug!("Got frame: {frame:#?}"); match frame { - frame::Frame::Settings(settings_frame) => { - if settings_frame.flags != 1 { - // ack that - let frame = frame::SettingsFrame { - settings: Vec::new(), - stream_id: settings_frame.stream_id, - flags: 1, - } - .serialize(); - self.inner.write_all(&frame).await?; - self.inner.flush().await?; + frame::Frame::Settings(settings_frame) if settings_frame.flags != 1 => { + // ack that + let frame = frame::SettingsFrame { + settings: Vec::new(), + stream_id: settings_frame.stream_id, + flags: 1, } + .serialize(); + self.inner.write_all(&frame).await?; + self.inner.flush().await?; } frame::Frame::Data(data_frame) => { debug!(