apple-cf 0.11.0

Safe Rust bindings for Apple's shared Core* frameworks (CoreFoundation, CoreMedia, CoreVideo, CoreGraphics, IOSurface, Dispatch).
Documentation
use apple_cf::cf::{
    CFFileDescriptor, CFMessagePort, CFNotificationCenter, CFRunLoop, CFRunLoopRunResult, CFSocket,
    CFStreamPair, CFString, CFTimer,
};
use apple_cf::cf::CFDictionary;
use std::io::{Read, Write};
use std::os::fd::{AsFd, AsRawFd, OwnedFd};
use std::os::unix::net::UnixStream;
use std::process;
use std::sync::{Arc, Mutex};
use std::time::Duration;

#[test]
fn cf_runtime_wrappers_work() {
    let center = CFNotificationCenter::local();
    center.post(
        &CFString::new("com.doomfish.apple-cf.runtime-tests"),
        None,
        false,
    );

    let timer = CFTimer::new(Duration::from_millis(10), false);
    let run_loop = CFRunLoop::current();
    run_loop.add_timer(&timer);
    assert!(timer.is_valid());
    let result = CFRunLoop::run_in_default_mode(Duration::from_millis(20), true);
    assert!(matches!(
        result,
        CFRunLoopRunResult::Finished
            | CFRunLoopRunResult::Stopped
            | CFRunLoopRunResult::TimedOut
            | CFRunLoopRunResult::HandledSource
    ));

    let name = format!("com.doomfish.apple-cf.echo.test.{}", process::id());
    let _local = CFMessagePort::create_echo_local(&name).expect("local port");
    let remote = CFMessagePort::connect_remote(&name).expect("remote port");
    let reply = remote
        .send_request(b"ping", Duration::from_millis(100))
        .expect("reply");
    assert_eq!(reply, b"ping");

    let pair = CFStreamPair::new(1024);
    assert!(pair.read.open());
    assert!(pair.write.open());
    assert_eq!(pair.write.write(b"ok").expect("write"), 2);
    let mut buffer = [0_u8; 2];
    assert_eq!(pair.read.read(&mut buffer).expect("read"), 2);
    assert_eq!(&buffer, b"ok");
    pair.read.close();
    pair.write.close();

    let socket = CFSocket::udp_ipv4().expect("socket");
    assert!(socket.is_valid());
    assert!(socket.native() >= 0);

    let stdin = std::io::stdin();
    let fd = CFFileDescriptor::from_borrowed_fd(std::os::fd::AsFd::as_fd(&stdin))
        .expect("file descriptor");
    assert_eq!(fd.native_descriptor(), 0);
}

#[test]
fn message_port_names_with_nul_are_rejected() {
    assert!(CFMessagePort::create_echo_local("com.doomfish.apple-cf\0echo").is_none());
    assert!(CFMessagePort::connect_remote("com.doomfish.apple-cf\0echo").is_none());
}

#[test]
fn owned_file_descriptor_is_closed_when_the_last_reference_drops() {
    let (local, mut peer) = UnixStream::pair().expect("socket pair");
    peer.set_read_timeout(Some(Duration::from_secs(5)))
        .expect("read timeout");
    let descriptor = CFFileDescriptor::from_owned_fd(OwnedFd::from(local)).expect("descriptor");
    let clone = descriptor.clone();
    drop(descriptor);
    drop(clone);
    let mut buffer = [0_u8; 1];
    assert_eq!(peer.read(&mut buffer).expect("peer read"), 0);
}

#[test]
fn owned_file_descriptor_is_closed_on_invalidate() {
    let (local, mut peer) = UnixStream::pair().expect("socket pair");
    peer.set_read_timeout(Some(Duration::from_secs(5)))
        .expect("read timeout");
    let descriptor = CFFileDescriptor::from_owned_fd(OwnedFd::from(local)).expect("descriptor");
    descriptor.invalidate();
    let mut buffer = [0_u8; 1];
    assert_eq!(peer.read(&mut buffer).expect("peer read"), 0);
}

#[test]
fn borrowed_file_descriptor_is_never_closed() {
    let (mut local, mut peer) = UnixStream::pair().expect("socket pair");
    let descriptor = CFFileDescriptor::from_borrowed_fd(local.as_fd()).expect("descriptor");
    assert_eq!(descriptor.native_descriptor(), local.as_raw_fd());
    descriptor.invalidate();
    drop(descriptor);
    local.write_all(b"x").expect("write");
    let mut buffer = [0_u8; 1];
    peer.read_exact(&mut buffer).expect("read");
    assert_eq!(&buffer, b"x");
}

#[test]
fn local_notification_observer_receives_posts_until_dropped() {
    let center = CFNotificationCenter::local();
    let name = CFString::new(&format!(
        "com.doomfish.apple-cf.observer.{}",
        process::id()
    ));
    let received = Arc::new(Mutex::new(Vec::new()));
    let sink = Arc::clone(&received);
    let observer = center.add_observer(&name, move |name, user_info| {
        sink.lock()
            .expect("sink")
            .push((name.to_string(), user_info.map(CFDictionary::len)));
    });
    let key = CFString::new("key");
    let value = CFString::new("value");
    let user_info = CFDictionary::from_pairs(&[(&key, &value)]);
    center.post(&name, Some(&user_info), true);
    center.post(&name, None, true);
    drop(observer);
    center.post(&name, None, true);
    assert_eq!(
        *received.lock().expect("received"),
        vec![(name.to_string(), Some(1)), (name.to_string(), None)]
    );
}

#[test]
fn panicking_notification_observer_does_not_unwind_into_core_foundation() {
    let center = CFNotificationCenter::local();
    let name = CFString::new(&format!(
        "com.doomfish.apple-cf.panicking-observer.{}",
        process::id()
    ));
    let calls = Arc::new(Mutex::new(0_u32));
    let counter = Arc::clone(&calls);
    let _observer = center.add_observer(&name, move |_, _| {
        *counter.lock().expect("counter") += 1;
        panic!("observer panic");
    });
    center.post(&name, None, true);
    center.post(&name, None, true);
    assert_eq!(*calls.lock().expect("calls"), 2);
}

#[test]
fn run_loops_can_be_stopped_from_another_thread() {
    fn assert_send_sync<T: Send + Sync>() {}
    assert_send_sync::<CFRunLoop>();

    let (run_loop_tx, run_loop_rx) = std::sync::mpsc::channel();
    let (result_tx, result_rx) = std::sync::mpsc::channel();
    let worker = std::thread::spawn(move || {
        let timer = CFTimer::new(Duration::from_secs(60), true);
        let run_loop = CFRunLoop::current();
        run_loop.add_timer(&timer);
        run_loop_tx.send(run_loop).expect("send run loop");
        let result = CFRunLoop::run_in_default_mode(Duration::from_secs(20), false);
        timer.invalidate();
        result_tx.send(result).expect("send result");
    });

    let worker_run_loop = run_loop_rx.recv().expect("worker run loop");
    assert_ne!(worker_run_loop, CFRunLoop::current());
    let result = loop {
        worker_run_loop.stop();
        worker_run_loop.wake_up();
        if let Ok(result) = result_rx.recv_timeout(Duration::from_millis(20)) {
            break result;
        }
    };
    assert_eq!(result, CFRunLoopRunResult::Stopped);
    worker.join().expect("join worker");
}

#[test]
fn run_loop_descriptions_do_not_read_the_loop_state() {
    let run_loop = CFRunLoop::current();
    let expected = format!("<CFRunLoop {:p}>", run_loop.as_ptr());
    let debug = format!("{run_loop:?}");
    assert!(debug.contains(&expected), "{debug}");
    assert!(!debug.contains("modes"), "{debug}");
    let erased = run_loop.into_cf_type();
    assert_eq!(erased.description(), expected);
    assert_eq!(erased.to_string(), expected);
    assert!(!format!("{erased:?}").contains("modes"));
}

#[test]
fn run_loops_can_be_described_while_another_thread_mutates_them() {
    let (run_loop_tx, run_loop_rx) = std::sync::mpsc::channel();
    let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
    let worker = std::thread::spawn(move || {
        let keep_alive = CFTimer::new(Duration::from_secs(60), true);
        CFRunLoop::current().add_timer(&keep_alive);
        run_loop_tx.send(CFRunLoop::current()).expect("send run loop");
        while done_rx.try_recv().is_err() {
            let _ = CFRunLoop::run_in_default_mode(Duration::from_millis(5), false);
        }
        keep_alive.invalidate();
    });
    let worker_run_loop = run_loop_rx.recv().expect("worker run loop");
    let describer = {
        let run_loop = worker_run_loop.clone();
        std::thread::spawn(move || {
            for _ in 0..2_000 {
                assert!(format!("{run_loop:?}").starts_with("CFRunLoop"));
            }
        })
    };
    for _ in 0..2_000 {
        let timer = CFTimer::new(Duration::from_secs(60), false);
        worker_run_loop.add_timer(&timer);
        assert!(timer.is_valid());
        timer.invalidate();
    }
    describer.join().expect("join describer");
    done_tx.send(()).expect("stop worker");
    worker.join().expect("join worker");
}