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(
68                "clang++",
69                "aarch64-unknown-linux-gnu-gcc",
70                "aarch64-linux-gnu-gcc"
71            ),
72            Ok("clang++".to_owned()),
73            "an explicit C++ compiler outranks anything derived, even a resolved linker"
74        );
75    }
76
77    #[test]
78    fn a_c_compiler_ending_in_gcc_answers_for_both() {
79        // The two cases docs/cross-compile.md documents, driven
80        // through the legacy BELA_CC path (no linker resolved).
81        assert_eq!(
82            shim_compiler_from("", "", "aarch64-linux-gnu-gcc"),
83            Ok("aarch64-linux-gnu-g++".to_owned())
84        );
85        assert_eq!(shim_compiler_from("", "", "gcc"), Ok("g++".to_owned()));
86    }
87
88    #[test]
89    fn neither_set_is_the_tap_default() {
90        assert_eq!(
91            shim_compiler_from("", "", ""),
92            Ok(DEFAULT_CXX.to_owned()),
93            "the same default scripts/aarch64-bela-linker.sh has"
94        );
95    }
96
97    #[test]
98    fn a_c_compiler_nothing_follows_from_is_refused() {
99        // Deriving `ar`, or a C++ name, from this would mix
100        // toolchains silently, which the build script fails on
101        // instead.
102        let error = shim_compiler_from("", "", "clang").unwrap_err();
103        assert!(
104            error.contains("BELA_CXX"),
105            "the message should say what to set, got: {error}"
106        );
107    }
108
109    #[test]
110    fn a_resolved_gcc_linker_answers_for_the_shim_too() {
111        // The direct-linker path (docs/cross-compile.md): Cargo
112        // resolved a compiler driver directly, so no BELA_CC is
113        // needed at all.
114        assert_eq!(
115            shim_compiler_from("", "aarch64-unknown-linux-gnu-gcc", ""),
116            Ok("aarch64-unknown-linux-gnu-g++".to_owned())
117        );
118        assert_eq!(shim_compiler_from("", "gcc", ""), Ok("g++".to_owned()));
119    }
120
121    #[test]
122    fn a_resolved_linker_outranks_a_stale_bela_cc() {
123        // RUSTC_LINKER reflects the toolchain that will actually link
124        // the binary; a leftover BELA_CC from before migrating off the
125        // wrapper must not silently win and build the shim with a
126        // different one.
127        assert_eq!(
128            shim_compiler_from("", "aarch64-unknown-linux-gnu-gcc", "gcc"),
129            Ok("aarch64-unknown-linux-gnu-g++".to_owned())
130        );
131    }
132
133    #[test]
134    fn the_wrapper_as_the_resolved_linker_falls_through_to_bela_cc() {
135        // .cargo/config.toml still names the wrapper: RUSTC_LINKER is
136        // set, but to something that names no C++ compiler on its own,
137        // so BELA_CC answers as it always has.
138        assert_eq!(
139            shim_compiler_from("", "scripts/aarch64-bela-linker.sh", "gcc"),
140            Ok("g++".to_owned())
141        );
142        assert_eq!(
143            shim_compiler_from(
144                "",
145                "/Users/dev/bela-rs/scripts/aarch64-bela-linker.sh",
146                "aarch64-linux-gnu-gcc"
147            ),
148            Ok("aarch64-linux-gnu-g++".to_owned()),
149            "an absolute path still matches by its last segment"
150        );
151        assert_eq!(
152            shim_compiler_from("", "scripts/aarch64-bela-linker.sh", ""),
153            Ok(DEFAULT_CXX.to_owned()),
154            "and with BELA_CC unset too, the tap default"
155        );
156    }
157
158    #[test]
159    fn a_resolved_linker_nothing_follows_from_is_refused() {
160        // A directly configured non-GNU linker (clang, lld, mold, ...)
161        // names no C++ compiler to derive, and BELA_CC is the legacy
162        // path's variable, not this one's — guessing here would risk
163        // the same toolchain mismatch BELA_CC guards against.
164        let error = shim_compiler_from("", "clang", "").unwrap_err();
165        assert!(
166            error.contains("BELA_CXX"),
167            "the message should say what to set, got: {error}"
168        );
169    }
170
171    #[test]
172    fn the_archiver_follows_the_compiler_it_belongs_to() {
173        assert_eq!(
174            shim_archiver("aarch64-unknown-linux-gnu-g++"),
175            Some("aarch64-unknown-linux-gnu-ar".to_owned()),
176            "cc would otherwise look for one named after the target triple"
177        );
178        assert_eq!(shim_archiver("g++"), Some("ar".to_owned()));
179    }
180
181    #[test]
182    fn an_archiver_that_does_not_follow_is_left_to_cc() {
183        // `clang++` wants llvm-ar, and it also ends in the letters
184        // `g++`: deriving from it would name `clanar`. AR is the way
185        // out for those, and cc reads it.
186        assert_eq!(shim_archiver("clang++"), None);
187        assert_eq!(shim_archiver("aarch64-linux-gnu-clang++"), None);
188    }
189
190    #[test]
191    fn a_compiler_named_by_its_path_is_still_one() {
192        // docs/cross-compile.md allows an absolute path, and
193        // /usr/bin/gcc is the board's own compiler.
194        assert_eq!(
195            shim_compiler_from("", "", "/usr/bin/gcc"),
196            Ok("/usr/bin/g++".to_owned())
197        );
198        assert_eq!(
199            shim_compiler_from("", "", "/opt/tc/bin/aarch64-linux-gnu-gcc"),
200            Ok("/opt/tc/bin/aarch64-linux-gnu-g++".to_owned())
201        );
202        assert_eq!(
203            shim_archiver("/usr/bin/g++"),
204            Some("/usr/bin/ar".to_owned()),
205            "and the archiver beside it"
206        );
207    }
208
209    #[test]
210    fn a_compiler_that_merely_ends_in_gcc_is_not_one() {
211        // Same trap on the compiler side: only a bare `gcc` or a
212        // `<triple>-gcc` names a toolchain to follow.
213        assert!(shim_compiler_from("", "", "notgcc").is_err());
214        assert_eq!(shim_archiver("notg++"), None);
215        assert_eq!(
216            shim_archiver("/usr/bin/clang++"),
217            None,
218            "a path does not make it one"
219        );
220    }
221}
222
223// The metadata encoding build.rs publishes so `bela` can relay device
224// link arguments to its own dependents; tested here for the same
225// reason as shim_compiler above. See link_args.rs and bela/link_args.rs.
226#[cfg(test)]
227mod link_args {
228    extern crate std;
229
230    use std::borrow::ToOwned;
231    use std::format;
232    use std::string::{String, ToString};
233    use std::vec;
234    use std::vec::Vec;
235
236    include!("../link_args.rs");
237
238    fn args(values: &[&str]) -> Vec<String> {
239        values.iter().map(|value| (*value).to_owned()).collect()
240    }
241
242    #[test]
243    fn no_arguments_still_publishes_a_zero_count() {
244        assert_eq!(
245            encode_link_args(&[]),
246            vec![("LINK_ARGS_COUNT".to_owned(), "0".to_owned())],
247            "a dependent has to see a count of zero, not an absent key, \
248             to tell \"nothing to add\" apart from \"never ran\""
249        );
250    }
251
252    #[test]
253    fn arguments_are_indexed_from_zero_in_order() {
254        assert_eq!(
255            encode_link_args(&args(&["--sysroot=/opt/bela", "-Bfoo"])),
256            vec![
257                ("LINK_ARGS_COUNT".to_owned(), "2".to_owned()),
258                ("LINK_ARGS_0".to_owned(), "--sysroot=/opt/bela".to_owned()),
259                ("LINK_ARGS_1".to_owned(), "-Bfoo".to_owned()),
260            ]
261        );
262    }
263
264    #[test]
265    fn whitespace_in_an_argument_survives_uninterpreted() {
266        // The reason for a count-plus-index encoding over one joined
267        // string: a BELA_SYSROOT with a space in it must not need a
268        // shell-style parser on the reading side.
269        let value = "--sysroot=/Volumes/Bela Sysroot";
270        assert_eq!(
271            encode_link_args(&args(&[value])),
272            vec![
273                ("LINK_ARGS_COUNT".to_owned(), "1".to_owned()),
274                ("LINK_ARGS_0".to_owned(), value.to_owned()),
275            ]
276        );
277    }
278}
279
280// The shim's header and the declarations mirroring it are edited by
281// hand, and a value that drifts between them is not a compile error —
282// it is a safe API that reads one failure as another. This is the
283// cheap half of the guard: the numbers.
284#[cfg(test)]
285mod shim_header {
286    extern crate std;
287
288    use std::format;
289
290    /// `shim/midi.h`, read at compile time from the crate this
291    /// declares the shim for.
292    const HEADER: &str = include_str!("../shim/midi.h");
293
294    /// The value of `#define <name> ...`, with one level of
295    /// parentheses taken off — the header writes negative constants as
296    /// `(-1000)`, as a C header should.
297    fn defined(name: &str) -> i64 {
298        let line = HEADER
299            .lines()
300            .find(|line| line.starts_with(&format!("#define {name} ")))
301            .unwrap_or_else(|| panic!("{name} is not defined in shim/midi.h"));
302        let value = line.split_whitespace().nth(2).expect("a value");
303        value
304            .trim_start_matches('(')
305            .trim_end_matches(')')
306            .parse()
307            .expect("a number")
308    }
309
310    #[test]
311    fn the_constants_match_the_header() {
312        assert_eq!(
313            defined("BELA_MIDI_MESSAGE_MAX"),
314            i64::try_from(super::BELA_MIDI_MESSAGE_MAX).expect("a small buffer size")
315        );
316        assert_eq!(
317            defined("BELA_MIDI_NO_SUCH_PORT"),
318            i64::from(super::BELA_MIDI_NO_SUCH_PORT)
319        );
320        assert_eq!(
321            defined("BELA_MIDI_ALREADY_OPEN"),
322            i64::from(super::BELA_MIDI_ALREADY_OPEN)
323        );
324    }
325}