apple-cf 0.10.0

Safe Rust bindings for Apple's shared Core* frameworks (CoreFoundation, CoreMedia, CoreVideo, CoreGraphics, IOSurface, Dispatch).
Documentation
use apple_cf::cf::CFString;
use apple_cf::cv::{
    CVAttachmentMode, CVBuffer, CVImageBuffer, CVMetalTextureCache, CVPixelBuffer,
    CVPixelBufferPool, CVPixelBufferPoolFlushFlags,
};
use std::collections::HashMap;
use std::sync::mpsc;
use std::thread;

#[test]
fn cv_buffer_attachment_round_trip() {
    let pixel_buffer = CVPixelBuffer::create(16, 16, 0x4247_5241).expect("pixel buffer");
    let buffer = CVBuffer::from_pixel_buffer(&pixel_buffer).expect("buffer");
    let key = CFString::new("com.doomfish.apple-cf.cv-buffer-tests");
    let value = CFString::new("attached");

    buffer.set_attachment(&key, &value, CVAttachmentMode::ShouldPropagate);
    assert!(buffer.attachment(&key).is_some());
    assert!(buffer
        .attachments(CVAttachmentMode::ShouldPropagate)
        .is_some());
    buffer.remove_all_attachments();
    assert!(buffer.attachment(&key).is_none());
}

#[test]
fn cv_image_buffer_smoke() {
    let pixel_buffer = CVPixelBuffer::create(8, 4, 0x4247_5241).expect("pixel buffer");
    let image_buffer = CVImageBuffer::from_pixel_buffer(&pixel_buffer).expect("image buffer");
    assert!((image_buffer.encoded_size().width - 8.0).abs() < f64::EPSILON);
    assert!((image_buffer.display_size().height - 4.0).abs() < f64::EPSILON);
}

#[test]
fn cv_metal_texture_cache_smoke() {
    let cache = CVMetalTextureCache::system_default().expect("system default Metal device");
    cache.flush();
}

fn allocation_threshold_key() -> String {
    let ptr = unsafe { apple_cf::raw::kCVPixelBufferPoolAllocationThresholdKey };
    unsafe { CFString::from_raw_borrowed(ptr.cast_mut().cast()) }
        .expect("allocation threshold key")
        .to_string()
}

#[test]
fn cv_pixel_buffer_pool_enforces_cap_and_reuses_buffers() {
    let pool = CVPixelBufferPool::create(16, 16, 0x4247_5241, 2).expect("pool");
    assert_eq!(pool.max_buffers(), 2);

    let first = pool.create_pixel_buffer().expect("first buffer");
    let second = pool.create_pixel_buffer().expect("second buffer");
    assert_eq!(pool.create_pixel_buffer().unwrap_err(), -6689);
    assert!(pool
        .try_create_pixel_buffer()
        .expect("threshold result")
        .is_none());

    drop(first);
    let reused = pool.create_pixel_buffer().expect("recycled buffer");
    drop((reused, second));
}

#[test]
fn cv_pixel_buffer_pool_cap_is_shared_by_clones_and_raw_imports() {
    let pool = CVPixelBufferPool::create(8, 8, 0x4247_5241, 1).expect("pool");
    let cloned = pool.clone();
    let first = cloned.create_pixel_buffer().expect("first buffer");
    assert_eq!(pool.create_pixel_buffer().unwrap_err(), -6689);

    let retained = unsafe { apple_cf::raw::CVPixelBufferPoolRetain(pool.as_ptr().cast()) };
    let imported = unsafe { CVPixelBufferPool::from_raw_with_max_buffers(retained.cast(), 1) }
        .expect("raw policy")
        .expect("retained pool");
    assert_eq!(imported.as_ptr(), pool.as_ptr());
    assert_eq!(imported.max_buffers(), 1);
    assert_eq!(imported.create_pixel_buffer().unwrap_err(), -6689);

    drop((first, cloned, pool));
    assert!(imported.create_pixel_buffer().is_ok());
}

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

    let pool = CVPixelBufferPool::create(8, 8, 0x4247_5241, 2).expect("pool");
    let (ready_tx, ready_rx) = mpsc::channel();
    let mut releases = Vec::new();
    let mut workers = Vec::new();
    for _ in 0..2 {
        let pool = pool.clone();
        let ready_tx = ready_tx.clone();
        let (release_tx, release_rx) = mpsc::channel();
        releases.push(release_tx);
        workers.push(thread::spawn(move || {
            let buffer = pool.create_pixel_buffer().expect("worker allocation");
            ready_tx.send(pool.max_buffers()).expect("ready signal");
            release_rx.recv().expect("release signal");
            drop(buffer);
        }));
    }
    drop(ready_tx);

    assert_eq!(ready_rx.recv().expect("first worker"), 2);
    assert_eq!(ready_rx.recv().expect("second worker"), 2);
    assert_eq!(pool.create_pixel_buffer().unwrap_err(), -6689);
    for release in releases {
        release.send(()).expect("release worker");
    }

    for worker in workers {
        worker.join().expect("pool worker");
    }
    assert!(pool.create_pixel_buffer().is_ok());
}

#[test]
fn cv_pixel_buffer_pool_applies_per_call_thresholds_and_flush_flags() {
    let pool = CVPixelBufferPool::create(8, 8, 0x4247_5241, 2).expect("pool");
    let mut attributes = HashMap::new();
    attributes.insert(allocation_threshold_key(), 1);

    let first = pool
        .create_pixel_buffer_with_aux_attributes(Some(&attributes))
        .expect("first threshold allocation");
    assert_eq!(
        pool.create_pixel_buffer_with_aux_attributes(Some(&attributes))
            .unwrap_err(),
        -6689
    );

    pool.flush_with_flags(CVPixelBufferPoolFlushFlags::NONE);
    pool.flush_with_flags(CVPixelBufferPoolFlushFlags::EXCESS_BUFFERS);
    pool.flush_excess_buffers();
    assert!(pool.attributes().is_some());
    assert!(pool.pixel_buffer_attributes().is_some());

    drop(first);
    assert!(pool
        .create_pixel_buffer_with_aux_attributes(Some(&attributes))
        .is_ok());

    let capped = CVPixelBufferPool::create(8, 8, 0x4247_5241, 1).expect("capped pool");
    let held = capped.create_pixel_buffer().expect("held buffer");
    attributes.insert(allocation_threshold_key(), 3);
    assert_eq!(
        capped
            .create_pixel_buffer_with_aux_attributes(Some(&attributes))
            .unwrap_err(),
        -6689
    );
    drop(held);

    let invalid_attributes = HashMap::from([("invalid\0key".to_string(), 1)]);
    assert_eq!(
        pool.create_pixel_buffer_with_aux_attributes(Some(&invalid_attributes))
            .unwrap_err(),
        -50
    );
}

#[test]
fn cv_pixel_buffer_pool_zero_cap_is_unlimited() {
    let pool = CVPixelBufferPool::create(4, 4, 0x4247_5241, 0).expect("pool");
    assert_eq!(pool.max_buffers(), 0);
    let buffers = [
        pool.create_pixel_buffer().expect("unlimited allocation"),
        pool.create_pixel_buffer().expect("unlimited allocation"),
        pool.create_pixel_buffer().expect("unlimited allocation"),
        pool.create_pixel_buffer().expect("unlimited allocation"),
    ];
    assert_eq!(buffers.len(), 4);
}