Zephyr×Rustのインテグレーションにチャレンジ!①~ライブラリとして組み込む~
はじめに
Zephyrで本格的にRustアプリケーションを作る環境を構築していきます。 ソースコードはこちらにあります。
tomo-wait-for-it-yuki.hatenablog.com
前回は、手動でかつ強引にRustを組み込んだので、今回はZephyrのビルドプロセス内で、Rustアプリケーションを構築できるようにしていきます。
対象とするアプリケーションは、変わらずhello_world
サンプルで、当面は、QEMUをターゲットにします。
課題としては、
- ZephyrのC言語の世界とRustとの連携
- Zephyrビルドプロセス内でRustコードをビルド
があります。
前者は、bindgenとcbindgenで解決できるでしょう。 後者は、Zephyrのビルドプロセス解析が必要です。
この記事では、Rustプロジェクトを外部ライブラリとして、Zephyr Cアプリケーションから呼び出せるようにします。
hello_worldサンプルアプリケーション
参考にするC言語サンプルです。
|> zephyr/samples/hello_world
|> CMakeLists.txt
cmake_minimum_required(VERSION 3.13.1) include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) project(hello_world) target_sources(app PRIVATE src/main.c)
ここから推測できることは、ボイラープレートの中で、app
というターゲットが作られており、アプリケーション用のCMakeとしては、app
にソースコードを指定するだけで使えるのだろう、ということです。
|> main.c
#include <zephyr.h> #include <misc/printk.h> void main(void) { printk("Hello World! %s\n", CONFIG_BOARD); }
ソースコードは特にコメントすることはないですね。
やること
hello_world
アプリケーションを次のように修正します。
#include <zephyr.h> #include <misc/printk.h> #include <bindings.h> // Added void main(void) { printk("Hello World! %s\n", CONFIG_BOARD); rust_main(); // Added }
Rust側は、次のようにしておきます。
#[no_mangle] pub extern "C" fn rust_main() { const HELLO: &[u8] = b"Hello from Rust.\0"; unsafe { puts_c(HELLO.as_ptr()) }; }
これで、Zephyrのninja run
実行時だけで、次のように出力されればOKです。
Hello World! qemu_cortex_m3 Hello from Rust.
Zephyr external_libサンプル
Zephyrには、外部ライブラリをリンクするサンプルがあります。 出発点として使うため、説明します。
|> zephyr/samples/application_development/external_lib
$ cmake -GNinja -DBOARD=qemu_cortex_m3 .. $ ninja run ***** Booting Zephyr OS zephyr-v1.13.0-3321-g7f956a9 ***** Hello World! mylib says: Hello World!
という感じに、別途ビルドしたライブラリから、Hello Worldを出力しています。
プロジェクトの構成は以下のようになっており、mylib
をMakefile
でビルドし、main.cからmylibの関数を呼び出すようになっています。
$ tree . ├── CMakeLists.txt ├── mylib │ ├── include │ │ └── mylib.h │ ├── Makefile │ └── src │ └── mylib.c ├── prj.conf ├── sample.yaml └── src └── main.c
mylibのMakefileは単純です。
PREFIX ?= . OBJ_DIR ?= $(PREFIX)/obj LIB_DIR ?= $(PREFIX)/lib all: mkdir -p $(OBJ_DIR) $(LIB_DIR) $(CC) -c $(CFLAGS) -Iinclude src/mylib.c -o $(OBJ_DIR)/mylib.o $(AR) -rcs $(LIB_DIR)/libmylib.a $(OBJ_DIR)/mylib.o clean: rm -rf $(OBJ_DIR) $(LIB_DIR)
では、サンプルプロジェクトのCMakeを見てみましょう。 こちらは、少し複雑です…。
cmake_minimum_required(VERSION 3.13.1) include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) project(external_lib) target_sources(app PRIVATE src/main.c) # The external static library that we are linking with does not know # how to build for this platform so we export all the flags used in # this zephyr build to the external build system. # # Other external build systems may be self-contained enough that they # do not need any build information from zephyr. Or they may be # incompatible with certain zephyr options and need them to be # filtered out. zephyr_get_include_directories_for_lang_as_string( C includes) zephyr_get_system_include_directories_for_lang_as_string(C system_includes) zephyr_get_compile_definitions_for_lang_as_string( C definitions) zephyr_get_compile_options_for_lang_as_string( C options) set(external_project_cflags "${includes} ${definitions} ${options} ${system_includes}" ) include(ExternalProject) # Add an external project to be able download and build the third # party library. In this case downloading is not necessary as it has # been committed to the repository. set(mylib_src_dir ${CMAKE_CURRENT_SOURCE_DIR}/mylib) set(mylib_build_dir ${CMAKE_CURRENT_BINARY_DIR}/mylib) set(MYLIB_LIB_DIR ${mylib_build_dir}/lib) set(MYLIB_INCLUDE_DIR ${mylib_src_dir}/include) ExternalProject_Add( mylib_project # Name for custom target PREFIX ${mylib_build_dir} # Root dir for entire project SOURCE_DIR ${mylib_src_dir} BINARY_DIR ${mylib_src_dir} # This particular build system is invoked from the root CONFIGURE_COMMAND "" # Skip configuring the project, e.g. with autoconf BUILD_COMMAND make PREFIX=${mylib_build_dir} CC=${CMAKE_C_COMPILER} AR=${CMAKE_AR} CFLAGS=${external_project_cflags} INSTALL_COMMAND "" # This particular build system has no install command BUILD_BYPRODUCTS ${MYLIB_LIB_DIR}/libmylib.a ) # Create a wrapper CMake library that our app can link with add_library(mylib_lib STATIC IMPORTED GLOBAL) add_dependencies( mylib_lib mylib_project ) set_target_properties(mylib_lib PROPERTIES IMPORTED_LOCATION ${MYLIB_LIB_DIR}/libmylib.a) set_target_properties(mylib_lib PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${MYLIB_INCLUDE_DIR}) target_link_libraries(app PUBLIC mylib_lib)
後ろから順番に見ていきます。CMakeの最後ですが、app
ターゲットにmylib_lib
ターゲットをリンクするようにしています。
target_link_libraries(app PUBLIC mylib_lib)
後は、mylib_lib
ターゲットライブラリを作っていく作業になります。
それをやっているのが、下記の部分です。
ここでは、CMakeのライブラリラッパーを作っています。
# Create a wrapper CMake library that our app can link with add_library(mylib_lib STATIC IMPORTED GLOBAL) add_dependencies( mylib_lib mylib_project ) set_target_properties(mylib_lib PROPERTIES IMPORTED_LOCATION ${MYLIB_LIB_DIR}/libmylib.a) set_target_properties(mylib_lib PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${MYLIB_INCLUDE_DIR})
libmylib.a
を作り出すために、CMakeのExternalProject_Add
を使います。
include(ExternalProject) ExternalProject_Add( mylib_project # Name for custom target PREFIX ${mylib_build_dir} # Root dir for entire project SOURCE_DIR ${mylib_src_dir} BINARY_DIR ${mylib_src_dir} # This particular build system is invoked from the root CONFIGURE_COMMAND "" # Skip configuring the project, e.g. with autoconf BUILD_COMMAND make PREFIX=${mylib_build_dir} CC=${CMAKE_C_COMPILER} AR=${CMAKE_AR} CFLAGS=${external_project_cflags} INSTALL_COMMAND "" # This particular build system has no install command BUILD_BYPRODUCTS ${MYLIB_LIB_DIR}/libmylib.a )
とりあえず、固定ターゲットで良いなら、BUILD_COMMAND
だけcargo build
としてあげれば良さそうです。PREFIX
、SOURCE_DIR
、BINARY_DIR
の設定は必要そうですね。
ただ、symbolをweakにしないといけないので、Makefileに書いてCargoを呼び出すのが安定そうです。
CMakeからCargoを呼ぶ
下記リンクのようにBUILD_COMMANDでCargoを呼び出します。
ExternalProject_Add( rust_example BUILD_COMMAND cargo build COMMAND cargo build --release BINARY_DIR "${CMAKE_SOURCE_DIR}/common-rust" INSTALL_COMMAND "" LOG_BUILD ON)
ソースコードの取得先も選べますが、今回は、Zephyrプロジェクト直下に置いておきます。
ビルドコマンドとしては、中からCargoを呼び出すMakefile
を使います。
注記 下記スクリプトではソースディレクトリにライブラリファイルができてしまいます。少しハマっていてうまくやる方法を調査中です。
include(ExternalProject) set(rust_src_dir ${CMAKE_CURRENT_SOURCE_DIR}/hello) set(rust_build_dir ${CMAKE_CURRENT_BINARY_DIR}/hello) ExternalProject_Add( rust_hello # Name for custom target PREFIX ${rust_build_dir} # Root dir for entire project SOURCE_DIR ${rust_src_dir} BINARY_DIR ${rust_src_dir} # This particular build system is invoked from the root CONFIGURE_COMMAND "" # Skip configuring the project, e.g. with autoconf BUILD_COMMAND make INSTALL_COMMAND "" # This particular build system has no install command BUILD_BYPRODUCTS ${rust_src_dir}/lib/librust.a ) add_library(rust_hello_lib STATIC IMPORTED GLOBAL) add_dependencies( rust_hello_lib rust_hello ) set_target_properties(rust_hello_lib PROPERTIES IMPORTED_LOCATION ${rust_src_dir}/lib/librust.a) target_link_libraries(app PUBLIC rust_hello_lib)
CMakeから叩くMakefileです。
今回は、ターゲットアーキテクチャを固定 (.cargo/configで指定) しています。
最終成果物は、一旦Cargoプロジェクトにlib
というディレクトリを掘り、そこに置くようにしています。
PREFIX ?= . LIB_DIR ?= $(PREFIX)/lib TARGET_PATH := target/thumbv7m-none-eabi/debug RUST_TARGET := librust.a RUST_FILES := $(shell find src/ -type f -name "*.rs") Cargo.toml Cargo.lock $(LIB_DIR)/$(RUST_TARGET): $(RUST_FILES) mkdir -p $(LIB_DIR) cargo build cp $(TARGET_PATH)/libapp.a $(LIB_DIR)/$(RUST_TARGET) cargo objcopy -- --weaken-symbol=memmove --weaken-symbol=memcpy --weaken-symbol=memset --weaken-symbol=memcmp $(LIB_DIR)/$(RUST_TARGET) clean: rm -rf $(LIB_DIR) cargo clean
cbindgen
インストールしていない方は、インストールしてください。
cargo install cbindgen
今のビルドプロセスでは、lib
ディレクトリが存在している必要があるので、lib
ディレクトリだけ作っておきます。
mkdir lib
Rustプロジェクトのbindings.h
を生成するようにMakefileを修正します。
C言語をターゲットに、ヘッダファイルを生成します。
C_BINDINGS := bindings.h $(LIB_DIR)/$(C_BINDINGS): $(RUST_FILES) cbindgen -l c -o $(LIB_DIR)/$(C_BINDINGS)
生成されるヘッダファイルは下記の通りです。
$ cat lib/bindings.h #include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdlib.h> void rust_main(void);
CMakeでapp
から、bindings.h
を読み込みます。
set_target_properties(rust_hello_lib PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${rust_src_dir}/lib/bindings.h)
これで準備完了です。
Let's ninja!
$ mkdir build && cd build $ cmake -GNinja -DBOARD=qemu_cortex_m3 .. $ ninja run [8/111] Preparing syscall dependency handling [11/111] Performing build step for 'rust_hello' make: Nothing to be done for 'all'. [105/111] Linking C executable zephyr/zephyr_prebuilt.elf Memory region Used Size Region Size %age Used FLASH: 8840 B 256 KB 3.37% SRAM: 4160 B 64 KB 6.35% IDT_LIST: 120 B 2 KB 5.86% ... [111/111] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3 qemu-system-arm: warning: nic stellaris_enet.0 has no peer ***** Booting Zephyr OS zephyr-v1.13.0-3321-g7f956a9 ***** Hello World! qemu_cortex_m3 Hello from Rust.
やったぜ!
次は直接アプリケーションとして、ビルドしたいですね。