开发 Rust/C/C++ cmake 混合应用

Posted on Mon, Sep 2, 2024 Rust 单片机

esp-idf 支持链接 C/C++ 或 Rust 编写的 component,而使用 cargo generate esp-rs/esp-idf-template cmake来创建 Rust + C/C++ 混合风格的 std 或 non_std 应用,该应用使用安装的 ~/esp/esp-idf/v5.2.1/ 中的idf.py 和 cmake 来构建。

默认创建启用 HAL( esp-idf-svc)的 std 应用。

Rust cmake 读取和使用的参数:

  1. RUST_DEPS
  2. CONFIG_IDF_TARGET_ARCH_RISCV
  3. SDKCONFIG
  4. IDF_PATH

使用 cargo generate esp-rs/esp-idf-template cmake 创建应用:

构建前需要 同时 source esp-idf 的 export.h 和 export-esp.sh。

构建命令是 idf.py build 而非 cargo build。

创建项目,默认是 std 应用

Copy

zj@a:~/code/esp32$ cargo generate esp-rs/esp-idf-template cmake
⚠️   Favorite `esp-rs/esp-idf-template` not found in config, using it as a git repository: https://github.com/esp-rs/esp-idf-template.git
🤷   Project Name: mycmake
🔧   Destination: /Users/alizj/code/esp32/mycmake ...
🔧   project-name: mycmake ... 
🔧   Generating template ...
✔ 🤷   Configure advanced template options? · true
✔ 🤷   Rust toolchain (beware: nightly works only for riscv MCUs!) · esp
✔ 🤷   Enable HAL support? · true
✔ 🤷   Enable STD support? · true
🔧   Moving generated files into: `/Users/alizj/code/esp32/mycmake`...
🔧   Initializing a fresh Git repository
✨   Done! New project created /Users/alizj/code/esp32/mycmake

zj@a:~/code/esp32/mycmake$ ls
CMakeLists.txt  components/  diagram.json  main/  sdkconfig.defaults  wokwi.toml

zj@a:~/code/esp32/mycmake$ ls main/
CMakeLists.txt  main.c

Rust 代码作为一个 component 被集成:

zj@a:~/code/esp32/mycmake$ ls components/rust-mycmake/
CMakeLists.txt  Cargo.toml  build.rs  placeholder.c  rust-toolchain.toml  src/

zj@a:~/code/esp32/mycmake$ ls components/rust-mycmake/src/
lib.rs 

Rust componet 的 CMakeLists.txt 文件封装了构建该 Rust 代码的 cargo 命令和配置参数。

Copy

zj@a:~/code/esp32/mycmake/components/rust-mycmake$ cat CMakeLists.txt
# If this component depends on other components - be it ESP-IDF or project-specific ones - enumerate those in the double-quotes below, separated by spaces
# Note that pthread should always be there, or else STD will not work
set(RUST_DEPS "pthread" "driver" "vfs")
# Here's a non-minimal, reasonable set of ESP-IDF components that one might want enabled for Rust:
#set(RUST_DEPS "pthread" "esp_http_client" "esp_http_server" "espcoredump" "app_update" "esp_serial_slave_link" "nvs_flash" "spi_flash" "esp_adc_cal" "mqtt")
 
idf_component_register(
    SRCS "placeholder.c"
    INCLUDE_DIRS ""
    PRIV_REQUIRES "${RUST_DEPS}"
)

if(CONFIG_IDF_TARGET_ARCH_RISCV)
    if (CONFIG_IDF_TARGET_ESP32C6 OR CONFIG_IDF_TARGET_ESP32C5 OR CONFIG_IDF_TARGET_ESP32H2)
        set(RUST_TARGET "riscv32imac-esp-espidf")
    else ()
        set(RUST_TARGET "riscv32imc-esp-espidf")
    endif()
elseif(CONFIG_IDF_TARGET_ESP32)
    set(RUST_TARGET "xtensa-esp32-espidf")
elseif(CONFIG_IDF_TARGET_ESP32S2)
    set(RUST_TARGET "xtensa-esp32s2-espidf")
elseif(CONFIG_IDF_TARGET_ESP32S3)
    set(RUST_TARGET "xtensa-esp32s3-espidf")
else()
    message(FATAL_ERROR "Unsupported target ${CONFIG_IDF_TARGET}")
endif()

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(CARGO_BUILD_TYPE "debug")
    set(CARGO_BUILD_ARG "")
else()
    set(CARGO_BUILD_TYPE "release")
    set(CARGO_BUILD_ARG "--release")
endif()


set(CARGO_BUILD_STD_ARG -Zbuild-std=std,panic_abort)


if(IDF_VERSION_MAJOR GREATER "4")
set(ESP_RUSTFLAGS "--cfg espidf_time64")
endif()

set(CARGO_PROJECT_DIR "${CMAKE_CURRENT_LIST_DIR}")
set(CARGO_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}")
set(CARGO_TARGET_DIR "${CARGO_BUILD_DIR}/target")

set(RUST_INCLUDE_DIR "${CARGO_TARGET_DIR}")
set(RUST_STATIC_LIBRARY "${CARGO_TARGET_DIR}/${RUST_TARGET}/${CARGO_BUILD_TYPE}/librust_mycmake.a")

# if this component uses CBindGen to generate a C header, uncomment the lines below and adjust the header name accordingly
#set(RUST_INCLUDE_HEADER "${RUST_INCLUDE_DIR}/RustApi.h")
#set_source_files_properties("${RUST_INCLUDE_HEADER}" PROPERTIES GENERATED true)

idf_build_get_property(sdkconfig SDKCONFIG)
idf_build_get_property(idf_path IDF_PATH)

ExternalProject_Add(
    project_rust_mycmake
    PREFIX "${CARGO_PROJECT_DIR}"
    DOWNLOAD_COMMAND ""
    CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env
        cargo clean --target ${RUST_TARGET} --target-dir ${CARGO_TARGET_DIR}
    USES_TERMINAL_BUILD true
    BUILD_COMMAND ${CMAKE_COMMAND} -E env
        CARGO_CMAKE_BUILD_INCLUDES=$<TARGET_PROPERTY:${COMPONENT_LIB},INCLUDE_DIRECTORIES>
        CARGO_CMAKE_BUILD_LINK_LIBRARIES=$<TARGET_PROPERTY:${COMPONENT_LIB},LINK_LIBRARIES>
        CARGO_CMAKE_BUILD_SDKCONFIG=${sdkconfig}
        CARGO_CMAKE_BUILD_ESP_IDF=${idf_path}
        CARGO_CMAKE_BUILD_COMPILER=${CMAKE_C_COMPILER}
        RUSTFLAGS=${ESP_RUSTFLAGS}
        MCU=${CONFIG_IDF_TARGET}
        cargo build --target ${RUST_TARGET} --target-dir ${CARGO_TARGET_DIR} ${CARGO_BUILD_ARG} ${CARGO_BUILD_STD_ARG}
    INSTALL_COMMAND ""
    BUILD_ALWAYS TRUE
    TMP_DIR "${CARGO_BUILD_DIR}/tmp"
    STAMP_DIR "${CARGO_BUILD_DIR}/stamp"
    DOWNLOAD_DIR "${CARGO_BUILD_DIR}"
    SOURCE_DIR "${CARGO_PROJECT_DIR}"
    BINARY_DIR "${CARGO_PROJECT_DIR}"
    INSTALL_DIR "${CARGO_BUILD_DIR}"
    BUILD_BYPRODUCTS
        "${RUST_INCLUDE_HEADER}"
        "${RUST_STATIC_LIBRARY}"
)

add_prebuilt_library(library_rust_mycmake "${RUST_STATIC_LIBRARY}" PRIV_REQUIRES "${RUST_DEPS}")
add_dependencies(library_rust_mycmake project_rust_mycmake)

target_include_directories(${COMPONENT_LIB} PUBLIC "${RUST_INCLUDE_DIR}")
target_link_libraries(${COMPONENT_LIB} PRIVATE library_rust_mycmake)

配置了 crate-type 为 staticlib,故会被编译为 esp-idf 可以链接的 C 库

zj@a:~/code/esp32/mycmake/components/rust-mycmake$ cat Cargo.toml
[package]
name = "rust-mycmake"
version = "0.1.0"
authors = ["alizj"]
edition = "2021" 
resolver = "2"
rust-version = "1.71"

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

[profile.release]
opt-level = "s"

[profile.dev]
debug = true # Symbols are nice and they don't increase the size on Flash
opt-level = "z"
[features]
default = ["std", "embassy", "esp-idf-svc/native"]

pio = ["esp-idf-svc/pio"]
std = ["alloc", "esp-idf-svc/std"]
alloc = ["esp-idf-svc/alloc"]
nightly = ["esp-idf-svc/nightly"]
experimental = ["esp-idf-svc/experimental"]
embassy = ["esp-idf-svc/embassy-sync", "esp-idf-svc/critical-section", "esp-idf-svc/embassy-time-driver"]

[dependencies]
log = { version = "0.4", default-features = false }
esp-idf-svc = { version = "0.48", default-features = false }

[build-dependencies]
embuild = "0.31.3"

# 编译为 staticlib,可以被 C 库调用的 Rust 代码
zj@a:~/code/esp32/mycmake/components/rust-mycmake$ cat src/lib.rs
#[no_mangle]
extern "C" fn rust_main() -> i32 {
    // It is necessary to call this function once. Otherwise some patches to the runtime
    // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
    esp_idf_svc::sys::link_patches();

    // Bind the log crate to the ESP Logging facilities
    esp_idf_svc::log::EspLogger::initialize_default();

    log::info!("Hello, world!");
    42
}

zj@a:~/code/esp32/mycmake/components/rust-mycmake$ cat placeholder.c
/* Hello World Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/

/* This is an empty source file to force the build of a CMake library that
   can participate in the CMake dependency graph. This placeholder library
   will depend on the actual library generated by external Rust build.
*/

# main.c 中调用 Rust 的 rust_main() 函数代码
zj@a:~/code/esp32/mycmake$ cat main/main.c
/* Hello World Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>

extern int rust_main(void);

void app_main(void) {
    printf("Hello world from C!\n");

    int result = rust_main();

    printf("Rust returned code: %d\n", result);
}

构建前需要 source esp-idf 的 export.h 和 export-esp.sh:

Copy

zj@a:~/code/esp32/mycmake$ source ~/esp/esp-idf/v5.2.1/export.sh   # esp idf
zj@a:~/code/esp32/mycmake$ source ~/export-esp.sh   # esp rs,因为后续会 build Rust component 

构建:

#idf.py set-target [esp32|esp32s2|esp32s3|esp32c2|esp32c3|esp32c6|esp32h2]
zj@a:~/code/esp32/mycmake$ idf.py build 
Executing action: all (aliases: build)
Running ninja in directory /Users/alizj/code/esp32/mycmake/build
Executing "ninja all"...
[0/9] Performing build step for 'project_rust_mycmake'    Finished release [optimized] target(s) in 0.25s
[1/1] cd /Users/alizj/code/esp32/mycmake/build/bootloader/esp-idf/esptool_py && /Users/alizj/.espressif/p...izes.py --offset 0x8000 bootloader 0x1000 /Users/alizj/code/esp32/mycmake/build/bootloader/bootloader.bi
Bootloader binary size 0x6860 bytes. 0x7a0 bytes (7%) free.
[3/3] cd /Users/alizj/code/esp32/mycmake/build/esp-idf/esptool_py && /Users/alizj/.espressif/python_env/i...esp32/mycmake/build/partition_table/partition-table.bin /Users/alizj/code/esp32/mycmake/build/mycmake.bi
mycmake.bin binary size 0x5b770 bytes. Smallest app partition is 0x100000 bytes. 0xa4890 bytes (64%) free.

Project build complete. To flash, run:
 idf.py flash
or
 idf.py -p PORT flash
or
 python -m esptool --chip esp32 -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 2MB --flash_freq 40m 0x1000 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0x10000 build/mycmake.bin
or from the "/Users/alizj/code/esp32/mycmake/build" directory
 python -m esptool --chip esp32 -b 460800 --before default_reset --after hard_reset write_flash "@flash_args"

# 构建的产物位于 build 目录下 。

烧录到设备 flash

Copy

idf.py -p /dev/cu.usbmodem2101 flash monitor

参考:

  1. esp-idf-template 的 cmake 文档;
  2. Integrating a Rust Component into an ESP-IDF Project