Create components for Rust and C integration (#10)

* Create components for Rust and C integration
* Create component for C code that will be callable from Rust
* Create component for Rust code that will be callable from C
* Remove circular dependencies between C and Rust code
* Run cbindgen to dynamically generate C headers for Rust functions
This commit is contained in:
Robert Cottrell 2021-06-25 12:57:03 +01:00 committed by GitHub
parent b57a505379
commit 8c94733638
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 130 additions and 83 deletions

View File

@ -4,10 +4,3 @@ cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32-hello-rust)
# The Xtensa Rust build includes compiler intrinsics that conflict with names
# in GCC. To avoid multiple definition errors, allow the linker to pick one of
# the available options and ignore the others.
if((${IDF_TARGET} MATCHES "esp32") OR (${IDF_TARGET} MATCHES "esp32s2"))
target_link_options(${CMAKE_PROJECT_NAME}.elf PRIVATE "-Wl,--allow-multiple-definition")
endif()

View File

@ -0,0 +1,2 @@
idf_component_register(SRCS "clib.c"
INCLUDE_DIRS "include")

View File

@ -0,0 +1,5 @@
# clib
This component exposes functions and data defined in C that will accessed from Rust.
The Rust build will automatically generate bindings to these functions by processing the exported header files with bindgen.

17
components/clib/clib.c Normal file
View File

@ -0,0 +1,17 @@
/* 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>
#include "CApi.h"
bool validate_param_in_c(int param, int value)
{
printf("C validated param #%d = %d\n", param, value);
return true;
}

View File

@ -0,0 +1,57 @@
set(RUST_DEPS "clib")
idf_component_register(
SRCS "placeholder.c"
INCLUDE_DIRS ""
PRIV_REQUIRES "${RUST_DEPS}"
)
set(CARGO_BUILD_TYPE "release")
set(CARGO_BUILD_ARG "--release")
if(CONFIG_IDF_TARGET_ARCH_RISCV)
set(CARGO_TARGET "riscv32i-unknown-none-elf")
set(CARGO_FEATURES_ARG "")
elseif(CONFIG_IDF_TARGET_ARCH_XTENSA)
set(CARGO_TARGET "xtensa-esp32-none-elf")
set(CARGO_FEATURES_ARG "--features=std")
endif()
set(RUST_PROJECT_DIR "${CMAKE_CURRENT_LIST_DIR}")
set(RUST_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}")
set(RUST_TARGET_DIR "${RUST_BUILD_DIR}/target")
set(RUST_INCLUDE_DIR "${RUST_TARGET_DIR}")
set(RUST_INCLUDE_HEADER "${RUST_INCLUDE_DIR}/RustApi.h")
set(RUST_STATIC_LIBRARY "${RUST_TARGET_DIR}/${CARGO_TARGET}/${CARGO_BUILD_TYPE}/librustlib.a")
ExternalProject_Add(
rustlib_project
PREFIX "${RUST_PROJECT_DIR}"
DOWNLOAD_COMMAND ""
CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env
CARGO_BUILD_TARGET=${CARGO_TARGET}
CARGO_BUILD_TARGET_DIR=${RUST_TARGET_DIR}
cargo clean
BUILD_COMMAND ${CMAKE_COMMAND} -E env
CARGO_BUILD_TARGET=${CARGO_TARGET}
CARGO_BUILD_TARGET_DIR=${RUST_TARGET_DIR}
cargo build ${CARGO_BUILD_ARG} ${CARGO_FEATURES_ARG}
INSTALL_COMMAND ""
BUILD_ALWAYS TRUE
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_INCLUDE_HEADER}"
"${RUST_STATIC_LIBRARY}"
)
set_source_files_properties("${RUST_INCLUDE_HEADER}" PROPERTIES GENERATED true)
add_prebuilt_library(rustlib_lib "${RUST_STATIC_LIBRARY}" PRIV_REQUIRES "${RUST_DEPS}")
add_dependencies(rustlib_lib rustlib_project)
target_include_directories(${COMPONENT_LIB} PUBLIC "${RUST_INCLUDE_DIR}")
target_link_libraries(${COMPONENT_LIB} PRIVATE rustlib_lib)

View File

@ -6,10 +6,10 @@ edition = "2018"
[lib]
crate-type = ["staticlib"]
#crate-type = ["rlib", "staticlib"]
[build-dependencies]
bindgen = "0.58"
cbindgen = "0.19"
[features]
std = []

View File

@ -0,0 +1,13 @@
# rustlib
This component generates a static library from Rust source code and exposes functions that will be callable from the main IDF application from C.
This component is responsible for three separate functions:
* Export C bindings for Rust functions with `cbindgen`.
* Import Rust binding for C functions with `bindgen`.
* Build a top level static library from Rust code. The `cargo` build command is wrapped in a CMake `ExternalProject_Add` command.
Currently the `build.rs` script expects the `clib` component to export a header with the name `CApi.h`. If the name of the include file changes or if more files are added, the build script must be updated to process the latest headers.
Depending on the complexity of the project, the `cbindgen` and `bindgen` steps could be broken out into one or more IDF components or Rust crates. However, to prevent redundant duplication of the Rust runtime, all Rust crates should be compiled into a single static library that will be linked into the final IDF application.

View File

@ -5,19 +5,32 @@ fn main() {
let target = env::var("TARGET").unwrap();
let cargo_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let target_dir = PathBuf::from(env::var("CARGO_BUILD_TARGET_DIR").unwrap());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
run_cbindgen(&cargo_dir, &out_dir);
run_cbindgen(&cargo_dir, &target_dir);
run_bindgen(&target, &out_dir);
}
fn run_cbindgen(_cargo_dir: &Path, _out_dir: &Path) {
// TODO: Run cbindgen
fn run_cbindgen(cargo_dir: &Path, target_dir: &Path) {
let out = target_dir.join("RustApi.h");
cbindgen::Builder::new()
.with_crate(cargo_dir)
.with_language(cbindgen::Language::C)
.generate()
.expect("Unable to generate bindings")
.write_to_file(&out);
println!("cargo:rerun-if-changed={}", out.display());
}
fn run_bindgen(target: &str, out_dir: &Path) {
let header = "../clib/include/CApi.h";
let out = out_dir.join("bindings.rs");
let mut builder = bindgen::Builder::default();
builder = builder.header("../main/CApi.h");
builder = builder.header(header);
match target {
"riscv32i-unknown-none-elf" => {
builder = builder.clang_arg("--target=riscv32");
@ -36,6 +49,9 @@ fn run_bindgen(target: &str, out_dir: &Path) {
let bindings = builder.generate().expect("Couldn't generate bindings!");
bindings
.write_to_file(out_dir.join("bindings.rs"))
.write_to_file(&out)
.expect("Couldn't save bindings!");
println!("cargo:rerun-if-changed={}", header);
println!("cargo:rerun-if-changed={}", out.display());
}

View File

@ -0,0 +1,13 @@
/* 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.
*/

View File

@ -26,7 +26,7 @@ pub mod sys {
pub extern "C" fn add_in_rust(x: i32, y: i32) -> i32 {
unsafe {
sys::validate_param_in_c(0, x);
sys::validate_param_in_c(2, y);
sys::validate_param_in_c(1, y);
}
x + y
}

View File

@ -1,46 +1,2 @@
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "")
set(CARGO_BUILD_TYPE "release")
set(CARGO_BUILD_ARG "--release")
if(CONFIG_IDF_TARGET_ARCH_RISCV)
set(CARGO_TARGET "riscv32i-unknown-none-elf")
set(CARGO_FEATURES_ARG "")
elseif(CONFIG_IDF_TARGET_ARCH_XTENSA)
set(CARGO_TARGET "xtensa-esp32-none-elf")
set(CARGO_FEATURES_ARG "--features=std")
endif()
set(RUST_PROJECT_DIR "${CMAKE_CURRENT_LIST_DIR}/../rustlib")
set(RUST_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/rustlib")
set(RUST_STATIC_LIBRARY "${RUST_BUILD_DIR}/${CARGO_TARGET}/${CARGO_BUILD_TYPE}/librustlib.a")
ExternalProject_Add(
rustlib_project
PREFIX "${RUST_PROJECT_DIR}"
BUILD_COMMAND ${CMAKE_COMMAND} -E env
CARGO_BUILD_TARGET=${CARGO_TARGET}
CARGO_BUILD_TARGET_DIR=${RUST_BUILD_DIR}
cargo build ${CARGO_BUILD_ARG} ${CARGO_FEATURES_ARG}
DOWNLOAD_COMMAND ""
CONFIGURE_COMMAND ""
INSTALL_COMMAND ""
BUILD_ALWAYS TRUE
BUILD_DIR "${RUST_BUILD_DIR}"
INSTALL_DIR "${RUST_BUILD_DIR}"
STAMP_DIR "${RUST_BUILD_DIR}/stamp"
TMP_DIR "${RUST_BUILD_DIR}/tmp"
DOWNLOAD_DIR "${RUST_BUILD_DIR}"
SOURCE_DIR "${RUST_PROJECT_DIR}"
BUILD_BYPRODUCTS "${RUST_STATIC_LIBRARY}"
)
# this adds "rustlib" static library target.
# PRIV_REQUIRES tells CMake that librustlib.a itself depends on libmain.a.
add_prebuilt_library(rustlib "${RUST_STATIC_LIBRARY}" PRIV_REQUIRES main)
# rustlib will be produced by building rustlib_project target
add_dependencies(rustlib rustlib_project)
# 'main' calls a function from the library, so link it to 'main'
target_link_libraries(${COMPONENT_LIB} PRIVATE rustlib)

View File

@ -1,5 +0,0 @@
/*
* Rust functions exposed to C.
*/
extern int add_in_rust(int x, int y);

View File

@ -1,5 +0,0 @@
#
# "main" pseudo-component makefile.
#
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)

View File

@ -13,7 +13,6 @@
#include "esp_system.h"
#include "esp_spi_flash.h"
#include "CApi.h"
#include "RustApi.h"
void app_main(void)
@ -49,16 +48,3 @@ void app_main(void)
fflush(stdout);
esp_restart();
}
// bool validate_param_in_c(const char *param, int value)
// {
// printf("C validated %s = %d\n", param, value);
// return true;
// }
bool validate_param_in_c(int param, int value)
{
printf("C validated %d = %d\n", param, value);
return true;
}

View File

@ -1 +0,0 @@
language = "C"