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.
- Installed ESP-IDF
- Installed Rust and Cargo
- Installed Xtensa LLVM toolchain for Rust
- Basic knowledge of ESP-IDF, CMake, and Rust
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
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
- The Rust code is stored in
components/esp_rust_component/rust_crate
subdirectory.
The component can be uploaded later to Component Manager.
Before starting the project, make sure that the Prerequisites are met, and that you have sourced the required export files.
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 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
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 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 = [ ]
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 file esp_rust_component/esp_rust_component.c
to include the Rust functions.
#include "rust_component.h"
Include the C header file in your esp_rust_component/include/esp_rust_component.h
:
extern const void* hello();
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);
}
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"
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.
- 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 toCARGO_BUILD_FLAGS
in yourCMakeLists.txt
.
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.
- Install VS Code
- Install Wokwi plugin
- Activate Wokwi plugin - command palette, search for
Wokwi: Start Simulator
, select and activate the plugin using web browser
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.
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
- Make sure that the Prerequisites are met and that you have sourced the required export files.
- Clone the repository:
git clone https://github.com/georgik/esp32-idf-no-std-rust-component
- Set the target:
idf.py set-target <target>
- Build and flash the project:
idf.py build flash monitor
- Make sure that the Prerequisites are met and that you have sourced the required export files.
- Install
cargo-generate
:cargo install cargo-generate
- Generate the template
cargo generate georgik/esp32-idf-no-std-rust-component
- Set the target:
idf.py set-target <target>
- Build and flash the project:
idf.py build flash monitor
- Make sure that the Prerequisites are met and that you have sourced the required export files.
- Create your own repository from
georgik/esp32-idf-no-std-rust-component
- Above the file list, click Use this template.
- Select Create a new repository.
- Fill the required information
- Create the repository
- Clone the repository that you just created:
git clone https://github.com/<owner>/<repository>
- Set the target:
idf.py set-target <target>
- Build and flash the project:
idf.py build flash monitor
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:
Also, the CI file needs to be modified if used for other targets.