Cidre
repository·main·Indexed 42 days ago
https://github.com/yury/cidreA 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.
What's inside Cidre
- Cidre provides Rust bindings for Apple frameworks. It allows developers to interface with Apple's native APIs directly from Rust code.
Cidre API Design Principles and Performance Features
mainCidre 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::Stringandns::Stringreferences using thens::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.
Run tests on iOS devices using cargo-box
mainTo run tests or examples directly on a physical iPhone or iPad, you must use the
cargo-boxplugin and a.boxconfiguration file. This allows you to targetaarch64-apple-iosand interact with connected mobile hardware.Setup Steps
- Install the plugin: Install
cargo-boxfrom the local path. - Identify Team ID: Use
cargo box teamsto find yourDEVELOPMENT_TEAMID. - Identify Device ID: Use
cargo box devicesto find yourDEVICE_ID. - Configure Box: Create a
.boxfile 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 theDEVICE_IDfrom your.boxfile; can be overridden with--device). - Debug with LLDB: To launch an app suspended so you can attach
lldb, use theBOX_DEBUG=1environment variable:BOX_DEBUG=1 cargo r --target aarch64-apple-ios --example <example_name>Insidelldb, usecontinueto run, orprocess kill/quitto 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- Install the plugin: Install
Run benchmarks on iOS with Criterion
mainThe
Runnersupports running benchmarks on iOS devices. If the arguments passed to the runner include--bench, the tool:- Launches the app under
lldbto prevent the iOS watchdog from killing the process (since benchmarks often don't have a UI). - Uses a helper script (
bench.py) to drive the app. - Moves the Criterion results directory to the app's
Documentsfolder. - After completion, copies the results from the device back to the host's
target/criteriondirectory.
- Launches the app under
Manage API Availability via deployment target features
mainCidre 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.
How AudioCodec components work
mainAudioCodec 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:
- Push: Input data is fed into the component via
append_data. - Pull: Resulting output data is extracted via
produce_data(orproduce_packets).
Basic Workflow:
- Find the appropriate codec component.
- Open the codec component.
- Configure it using properties (e.g.,
AudioCodecSetProperty). - Call
AudioCodecInitializeto enter the initialized state. - Loop:
append_data(signal EOF by passing a 0-sized buffer).produce_packets.
- Close the codec component.
- Decoders (
Manage AudioCodec lifecycle and states
mainAn
AudioCodectransitions betweenUninitializedStateandInitializedState.- 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
initializewith the desired input and outputStreamBasicDescand an optionalmagic_cookie.To return to the uninitialized state and release allocated resources, call
uninitializeon 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()?;How manual rendering modes work
mainThe
Enginesupports two types of manual rendering viaManualRenderingMode:Offline: The engine operates in an offline mode without any realtime constraints.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);Manage Audio Converter lifecycle
mainConverterRefimplementsDrop, meaning the underlyingAudioConverterDisposefunction 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-styleAudioConverterDisposefunction.Debug an app with BOX_DEBUG
mainWhen using the
Runnerfunctionality, you can instruct the tool to launch the app underlldbinstead of running it to completion by setting theBOX_DEBUGenvironment variable.Setting
BOX_DEBUGto any value other than0,false,no, or an empty string will trigger a debug session. The app will be launched in a suspended state, andlldbwill attach to the process, handing control to your terminal.Use DictionaryOf<K, V> for type-safe collections
mainTo avoid working with the generic
cf::Typeand instead use specific types, use theDictionaryOf<K, V>wrapper. This provides a type-safe interface whereKandVmust implementarc::Retain.DictionaryOf<K, V>: An immutable, type-safe dictionary. Usewith_keys_valuesto create it. Use.get(&K)to retrieve values.DictionaryOfMut<K, V>: A mutable, type-safe dictionary. You can create it from an existingDictionaryOfusing.copy_mut(). Use.insert(&K, &V)and.remove(&K).
If the
objcfeature is enabled andK/Vareobjc::Objtypes,DictionaryOfsupports toll-free bridging tons::Dictionaryvia.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);Implement Objective-C protocols with `#[protocol]`
mainThe
#[protocol]attribute macro allows you to define a Rust trait that represents an Objective-C protocol.Usage:
- Pass the Objective-C protocol name as the first argument to the macro.
- Define a trait (usually named
...Impl) containing the methods required by the protocol. - Use
#[objc::add_methods]on animplblock 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 { // ... }