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//! Two things here are neither the core API nor generated, and they
11//! are two different kinds of thing:
12//!
13//! - The [`bela_midi_*`](bela_midi_new) functions are a C surface this
14//!   crate compiles (`shim/midi.cpp`) over Bela's `Midi` class in
15//!   `libbelaextra`. MIDI is what a Bela program reaches for first
16//!   after audio, and the class is C++ with only half a C surface of
17//!   its own.
18//! - The [`ne10_fft_*`](ne10_fft_alloc_r2c_float32) functions are
19//!   plain C on the board, in `libNE10.so.10`, declared here by hand.
20//!   They are the real-to-complex FFT, reached directly rather than
21//!   through Bela's `Fft` class, which wraps these same calls and
22//!   little else; `docs/fft.md` in the repository records why. Nothing
23//!   is compiled for them — the library is already on the board — but
24//!   `abi/ne10_abi.c` asserts at build time that its headers still
25//!   describe what is declared here.
26//!
27//! Bela's own higher-level C++ libraries (Scope, Trill, Fft, Gui)
28//! remain out of scope.
29//!
30//! The `setup` / `render` / `cleanup` callbacks are not bound: they are
31//! either provided to `Bela_initAudio` via [`BelaInitSettings`] or
32//! defined as `#[unsafe(no_mangle)]` symbols by the linking crate.
33//!
34//! Target platform is Bela Gem on `PocketBeagle` 2
35//! (`aarch64-unknown-linux-gnu`). For a safe API, use the `bela`
36//! crate instead.
37//!
38//! [Bela Gem]: https://bela.io
39#![no_std]
40
41#[allow(
42    missing_docs,
43    nonstandard_style,
44    unsafe_op_in_unsafe_fn,
45    unused,
46    clippy::all,
47    clippy::pedantic,
48    clippy::nursery,
49    clippy::restriction,
50    rustdoc::all,
51    reason = "generated by bindgen; regenerate with `cargo xtask bindgen`"
52)]
53mod bindings;
54mod midi;
55mod ne10;
56
57pub use bindings::*;
58pub use midi::{
59    BELA_MIDI_ALREADY_OPEN, BELA_MIDI_MESSAGE_MAX, BELA_MIDI_NO_SUCH_PORT, BelaMidi,
60    bela_midi_available_messages, bela_midi_delete, bela_midi_get_message, bela_midi_list_ports,
61    bela_midi_new, bela_midi_read_from, bela_midi_write_output, bela_midi_write_to,
62};
63pub use ne10::{
64    ne10_fft_alloc_r2c_float32, ne10_fft_c2r_1d_float32_neon, ne10_fft_cpx_float32_t,
65    ne10_fft_destroy_r2c_float32, ne10_fft_r2c_1d_float32_neon, ne10_fft_r2c_state_float32_t,
66};
67
68// The build script's toolchain logic, tested where a build script
69// cannot be: `cargo test` builds this crate, not `build.rs`. See
70// ../shim_compiler.rs.
71#[cfg(test)]
72mod shim_compiler {
73    extern crate std;
74
75    use std::borrow::ToOwned;
76    use std::format;
77    use std::string::String;
78
79    include!("../shim_compiler.rs");
80
81    #[test]
82    fn bela_cxx_is_taken_as_it_stands() {
83        assert_eq!(
84            shim_compiler_from(
85                "clang++",
86                "aarch64-unknown-linux-gnu-gcc",
87                "aarch64-linux-gnu-gcc"
88            ),
89            Ok("clang++".to_owned()),
90            "an explicit C++ compiler outranks anything derived, even a resolved linker"
91        );
92    }
93
94    #[test]
95    fn a_c_compiler_ending_in_gcc_answers_for_both() {
96        // The two cases docs/cross-compile.md documents, driven
97        // through the legacy BELA_CC path (no linker resolved).
98        assert_eq!(
99            shim_compiler_from("", "", "aarch64-linux-gnu-gcc"),
100            Ok("aarch64-linux-gnu-g++".to_owned())
101        );
102        assert_eq!(shim_compiler_from("", "", "gcc"), Ok("g++".to_owned()));
103    }
104
105    #[test]
106    fn neither_set_is_the_tap_default() {
107        assert_eq!(
108            shim_compiler_from("", "", ""),
109            Ok(DEFAULT_CXX.to_owned()),
110            "the same default scripts/aarch64-bela-linker.sh has"
111        );
112    }
113
114    #[test]
115    fn a_c_compiler_nothing_follows_from_is_refused() {
116        // Deriving `ar`, or a C++ name, from this would mix
117        // toolchains silently, which the build script fails on
118        // instead.
119        let error = shim_compiler_from("", "", "clang").unwrap_err();
120        assert!(
121            error.contains("BELA_CXX"),
122            "the message should say what to set, got: {error}"
123        );
124    }
125
126    #[test]
127    fn a_resolved_gcc_linker_answers_for_the_shim_too() {
128        // The direct-linker path (docs/cross-compile.md): Cargo
129        // resolved a compiler driver directly, so no BELA_CC is
130        // needed at all.
131        assert_eq!(
132            shim_compiler_from("", "aarch64-unknown-linux-gnu-gcc", ""),
133            Ok("aarch64-unknown-linux-gnu-g++".to_owned())
134        );
135        assert_eq!(shim_compiler_from("", "gcc", ""), Ok("g++".to_owned()));
136    }
137
138    #[test]
139    fn a_resolved_linker_outranks_a_stale_bela_cc() {
140        // RUSTC_LINKER reflects the toolchain that will actually link
141        // the binary; a leftover BELA_CC from before migrating off the
142        // wrapper must not silently win and build the shim with a
143        // different one.
144        assert_eq!(
145            shim_compiler_from("", "aarch64-unknown-linux-gnu-gcc", "gcc"),
146            Ok("aarch64-unknown-linux-gnu-g++".to_owned())
147        );
148    }
149
150    #[test]
151    fn the_wrapper_as_the_resolved_linker_falls_through_to_bela_cc() {
152        // .cargo/config.toml still names the wrapper: RUSTC_LINKER is
153        // set, but to something that names no C++ compiler on its own,
154        // so BELA_CC answers as it always has.
155        assert_eq!(
156            shim_compiler_from("", "scripts/aarch64-bela-linker.sh", "gcc"),
157            Ok("g++".to_owned())
158        );
159        assert_eq!(
160            shim_compiler_from(
161                "",
162                "/Users/dev/bela-rs/scripts/aarch64-bela-linker.sh",
163                "aarch64-linux-gnu-gcc"
164            ),
165            Ok("aarch64-linux-gnu-g++".to_owned()),
166            "an absolute path still matches by its last segment"
167        );
168        assert_eq!(
169            shim_compiler_from("", "scripts/aarch64-bela-linker.sh", ""),
170            Ok(DEFAULT_CXX.to_owned()),
171            "and with BELA_CC unset too, the tap default"
172        );
173    }
174
175    #[test]
176    fn the_abi_check_takes_the_c_compiler_as_it_stands() {
177        // BELA_CC already names a C compiler, and the assertions in
178        // abi/ne10_abi.c are C: nothing to derive.
179        assert_eq!(
180            abi_compiler_from("aarch64-linux-gnu-gcc", "", "clang++"),
181            Some("aarch64-linux-gnu-gcc".to_owned())
182        );
183    }
184
185    #[test]
186    fn the_abi_check_follows_the_resolved_linker() {
187        // The direct-linker path: what Cargo resolved is a compiler
188        // driver, which is what should read the headers.
189        assert_eq!(
190            abi_compiler_from("", "aarch64-unknown-linux-gnu-gcc", ""),
191            Some("aarch64-unknown-linux-gnu-gcc".to_owned())
192        );
193        assert_eq!(
194            abi_compiler_from("gcc", "scripts/aarch64-bela-linker.sh", ""),
195            Some("gcc".to_owned()),
196            "the wrapper names no compiler of its own, so BELA_CC answers"
197        );
198    }
199
200    #[test]
201    fn the_abi_check_derives_a_c_compiler_from_a_cxx_one() {
202        // Only BELA_CXX is set, which is the shim's variable: the C
203        // compiler beside it reads the same headers with the same
204        // defines.
205        assert_eq!(
206            abi_compiler_from("", "", "aarch64-linux-gnu-g++"),
207            Some("aarch64-linux-gnu-gcc".to_owned())
208        );
209        assert_eq!(
210            abi_compiler_from("", "", "clang++"),
211            Some("clang".to_owned())
212        );
213        assert_eq!(
214            abi_compiler_from("", "", ""),
215            Some("aarch64-unknown-linux-gnu-gcc".to_owned()),
216            "with nothing set, the tap's C compiler beside DEFAULT_CXX"
217        );
218    }
219
220    #[test]
221    fn a_cxx_compiler_nothing_follows_from_skips_the_abi_check() {
222        // No error: the check is a guard against a board image moving
223        // NE10, and a build that cannot run it links exactly as it did
224        // before the check existed. build.rs warns instead.
225        assert_eq!(abi_compiler_from("", "", "my-cross-compiler"), None);
226    }
227
228    #[test]
229    fn the_abi_archiver_follows_a_gcc_name() {
230        assert_eq!(
231            abi_archiver("aarch64-linux-gnu-gcc"),
232            Some("aarch64-linux-gnu-ar".to_owned())
233        );
234        assert_eq!(abi_archiver("gcc"), Some("ar".to_owned()));
235        assert_eq!(
236            abi_archiver("clang"),
237            None,
238            "clang wants llvm-ar, not an ar beside it; cc resolves that"
239        );
240    }
241
242    #[test]
243    fn a_resolved_linker_nothing_follows_from_is_refused() {
244        // A directly configured non-GNU linker (clang, lld, mold, ...)
245        // names no C++ compiler to derive, and BELA_CC is the legacy
246        // path's variable, not this one's — guessing here would risk
247        // the same toolchain mismatch BELA_CC guards against.
248        let error = shim_compiler_from("", "clang", "").unwrap_err();
249        assert!(
250            error.contains("BELA_CXX"),
251            "the message should say what to set, got: {error}"
252        );
253    }
254
255    #[test]
256    fn the_archiver_follows_the_compiler_it_belongs_to() {
257        assert_eq!(
258            shim_archiver("aarch64-unknown-linux-gnu-g++"),
259            Some("aarch64-unknown-linux-gnu-ar".to_owned()),
260            "cc would otherwise look for one named after the target triple"
261        );
262        assert_eq!(shim_archiver("g++"), Some("ar".to_owned()));
263    }
264
265    #[test]
266    fn an_archiver_that_does_not_follow_is_left_to_cc() {
267        // `clang++` wants llvm-ar, and it also ends in the letters
268        // `g++`: deriving from it would name `clanar`. AR is the way
269        // out for those, and cc reads it.
270        assert_eq!(shim_archiver("clang++"), None);
271        assert_eq!(shim_archiver("aarch64-linux-gnu-clang++"), None);
272    }
273
274    #[test]
275    fn a_compiler_named_by_its_path_is_still_one() {
276        // docs/cross-compile.md allows an absolute path, and
277        // /usr/bin/gcc is the board's own compiler.
278        assert_eq!(
279            shim_compiler_from("", "", "/usr/bin/gcc"),
280            Ok("/usr/bin/g++".to_owned())
281        );
282        assert_eq!(
283            shim_compiler_from("", "", "/opt/tc/bin/aarch64-linux-gnu-gcc"),
284            Ok("/opt/tc/bin/aarch64-linux-gnu-g++".to_owned())
285        );
286        assert_eq!(
287            shim_archiver("/usr/bin/g++"),
288            Some("/usr/bin/ar".to_owned()),
289            "and the archiver beside it"
290        );
291    }
292
293    #[test]
294    fn a_compiler_that_merely_ends_in_gcc_is_not_one() {
295        // Same trap on the compiler side: only a bare `gcc` or a
296        // `<triple>-gcc` names a toolchain to follow.
297        assert!(shim_compiler_from("", "", "notgcc").is_err());
298        assert_eq!(shim_archiver("notg++"), None);
299        assert_eq!(
300            shim_archiver("/usr/bin/clang++"),
301            None,
302            "a path does not make it one"
303        );
304    }
305}
306
307// The metadata encoding build.rs publishes so `bela` can relay device
308// link arguments to its own dependents; tested here for the same
309// reason as shim_compiler above. See link_args.rs and bela/link_args.rs.
310#[cfg(test)]
311mod link_args {
312    extern crate std;
313
314    use std::borrow::ToOwned;
315    use std::format;
316    use std::string::{String, ToString};
317    use std::vec;
318    use std::vec::Vec;
319
320    include!("../link_args.rs");
321
322    fn args(values: &[&str]) -> Vec<String> {
323        values.iter().map(|value| (*value).to_owned()).collect()
324    }
325
326    #[test]
327    fn no_arguments_still_publishes_a_zero_count() {
328        assert_eq!(
329            encode_link_args(&[]),
330            vec![("LINK_ARGS_COUNT".to_owned(), "0".to_owned())],
331            "a dependent has to see a count of zero, not an absent key, \
332             to tell \"nothing to add\" apart from \"never ran\""
333        );
334    }
335
336    #[test]
337    fn arguments_are_indexed_from_zero_in_order() {
338        assert_eq!(
339            encode_link_args(&args(&["--sysroot=/opt/bela", "-Bfoo"])),
340            vec![
341                ("LINK_ARGS_COUNT".to_owned(), "2".to_owned()),
342                ("LINK_ARGS_0".to_owned(), "--sysroot=/opt/bela".to_owned()),
343                ("LINK_ARGS_1".to_owned(), "-Bfoo".to_owned()),
344            ]
345        );
346    }
347
348    #[test]
349    fn whitespace_in_an_argument_survives_uninterpreted() {
350        // The reason for a count-plus-index encoding over one joined
351        // string: a BELA_SYSROOT with a space in it must not need a
352        // shell-style parser on the reading side.
353        let value = "--sysroot=/Volumes/Bela Sysroot";
354        assert_eq!(
355            encode_link_args(&args(&[value])),
356            vec![
357                ("LINK_ARGS_COUNT".to_owned(), "1".to_owned()),
358                ("LINK_ARGS_0".to_owned(), value.to_owned()),
359            ]
360        );
361    }
362}
363
364// The shim's header and the declarations mirroring it are edited by
365// hand, and a value that drifts between them is not a compile error —
366// it is a safe API that reads one failure as another. This is the
367// cheap half of the guard: the numbers.
368#[cfg(test)]
369mod shim_header {
370    extern crate std;
371
372    use std::format;
373
374    /// `shim/midi.h`, read at compile time from the crate this
375    /// declares the shim for.
376    const HEADER: &str = include_str!("../shim/midi.h");
377
378    /// The value of `#define <name> ...`, with one level of
379    /// parentheses taken off — the header writes negative constants as
380    /// `(-1000)`, as a C header should.
381    fn defined(name: &str) -> i64 {
382        let line = HEADER
383            .lines()
384            .find(|line| line.starts_with(&format!("#define {name} ")))
385            .unwrap_or_else(|| panic!("{name} is not defined in shim/midi.h"));
386        let value = line.split_whitespace().nth(2).expect("a value");
387        value
388            .trim_start_matches('(')
389            .trim_end_matches(')')
390            .parse()
391            .expect("a number")
392    }
393
394    #[test]
395    fn the_constants_match_the_header() {
396        assert_eq!(
397            defined("BELA_MIDI_MESSAGE_MAX"),
398            i64::try_from(super::BELA_MIDI_MESSAGE_MAX).expect("a small buffer size")
399        );
400        assert_eq!(
401            defined("BELA_MIDI_NO_SUCH_PORT"),
402            i64::from(super::BELA_MIDI_NO_SUCH_PORT)
403        );
404        assert_eq!(
405            defined("BELA_MIDI_ALREADY_OPEN"),
406            i64::from(super::BELA_MIDI_ALREADY_OPEN)
407        );
408    }
409}