bela-sys 0.4.0

Raw FFI bindings to the Bela core API (libbela) for Bela Gem
Documentation
//! Raw FFI bindings to the Bela core API (`libbela`) for [Bela Gem].
//!
//! This crate exposes the C surface of the Bela core API (`Bela.h`):
//! `BelaContext`, `BelaInitSettings`, the `Bela_*` lifecycle and
//! auxiliary-task functions, and `rt_printf`. Bindings are generated
//! from vendored headers (see `vendor/bela/COMMIT` for the pinned
//! upstream commit) with `cargo xtask bindgen`; see the crate README
//! for how to regenerate them.
//!
//! One thing here is not the core API and not generated: the
//! [`bela_midi_*`](bela_midi_new) functions, a C surface this crate
//! compiles (`shim/midi.cpp`) over Bela's `Midi` class in
//! `libbelaextra`. MIDI is what a Bela program reaches for first after
//! audio, and the class is C++ with only half a C surface of its own.
//! The other higher-level C++ libraries (Scope, Trill, Fft, Gui)
//! remain out of scope.
//!
//! The `setup` / `render` / `cleanup` callbacks are not bound: they are
//! either provided to `Bela_initAudio` via [`BelaInitSettings`] or
//! defined as `#[unsafe(no_mangle)]` symbols by the linking crate.
//!
//! Target platform is Bela Gem on `PocketBeagle` 2
//! (`aarch64-unknown-linux-gnu`). For a safe API, use the `bela`
//! crate instead.
//!
//! [Bela Gem]: https://bela.io
#![no_std]

#[allow(
    missing_docs,
    nonstandard_style,
    unsafe_op_in_unsafe_fn,
    unused,
    clippy::all,
    clippy::pedantic,
    clippy::nursery,
    clippy::restriction,
    rustdoc::all,
    reason = "generated by bindgen; regenerate with `cargo xtask bindgen`"
)]
mod bindings;
mod midi;

pub use bindings::*;
pub use midi::{
    BELA_MIDI_ALREADY_OPEN, BELA_MIDI_MESSAGE_MAX, BELA_MIDI_NO_SUCH_PORT, BelaMidi,
    bela_midi_available_messages, bela_midi_delete, bela_midi_get_message, bela_midi_list_ports,
    bela_midi_new, bela_midi_read_from, bela_midi_write_output, bela_midi_write_to,
};

// The build script's toolchain logic, tested where a build script
// cannot be: `cargo test` builds this crate, not `build.rs`. See
// ../shim_compiler.rs.
#[cfg(test)]
mod shim_compiler {
    extern crate std;

    use std::borrow::ToOwned;
    use std::format;
    use std::string::String;

    include!("../shim_compiler.rs");

    #[test]
    fn bela_cxx_is_taken_as_it_stands() {
        assert_eq!(
            shim_compiler_from("clang++", "aarch64-linux-gnu-gcc"),
            Ok("clang++".to_owned()),
            "an explicit C++ compiler outranks anything derived"
        );
    }

    #[test]
    fn a_c_compiler_ending_in_gcc_answers_for_both() {
        // The two cases docs/cross-compile.md documents.
        assert_eq!(
            shim_compiler_from("", "aarch64-linux-gnu-gcc"),
            Ok("aarch64-linux-gnu-g++".to_owned())
        );
        assert_eq!(shim_compiler_from("", "gcc"), Ok("g++".to_owned()));
    }

    #[test]
    fn neither_set_is_the_tap_default() {
        assert_eq!(
            shim_compiler_from("", ""),
            Ok(DEFAULT_CXX.to_owned()),
            "the same default scripts/aarch64-bela-linker.sh has"
        );
    }

    #[test]
    fn a_c_compiler_nothing_follows_from_is_refused() {
        // Deriving `ar`, or a C++ name, from this would mix
        // toolchains silently, which the build script fails on
        // instead.
        let error = shim_compiler_from("", "clang").unwrap_err();
        assert!(
            error.contains("BELA_CXX"),
            "the message should say what to set, got: {error}"
        );
    }

    #[test]
    fn the_archiver_follows_the_compiler_it_belongs_to() {
        assert_eq!(
            shim_archiver("aarch64-unknown-linux-gnu-g++"),
            Some("aarch64-unknown-linux-gnu-ar".to_owned()),
            "cc would otherwise look for one named after the target triple"
        );
        assert_eq!(shim_archiver("g++"), Some("ar".to_owned()));
    }

    #[test]
    fn an_archiver_that_does_not_follow_is_left_to_cc() {
        // `clang++` wants llvm-ar, and it also ends in the letters
        // `g++`: deriving from it would name `clanar`. AR is the way
        // out for those, and cc reads it.
        assert_eq!(shim_archiver("clang++"), None);
        assert_eq!(shim_archiver("aarch64-linux-gnu-clang++"), None);
    }

    #[test]
    fn a_compiler_named_by_its_path_is_still_one() {
        // docs/cross-compile.md allows an absolute path, and
        // /usr/bin/gcc is the board's own compiler.
        assert_eq!(
            shim_compiler_from("", "/usr/bin/gcc"),
            Ok("/usr/bin/g++".to_owned())
        );
        assert_eq!(
            shim_compiler_from("", "/opt/tc/bin/aarch64-linux-gnu-gcc"),
            Ok("/opt/tc/bin/aarch64-linux-gnu-g++".to_owned())
        );
        assert_eq!(
            shim_archiver("/usr/bin/g++"),
            Some("/usr/bin/ar".to_owned()),
            "and the archiver beside it"
        );
    }

    #[test]
    fn a_compiler_that_merely_ends_in_gcc_is_not_one() {
        // Same trap on the compiler side: only a bare `gcc` or a
        // `<triple>-gcc` names a toolchain to follow.
        assert!(shim_compiler_from("", "notgcc").is_err());
        assert_eq!(shim_archiver("notg++"), None);
        assert_eq!(
            shim_archiver("/usr/bin/clang++"),
            None,
            "a path does not make it one"
        );
    }
}

// The shim's header and the declarations mirroring it are edited by
// hand, and a value that drifts between them is not a compile error —
// it is a safe API that reads one failure as another. This is the
// cheap half of the guard: the numbers.
#[cfg(test)]
mod shim_header {
    extern crate std;

    use std::format;

    /// `shim/midi.h`, read at compile time from the crate this
    /// declares the shim for.
    const HEADER: &str = include_str!("../shim/midi.h");

    /// The value of `#define <name> ...`, with one level of
    /// parentheses taken off — the header writes negative constants as
    /// `(-1000)`, as a C header should.
    fn defined(name: &str) -> i64 {
        let line = HEADER
            .lines()
            .find(|line| line.starts_with(&format!("#define {name} ")))
            .unwrap_or_else(|| panic!("{name} is not defined in shim/midi.h"));
        let value = line.split_whitespace().nth(2).expect("a value");
        value
            .trim_start_matches('(')
            .trim_end_matches(')')
            .parse()
            .expect("a number")
    }

    #[test]
    fn the_constants_match_the_header() {
        assert_eq!(
            defined("BELA_MIDI_MESSAGE_MAX"),
            i64::try_from(super::BELA_MIDI_MESSAGE_MAX).expect("a small buffer size")
        );
        assert_eq!(
            defined("BELA_MIDI_NO_SUCH_PORT"),
            i64::from(super::BELA_MIDI_NO_SUCH_PORT)
        );
        assert_eq!(
            defined("BELA_MIDI_ALREADY_OPEN"),
            i64::from(super::BELA_MIDI_ALREADY_OPEN)
        );
    }
}