Five simple steps to use any Arduino C++ library in a Rust project 🦀
Tags: rust, arduino, cpp, tutorial
Published: 2022-11-13 14:27:16

Arduino helps circuit developers to build electronic projects and is, perhaps, the most used open-source hardware and software platform. It is popular across millions of hobbyists across the world. Historically, Arduino boards are programmed with C++ programming language using the [Arduino IDE](https://www.arduino.cc/en/software). The availability of powerful ARM-based Arduino-compatible boards made it possible to use [python](https://circuitpython.org/), [JavaScript](http://www.espruino.com/), or even a [browser](https://edukits.co/) to program your circuit. While they are easier to study for a new joiner without an existing programming background, C++ stays a default language choice, especially when dealing with cheap and low-memory AVR-controller boards and having a need to run more or less complex projects.

However, the compact binary size and efficiency of compiled code is probably not the main advantage of the traditional Arduino ecosystem. If the project is more complex than blinking the led, it would likely require integration with the range of sensors, servo motors, and other third-party peripheries. Most manufacturers develop precisely C++ Arduino-compatible driver libraries to be used "out of the box".

Rust language shares all advantages of efficient C++ code. With the rust community growing year after year, more and more people try using rust to program their Arduino boards. Consequently, the Arduino Rust ecosystem have significantly developed in the last couple of years. The Hardware Abstraction Layer for AVR microcontrollers [avr-hal](https://github.com/Rahix/avr-hal), [Rudino](https://github.com/avr-rust/ruduino) library and [ravedude](https://github.com/Rahix/avr-hal/tree/main/ravedude) CLI utility to make Rust development for AVR microcontrollers easier are just a few examples of the solid foundation developed so far. 

Third-party library availability is, however, still lagging behind. Luckily enough, Rust supports nearly seamless interaction with the C code! While it is very well possible to link almost any Arduino library to the rust project, I was not able to find any meaningful description of the steps required to do so! My best hit was the [rust-arduino-helpers](https://github.com/mutantbob/rust-arduino-helpers) project. Despite that the code quality is, unfortunately, not great (it contains large fragments of commented code and was not updated for a while), it was a great help for me to find the right direction.

The absence of the well documented steps was my motivation to write this tutorial.

# Project and Goals

Here is the set of steps we will cover today:
- Prepare environment to program Arduino board with Rust.
- Create the [avr-hal](https://github.com/Rahix/avr-hal) based rust project and blink the led.
- Compile Arduino SDK and the third-party library and link it to the rust project.
- Generate rust bindings for the Arduino library.
- Write the code and run it on your board!


The list of the hardware I've had in my possession for this project is the following:
- Arduino Uno board
- 1602 LCD Display Module with I2C Interface
- LED
- Linux or Windows PC

What we are going to do is simply try writing a text message to the LCD Display using Rust. In order to do so, we will have to figure out how to link and use the [LiquidCrystal-I2C](https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library) Arduino library in our rust crate. In other words, we would like to do something like [this](https://create.arduino.cc/projecthub/Arnov_Sharma_makes/lcd-i2c-tutorial-664e5a), but in Rust.

Here is the little demonstration of the final project:
{% youtube GJqBYXa5j3A %}

As a bonus, we will write a nice configuration file to configure Arduino dependencies to make the setup extensible and reusable.

The source code is available on [GitHub](https://github.com/kgrech/rust_arduino). I was able to run it on both Linux and Windows PC. I would be focusing on the Linux (Fedora 35) installation steps in this tutorial, windows users could refer to the corresponding section in the [readme.md](https://github.com/kgrech/rust_arduino/blob/main/README.md) file. 

This article assumes the reader has the basic knowledge about Rust project setup. I also assume the reader has a rust standard development environment (``rustup`` and ``cargo``) configured.

# Step 1: Environment setup

## Compiler and Arduino IDE

Arduino IDE and Arduino libraries are intended to be used by non-professional programmers and so they are designed to just work out of the box. We will have to gain some understanding of what happens under the hood of them to be able to compile the Arduino code outside of the Arduino IDE. 

First all, we would need to download and install the [Arduino IDE](https://docs.arduino.cc/software/ide-v2/tutorials/getting-started/ide-v2-downloading-and-installing). I am using version 2.0.1.
Linux users could install it running the following command:
```sh
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
```
At this point, you can run some Arduino C++ Hello World project to ensure that your board is recognized by your PC and working fine. It is also important, because it would force Arduino IDE to download require tools and libraries.

While you are there, install [LiquidCrystal_I2C](https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library) library to Arduino Libraries folder using Arduino IDE like how you would normally do.

Next, we need to familiarize ourselves with a set of tools used for AVR development:
- ``avrdude`` is a command-line program for programming AVR chips. It allows uploading compiled sketch to your board.
- ``avr-gcc`` is a compiler that takes C language high level code and creates a binary source which can be uploaded into an AVR micro controller.
- ``avr-libc`` is C standard library for use with ``avr-gcc`` on Atmel AVR microcontrollers.

These tools come bundled with the Arduino IDE. On my laptop, they are installed in the ``~/.arduino15/packages/arduino/tools/`` folder. Despite this, I prefer to also install the system-wide copy of them from Fedora repositories to avoid adding the above folder to the %PATH manually:
```sh
sudo dnf install avrdude avr-gcc avr-libc
```
The above step is technically optional, I skipped it on Windows as there is no easy way to the same there.

## Ravedude

We could have been just using ``avrdude`` to program our board. However since we are planning to develop in rust, [ravedude](https://github.com/Rahix/avr-hal/tree/main/ravedude) would be a better alternative as we could just configure it as a runner for ``cargo`` build tool.

I had to install some dependencies to compile ``ravedude`` first:
```sh
sudo dnf install systemd-devel pkgconf-pkg-config
```
Installing ``ravedude`` is then as easy as running
```sh
cargo install ravedude
```

I also assume the reader has a rust standard development environment (``rustup`` and ``cargo``) configured.

## Bindgen

We are planning to use [rust-bindgen](https://github.com/rust-lang/rust-bindgen) project to automatically generate rust bindings based on the C++ library header. We will use bindgen as rust library during the build time, however it require ``libclang`` to operate, so we have to install it:
```sh
sudo dnf install clang-devel
```

## Cargo-generate

We would like to simply the next step and use [cargo-generate](https://github.com/cargo-generate/cargo-generate) tool to create our Arduino project from a template. Somehow (please, do not ask me why) it requires Perl to compile, so we have to do:
```sh
sudo dnf install perl
cargo install cargo-generate
```

# Step 2: Project setup

As I've just mentioned, we would create out project using ``cargo generate`` command:
```
cargo generate --git https://github.com/Rahix/avr-hal-template.git
```
You will have to specify the project name and select the board type. There is an excellent creativcoder's [article](https://dev.to/creativcoder/how-to-run-rust-on-arduino-uno-40c0) outlining the process step-by-step in case you would like to do project the setup manually. 

Have a look to ``.cargo/config.toml`` to understand why we've installed ``ravedude`` earlier. The following section:
```toml
[target.'cfg(target_arch = "avr")']
runner = "ravedude uno -cb 57600"
```
passing compiled file to ``ravedude`` when you execute ``cargo run`` command, so it would be programmed to your board. ``ravedude`` would also listen the serial port to print the debug output of your sketch. 

Let's update ``main.rs`` with a simple program to use the serial port and blink the LED on a pin #13:
```rust
use arduino_hal::prelude::*;
use panic_halt as _;

#[arduino_hal::entry]
unsafe fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);

    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);
    let mut led = pins.d13.into_output();
    ufmt::uwriteln!(&mut serial, "Hello world\r").void_unwrap();

    loop {
        led.toggle();
        arduino_hal::delay_ms(1000);
    }
}
```
Simply execute
```
cargo run
```
and the above code should be running on your board.

If you are having trouble understanding the above code, consider referring to [avr-hal](https://rahix.github.io/avr-hal/arduino_hal/index.html) documentation and creativcoder's [article](https://dev.to/creativcoder/how-to-run-rust-on-arduino-uno-40c0) for more information. 

# Step 3: Compile and Link Arduino SDK and library

## Arduino SDK dependency

Ok, we have a working rust project. Let's connect our I2C display to the Arduino board as explained in the [article](https://lastminuteengineers.com/i2c-lcd-arduino-tutorial/) and try figure out how we can port the C++ example outlined there to rust:
```cpp
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x3F,16,2);  // set the LCD address to 0x3F for a 16 chars and 2 line display

void setup() {
  lcd.init();
  lcd.clear();         
  lcd.backlight();      // Make sure backlight is on
  
  // Print a message on both lines of the LCD.
  lcd.setCursor(2,0);   //Set cursor to character 2 on line 0
  lcd.print("Hello world!");
  
  lcd.setCursor(2,1);   //Move cursor to character 2 on line 1
  lcd.print("LCD Tutorial");
}

void loop() {
}
```

Clearly, we would like to compile ``LiquidCrystal_I2C``, link it to our crate as a dependency and call the same functions from rust. 

Ok, should be easy, right? Well, if you just have a standalone C library, compiling and calling it from rust is [straightforward](https://dev.to/xphoniex/how-to-call-c-code-from-rust-56do). Let's have a closer look at the source code of [LiquidCrystal_I2C.cpp](https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library/blob/master/LiquidCrystal_I2C.cpp):
```cpp
#include <inttypes.h>
#include <Arduino.h>
#include <Wire.h>
``` 

What are they? 
- [Arduino.h](https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/Arduino.h) is the main include file for the [Arduino SDK](https://github.com/arduino/ArduinoCore-avr)
- [Wire.h](https://github.com/arduino/ArduinoCore-avr/blob/master/libraries/Wire/src/Wire.h) is TWI/I2C library for Arduino & Wiring provided as part of Arduino SDK
- ``inttypes.h`` is standard library [header file](https://cplusplus.com/reference/cinttypes/) providing the 
support for width-based integral types.

What does it mean? Well, it means we can't just use ``LiquidCrystal_I2C`` alone without compiling Arduino SDK.


## Compiling Arduino SDK and the library

### Configuring Arduino SDK location

Let's create an empty file named ``build.rs`` in the root of our project. This is where we would write steps to compile a link to Arduino dependencies.

First, we should find the location of Arduino SDK and 3rd party ``LiquidCrystal_I2C`` library installed earlier. 

In my case, Arduino is installed at `` ~/.arduino15/``, and 3rd party library folder is ``~/Arduino/libraries/``. We could have just hardcoded the above as a constant in our ``build.rs``, but let's try to apply some better engineering principles and make our configuration more reusable! I propose to create a file called ``arduino.yaml`` next to the ``build.rs``. We could then use [serde_yaml](https://docs.rs/serde_yaml/latest/serde_yaml/) to read it in the ``build.rs`` file to avoid making code changes if the location changes in the future:
```yaml
arduino_home: $HOME/.arduino15
external_libraries_home: $HOME/Arduino/libraries
```
Now, let's locate the Arduino SDK inside the home folder of Arduino. In my case, it is ``~/.arduino15/packages/arduino/hardware/avr/1.8.6/``. The path is pretty standard, but the version might change in future, so we can move the version to our yaml file as well.

The Arduino SDK folder contains a set of the subfolders, but we are interested in two of them ``libraries`` and ``variants``:
- The ``libraries`` folder contains the code which can run on any Arduino board. This is where we can find ``Wire`` library we need. Nice!
- The ``variants`` folder definition of the pins specific to the given board. The folder for Arduino UNO is called ``eightanaloginputs`` (again, do not ask my why please).

Finally, we would also need C standard library headers (e.g. ``inttypes.h``) provided by the compiler. It is also installed with Arduino. In my case the path is ``~/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/avr/include/``

We can now add more information to our ``arduino.yaml``, specifying the core version, the compiler version, list of Arduino and external libraries to build:
```yaml
arduino_home: $HOME/.arduino15
external_libraries_home: $HOME/Arduino/libraries
core_version: 1.8.6
variant: eightanaloginputs
avr_gcc_version: 7.3.0-atmel3.6.1-arduino7
arduino_libraries:
  - Wire
external_libraries:
  - LiquidCrystal_I2C
```

### Reading the configuration

We can now read the above file in ``build.rs``. In order to do some add ``serde_yaml`` compile time dependency. We would also use [envmnt](https://docs.rs/envmnt/latest/envmnt/) crate to resolve the environment variables mentioned in the file:
 
```toml
[build-dependencies]
envmnt = "0.10.4"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
```

Then, we can define the ``Config`` struct and helper methods to construct the above path in the code
```rust
const CONFIG_FILE: &str = "arduino.yaml";

#[derive(Debug, Deserialize)]
struct Config {
    pub arduino_home: String,
    pub external_libraries_home: String,
    pub core_version: String,
    pub variant: String,
    pub avr_gcc_version: String,
    pub arduino_libraries: Vec<String>,
}

impl Config {
    fn arduino_package_path(&self) -> PathBuf {
        let expanded = envmnt::expand(&self.arduino_home, None);
        let arduino_home_path = PathBuf::from(expanded);
        arduino_home_path.join("packages").join("arduino")
    }

    fn core_path(&self) -> PathBuf {
        self.arduino_package_path()
            .join("hardware")
            .join("avr")
            .join(&self.core_version)
    }

    fn avr_gcc_homeavr_gcc_home(&self) -> PathBuf {
        self.arduino_package_path()
            .join("tools")
            .join("avr-gcc")
            .join(&self.avr_gcc_version)
    }

    fn avg_gcc(&self) -> PathBuf {
        self.avr_gcc_home().join("bin").join("avr-gcc")
    }

    fn arduino_core_path(&self) -> PathBuf {
        self.core_path().join("cores").join("arduino")
    }
}
```
Since we were dealing with path, we also defined the  helper ``avg_gcc`` method to provide the path to the compiler binary

We can now read this file and print it to console
```rust
fn main() {
    println!("cargo:rerun-if-changed={}", CONFIG_FILE);
    let config_string = std::fs::read_to_string(CONFIG_FILE)
        .unwrap_or_else(|e| panic!("Unable to read {} file: {}", CONFIG_FILE, e));
    let config: Config = serde_yaml::from_str(&config_string)
        .unwrap_or_else(|e| panic!("Unable to parse {} file: {}", CONFIG_FILE, e));

    println!("Arduino configuration: {:#?}", config);
}
```

The following line ensure the project is rebuild each time the yaml file is updated.
```rust
println!("cargo:rerun-if-changed={}", CONFIG_FILE);
```

At this point, we can run ``cargo build -vv`` (very verbose) and be able to see the parse config printed.

### Headers and sources

Ok, we know where our dependencies are. We will need to pass them to ``avr-gcc`` compiler soon. There are 3 types of files  we need to take care of:
- Header files ``.h``
- C source code ``.c``
- C++ source code ``.cpp``

Headers are the easiest to deal with. We would just pass it to the compiler using ``-I`` flag. We need 4 header folders to be included:
- Arduino variant-specific header(s)
- C standard library
- Arduino libraries
- External libraries

Let's implement the helper methods to get the paths of them:
```rust
impl Config {
    fn arduino_include_dirs(&self) -> Vec<PathBuf> {
        let variant_path = self.core_path().join("variants").join(&self.variant);
        let avr_gcc_include_path = self.avr_gcc_home().join("avr").join("include");
        vec![self.arduino_core_path(), variant_path, avr_gcc_include_path]
    }

    fn arduino_libraries_path(&self) -> Vec<PathBuf> {
        let library_root = self.core_path().join("libraries");
        let mut result = vec![];
        for library in &self.arduino_libraries {
            result.push(library_root.join(library).join("src"))
        }
        result
    }

    fn external_libraries_path(&self) -> Vec<PathBuf> {
        let expanded = envmnt::expand(&self.external_libraries_home, None);
        let external_library_root = PathBuf::from(expanded);
        let mut result = vec![];
        for library in &self.external_libraries {
            result.push(external_library_root.join(library))
        }
        result
    }

    fn include_dirs(&self) -> Vec<PathBuf> {
        let mut result = self.arduino_include_dirs();
        result.extend(self.arduino_libraries_path());
        result.extend(self.external_libraries_path());
        result
    }

}
```

C and C++ files would be passed to the compiler as an input one by one. The will need to be compiled with a slightly different set of the compiler flags, so let's define separate helpers to get the list of them:

```rust
impl Config {
    fn project_files(&self, patten: &str) -> Vec<PathBuf> {
        let mut result =
            files_in_folder(self.arduino_core_path().to_string_lossy().as_ref(), patten);
        let mut libraries = self.arduino_libraries_path();
        libraries.extend(self.external_libraries_path());

        let pattern = format!("**/{}", patten);
        for library in libraries {
            let lib_sources = files_in_folder(library.to_string_lossy().as_ref(), &pattern);
            result.extend(lib_sources);
        }

        result
    }

    fn cpp_files(&self) -> Vec<PathBuf> {
        self.project_files("*.cpp")
    }

    fn c_files(&self) -> Vec<PathBuf> {
        self.project_files("*.c")
    }
}

fn files_in_folder(folder: &str, pattern: &str) -> Vec<PathBuf> {
    let cpp_pattern = format!("{}/{}", folder, pattern);
    let mut results = vec![];
    for cpp_file in glob(&cpp_pattern).unwrap() {
        let file = cpp_file.unwrap();
        if !file.ends_with("main.cpp") {
            results.push(file);
        }
    }
    results
}
```

Here we use the ``glob`` create to look for the files recursively, so we need to also add it to ``Cargo.toml`` as a build dependency:
```toml
glob = "0.3"
```

### Compiler flags and definitions

In order to compile for Arduino UNO, we would need to pass some board specific flags to the compiler, in paricular:
```sh
-DARDUINO=10807 -DF_CPU: 16000000L -DARDUINO_AVR_UNO=1 -DARDUINO_ARCH_AVR -mmcu=atmega328p
```
As they are board specific, let's move the definition of them to ``arduino.yaml`` as well:
```yaml
definitions:
  ARDUINO: '10807'
  F_CPU: 16000000L
  ARDUINO_AVR_UNO: '1'
  ARDUINO_ARCH_AVR: '1'
flags:
  - '-mmcu=atmega328p'
```
We will also have to add corresponding fields to a ``Config`` struct
```rust
#[derive(Debug, Deserialize)]
struct Config {
    // Existing fields here
    pub definitions: HashMap<String, String>,
    pub flags: Vec<String>,
}
```

### Compilation and linking

While it is technically possible to construct the command line to invoke compiler manually using the CC crate is a much better option. Let's add the CC dependency to ``Cargo.toml``:
```toml
[build-dependencies]
cc = "1.0.74"
```

As mentioned above, C and C++ code needs be compiled with a slightly different set of flags. Let's define a helper function to define the common part of the configuration
```rust
fn configure_arduino(config: &Config) -> Build {
    let mut builder = Build::new();
    for (k, v) in &config.definitions {
        builder.define(k, v.as_str());
    }
    for flag in &config.flags {
        builder.flag(flag);
    }
    builder
        .compiler(config.avg_gcc())
        .flag("-Os")
        .cpp_set_stdlib(None)
        .flag("-fno-exceptions")
        .flag("-ffunction-sections")
        .flag("-fdata-sections");

    for include_dir in config.include_dirs() {
        builder.include(include_dir);
    }
    builder
}
```

Here we create CC provided builder and pass compile flags, definition and include folders.

We can then use configured build to compile C/C++ parts of Arduino standard library:
```rust
pub fn add_source_file(builder: &mut Build, files: Vec<PathBuf>) {
    for file in files {
        println!("cargo:rerun-if-changed={}", file.to_string_lossy());
        builder.file(file);
    }
}

fn compile_arduino(config: &Config) {
    let mut builder = configure_arduino(&config);
    builder
        .cpp(true)
        .flag("-std=gnu++11")
        .flag("-fpermissive")
        .flag("-fno-threadsafe-statics");
    add_source_file(&mut builder, config.cpp_files());
    builder.compile("libarduino_c++.a");


    let mut builder = configure_arduino(&config);
    builder.flag("-std=gnu11");
    add_source_file(&mut builder, config.c_files());
    builder.compile("libarduino_c.a");

    println!("cargo:rustc-link-lib=static=arduino_c++");
    println!("cargo:rustc-link-lib=static=arduino_c");
}
```
We simply compile each C and CPP file  we can find together with the only exception of the ``main.cpp`` as the main method is defined in rust. CC crate will call ``avr-gcc`` and produce to object files: ``libarduino_c++.a`` and ``libarduino_c.a``. 

We then instruct cargo to statically link rust code with them by printing 
```
cargo:rustc-link-lib=static=arduino_c++
cargo:rustc-link-lib=static=arduino_c
```
lines to the standard output.

# Generate rust bindings 

So far we've compiled the required dependencies with our rust project. But running ``cargo run`` still does nothing, but blinks the LED. In order to interact with the display, we need to write code to do so. But how could we call a C++ function defined in liquidcrystal_i2c.h from rust?

This is where [bingen](https://rust-lang.github.io/rust-bindgen/) is coming to help. Bingen takes C//C++ header files as an input and generates rust definitions of the functions and types provided by them. Let's add it to our ``Cargo.toml`` as well:
```toml
[build-dependencies]
bindgen = "0.61"
```

First step is to define the headers files to pass to bindgen. We can just implement a small helper method to find them in the external libraries folder:
```
impl Config {
    fn bindgen_headers(&self) -> Vec<PathBuf> {
        let mut result = vec![];
        for library in self.external_libraries_path() {
            let lib_headers = files_in_folder(library.to_string_lossy().as_ref(), "*.h");
            result.extend(lib_headers);
        }
        result
    }
}
```

Next, we need to define the bingen configuration:
```rust
fn configure_bindgen_for_arduino(config: &Config) -> Builder {
    let mut builder = Builder::default();
    for (k, v) in &config.definitions {
        builder = builder.clang_arg(&format!("-D{}={}", k, v));
    }
    for flag in &config.flags {
        builder = builder.clang_arg(flag);
    }
    builder = builder
        .clang_args(&["-x", "c++", "-std=gnu++11"])
        .use_core()
        .layout_tests(false)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks));

    for include_dir in config.include_dirs() {
        builder = builder.clang_arg(&format!("-I{}", include_dir.to_string_lossy()));
    }
    for header in config.bindgen_headers() {
        builder = builder.header(header.to_string_lossy());
    }
    builder
}
```
Note that we need to pass the same set of compiler flags and definitions to bingen as we passed to ``avr-gcc`` earlier.

Then, we can call ``generate`` method to generate the bindings and write them to ``src/arduino.rs`` file:
```rust 
fn generate_bindings(config: &Config) {
    let bindings: Bindings = configure_bindgen_for_arduino(&config)
        .generate()
        .expect("Unable to generate bindings");
    let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("src")
        .join("arduino.rs");
    bindings
        .write_to_file(project_root)
        .expect("Couldn't write bindings!");
}
```

We can now open ``src/arduino.rs`` and see generated code. A lot of generated code! This is because by default bingen goes over each header file recursively and generates bindings for every type and method. As we do not need most of them, we can configure allow and block list to exclude useless parts.

To do so, we can add a new section to ``arduino.yaml``:
```yaml
bindgen_lists:
  allowlist_function:
    - LiquidCrystal_I2C.*
  allowlist_type:
    - LiquidCrystal_I2C.*
  blocklist_function:
    - Print.*
    - String.*
  blocklist_type:
    - Print.*
    - String.*
```

We can then modify the config object:
```rust
#[derive(Debug, Deserialize)]
struct BindgenLists {
    pub allowlist_function: Vec<String>,
    pub allowlist_type: Vec<String>,
    pub blocklist_function: Vec<String>,
    pub blocklist_type: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct Config {
    // Existing fields
    pub bindgen_lists: BindgenLists,
}
```
and ``configure_bindgen_for_arduino`` function to make use of them
```rust
fn configure_bindgen_for_arduino(config: &Config) -> Builder {
    // Existing code
   
    for item in &config.bindgen_lists.allowlist_function {
        builder = builder.allowlist_function(item);
    }
    for item in &config.bindgen_lists.allowlist_type {
        builder = builder.allowlist_type(item);
    }
    for item in &config.bindgen_lists.blocklist_function {
        builder = builder.blocklist_function(item);
    }
    for item in &config.bindgen_lists.blocklist_type {
        builder = builder.blocklist_type(item);
    }
    builder
}
```

arduino.rs looks so much nicer now!

# Step 5: Writing and running the code

Have you ever wondered why there is no main method in the Arduino sketches? Indeed, the standard template of the Arduino file is the following:
```cpp
void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:
}
```

Remember the ``main.cpp`` file we've excluded in the step 3? Let's look inside (simplified code):
```cpp
#include <Arduino.h>

int main(void)
{
        init();
        setup();
    
        for (;;) {
                loop();
                if (serialEventRun) serialEventRun();
        }
        return 0;
}
```
Looks like no magic at all! It just calls ``setup()`` and then ``loop`` method in the loop. We do not care about ``setup`` and ``loop`` in rust, but what is the init call?! It turns out that it is Arduino SDK provided [function](https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/wiring.c#L241) to initialize the board. We would need to call it in rust as well before we start using any Arduino library. We could have just generated it using bindgen, but since its signature is so simple (no arguments) we can just define it in place:
```rust
extern "C" {
    fn init();
}
``` 
Now we are ready to modify our ``main.rs`` with some code to initialize our display and print some text:
```rust
#[arduino_hal::entry]
unsafe fn main() -> ! {
    init();

    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);

    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

    let mut led = pins.d13.into_output();

    ufmt::uwriteln!(&mut serial, "starting on {}\r", 0x27).void_unwrap();

    let mut lcd = LiquidCrystal_I2C::new(0x27, 16, 2);

    let ferris = &[
        0b01010u8, 0b01010u8, 0b00000u8, 0b00100u8, 0b10101u8, 0b10101u8, 0b11111u8, 0b10101u8,
    ];

    lcd.begin(16, 2, 0);
    lcd.init();
    lcd.backlight();

    lcd.clear();
    lcd.printstr("Good morning\0".as_ptr().cast());
    lcd.setCursor(0, 1);
    lcd.printstr("from Rust!!\0".as_ptr().cast());

    lcd.createChar(0, ferris.as_ptr() as *mut _);
    lcd.setCursor(12, 1);

    LiquidCrystal_I2C_write((&mut lcd as *mut LiquidCrystal_I2C).cast(), 0);

    loop {
        led.toggle();
        arduino_hal::delay_ms(1000);
    }
}
```
Here we combine the ``arduino_hal`` method and the methods provided by LiquidCrystal_I2C library. 
It is now time to run ``cargo run`` and see the message on the display! Well done!

# Summary

I really hope you've learned something by reading this article! We covered the following topics today:
- Rust Arduino Environment setup
- Structure of Arduino SDK
- Compilation Arduino library, linking it to rust crate and generation of the rust definitions for C++ methods.

If the article was useful for you, please put a reaction and start the [GitHub repository](https://github.com/kgrech/rust_arduino). 

I am also happy to hear a feedback about the ``arduino.yaml`` DSL developed as part of this article. Would it be useful to move it into separate library to be used in the build time? Is there a better way to do the same? Please share your opinion in the comments!