Rust ❤️ Bela – FFI API Design

Con­tin­u­ing with the cur­rent se­ries of blog posts on us­ing Rust to de­vel­op Bela projects, we have so far talked about set­ting up a Rust cross com­pi­la­tion project for Bela and the projects I plan to cov­er at some point, as well as some ba­sic fea­si­bil­i­ty checks us­ing por­ta­ble SIMD code and criterion. This time, we'll delve in­to the Bela C API, the rea­son­ing be­hind the safe Rust FFI API, and al­so fi­nal­ly pro­duce some sound.

So just what is an FFI? A for­eign func­tion in­ter­face (FFI), is how one pro­gram­ming lan­guage (in our case Rust) calls func­tions from an­oth­er “for­eign” pro­gram­ming lan­guage (typ­i­cal­ly C). So FF­Is are need­ed when­ev­er a piece of code in a new pro­gram­ming lan­guage has to in­ter­act with an ex­ist­ing API de­signed for a dif­fer­ent pro­gram­ming lan­guage. Luck­i­ly, the Bela API is well doc­u­ment­ed and (most­ly, some ex­ten­sions have a C++ API) a C API, so no need to write C wrap­pers, which is typ­i­cal­ly nec­es­sary when two high­er-lev­el lan­guages need to talk to one an­oth­er. By the way, the link above points to the low-lev­el Doxy­gen doc­u­men­ta­tion of the Bela API. Gen­er­al­ly, the Bela Knowl­edge Base is a bet­ter learn­ing re­source, just not for our cur­rent use case.

While Rust pro­vides com­pile-time guar­an­tees about mem­o­ry safe­ty, no un­de­fined be­hav­ior, and free­dom from da­ta races, C gives no such guar­an­tees. Nasal demons abound. So keep your Necro­nomi­con… ahem… I meant your Rusto­nomi­con at hand, since we'll be forced to delve in­to the realm of unsafe Rust. Why? Be­cause ev­ery call through an FFI is un­safe by def­i­ni­tion, since the Rust com­pil­er can't know which — if any — com­pile time guar­an­tees the oth­er lan­guage pro­vides. We will have to en­sure any and all guar­an­tees our­selves. To this end, Rust FFI crates are typ­i­cal­ly de­signed in a two-lev­el fash­ion:

  1. A sys-crate that makes the raw, un­safe C func­tions avail­able to Rust and en­sures the cor­re­spond­ing li­braries are linked.
  2. A safe wrap­per crate that builds safe high­er-lev­el ab­strac­tions around the un­safe code, us­ing a va­ri­ety of tech­niques to en­sure the pre­con­di­tions of all API calls are ful­filled.

The lat­ter may not be pos­si­ble in all cas­es or re­quire some clever tricks us­ing Rust's far more ad­vanced type sys­tem (and clever is rarely good), but we'll try our best.

The un­safe sys-Crate

In any case, first things first: the sys-crate. I won't go in­to too much de­tail about it, as it is most­ly bor­ing busy­work, but if you are in­ter­est­ed in more de­tails, check out the bindgen book, since bindgen is the de fac­to stan­dard tool for cre­at­ing C FFI bind­ings for Rust. But let's have a quick look at andrewcsmith/bela-sys, the sys-crate for Bela. The crate root src/lib.rs is in­cred­i­bly sim­ple:

#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

#[cfg(feature = "midi")]
pub mod midi {
    include!(concat!(env!("OUT_DIR"), "/midi_bindings.rs"));
}

It dis­ables some warn­ings on non-stan­dard nam­ing con­ven­tions (which are com­mon in C), then in­cludes some files as a string us­ing the include! macro (yes, Rust has a #include-like mech­a­nism too for when it is nec­es­sary). The sec­ond of which is op­tion­al and on­ly in­clud­ed when the midi fea­ture is re­quest­ed. The file paths them­selves are con­struct­ed by con­cate­nat­ing the en­vi­ron­ment vari­able OUT_DIR, which is de­fined by Car­go, with the cor­re­spond­ing file name. But who or what gen­er­ates these files? That would be the build.rs build script, which lies in the crate root, next to Cargo.toml, and does all the heavy lift­ing. So let's have a look-see.

    let bela_root = PathBuf::from(
        env::var("BELA_SYSROOT").unwrap_or_else(|_| "/".into()),
    );
    let bela_include = bela_root.join("root/Bela/include");
    let bela_h = bela_include.join("Bela.h");
    if !bela_h.exists() {
        panic!(
            "Set BELA_SYSROOT to the location of your extracted Bela \
            image"
        );
    }
    let bela_lib = bela_root.join("root/Bela/lib");
    let xenomai_lib = bela_root.join("usr/xenomai/lib");
    let local_lib = bela_root.join("usr/local/lib");

The first part of our build script tries to read the BELA_SYSROOT en­vi­ron­ment vari­able, which tells the crate where to look for all re­quired head­ers and de­pen­den­cies when cross com­pil­ing (on the Bela it­self / should do), then con­structs a num­ber of paths rel­a­tive to this root and checks if Bela.h is found. For doc­u­men­ta­tion on how to ex­tract the Bela im­age to cre­ate the sys­root fold­er, check out my new­ly re­leased cargo-generate tem­plate for Bela projects: l0calh05t/bela-template.

    #[cfg(feature = "static")]
    {
        println!("cargo:rustc-link-lib=static=bela");
        println!("cargo:rustc-link-lib=stdc++");
    }
    #[cfg(not(feature = "static"))]
    {
        println!("cargo:rustc-link-lib=bela");
    }
    println!("cargo:rustc-link-lib=cobalt");
    println!("cargo:rustc-link-lib=prussdrv");
    println!(
        "cargo:rustc-link-search=all={}",
        bela_lib.to_str().unwrap()
    );
    println!(
        "cargo:rustc-link-search=all={}",
        xenomai_lib.to_str().unwrap()
    );
    println!(
        "cargo:rustc-link-search=all={}",
        local_lib.to_str().unwrap()
    );

Here, the build script tells Car­go (and in turn rustc) which li­braries to link (cargo:rustc-link-lib) and where to find them (cargo:rustc-link-search). The main de­pen­den­cy (bela) should al­so be list­ed in the links field of the Cargo.toml. This is al­so where the sec­ond of the crate's two fea­tures, static, comes in­to play, to de­cide if we want to link libbela.a (a stat­ic ar­chive) or libbela.so (a dy­nam­i­cal­ly linked shared ob­ject).

    let bindings = bindgen::Builder::default()
        .header(bela_h.to_str().unwrap())
        .clang_arg(format!("--sysroot={}", bela_root.to_str().unwrap()))
        .clang_arg(format!("-I{}", bela_include.to_str().unwrap()))
        .allowlist_type("Bela.*")
        .allowlist_function("Bela_.*")
        .blocklist_function("Bela_userSettings")
        .allowlist_function("rt_.*printf")
        .allowlist_var("BELA_.*")
        .allowlist_var("DEFAULT_.*")
        .generate()
        .expect("Unable to generate bindings");
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings");

This is where the mag­ic hap­pens, and we tell bindgen for which file it should gen­er­ate bind­ings. Ad­di­tion­al­ly, it needs to know where to find oth­er head­ers re­cur­sive­ly in­clud­ed by Bela.h. We al­so per­form some fil­ter­ing us­ing al­low- and block­list­ing, since these re­cur­sive in­cludes al­so de­clare a bunch of stuff not re­lat­ed to Bela at all. And bindgen can't re­al­ly dif­fer­en­ti­ate here, as C doesn't sup­port any­thing like names­paces. Lead­ing to nam­ing con­ven­tions like all func­tions and types be­ing pre­fixed with Bela_ and con­stants with BELA_, as is the case here. Of course, like all con­ven­tions, there are some ex­cep­tions. For ex­am­ple, we ex­plic­it­ly don't want Bela_userSettings as this is on­ly pro­vid­ed as a weak sym­bol and the us­er is sup­posed to over­ride it. Fi­nal­ly, we write the gen­er­at­ed bind­ings to a file. The one we in­clud­ed in the root mod­ule in lib/src.rs.

    #[cfg(feature = "midi")]
    {
        let midi_root = bela_root.join("root/Bela/libraries/Midi");

        // TODO: asound is in usr/lib/arm-linux-gnueabihf, so copy it to
        // OUT_DIR, as otherwise the link step will also find
        // libpthread.so and other link scripts there which reference
        // absolute paths. Is there a cleaner solution?
        let asound_lib =
            bela_root.join("usr/lib/arm-linux-gnueabihf/libasound.so");
        let asound_copy = out_path.join("libasound.so");
        if !asound_copy.exists() {
            std::fs::copy(asound_lib, asound_copy).unwrap();
        }

        cc::Build::new()
            .cpp(true)
            .object(midi_root.join("build/Midi_c.o"))
            .object(midi_root.join("build/Midi.o"))
            .compile("midi");
        println!("cargo:rustc-link-lib=asound");
        println!("cargo:rustc-link-lib=modechk");

        let midi_h = midi_root.join("Midi_c.h");
        let bindings = bindgen::Builder::default()
            .header(midi_h.to_str().unwrap())
            .clang_arg(format!(
                "--sysroot={}",
                bela_root.to_str().unwrap()
            ))
            .clang_arg(format!("-I{}", bela_include.to_str().unwrap()))
            .allowlist_function("Midi_.*")
            .generate()
            .expect("Unable to generate bindings");
        bindings
            .write_to_file(out_path.join("midi_bindings.rs"))
            .expect("Couldn't write bindings");
    }
}

The fi­nal part of our build script gen­er­ates the (op­tion­al) bind­ings for Bela's MI­DI sup­port. These are slight­ly more com­plex and re­quire the cc crate in ad­di­tion to bindgen, since the MI­DI sup­port is not pro­vid­ed as an ar­chive or shared ob­ject, but as a pair of un­linked ob­ject files. Ad­di­tion­al­ly, libasound.so is in a fold­er that al­so con­tains some link scripts with ab­so­lute paths, which are in­valid on our ex­tract­ed im­age when cross com­pil­ing, so we copy it to our out­put fold­er (if you have a bet­ter sug­ges­tion, feel free to con­tact me). The rest is very sim­i­lar to what we have seen be­fore.

At this point, we could al­ready use bela-sys to write pro­grams for our Bela, but al­most ev­ery­thing would be unsafe, and we'd have to make sure our­selves that we don't vi­o­late any in­vari­ants. If you re­al­ly want to see how that looks, check out examples/hello.rs. For now, let's just have a look at the first cou­ple of lines of the main-func­tion:

fn main() {
    unsafe {
        let mut settings = {
            let mut settings: mem::MaybeUninit<BelaInitSettings> =
                mem::MaybeUninit::uninit();
            bela_sys::Bela_defaultSettings(settings.as_mut_ptr());
            settings.assume_init()
        };

Ev­ery sin­gle line is un­safe. We have to deal with unini­tial­ized mem­o­ry (tip: nev­er, ev­er, use std::mem::uninitialized, as it can­not be used cor­rect­ly), call an un­safe func­tion, and then swear that what Bela_defaultSettings did def­i­nite­ly ini­tial­ized all fields of the BelaInitSettings struc­ture settings. Oth­er­wise? Nasal demons. And the com­pil­er can do what­ev­er it wants with our code. Al­so, that unsafe block? It clos­es at the end of main, as prac­ti­cal­ly all of its 44 lines of code are un­safe. And the render func­tion (not in­clud­ed in those 44 lines), has to deref­er­ence raw point­ers and ac­cess­es a static mut vari­able. Yup, a glob­al vari­able. Yuck. And to­tal­ly un­safe, los­ing near­ly all ben­e­fits of writ­ing Rust in the first place. So let's not do that and look in­to cre­at­ing a safe wrap­per in­stead, ok?

The Safe Wrap­per Crate

So how do we ap­proach writ­ing a safe wrap­per around the raw, un­safe, C API? Well, that re­al­ly de­pends on the API in ques­tion and what the (hope­ful­ly well-doc­u­ment­ed) pre­con­di­tions for each func­tion are! But first, let's take a step back and have a look at a stan­dard C (well, tech­ni­cal­ly C++ due to the bool) Bela pro­gram:

#include <Bela.h>

bool setup(BelaContext *context, void *userData) {
    // set up any required state, perform allocations, etc.
    return true;
}

void render(BelaContext *context, void *userData) {
    // read/write audio, analog, and digital signals from their
    // respective buffers via context, update state, etc.
}

void cleanup(BelaContext *context, void *userData) {
    // free any allocations made in setup, etc.
}

The first thing you may no­tice: there is no main. So what is the en­try point of our pro­gram? Well, Bela's IDE im­plic­it­ly links against core/default_main.cpp, which con­tains a bunch of Bela API calls to set ev­ery­thing up and us­es the setup, render, and cleanup func­tions de­clared (but not de­fined, the us­er has to do that) in Bela.h to fill the setup, render, and cleanup fields of a Bela_InitSettings ob­ject, which are func­tion point­ers. And what about that userData point­er? Most Bela ex­am­ples do not use it at all, in­stead us­ing glob­al vari­ables for pret­ty much ev­ery­thing. The hor­ror. In fact, there is no way to use userData with­out writ­ing our own main, since the point­er must be passed to Bela_initAudio and core/default_main.cpp pass­es 0 (i.e., a null point­er), so the C API doesn't ex­act­ly make it easy to avoid glob­al vari­ables. But ac­cess­ing static mut (i.e., mu­ta­ble, glob­al vari­ables) is al­ways un­safe (very easy to make mis­takes and ac­cess them from an­oth­er thread, for ex­am­ple), we need a dif­fer­ent so­lu­tion to achieve the goal of a safe Rust wrap­per.

How­ev­er, there is good news too. And that good news is that the API sup­ports pass­ing in an ar­bi­trary userData point­er, so we ab­so­lute­ly can de­sign a safe API. It just can't in­volve sim­ply writ­ing three un­con­nect­ed func­tions. So let's have a look at the safe wrap­per l0calh05t/bela-rs:next. Orig­i­nal­ly, I start­ed out ex­tend­ing andrewcsmith/bela-rs, but no­ticed that a fair­ly sig­nif­i­cant re­design was nec­es­sary to ac­tu­al­ly achieve a safe de­sign, which is what I did in my next branch. I may merge it (or de­tach the fork) at some point. Now let's look at the two most im­por­tant meth­ods in the crate, Bela::new and Bela::run.

impl<Application, Constructor> Bela<Constructor>
where
    Application: BelaApplication,
    Constructor: Send
        + UnwindSafe
        + FnOnce(&mut SetupContext) -> Option<Application>,
{
    /// Create a new `Bela` builder with default settings from a
    /// `BelaApplication` constructor function object
    pub fn new(constructor: Constructor) -> Self {
        Self {
            settings: InitSettings::default(),
            constructor,
        }
    }

    /* We'll skip all the setter functions here */
    /* ... */

    /// Consumes the `Bela` object and runs the application
    ///
    /// Terminates on error, or as soon as the application stops
    pub fn run(self) -> Result<(), Error> {
        /* We'll look into the innards of run later */
        /* ... */
    }
}

To de­con­struct this a lit­tle: Bela fol­lows the builder pat­tern. Bela::new cre­ates a new builder, then a bunch of set­ters can be used to con­fig­ure our pro­gram, then Bela::run con­sumes the builder and runs our pro­gram. The impl-block de­pends on two types: Application and Constructor. Application is a type that im­ple­ments the trait BelaApplication, which we'll in­tro­duce in a mo­ment. Constructor is a type that im­ple­ments

  • FnOnce(&mut SetupContext) -> Option<Application>, which ba­si­cal­ly just means that it is a func­tion or clo­sure that takes a mu­ta­ble ref­er­ence to a SetupContext and (maybe) cre­ates an in­stance of Application while be­ing callable at least once (i.e., is con­sumed on call),
  • Send, which means that it has to sup­port be­ing sent (moved) to an­oth­er thread (Bela doesn't spec­i­fy if setup is called in the orig­i­nal thread, so we have to err on the side of cau­tion, since the ren­der sys­tem may call it on its own thread), and
  • UnwindSafe, which is due to the fact, that Rust code can panic! and un­wind the stack (sim­i­lar to a C++ ex­cep­tion), but un­wind­ing across an FFI bound­ary is un­de­fined be­hav­ior, so we have to catch any pan­ics, which re­quires the code to be UnwindSafe (this trait is usu­al­ly au­to­mat­i­cal­ly im­ple­ment­ed).

Ok, that was a lot to di­gest, but the rest is pret­ty sim­ple. Bela::new just cre­ates a new Bela from a de­fault­ed (i.e., with bela_sys::Bela_defaultSettings) InitSettings, an in­ter­nal wrap­per around BelaInitSettings that takes care of the un­safe ini­tial­iza­tion and deal­lo­cates on drop, and a Constructor. Bela::run then con­sumes our Bela and gen­er­ates noth­ing or an er­ror, just like your typ­i­cal main func­tion. As you may have guessed, our Constructor func­tion is run in setup, cre­at­ing an Application. But what about render and cleanup? Well, render is sim­ply the one mem­ber of our trait BelaApplication!

pub unsafe trait BelaApplication: Sized + Send {
    fn render(&mut self, context: &mut RenderContext);
}

Wait. Aren't we writ­ing a safe wrap­per? Why is the trait unsafe, and what does that even mean? An unsafe trait is a trait which is un­safe to im­ple­ment if you don't en­sure some in­vari­ants are ful­filled that the com­pil­er can't check. And the big one here is that render (which in it­self is a safe method!) must be re­al-time safe, as per the of­fi­cial doc­u­men­ta­tion. What does that mean? Ba­si­cal­ly, since the ren­der­ing thread is high­er pri­or­i­ty than all op­er­at­ing sys­tem threads, any and all sys­tem calls must be avoid­ed. That in­cludes, but is not lim­it­ed to:

  • Al­lo­ca­tion, so no Box::new in render.
  • Print­ing, so not even println! is al­lowed in render.
  • File I/O, so no ac­cess­ing the SD card to read sam­ples in render.

Do­ing any of these would in­cur a mode switch, which “can cause au­dio ir­reg­u­lar­i­ties, sen­sor pro­cess­ing prob­lems, or can cause the sys­tem to stop al­to­geth­er.” Es­pe­cial­ly that last one is why that unsafe is here. Al­so, since we can't al­lo­cate, we al­so can't catch_unwind any pan­ics, since that cre­ates a boxed (heap-al­lo­cat­ed) er­ror. And since we can't catch the pan­ic and un­wind­ing across the FFI bound­ary is un­de­fined be­hav­ior (there is work be­ing done to al­le­vi­ate this is­sue and de­fine cross-lan­guage un­wind­ing, which may solve this at some point), so any and all pan­ics are ver­boten (strict­ly for­bid­den). We al­ready looked at Send ear­li­er and render is def­i­nite­ly in a sep­a­rate thread. Sized is an­oth­er au­to­mat­ic mark­er trait that in­di­cates that the size is known at com­pile time, i.e., it isn't a dy­nam­ic trait ob­ject, which can af­fect things like point­er size and gen­er­al­ly make our life dif­fi­cult when deal­ing with FF­Is.

What about cleanup? Rust al­ready has a clear­ly de­fined way to clean up used re­sources. The Drop trait! So all the cleanup func­tion has to do, is to con­sume the Application in­stance, and au­to­mat­i­cal­ly call any drop han­dlers while do­ing so. No need for you, the bela:next us­er, to do any­thing! Putting it all to­geth­er, you'd just write some­thing like:

use bela::{Bela, BelaApplication, Error, RenderContext};

struct Example(usize);

unsafe impl BelaApplication for Example {
    fn render(&mut self, context: &mut RenderContext) {
        let audio_out_channels = context.audio_out_channels();
        for frame in context
            .audio_out()
            .chunks_exact_mut(audio_out_channels)
        {
            let gain = 0.5;
            let signal = 2. * (self.0 as f32 * 110. / 44100.) - 1.;
            self.0 += 1;
            if self.0 as f32 > 44100. / 110. {
                self.0 = 0;
            }
            for sample in frame {
                *sample = gain * signal;
            }
        }
    }
}

fn main() -> Result<(), Error> {
    Bela::new(|_context| Some(Example(0))).run()
}

Where RenderContext is a wrap­per for BelaContext, al­low­ing safe ac­cess to ana­log, au­dio, and dig­i­tal buf­fers. But how do all these Rust func­tions con­nect to the un­safe API? That all hap­pens in Bela::run:

    pub fn run(self) -> Result<(), Error> {
        let Self {
            mut settings,
            constructor,
        } = self;

        let mut user_data: UserData<Application, _> =
            UserData::Constructor(constructor);

In this first part, self is de­com­posed in­to its parts us­ing pat­tern match­ing, and a UserData in­stance is cre­at­ed from the con­struc­tor. UserData is just an enum that can hold a con­struc­tor, an ap­pli­ca­tion, or noth­ing at all, a bit like a ternary Option type. The next thing we need is a way to con­nect our Rust func­tions, meth­ods, and clo­sures to a C API. This is done us­ing so-called tram­po­line func­tions (since they al­low the pro­gram to jump — as in branch/call — some­where they usu­al­ly could not) that are de­fined with a C ABI/call­ing con­ven­tion:

        /// C-compatible trampoline function to call constructor
        extern "C" fn setup_trampoline<Application, Constructor>(
            context: *mut bela_sys::BelaContext,
            user_data: *mut c_void,
        ) -> bool
        where
            Application: BelaApplication,
            Constructor: Send
                + UnwindSafe
                + FnOnce(&mut SetupContext) -> Option<Application>,
        {
            // create application instance
            // constructor is consumed
            let user_data = unsafe {
                &mut *(user_data
                    as *mut UserData<Application, Constructor>)
            };
            let constructor = user_data.take();
            if let UserData::Constructor(constructor) = constructor {
                *user_data = match catch_unwind(|| {
                    let mut context =
                        unsafe { Context::<SetupTag>::new(context) };
                    constructor(&mut context)
                }) {
                    Ok(application) => application
                        .map_or(UserData::None, UserData::Application),
                    Err(_) => UserData::None,
                };
            }
            user_data.is_application()
        }

        /// C-compatible trampoline function to call our render function
        extern "C" fn render_trampoline<Application, Constructor>(
            context: *mut bela_sys::BelaContext,
            user_data: *mut c_void,
        ) where
            Application: BelaApplication,
        {
            let user_data = unsafe {
                &mut *(user_data
                    as *mut UserData<Application, Constructor>)
            };
            if let UserData::Application(user_data) = user_data {
                // NOTE: cannot use catch_unwind safely here, as it
                // returns a boxed error (-> allocation in RT thread)
                let mut context =
                    unsafe { Context::<RenderTag>::new(context) };
                user_data.render(&mut context);
            };
        }

        /// C-compatible trampoline function to consume (and thereby
        /// drop) our application object
        extern "C" fn cleanup_trampoline<Application, Constructor>(
            _context: *mut bela_sys::BelaContext,
            user_data: *mut c_void,
        ) {
            let _ = catch_unwind(|| {
                // drop application instance
                let user_data = unsafe {
                    &mut *(user_data
                        as *mut UserData<Application, Constructor>)
                };
                user_data.take();
            });
        }

        // set up our trampoline functions as setup/render/cleanup
        settings.setup =
            Some(setup_trampoline::<Application, Constructor>);
        settings.render =
            Some(render_trampoline::<Application, Constructor>);
        settings.cleanup =
            Some(cleanup_trampoline::<Application, Constructor>);

This is fair­ly com­pli­cat­ed, but fol­lows the same pat­tern in all three in­stances. The user_data point­er is rein­ter­pret­ed as a point­er to UserData and turned in­to a mu­ta­ble ref­er­ence, which is of course un­safe since the com­pil­er doesn't know what that c_void point­er points to, if we are re­al­ly the on­ly func­tion to have ac­cess to it, or if it is non-null and valid. The context point­er is wrapped in our safe Context struct and “tagged” to in­di­cate in which func­tion we cur­rent­ly are. This is al­so un­safe, since the com­pil­er has no way of know­ing if we are ac­tu­al­ly in setup or render. You'll see lat­er why this is rel­e­vant. Fi­nal­ly, user_data is “tak­en” (anal­o­gous to Option::take, re­plac­ing it with a UserData::None) and/or pat­tern matched to re­trieve the con­tained con­struc­tor or BelaApplication. Where pos­si­ble, this is all pro­tect­ed with a catch_unwind.

        if unsafe {
            bela_sys::Bela_initAudio(
                settings.deref_mut() as *mut _,
                &mut user_data as *mut _ as *mut _,
            )
        } != 0
        {
            return Err(Error::Init);
        }

        if unsafe { bela_sys::Bela_startAudio() } != 0 {
            unsafe {
                bela_sys::Bela_stopAudio();
                bela_sys::Bela_cleanupAudio();
            }
            return Err(Error::Start);
        }

        setup_signal_handler();

        while unsafe { bela_sys::Bela_stopRequested() == 0 } {
            sleep(Duration::new(0, 100000));
        }

        unsafe {
            bela_sys::Bela_stopAudio();
            bela_sys::Bela_cleanupAudio();
        }

        Ok(())
    }

The rest of Bela::run takes care of ac­tu­al­ly run­ning our pro­gram, by ini­tial­iz­ing the Bela au­dio sys­tem (Bela_initAudio) us­ing our set­tings struct (the deref_mut pro­duces a Bela_InitSettings ref­er­ence from the wrap­per InitSettings) and our user_data enum. The lat­ter re­quires a dou­ble point­er con­ver­sion: one to con­vert from &mut UserData to *mut UserData and one more to get from there to *mut c_void. This is what we lat­er get passed as user_data in our call­back tram­po­lines, so the casts there are valid. Af­ter ini­tial­iz­ing the au­dio sys­tem, we need to start it (Bela_startAudio), check­ing for er­rors as we go (al­ways check the doc­u­men­ta­tion if a ze­ro or non-ze­ro val­ue is passed, no neat Result type in C). Then we set up some sig­nal han­dlers to re­quest the au­dio sys­tem to stop on SIGINT or SIGTERM (us­ing the nix crate). Af­ter that? Wait un­til the pro­gram is done, by sleep­ing in a loop, 100 ms at a time. Then we clean up af­ter our­selves and are done!

That's all of Bela::run cov­ered! Of course, there are lots of de­tails we glossed over. For ex­am­ple, why do we tag our Context wrap­per de­pend­ing on if we are in setup or render? There are two rea­sons for this:

  1. Sev­er­al fields in the BelaContext struct — in par­tic­u­lar all in­put and out­put buf­fer point­ers — are an­no­tat­ed with “this el­e­ment is avail­able in render() on­ly.” So all the ac­ces­sors that con­vert the raw point­ers in­to ap­pro­pri­ate­ly sized, safe slices are on­ly avail­able in impl Context<RenderTag>, not in the gener­ic impl<T> Context<T>.
  2. Even though Bela's aux­il­iary task API (and by ex­ten­sion the MI­DI API since it us­es an aux­il­iary task in­ter­nal­ly) doesn't re­quire a BelaContext, Bela_createAuxiliaryTask may on­ly be called in setup, so nei­ther in render, nor be­fore the au­dio sys­tem is ini­tial­ized or af­ter it is stopped. So while the orig­i­nal API doesn't re­quire a BelaContext to cre­ate a task, we im­ple­ment task cre­ation as a mem­ber of impl SetupContext (an alias of Context<SetupTag>) to en­sure it is called in a valid state:

    pub struct AuxiliaryTask(bela_sys::AuxiliaryTask);
    
    unsafe impl Send for AuxiliaryTask {}
    
    impl SetupContext {
        /// Create an auxiliary task that runs on a lower-priority thread
        ///
        /// # Safety
        /// `name` must be globally unique across all Xenomai processes,
        /// which cannot be verified at compile time
        pub unsafe fn create_auxiliary_task<Auxiliary>(
            &mut self, // unused reference to SetupContext, as this
                       // should only be called in setup
            task: Box<Auxiliary>,
            priority: i32,
            name: &std::ffi::CStr,
        ) -> Result<AuxiliaryTask, Error>
        where
            Auxiliary: FnMut() + Send + 'static,
        {
            /* ... */
        }
    }
    

This split of Context in­to dif­fer­ent types ac­cord­ing to pro­gram state, is a very sim­ple form of the type­s­tate pat­tern, which en­codes state in the type sys­tem to en­sure at com­pile time that func­tions which re­quire the pro­gram to be in a spe­cif­ic state can on­ly be called in that state. To en­sure that the us­er doesn't pass these con­texts out of their re­spec­tive func­tions, they are on­ly passed in as mu­ta­ble ref­er­ences, have no Clone im­ple­men­ta­tion, and all con­struc­tors are pri­vate to the bela crate.

We won't go in­to the nit­ty-grit­ty de­tails of aux­il­iary tasks, but here's a brief sum­ma­ry. Aux­il­iary tasks are ba­si­cal­ly low­er-pri­or­i­ty Xeno­mai threads, for slow­er tasks such as com­put­ing large FFTs. Cre­at­ing them is slow and in­volves sys­tem calls, so it should nev­er hap­pen in the ren­der­ing thread, but they can be sched­uled from ei­ther. Cur­rent­ly, sched­ul­ing is im­ple­ment­ed on impl<T> Context<T> for sym­me­try, but could just as well be on impl AuxiliaryTask. The FnMut() and Send re­stric­tions on task clo­sures should be quite clear, but why the 'static, in­di­cat­ing that they must live un­til the end of the pro­gram? One rea­son is the same as std::thread::spawn, which al­so re­quires 'static: threads can be de­tached and there is no way to force a join. Even if a join han­dle with ap­pro­pri­ate Drop im­ple­men­ta­tion is re­turned, the us­er could ex­plic­it­ly leak that han­dle with std::mem::forget, which is safe. Wait. Why is it safe? Leak­ing mem­o­ry and not run­ning drop han­dlers does not fall un­der Rust's safe­ty rules (oth­er­wise std::process::exit would be il­le­gal!). Leak­ing mem­o­ry doesn't cre­ate any il­le­gal mem­o­ry ac­cess­es or dou­ble deletes, so it's ok! The sec­ond rea­son is a bit ug­ly: Bela doesn't pro­vide an API to stop and de­stroy in­di­vid­u­al tasks (you can on­ly stop and de­stroy all of them, but those func­tions aren't doc­u­ment­ed at this time), so in­ter­nal­ly, we have to box and leak the clo­sure any­way. Oh, well. As an aside: 'static isn't nec­es­sary on our BelaApplication or on the con­struc­tor, since the us­er can­not in­ter­fere with­in Bela::run, and it prop­er­ly stops the au­dio sys­tem and calls all drop han­dlers.

Since MI­DI sup­port is based on an aux­il­iary task in­ter­nal­ly, a MI­DI port can on­ly be opened dur­ing set­up as well. In­ter­nal­ly, a Xeno­mai chan­nel is used to com­mu­ni­cate with the ren­der­ing thread, so read­ing is im­ple­ment­ed on RenderContext:

impl SetupContext {
    pub fn new_midi(
        &mut self,
        port: &std::ffi::CStr
    ) -> Result<Midi, Error> {
        /* ... */
    }
}

impl RenderContext {
    pub fn get_midi_message<'buffer>(
        &mut self,
        midi: &mut Midi,
        buffer: &'buffer mut [u8; 3],
    ) -> Option<&'buffer [u8]> {
        /* ... */
    }
}

While get_midi_message has some sim­i­lar­i­ty to an it­er­a­tor's next method, new mes­sages can be added to the queue lat­er on, which doesn't re­al­ly match it­er­a­tor be­hav­ior, and we re­quire a us­er-pro­vid­ed buf­fer. Loop­ing over MI­DI mes­sages is sim­ple enough with while let, though:

let mut buf = [0u8; 3];
while let Some(msg) = context.get_midi_message(&mut self.midi, &mut buf) {
    /* ... */
}

What's Left?

While we have cov­ered the main APIs the us­er will in­ter­act with, we can't re­al­ly cov­er ev­ery­thing here. So check out the doc­u­men­ta­tion (cargo doc --no-deps --all-features --open). There are a num­ber of APIs I haven't wrapped yet ei­ther:

  • Com­mand line op­tion han­dling. Bela pro­vides Bela_getopt_long which im­plic­it­ly han­dles ex­ist­ing Bela op­tions, but it might be sim­pler to just re-im­ple­ment it, than to wrap it in a safe API.
  • Au­dio lev­el con­trols. Bela pro­vides a num­ber of func­tions to change ADC and DAC gains. Cur­rent­ly, these can on­ly be set via the ini­tial set­tings.
  • rt_printf for re­al-time con­sole out­put. Since println! and eprintln! can't be used in render, sup­port­ing rt_printf would be very use­ful. How­ev­er, check­ing that the pa­ram­e­ters and for­mat string fit to­geth­er at com­pile time to en­sure safe­ty would re­quire a some­what com­plex pro­ce­dur­al macro, as far as I can tell.
  • Sup­port li­brary APIs be­yond MI­DI, such as the GUI li­braries. Most of these have to be linked sep­a­rate­ly (like MI­DI) and many of them on­ly have C++ APIs which re­quire ad­di­tion­al ef­fort to bind. For the non-Bela spe­cif­ic APIs (for ex­am­ple for sound file I/O) it's eas­i­er to just use ex­ist­ing crates.

That's it for now, folks. Next time we'll look a lit­tle deep­er in­to con­nect­ing MI­DI sup­port and our SIMD tone gen­er­a­tion. As usu­al, feel free to fol­low me and send me a DM on Mastodon if you have any ques­tions or com­ments. I'm al­so ac­tive on the Bela fo­rum.