Cidre

repository·main·Indexed 42 days ago

https://github.com/yury/cidre

A high-performance Rust library providing zero-cost bindings to Apple's Objective-C, C, and Swift APIs. Designed for low-latency applications like streaming and ML, it supports macOS, iOS, tvOS, watchOS, and visionOS. Includes the cargo-box plugin for running tests, benchmarks, and capturing screenshots on physical iOS devices.

Tokens
27.1K
Snippets
91
Records
112
Agent score
52%

What's inside Cidre

  1. Cidre API Design Principles and Performance Features

    main

    Cidre is designed for high-performance, zero-cost interoperability with Apple's Objective-C, C, and Swift APIs.

    Key Performance Characteristics

    • Zero-cost Interop: Provides zero-cost selector calls on ARM (XCode 14.3+) and zero-cost interfaces for Apple's C APIs.
    • Memory Management: Allows developers to complete critical paths (such as Metal render passes) without deallocations.
    • Async Support: Integrated async blocks and async API support.
    • String Handling: Provides static cf::String and ns::String references using the ns::str! macro.

    Core Goals

    • Mobile device support (macOS, iOS/iPadOS, tvOS; watchOS and visionOS in development).
    • ML-friendly architecture.
    • Rusty API surface for Apple frameworks.
  2. Run tests on iOS devices using cargo-box

    main

    To run tests or examples directly on a physical iPhone or iPad, you must use the cargo-box plugin and a .box configuration file. This allows you to target aarch64-apple-ios and interact with connected mobile hardware.

    Setup Steps

    1. Install the plugin: Install cargo-box from the local path.
    2. Identify Team ID: Use cargo box teams to find your DEVELOPMENT_TEAM ID.
    3. Identify Device ID: Use cargo box devices to find your DEVICE_ID.
    4. Configure Box: Create a .box file in your project root with the following structure:
      • BOX_ORG_ID: A unique organization identifier (e.g., org.cidre).
      • DEVELOPMENT_TEAM: The team ID found in step 2.
      • DEVICE_ID: The device ID found in step 3.

    Execution Commands

    • Run tests: cargo t --target aarch64-apple-ios (ensure device is connected and unlocked).
    • Run examples: cargo r --target aarch64-apple-ios --example <example_name>.
    • Capture screenshot: cargo box capture (uses the DEVICE_ID from your .box file; can be overridden with --device).
    • Debug with LLDB: To launch an app suspended so you can attach lldb, use the BOX_DEBUG=1 environment variable: BOX_DEBUG=1 cargo r --target aarch64-apple-ios --example <example_name> Inside lldb, use continue to run, or process kill/quit to stop.
    # 1. Install cargo-box
    cargo install --path ./cargo-box
    
    # 2. Get IDs
    cargo box teams
    cargo box devices
    
    # 3. Create .box file
    # BOX_ORG_ID = org.yourname
    # DEVELOPMENT_TEAM = <team_id>
    # DEVICE_ID = <device_id>
    
    # 4. Run tests
    cargo t --target aarch64-apple-ios
    
    # 5. Debugging
    BOX_DEBUG=1 cargo r --target aarch64-apple-ios --example device-formats
  3. Run benchmarks on iOS with Criterion

    main

    The Runner supports running benchmarks on iOS devices. If the arguments passed to the runner include --bench, the tool:

    1. Launches the app under lldb to prevent the iOS watchdog from killing the process (since benchmarks often don't have a UI).
    2. Uses a helper script (bench.py) to drive the app.
    3. Moves the Criterion results directory to the app's Documents folder.
    4. After completion, copies the results from the device back to the host's target/criterion directory.
  4. Manage API Availability via deployment target features

    main

    Cidre uses Cargo features to control deployment targets for different Apple platforms. This allows you to manage which APIs are available based on the OS version you are targeting.

    Available Platform Features

    • macos_x_x (e.g., macos_15_0)
    • ios_x_x (e.g., ios_18_0)
    • tvos_x_x (e.g., tvos_18_0)
    • maccatalyst_x_x (e.g., maccatalyst_18_0)
    • watchos_x_x (e.g., watchos_11_0)
    • visionos_x_x (e.g., visionos_2_0)

    Safety Warning

    If a selector is not defined within your specified deployment target feature, calling it becomes unsafe. Developers must manually check if an object responds to a selector before invoking it to prevent crashes on older OS versions.

  5. How AudioCodec components work

    main

    AudioCodec components translate audio data between formats. There are three types:

    • Decoders (adec): Translate non-linear PCM data into linear PCM.
    • Encoders (aenc): Translate linear PCM data into a compressed format.
    • Unity codecs (acdc): Translate between different flavors of the same type (e.g., 16-bit signed integer PCM to 32-bit floating point PCM).

    AudioCodecs follow a push-pull model:

    1. Push: Input data is fed into the component via append_data.
    2. Pull: Resulting output data is extracted via produce_data (or produce_packets).

    Basic Workflow:

    1. Find the appropriate codec component.
    2. Open the codec component.
    3. Configure it using properties (e.g., AudioCodecSetProperty).
    4. Call AudioCodecInitialize to enter the initialized state.
    5. Loop:
      • append_data (signal EOF by passing a 0-sized buffer).
      • produce_packets.
    6. Close the codec component.
  6. Manage AudioCodec lifecycle and states

    main

    An AudioCodec transitions between UninitializedState and InitializedState.

    • Uninitialized State: The codec can be opened and configured via properties.
    • Initialized State: The codec is ready for data processing. In this state, format information cannot be changed.

    To move from uninitialized to initialized, call initialize with the desired input and output StreamBasicDesc and an optional magic_cookie.

    To return to the uninitialized state and release allocated resources, call uninitialize on an initialized codec. This allows the codec to be reconfigured.

    // Example transition from Uninitialized to Initialized
    let codec_ref = codec_ref.initialize(
        &input_format,
        &output_format,
        Some(&magic_cookie_bytes),
    )?;
    
    // Later, return to uninitialized
    let codec_ref_uninit = codec_ref.uninitialize()?;
  7. How manual rendering modes work

    main

    The Engine supports two types of manual rendering via ManualRenderingMode:

    1. Offline: The engine operates in an offline mode without any realtime constraints.
    2. Realtime: The engine operates under realtime constraints. It will not make any blocking calls (e.g., libdispatch, blocking on a mutex, or memory allocation) during rendering. This mode requires using the block-based render mechanism (AVAudioEngine(manualRenderingBlock)).
    // Example of Offline mode
    engine.enable_manual_rendering_mode(ManualRenderingMode::Offline, &format, 1024);
    
    // Example of Realtime mode (requires block-based rendering)
    engine.enable_manual_rendering_mode(ManualRenderingMode::Realtime, &format, 1024);
  8. Manage Audio Converter lifecycle

    main
    ConverterRef implements Drop, meaning the underlying AudioConverterDispose function is automatically called when the object goes out of scope, ensuring proper resource cleanup. You do not need to manually call dispose unless you are working with the raw C-style AudioConverterDispose function.
  9. Debug an app with BOX_DEBUG

    main

    When using the Runner functionality, you can instruct the tool to launch the app under lldb instead of running it to completion by setting the BOX_DEBUG environment variable.

    Setting BOX_DEBUG to any value other than 0, false, no, or an empty string will trigger a debug session. The app will be launched in a suspended state, and lldb will attach to the process, handing control to your terminal.

  10. Use DictionaryOf<K, V> for type-safe collections

    main

    To avoid working with the generic cf::Type and instead use specific types, use the DictionaryOf<K, V> wrapper. This provides a type-safe interface where K and V must implement arc::Retain.

    • DictionaryOf<K, V>: An immutable, type-safe dictionary. Use with_keys_values to create it. Use .get(&K) to retrieve values.
    • DictionaryOfMut<K, V>: A mutable, type-safe dictionary. You can create it from an existing DictionaryOf using .copy_mut(). Use .insert(&K, &V) and .remove(&K).

    If the objc feature is enabled and K/V are objc::Obj types, DictionaryOf supports toll-free bridging to ns::Dictionary via .as_ns().

    use cidre::cf;
    
    // Creating a type-safe immutable dictionary
    let key = cf::String::from_static("key");
    let val = cf::Number::from_i8(100);
    let d = cf::DictionaryOf::<cf::String, cf::Number>::with_keys_values(&[&key], &[&val]).unwrap();
    
    if let Some(v) = d.get(&key) { 
        // v is &cf::Number
    }
    
    // Creating a type-safe mutable dictionary from an immutable one
    let mut d_mut = d.copy_mut().unwrap();
    let new_val = cf::Number::from_i8(200);
    d_mut.insert(&key, &new_val);
  11. Implement Objective-C protocols with `#[protocol]`

    main

    The #[protocol] attribute macro allows you to define a Rust trait that represents an Objective-C protocol.

    Usage:

    1. Pass the Objective-C protocol name as the first argument to the macro.
    2. Define a trait (usually named ...Impl) containing the methods required by the protocol.
    3. Use #[objc::add_methods] on an impl block to register these methods with an Objective-C class.

    Methods in the trait can be marked as #[objc::optional] if they are not required by the protocol.

    #[protocol(MyProtocolName)]
    trait MyProtocol { 
        // ... 
    }