#apple #xpc #wrapper #shared-memory #conversion #xpc-dictionary

sys xpc-sys

Conveniently call routines with wrappers for xpc_pipe_routine() and go from Rust types to XPC objects and back!

11 releases

0.5.1 Apr 15, 2024
0.5.0 Dec 21, 2023
0.4.2 Nov 17, 2023
0.4.1 Aug 13, 2023
0.1.0 May 29, 2021

#33 in macOS and iOS APIs

Download history 69/week @ 2024-07-24 21/week @ 2024-07-31 7/week @ 2024-09-18 14/week @ 2024-09-25 2/week @ 2024-10-02

599 downloads per month

MIT license

61KB
1K SLoC

xpc-sys

Rust crates.io

Various utilities for conveniently dealing with XPC in Rust.

Getting Started

Conversions to/from Rust/XPC objects uses the xpc.h functions documented on Apple Developer using the From trait. Complex types such as arrays and shared memory objects described in greater detail below.

Rust XPC
i64 _xpc_type_int64
u64 _xpc_type_uint64
f64 _xpc_type_double
bool _xpc_bool_true/false
Into _xpc_type_string
HashMap<Into, Into> _xpc_type_dictionary
Vec<Into> _xpc_type_array
std::os::unix::prelude::RawFd _xpc_type_fd
(MachPortType::Send, mach_port_t) _xpc_type_mach_send
(MachPortType::Recv, mach_port_t) _xpc_type_mach_recv
XPCShmem _xpc_type_shmem

Make XPC objects for anything with From<T>. Make sure to use the correct type for file descriptors and Mach ports:

let mut message: HashMap<&str, XPCObject> = HashMap::new();

message.insert(
    "domain-port",
    XPCObject::from((MachPortType::Send, unsafe {
        get_bootstrap_port() as mach_port_t
    })),
);

Go from an XPC object to value via the TryXPCValue trait. It checks your object's type via xpc_get_type() and yields a clear error if you're using the wrong type:

#[test]
fn deserialize_as_wrong_type() {
    let an_i64: XPCObject = XPCObject::from(42 as i64);
    let as_u64: Result<u64, XPCError> = an_i64.xpc_value();
    assert_eq!(
        as_u64.err().unwrap(),
        XPCValueError("Cannot get int64 as uint64".to_string())
    );
}

Top

Object lifecycle

XPCObject wraps a xpc_object_t:

pub struct XPCObject(pub xpc_object_t, pub XPCType);

When it is dropped, xpc_release is called.

Top

QueryBuilder

While we can go from HashMap<&str, XPCObject> to XPCObject, it can be a little verbose. A QueryBuilder trait exposes some builder methods to make building an XPC dictionary a little easier (without all of the into()s, and some additional error checking).

To write the query for launchctl list:

    let LIST_SERVICES: XPCDictionary = XPCDictionary::new()
        // "list com.apple.Spotlight" (if specified)
        // .entry("name", "com.apple.Spotlight");
        .entry("subsystem", 3 as u64)
        .entry("handle", 0 as u64)
        .entry("routine", 815 as u64)
        .entry("legacy", true);

    let reply: Result<XPCDictionary, XPCError> = XPCDictionary::new()
        // LIST_SERVICES is a proto 
        .extend(&LIST_SERVICES)
        // Specify the domain type, or fall back on requester domain
        .with_domain_type_or_default(Some(domain_type))
        .entry_if_present("name", name)
        .pipe_routine_with_error_handling();

In addition to checking errno is 0, pipe_routine_with_error_handling also looks for possible error and errors keys in the response dictionary and provides an Err() with xpc_strerror contents.

Top

XPC Dictionary

Go from a HashMap to xpc_object_t with the XPCObject type:

let mut message: HashMap<&str, XPCObject> = HashMap::new();
message.insert("type", XPCObject::from(1 as u64));
message.insert("handle", XPCObject::from(0 as u64));
message.insert("subsystem", XPCObject::from(3 as u64));
message.insert("routine", XPCObject::from(815 as u64));
message.insert("legacy", XPCObject::from(true));

let xpc_object: XPCObject = message.into();

Call xpc_pipe_routine and receive Result<XPCObject, XPCError>:

let xpc_object: XPCObject = message.into();

match xpc_object.pipe_routine() {
    Ok(xpc_object) => { /* do stuff and things */ },
    Err(XPCError::PipeError(err)) => { /* err is a string w/strerror(errno) */ }
}

The response is likely an XPC dictionary -- go back to a HashMap:

let xpc_object: XPCObject = message.into();
let response: Result<XPCDictionary, XPCError> = xpc_object
    .pipe_routine()
    .and_then(|r| r.try_into());

let XPCDictionary(hm) = response.unwrap();
let whatever = hm.get("...");

Response dictionaries can be nested, so XPCDictionary has a helper included for this scenario:

let xpc_object: XPCObject = message.into();

// A string: either "Aqua", "StandardIO", "Background", "LoginWindow", "System"
let response: Result<String, XPCError> = xpc_object
    .pipe_routine()
    .and_then(|r: XPCObject| r.try_into());
    .and_then(|d: XPCDictionary| d.get(&["service", "LimitLoadToSessionType"])
    .and_then(|lltst: XPCObject| lltst.xpc_value());

Or, retrieve the service key (a child XPC Dictionary) from this response:

let xpc_object: XPCObject = message.into();

// A string: either "Aqua", "StandardIO", "Background", "LoginWindow", "System"
let response: Result<XPCDictionary, XPCError> = xpc_object
    .pipe_routine()
    .and_then(|r: XPCObject| r.try_into());
    .and_then(|d: XPCDictionary| d.get_as_dictionary(&["service"]);

let XPCDictionary(hm) = response.unwrap();
let whatever = hm.get("...");

Top

XPC Array

An XPC array can be made from either Vec<XPCObject> or Vec<Into<XPCObject>>:

let xpc_array = XPCObject::from(vec![XPCObject::from("eins"), XPCObject::from("zwei"), XPCObject::from("polizei")]);

let xpc_array = XPCObject::from(vec!["eins", "zwei", "polizei"]);

Go back to Vec using xpc_value:

let rs_vec: Vec<XPCObject> = xpc_array.xpc_value().unwrap();

Top

XPC Shmem

Make XPC shared memory objects by providing a size and vm_allocate/mmap flags. vm_allocate is used to create the memory region, and vm_deallocate when XPCShmem is dropped.

let shmem = XPCShmem::new_task_self(
    0x1400000,
    i32::try_from(MAP_SHARED).expect("Must conv flags"),
)?;

// Use as _xpc_type_shmem argument in XPCDictionary
let response = XPCDictionary::new()
    .extend(&DUMPSTATE)
    .entry("shmem", shmem.xpc_object.clone())
    .pipe_routine_with_error_handling()?;

To work with the shmem region, use slice_from_raw_parts:

let bytes: &[u8] = unsafe {
    &*slice_from_raw_parts(shmem.region as *mut u8, size)
};

// Make a string from bytes in the shmem
let mut hey_look_a_string = String::new();
bytes.read_to_string(buf);

Top

Credits

A big thanks to these open source projects and general resources:

Everything else (C) David Stancu & Contributors 2021

Dependencies

~0.1–2.1MB
~42K SLoC