Skip to content

Repo demonstrates option of using no_std Rust as ESP-IDF component

Notifications You must be signed in to change notification settings

georgik/esp32-idf-no-std-rust-component

Repository files navigation

Integrating a Rust Component into an ESP-IDF Project

ESP-IDF, the official development framework for the ESP32 Series SoCs, supports integration of components written in C/C++ and Rust which is gaining traction for embedded development due to its safety features. This article outlines the steps to add a Rust component to your ESP-IDF project.

Prerequisites

Structure

Here's how your project directory might look after the following the guide:

esp_idf_project/
|-- CMakeLists.txt
|-- main/
|   |-- CMakeLists.txt
|   |-- esp_idf_project.c
|-- sdkconfig
|-- components/
|   |-- esp_rust_component/
|       |-- CMakeLists.txt
|       |-- include/
|           |-- esp_rust_component.h
|       |-- esp_rust_component.c
|       |-- rust_crate/
|           |-- Cargo.toml
|           |-- rust-toolchain.toml
|           |-- src/
|               |-- lib.rs

Architecture

Key elements:

  • esp_idf_project contains main C code like any other ESP-IDF application.
  • The ESP-IDF componet with name esp_rust_component is stored in subdirectory with components.
    • The esp_rust_component component contains C adapter layer, which helps interfacing with Rust library.
  • The Rust code is stored in components/esp_rust_component/rust_crate subdirectory.

The component can be uploaded later to Component Manager.

Step-by-Step Guide

Set-up the environment

Before starting the project, make sure that the Prerequisites are met, and that you have sourced the required export files.

Create ESP-IDF project

Use ESP-IDF tooling to create new project with name esp_idf_project.

idf.py create-project esp_idf_project
cd esp_idf_project

Create the ESP-IDF Component

Create a new directory in your components/ folder. You can name it esp_rust_component.

mkdir components
cd components
idf.py create-component esp_rust_component

If you've decided to create new component manually make sure to run, so that CMake will pick newly created component:

idf.py reconfigure

Set up the CMakeLists.txt File

In your esp_rust_component directory, edit the CMakeLists.txt file with the following content:

idf_component_register(
    SRCS "esp_rust_component.c"
    INCLUDE_DIRS "include"
)

# Define the Rust target for the Xtensa and RISC-V architecture
if (CONFIG_IDF_TARGET_ARCH_XTENSA)
    set(RUST_CARGO_TOOLCHAIN "+esp")
    set(RUST_CARGO_TARGET "xtensa-${IDF_TARGET}-none-elf")
elseif (CONFIG_IDF_TARGET_ARCH_RISCV)
    set(RUST_CARGO_TOOLCHAIN "+nightly")
    set(RUST_CARGO_TARGET "riscv32imac-unknown-none-elf")
else()
    message(FATAL_ERROR "Architecture currently not supported")
endif()

# Set the flags for cargo build
set(CARGO_BUILD_FLAGS "-Zbuild-std=core")

# Set directories and target
set(RUST_PROJECT_DIR "${CMAKE_CURRENT_LIST_DIR}/rust_crate")
set(RUST_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}")
set(RUST_TARGET_DIR "${RUST_BUILD_DIR}/target")
set(RUST_STATIC_LIBRARY "${RUST_TARGET_DIR}/${RUST_CARGO_TARGET}/release/librust_crate.a")

# ExternalProject_Add for building the Rust project
ExternalProject_Add(
    rust_crate_target
    PREFIX "${RUST_PROJECT_DIR}"
    DOWNLOAD_COMMAND ""
    CONFIGURE_COMMAND ""
    BUILD_COMMAND ${CMAKE_COMMAND} -E env
        CARGO_BUILD_TARGET=${RUST_CARGO_TARGET}
        CARGO_BUILD_TARGET_DIR=${RUST_TARGET_DIR}
        cargo ${RUST_CARGO_TOOLCHAIN} build --release ${CARGO_BUILD_FLAGS} -Zbuild-std-features=compiler-builtins-weak-intrinsics
    BUILD_ALWAYS TRUE
    INSTALL_COMMAND ""
    WORKING_DIRECTORY ${RUST_PROJECT_DIR}
    TMP_DIR "${RUST_BUILD_DIR}/tmp"
    STAMP_DIR "${RUST_BUILD_DIR}/stamp"
    DOWNLOAD_DIR "${RUST_BUILD_DIR}"
    SOURCE_DIR "${RUST_PROJECT_DIR}"
    BINARY_DIR "${RUST_PROJECT_DIR}"
    INSTALL_DIR "${RUST_BUILD_DIR}"
    BUILD_BYPRODUCTS "${RUST_STATIC_LIBRARY}"
)

# Add prebuilt Rust library
add_prebuilt_library(rust_crate_lib "${RUST_STATIC_LIBRARY}" REQUIRES "")

# Add dependencies and link Rust library
add_dependencies(${COMPONENT_LIB} rust_crate_target)
target_link_libraries(${COMPONENT_LIB} PUBLIC rust_crate_lib)

Create a Rust Project Inside the Component

Create a new Rust crate, which will be a library, inside esp_rust_component called rust_crate:

cargo init --lib rust_crate

Update the Cargo.toml to match the settings for your target board. Also set the crate type to staticlib:

[package]
name = "rust_crate"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["staticlib"]

[dependencies]

[features]
default = [ ]

Rust to C Interoperability

Add a Rust function with C linkage in your lib.rs that will be callable from C code. An example might be:

#![cfg_attr(not(feature = "std"), no_std)]

use core::ffi::c_void;
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {
    }
}

static HELLO_ESP32: &'static [u8] = b"Hello ESP-RS. https://github.com/esp-rs\0";

#[no_mangle]
pub extern "C" fn hello() -> *const c_void {
    HELLO_ESP32.as_ptr() as *const c_void
}

Create a C Wrapper

Create file esp_rust_component/esp_rust_component.c to include the Rust functions.

#include "rust_component.h"

Update the Header File

Include the C header file in your esp_rust_component/include/esp_rust_component.h:

extern const void* hello();

Call Rust code from C

Update main ESP-IDF project file main/esp_idf_project.c:

#include "stdio.h"
#include "esp_rust_component.h"

void app_main() {
    const char* message = hello();
    printf("%s\n", message);
}

Select target

Set target for main ESP-IDF application:

idf.py set-target <target>
# idf.py set-target esp32
# idf.py set-target esp32-c3
# idf.py set-target esp32-s3

Optional step when developers need to build Rust components manually: Define which toolchain should be used for the Rust component in file esp_rust_component/rust_crate/rust-toolchain.toml

[toolchain]
# Use "esp" for ESP32, ESP32-S2, and ESP32-S3
channel = "esp"
# Use "nightly" for ESP32-C*, ESP32-H* targets
# channel = "nightly"

Build the Project

From the base folder of the project (esp_idf_project), run the build process as you usually would for an ESP-IDF project:

idf.py build flash monitor

This command will build, flash the resulting binary to your board and open a serial monitor.

Troubleshooting

  • If you encounter linker errors, you may need to update your Rust flags. For example, you might need to add the -Zbuild-std-features=compiler-builtins-weak-intrinsics flag to CARGO_BUILD_FLAGS in your CMakeLists.txt.

Simulation

Simulation with Wokwi in VS Code

Wokwi for Visual Studio Code provides a simulation solution for embedded and IoT system engineers. The extension integrates with your existing development environment, allowing you to simulate your projects directly from your code editor.

Add files for Wokwi simulator

Create wokwi.toml in the root of the project. The file contains references to BIN and ELF previously built by idf.py.

[wokwi]
version = 1
elf = "build/esp_idf_project.elf"
firmware = "build/esp_idf_project.bin"

Create diagram.json. The file contains board selected for the simulation.

{
  "version": 1,
  "author": "Espressif Systems",
  "editor": "wokwi",
  "parts": [ { "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0, "attrs": {} } ],
  "connections": [ [ "esp:TX0", "$serialMonitor:RX", "", [] ], [ "esp:RX0", "$serialMonitor:TX", "", [] ] ],
  "dependencies": {}
}

Open VS Code, open command palette (CMD/Ctrl+Shift+P), search for Wokwi: Start Simulator, select the option to start simulation.

Use Pause button to display state of pins.

The plugin auto-reload application if the binary was updated.

Using this repository as template

If you prefer starting the project from a template, you can use different methods:

  • git: Simply clone the repository and avoid generating and populating the different files
  • cargo-generate to get the project ready to build and flash!
  • GitHub templates

git

  1. Make sure that the Prerequisites are met and that you have sourced the required export files.
  2. Clone the repository: git clone https://github.com/georgik/esp32-idf-no-std-rust-component
  3. Set the target: idf.py set-target <target>
  4. Build and flash the project: idf.py build flash monitor

cargo-generate

  1. Make sure that the Prerequisites are met and that you have sourced the required export files.
  2. Install cargo-generate: cargo install cargo-generate
  3. Generate the template cargo generate georgik/esp32-idf-no-std-rust-component
  4. Set the target: idf.py set-target <target>
  5. Build and flash the project: idf.py build flash monitor

GitHub templates

  1. Make sure that the Prerequisites are met and that you have sourced the required export files.
  2. Create your own repository from georgik/esp32-idf-no-std-rust-component
    1. Above the file list, click Use this template.
    2. Select Create a new repository.
    3. Fill the required information
    4. Create the repository
  3. Clone the repository that you just created: git clone https://github.com/<owner>/<repository>
  4. Set the target: idf.py set-target <target>
  5. Build and flash the project: idf.py build flash monitor

Adding GitHub Action tests

If we want to add some CI to our project, we can leverage Wokwi CI, the following YAML file will build and check that the generated project runs properly:

name: CI
on:
  push:
  pull_request:
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  ESP_TARGET: esp32
  ESP_IDF_VERSION: v5.1


jobs:
  build-check:
    name: Checks
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup | Rust (RISC-V)
        if: env.ESP_TARGET != 'esp32' && env.ESP_TARGET != 'esp32s2' && env.ESP_TARGET != 'esp32s3'
        uses: dtolnay/rust-toolchain@v1
        with:
          target: riscv32imc-unknown-none-elf
          toolchain: nightly
          components: rust-src
      - name: Setup | Rust (Xtensa)
        if: env.ESP_TARGET == 'esp32' && env.ESP_TARGET == 'esp32s2' && env.ESP_TARGET == 'esp32s3'
        uses: esp-rs/[email protected]
        with:
          default: true
          buildtargets: {{ env.ESP_TARGET }}
          ldproxy: false
      - uses: Swatinem/rust-cache@v2
      - name: Setup | ESP-IDF
        shell: bash
        run: |
          git clone -b {{ env.ESP_IDF_VERSION }} --shallow-submodules --single-branch --recursive https://github.com/espressif/esp-idf.git /home/runner/work/esp-idf
          /home/runner/work/esp-idf/install.sh  {{ env.ESP_TARGET }}
      - name: Build project
        shell: bash
        run: |
          . /home/runner/work/esp-idf/export.sh
          idf.py set-target  {{ env.ESP_TARGET }}
          idf.py build
      - name: Wokwi CI check
        uses: wokwi/wokwi-ci-action@v1
        with:
          token: ${{ secrets.WOKWI_CLI_TOKEN }}
          timeout: 10000
          expect_text: 'Hello ESP-RS. https://github.com/esp-rs'
          fail_text: 'Error'

I'ts important to note that we need to set the WOKWI_CLI_TOKEN secret:

  1. Create a Wokwi CI token
  2. Add it as a secret in your GitHub repository

Also, the CI file needs to be modified if used for other targets.

About

Repo demonstrates option of using no_std Rust as ESP-IDF component

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published