Zephyr×Rustのインテグレーションにチャレンジ!①~ライブラリとして組み込む~

はじめに

Zephyrで本格的にRustアプリケーションを作る環境を構築していきます。 ソースコードはこちらにあります。

github.com

tomo-wait-for-it-yuki.hatenablog.com

前回は、手動でかつ強引にRustを組み込んだので、今回はZephyrのビルドプロセス内で、Rustアプリケーションを構築できるようにしていきます。 対象とするアプリケーションは、変わらずhello_worldサンプルで、当面は、QEMUをターゲットにします。

課題としては、

  • ZephyrのC言語の世界とRustとの連携
  • Zephyrビルドプロセス内でRustコードをビルド

があります。

前者は、bindgencbindgenで解決できるでしょう。 後者は、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を出力しています。

プロジェクトの構成は以下のようになっており、mylibMakefileでビルドし、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としてあげれば良さそうです。PREFIXSOURCE_DIRBINARY_DIRの設定は必要そうですね。 ただ、symbolをweakにしないといけないので、Makefileに書いてCargoを呼び出すのが安定そうです。

CMakeからCargoを呼ぶ

下記リンクのようにBUILD_COMMANDでCargoを呼び出します。

stackoverflow.com

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.

やったぜ!

次は直接アプリケーションとして、ビルドしたいですね。