Best practices for using Simulink autogenerated code in an embedded Rust project

I am working on a project where certain key features like simultaneous localization & mapping and controls are developed using Simulink in MATLAB. This is an unchangeable, non-negotiable constraint.

I want to take the C or C++ code you can automatically generate with Simulink and use it in the wider embedded Rust project.

I have done some very basic experimentation with using C and C++ in Rust via the native C FFI and cxx crate, respectively, so I know it’s possible.

My overarching question is this: What would the best practices be here?

  • Would C or C++ be better to generate from Simulink for this purpose? The C FFI is native to Rust but calling such functionality must be marked unsafe, while cxx allows for “safe” calling of C++ but is a crate as opposed to native.
  • What code autogeneration/Embedded Coder configuration parameters in Simulink should be used to make things easier? How should things like interface names be enforced?
  • I don’t think I’d need to be having the C/C++ call Rust functions, just leave as-is after autogeneration and have Rust just give the untouched external functions data and receive outputs. If it’s somehow best practice/necessary to have some external functions calling Rust functionality, can/should that be through another external interface layer to avoid editing the autogenerated code?
  • What should the workflows look like? Autogenerate C or C++ -> handwrite C FFI/cxxbridge layer? Autogenerate -> use/write other tool to generate better C or C++ interface -> handwrite or generate C FFI/cxxbridge layer?
  • What might be some pitfalls to avoid here?

Thanks for any and all help. I’ve done a lot of research and have found next to nothing for an application like this.

1 Like

Hi! I'm trying to do something similar myself. Except I have an additional layer of complexity - I'm trying to compile for an embedded target. This adds some additional complexities because bindgen does not work with #![no_std]. But if that is not your case, then it should be possible to hand write the relevant bindings.

It does if you pass --use-core to bindgen.You may also need --ctypes-prefix core::ffi.

1 Like

I have no experience with doing this, but here is what I think:

  • Would C or C++ be better to generate from Simulink for this purpose? The C FFI is native to Rust but calling such functionality must be marked unsafe, while cxx allows for “safe” calling of C++ but is a crate as opposed to native.

Is there a difference in the quality of the code output? If not, then is there a difference in the quality of the generated Rust (you can probably use bindgen to generate Rust bindings for C code) interface for the C/C++ code? If not, then it does not matter much. Do you need to provide a stable Rust API? What are the consequences for the Simulink models in that case, or for the team writing the stable Rust API.

  • I don’t think I’d need to be having the C/C++ call Rust functions, just leave as-is after autogeneration and have Rust just give the untouched external functions data and receive outputs. If it’s somehow best practice/necessary to have some external functions calling Rust functionality, can/should that be through another external interface layer to avoid editing the autogenerated code?

I would definitely try to avoid touching the autogenerated code and provide any necessary abstractions on top of that. Perhaps it is okay to expose some things from the generated bindings, like constants, enumerations or basic types that won't change.

  • What should the workflows look like? Autogenerate C or C++ -> handwrite C FFI/cxxbridge layer? Autogenerate -> use/write other tool to generate better C or C++ interface -> handwrite or generate C FFI/cxxbridge layer?

Generate C from Simulink models, Generate Rust bindings for C, Hand write abstraction on top of Rust bindings. Distribute abstraction as a library.

  • What might be some pitfalls to avoid here?

No idea, no real experience. I'd try to figure out if the generated code can allocate and if so, how to deal with it.

If your control logic is not dependent on Simulink alone, you can actually write the logic as a Matlab function and generate code for the function in C. This makes things a little bit simpler and I've managed to do this.


Update

OK, I finally got the compilation to work properly, thanks to the advice from @bjorn3 Here's what I needed to do.

  1. Generate C from Simulink and put all the code into one folder.
  2. Use CMake and arm-none-eabi-gcc toolchain to compile a static library libsimulink.a.
  3. In build.rs use the code below to link against your library, the arm math library -lm and the arm libc -lc. The static versions of these libraries are available in the ARM toolchain distribution folder. In my case it was (on macOS) /Applications/ARM/arm-none-eabi/lib
  println!("cargo:rustc-link-lib=static=libname");
    println!(
        "cargo:rustc-link-search=<path-to-lib>"
    );
  1. Tell bindgen to look for headers in your simulink folder as well as the arm folders
let bindings = bindgen::Builder::default()
        // The header we would like to generate bindings for.
        .header("<simulink-source-path>/<model-name>.h")
        .use_core()
        .clang_arg("-I<simulink-source-path>")
        .clang_arg("-I/Applications/ARM/arm-none-eabi/include")
        // Tell cargo to invalidate the built crate whenever any of the
        // included header files changed.
        // Finish the builder and generate the bindings.
        .generate()
        // Unwrap the Result and panic on failure.
        .expect("Unable to generate bindings");

Thanks for all the help! Now I can run some tests to see if it works OK.

Thanks! I'll try this out.