Skip to main content

bela_sys/
lib.rs

1//! Raw FFI bindings to the Bela core API (`libbela`) for [Bela Gem].
2//!
3//! This crate exposes the C surface of the Bela core API (`Bela.h`):
4//! `BelaContext`, `BelaInitSettings`, the `Bela_*` lifecycle and
5//! auxiliary-task functions, and `rt_printf`. Bindings are generated
6//! from vendored headers (see `vendor/bela/COMMIT` for the pinned
7//! upstream commit) with `cargo xtask bindgen`; see the crate README
8//! for how to regenerate them.
9//!
10//! One thing here is not the core API and not generated: the
11//! [`bela_midi_*`](bela_midi_new) functions, a C surface this crate
12//! compiles (`shim/midi.cpp`) over Bela's `Midi` class in
13//! `libbelaextra`. MIDI is what a Bela program reaches for first after
14//! audio, and the class is C++ with only half a C surface of its own.
15//! The other higher-level C++ libraries (Scope, Trill, Fft, Gui)
16//! remain out of scope.
17//!
18//! The `setup` / `render` / `cleanup` callbacks are not bound: they are
19//! either provided to `Bela_initAudio` via [`BelaInitSettings`] or
20//! defined as `#[unsafe(no_mangle)]` symbols by the linking crate.
21//!
22//! Target platform is Bela Gem on `PocketBeagle` 2
23//! (`aarch64-unknown-linux-gnu`). For a safe API, use the `bela`
24//! crate instead.
25//!
26//! [Bela Gem]: https://bela.io
27#![no_std]
28
29#[allow(
30    missing_docs,
31    nonstandard_style,
32    unsafe_op_in_unsafe_fn,
33    unused,
34    clippy::all,
35    clippy::pedantic,
36    clippy::nursery,
37    clippy::restriction,
38    rustdoc::all,
39    reason = "generated by bindgen; regenerate with `cargo xtask bindgen`"
40)]
41mod bindings;
42mod midi;
43
44pub use bindings::*;
45pub use midi::{
46    BELA_MIDI_ALREADY_OPEN, BELA_MIDI_MESSAGE_MAX, BELA_MIDI_NO_SUCH_PORT, BelaMidi,
47    bela_midi_available_messages, bela_midi_delete, bela_midi_get_message, bela_midi_list_ports,
48    bela_midi_new, bela_midi_read_from, bela_midi_write_output, bela_midi_write_to,
49};
50
51// The build script's toolchain logic, tested where a build script
52// cannot be: `cargo test` builds this crate, not `build.rs`. See
53// ../shim_compiler.rs.
54#[cfg(test)]
55mod shim_compiler {
56    extern crate std;
57
58    use std::borrow::ToOwned;
59    use std::format;
60    use std::string::String;
61
62    include!("../shim_compiler.rs");
63
64    #[test]
65    fn bela_cxx_is_taken_as_it_stands() {
66        assert_eq!(
67            shim_compiler_from("clang++", "aarch64-linux-gnu-gcc"),
68            Ok("clang++".to_owned()),
69            "an explicit C++ compiler outranks anything derived"
70        );
71    }
72
73    #[test]
74    fn a_c_compiler_ending_in_gcc_answers_for_both() {
75        // The two cases docs/cross-compile.md documents.
76        assert_eq!(
77            shim_compiler_from("", "aarch64-linux-gnu-gcc"),
78            Ok("aarch64-linux-gnu-g++".to_owned())
79        );
80        assert_eq!(shim_compiler_from("", "gcc"), Ok("g++".to_owned()));
81    }
82
83    #[test]
84    fn neither_set_is_the_tap_default() {
85        assert_eq!(
86            shim_compiler_from("", ""),
87            Ok(DEFAULT_CXX.to_owned()),
88            "the same default scripts/aarch64-bela-linker.sh has"
89        );
90    }
91
92    #[test]
93    fn a_c_compiler_nothing_follows_from_is_refused() {
94        // Deriving `ar`, or a C++ name, from this would mix
95        // toolchains silently, which the build script fails on
96        // instead.
97        let error = shim_compiler_from("", "clang").unwrap_err();
98        assert!(
99            error.contains("BELA_CXX"),
100            "the message should say what to set, got: {error}"
101        );
102    }
103
104    #[test]
105    fn the_archiver_follows_the_compiler_it_belongs_to() {
106        assert_eq!(
107            shim_archiver("aarch64-unknown-linux-gnu-g++"),
108            Some("aarch64-unknown-linux-gnu-ar".to_owned()),
109            "cc would otherwise look for one named after the target triple"
110        );
111        assert_eq!(shim_archiver("g++"), Some("ar".to_owned()));
112    }
113
114    #[test]
115    fn an_archiver_that_does_not_follow_is_left_to_cc() {
116        // `clang++` wants llvm-ar, and it also ends in the letters
117        // `g++`: deriving from it would name `clanar`. AR is the way
118        // out for those, and cc reads it.
119        assert_eq!(shim_archiver("clang++"), None);
120        assert_eq!(shim_archiver("aarch64-linux-gnu-clang++"), None);
121    }
122
123    #[test]
124    fn a_compiler_named_by_its_path_is_still_one() {
125        // docs/cross-compile.md allows an absolute path, and
126        // /usr/bin/gcc is the board's own compiler.
127        assert_eq!(
128            shim_compiler_from("", "/usr/bin/gcc"),
129            Ok("/usr/bin/g++".to_owned())
130        );
131        assert_eq!(
132            shim_compiler_from("", "/opt/tc/bin/aarch64-linux-gnu-gcc"),
133            Ok("/opt/tc/bin/aarch64-linux-gnu-g++".to_owned())
134        );
135        assert_eq!(
136            shim_archiver("/usr/bin/g++"),
137            Some("/usr/bin/ar".to_owned()),
138            "and the archiver beside it"
139        );
140    }
141
142    #[test]
143    fn a_compiler_that_merely_ends_in_gcc_is_not_one() {
144        // Same trap on the compiler side: only a bare `gcc` or a
145        // `<triple>-gcc` names a toolchain to follow.
146        assert!(shim_compiler_from("", "notgcc").is_err());
147        assert_eq!(shim_archiver("notg++"), None);
148        assert_eq!(
149            shim_archiver("/usr/bin/clang++"),
150            None,
151            "a path does not make it one"
152        );
153    }
154}
155
156// The shim's header and the declarations mirroring it are edited by
157// hand, and a value that drifts between them is not a compile error —
158// it is a safe API that reads one failure as another. This is the
159// cheap half of the guard: the numbers.
160#[cfg(test)]
161mod shim_header {
162    extern crate std;
163
164    use std::format;
165
166    /// `shim/midi.h`, read at compile time from the crate this
167    /// declares the shim for.
168    const HEADER: &str = include_str!("../shim/midi.h");
169
170    /// The value of `#define <name> ...`, with one level of
171    /// parentheses taken off — the header writes negative constants as
172    /// `(-1000)`, as a C header should.
173    fn defined(name: &str) -> i64 {
174        let line = HEADER
175            .lines()
176            .find(|line| line.starts_with(&format!("#define {name} ")))
177            .unwrap_or_else(|| panic!("{name} is not defined in shim/midi.h"));
178        let value = line.split_whitespace().nth(2).expect("a value");
179        value
180            .trim_start_matches('(')
181            .trim_end_matches(')')
182            .parse()
183            .expect("a number")
184    }
185
186    #[test]
187    fn the_constants_match_the_header() {
188        assert_eq!(
189            defined("BELA_MIDI_MESSAGE_MAX"),
190            i64::try_from(super::BELA_MIDI_MESSAGE_MAX).expect("a small buffer size")
191        );
192        assert_eq!(
193            defined("BELA_MIDI_NO_SUCH_PORT"),
194            i64::from(super::BELA_MIDI_NO_SUCH_PORT)
195        );
196        assert_eq!(
197            defined("BELA_MIDI_ALREADY_OPEN"),
198            i64::from(super::BELA_MIDI_ALREADY_OPEN)
199        );
200    }
201}