2025年3月23日日曜日

RustのbindgenでC++を呼出してみた

 

目的

Rust のbindgenを使い、Foreign Function Interface (FFI)でC++の呼び出し方と
制限事項などを調べた。

前提条件

以下の環境で実施した。

項目
PCASUS Chromebook CM30 Detachable
CPUMediaTek Kompanio 520( Cortext-A76,Cortext-A55)
rustc1.85.0
llvm14.0.6

※CPUはツール類で最新バージョンなどが違うため

bindgenでC++を呼び出す環境構築方法としては、以下の2種類存在する。

  1. bindgenコマンドをインストールし、bindgenコマンドを実行して、bindingのコードを生成する
  2. build.rsを作成し、cargo buildでbindingコードを生成する

今回は、2を試す。
理由はコード生成とビルドが一括で行えるためである。

やってみたこと

bindgenのページでC++対応の機能の確認をしてみた。

  • サポート機能
    • 継承
    • Method
    • コンストラクタとデストラクタ(暗黙的なものではない)
    • オーバーロード
    • Specializationなしのテンプレート
  • 未サポート機能
    • レイアウト、サイズ、アラインメント
    • inline function
    • template function method , class ,struct
    • specializationのtype
    • Cross 言語の継承
    • 自動的なcopy,moveコンストラクタ、デストラクタ
    • moveセマンティクス
    • 例外

C++としてtemplateが使えないのはしんどい。
ただし、FFIとして定義するのであれば、templateは使わない方針とするのが良さそう。
コンストラクタ、デストラクタは明示的に宣言しなければならないのはしんどい。

C++のコード作成

簡単なクラスを定義する。

bindgen/sample/inc/myclass.h

ひとまず簡単な引数と戻り値のメソッドを定義

#ifndef MCLASS_H
#define MCLASS_H
class MyClass
{
public:
  MyClass();
  ~MyClass();
  void method(void);
  bool method_bool(bool val);

};
#endif  

bindgen/sample/src/myclass.cpp

#include "myclass.h"
#include <iostream>
MyClass ::MyClass()
{
}

MyClass ::~MyClass()
{
  std::cout << "call ~MyClass" << std::endl;
}
void MyClass::method(void)
{
  std::cout << "sample" << std::endl;
  return;
}
bool MyClass::method_bool(bool val)
{
  std::cout << "method_bool:" << val << std::endl;
  return val;
}

bindgen/sample/CMakeList.txt

ライブラリ作成用CMakeLists

cmake_minimum_required(VERSION 3.10)

file(GLOB SAMPLE_SOURCES src/*.cpp)
# ライブラリ名
add_library(sample SHARED ${SAMPLE_SOURCES})

# インクルードディレクトリを追加(ヘッダファイルがある場合)
target_include_directories(sample PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/inc)

bindgen/CMakeList.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(SUBDIRS 
  sample 
)


foreach(SUBDIR ${SUBDIRS})
    if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/CMakeLists.txt)
        message(STATUS "Adding subdirectory: ${SUBDIR}")
        add_subdirectory(${SUBDIR})
    else()
        message(WARNING "Skipping ${SUBDIR}, CMakeLists.txt not found.")
    endif()
endforeach()

以下コマンドでlibsample.soが生成される。

cmake -S . -B build
cmake --build build

RustのFFIのプロジェクト作成

cargo new bindgen_sample
cargo add bindgen@0.71.0

bindgen/sample_bindgen/Cargo.toml

[package]
name = "bindgen_sample"
version = "0.1.0"
edition = "2021"
build = "build.rs"    # for bindgen


[build-dependencies]
bindgen = "0.71.0"

[dependencies]

bindgen/sample_bindgen/build.rs

extern crate bindgen;

use std::env;
use std::path::PathBuf;

fn main() {
    //
    // Link to `libdemo` dynamic library file
    //
    println!("cargo:rustc-link-lib=dylib=sample");
    println!("cargo:rustc-link-search=native=../build/sample/");
    println!("cargo:rerun-if-changed=../sample/inc/*.h");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    println!("out_put: {:#?}", &out_path);
    let bindings = bindgen::Builder::default()
        .header("../sample/inc/myclass.h")
        // Enable C++ namespace support
        .enable_cxx_namespaces()
        // Add extra clang args for supporting `C++`
        .clang_arg("-xc++")
        .clang_arg("-std=c++14")
        .clang_arg("-stdlib=libc++")
        .clang_arg("-I./")
        //.size_t_is_usize(true)
        //.opaque_type("std::*")
        // Tell cargo to invalidate the built crate whenever any of the
        // included header files changed.
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        // deprecated at 0.71
        //.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        // Finish the builder and generate the bindings.
        .generate()
        // Unwrap the Result and panic on failure.
        .expect("Unable to generate bindings");
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

target/debug/build/bindgen_sample_xxxxx/out/bindings.rs

に出力される。

bindgensample_bindgen/src/main.rs

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
use crate::root::MyClass as ffi_myClass;
fn main() {}

pub struct Sample_MyClass {
    raw: ffi_myClass,
}

impl Sample_MyClass {
    pub fn new() -> Self {
        unsafe {
            let test = ffi_myClass::new();
            Sample_MyClass { raw: test }
        }
    }

    pub fn method(&mut self) {
        unsafe {
            self.raw.method();
        }
    }

    pub fn method_bool(&mut self, arg1: bool) -> bool {
        unsafe { self.raw.method_bool(arg1) }
    }
}
impl Drop for Sample_MyClass {
    fn drop(&mut self) {
        println!("call Drop");
        unsafe {
            self.raw.destruct();
        }
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    pub fn method() {
        let mut test = Sample_MyClass::new();
        test.method();
        test.method_bool(true);
    }
    #[test]
    pub fn method_bool() {
        let mut test = Sample_MyClass::new();
        let result = test.method_bool(true);
        assert_eq!(result, true);
    }
}
  • include!(concat!(env!(“OUT_DIR”), “/bindings.rs”)); でbindings.rsを取り込む
  • bindings.rs にあるヘッダファイルと同じクラス名で実装されている関数を使用する。
    • コンストラクタがnew関数に置き換わる。
    • すべての関数がunsafe関数のため、呼び出すときにunsafeでくくる
      • unsafeをくくった関数を作っておくと呼び出し側は簡易的になる。

複数のヘッダファイルを読み込ませたい場合

  • bindgenの.header("…/sample/inc/myclass.h") は一つのファイルしか指定できないとのこと。
    • 以下のようなclang_argsで一つずつファイルを指定する必要がある。
    let bindings = bindgen::Builder::default()
        .clang_args(&[
            "-include",
            "../sample/inc/myclass.h",
            "-include",
            "../sample/inc/myclass_xx.h",
        ])

C++のI/Fにstd::functionを使用したい場合

#include <functional> の解析でエラーが発生する。理由はtemplateを使っているためbindgenが正しく解析できないため。

対策としては、std::functionを関数ポインタなどでwrapすることで実現することができる。

bindgen/sample/inc/callback.h

#ifndef HEADER_H
#define HEADER_H

// std::function を隠すための不透明ポインタ型
typedef void *function_handle_t;

class CallBackClass
{
public:
  CallBackClass();
  ~CallBackClass();
  void method(void);
  void setCallback(void (*callback)(int));
  void call_function(int value);

private:
  function_handle_t cbk;

  function_handle_t create_function(void (*callback)(int));

  void destroy_function();
};

#endif

bindgen/sample/src/callback.cpp

#include "callback.h"
#include <functional>
#include <iostream>
// `std::function<void(int)>` を隠蔽するための構造体
struct FunctionWrapper
{
  std::function<void(int)> func;
};

CallBackClass::CallBackClass()
{
}
CallBackClass::~CallBackClass()
{
  destroy_function();
}

void CallBackClass::method(void)
{
}
void CallBackClass::set_callback(void (*callback)(int))
{
  cbk = create_function(callback);
}


// `std::function` を作成し、そのポインタを返す
function_handle_t CallBackClass::create_function(void (*callback)(int)) {
    return new FunctionWrapper{[callback](int value) {
        callback(value);
    }};
}
// `std::function` を実行する
void CallBackClass::call_function( int value)
{
  if (cbk)
  {
    static_cast<FunctionWrapper *>(cbk)->func(value);
  }
}

// `std::function` を解放する
void CallBackClass::destroy_function()
{
  delete static_cast<FunctionWrapper *>(cbk);
}
  
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

use crate::root::CallBackClass as ffi_myClass;

fn main() {}
  
pub struct Sample_MyClass {
    raw: ffi_myClass,
}
impl Sample_MyClass {
    pub fn new() -> Self {
        unsafe {
            let test = ffi_myClass::new();
            Sample_MyClass { raw: test }
        }
    }
    pub fn method(&mut self) {
        unsafe {
            self.raw.method();
        }
    }

    pub fn set_callback(&mut self, callback: Option<unsafe extern "C" fn(i32)>) {
        unsafe {
            self.raw.set_callback(callback);
        }
    }
    pub fn call_function(&mut self, value: i32) {
        unsafe {
            self.raw.call_function(value);
        }
    }
}
impl Drop for Sample_MyClass {
    fn drop(&mut self) {
        println!("call Drop");
        unsafe {
            self.raw.destruct();
        }
    }
}

#[cfg(test)]
mod tests {
    static mut result: i32 = 0;
    use super::*;
    // Rust のコールバック関数
    extern "C" fn rust_callback(value: i32) {
        println!("Rust callback called with value: {}", value);
        unsafe {
            result = value;
        }
    }

    #[test]
    pub fn method() {
        let mut test = Sample_MyClass::new();
        test.method();
    }
    #[test]
    pub fn method_callback() {
        let mut test = Sample_MyClass::new();
        test.method_callback(Some(rust_callback));
    }
    #[test]
    pub fn call_function() {
        let val = 42;
        let mut test = Sample_MyClass::new();

        test.method_callback(Some(rust_callback));
        test.call_function(val);
        unsafe {
            assert_eq!(result, 42);
        }
    }
}

これで実現することは可能。

結論

build.rsさえ定義できてしまえば、簡単にクラス追加はできる。
呼出し方法もさほど難しくないため、手間なくC++の呼出しができる。
ただし、C++特有の機能を使うと、FFIが簡単にできないため、設計方針を定めておかないと、
すべてwrapperを作るためになりそう 。

参考

2025年1月21日火曜日

Eclipse SDV の概要

この記事では、Eclipse SDVとプロジェクトの一覧を紹介します。 内容は2025年1月19日時点の調査に基づいていますので、最新の情報とは異なる場合があります。

前置き

車載ソフトウェアの分野で注目を集めているSDV(Software Defined Vehicle: ソフトウェア定義車両)。 Eclipse Foundationもこの動きを捉え、2022年3月に「Eclipse SDV」というワーキンググループを発足しました。 創設メンバーにはETAS、Microsoft、ZFなどが名を連ねています。 実際の活動は2021年から始まっており、Charter(憲章)のドラフトがその頃から作成されていました。

Eclipse SDVの目的

Charterによると、 Eclipse SDVの目的は以下の通りです。

Eclipse SDVのミッション

  • 車載および車両周辺のシステムの開発と展開をサポートするオープンソースソフトウェアや仕様、オープンコラボレーションモデルの構築、促進
  • 業界標準に対応した、スケーラブル、モジュラーで、拡張可能なプラットフォーム

目的

  • 自動車産業の断片化された市場で競争できるOSSソリューションを推奨、開発、促進
  • コードファーストのアプローチでエコシステムを構築
    • コード成果物の実装とオンボーディングに重点

3つのコンポーネントモデルを定義

  • SDV.Dev
    • クラウドベースのデプロイおよびアプリケーション管理環境と統合された、車載および関連するオフビークルアプリケーションの最新開発を可能にする開発ツールチェインとワークフローに焦点
  • SDV.Ops
    • 既存のモビリティクラウドソリューションと統合し、大規模な車両フリートのソフトウェアスタック管理を可能にし、確立されたオープンスタンダードをサポート
  • SDV.Edge
    • クラウドネイティブ技術を品質管理(QM)や安全性に関連するドメインを含む、さまざまな車載ソフトウェアドメインに導入することを焦点

その他取り組み

  • SDVに必要なコンポーネントのオープンソース実装の促進
  • SDV 関連のオープンソース プロジェクトが開発で活用する可能性のある品質管理、機能安全、サプライチェーン、セキュリティなどのプロセスを定義、文書化、推進
    • これらのプロセスを活用するプロジェクトのルールとブランディング プロセスを定義し、プロセスの成熟度を市場に知らせる
  • このワーキング グループの範囲内で定義されているインターフェース仕様を正式化するために、EFSP を調整および管理
  • 相互運用性を確保するために、これらのコンポーネントの実装の互換性ルールと互換性およびブランディング プロセスを定義
  • リファレンス配布を有効にし、さまざまな開発者ツールチェーンでの統合を促進
  • コミュニティが簡単に使用および拡張できるソフトウェア定義車両エコシステムを確立するソフトウェア プラットフォームを定義
  • ソフトウェア プラットフォームの作成と進化において開発者コミュニティをサポート
  • このワーキング グループとそのコミュニティが持続可能な形で運営できるようにする資金調達モデルの推進
  • Eclipse SDV ブランド、および運営委員会と Eclipse Foundation によって承認されたその他のブランド、および市場でのその価値の促進
  • Eclipse SDV エコシステムにベンダー中立のマーケティングおよびその他のサービスを提供
  • 他のオープンソースおよび仕様コミュニティとコミュニケーション/連絡
  • コミュニティの参加を促進し、コミュニティ メンバーを保護し、使用を促進する、Eclipse 定義のライセンスと知的財産フローを活用します。
  • オープンソース プロジェクトに関連する全体的な技術およびビジネス戦略を管理

参画企業

members

  • ストラテジメンバー
    • 9社
  • 参加メンバー
    • 24社
  • サポーティングメンバー
    • 21社
  • ゲストメンバー
    • 6社

特筆すべきは、Microsoftがストラテジーメンバーである点です。

他プロジェクトとの関連

2024年に「COVESA」「AUTOSAR」「Eclipse」「SOAFEE」が共同で「SDV Alliance」を発足することを発表。 各団体が提供するコンポーネントを組み合わせたエコシステム構築が進行しています。 この動きには、EUの「Horizon Europe」戦略も影響を与えていると推測されます。

プロジェクトの一覧

2025/01/18現在、27個のプロジェクトが存在します。

※プロジェクト名の下に記載したのは、Eclipse Projectに付与されているタグです。

まとめ

この記事では、Eclipse SDVワーキンググループの概要とプロジェクト一覧を紹介しました。   プロジェクトによって活動の活発さは異なりますが、今後は特に活発なプロジェクトや、動作確認可能なものについて詳しく紹介する予定です。

2024年5月19日日曜日

zenohについて調べてみる

Eclipse SDV Projectを調べていたら、 zenoh というのが出てきた。

見てみるとROS界隈でも使われている軽量スタックだったので、調べて見ようと思いました。

zenohとは

Zero Network Overhead Protocolからzenohと名付けているようです。
サイトを見るとZeroOverHead Network Protocolになっているため、あれ?って部分はありますが。

主な機能

機能としては

  • Pub/Sub
  • Storages
  • Queries and Queryables

があります。

また、トポロジー構成は

  • Peer-to-Peer
    • Clique , Mesh トポロジを構築
  • Brokered
    • zenoh routerで複数clientを接続
  • Routed
    • routerを構築し、client間や別subnetを接続

が実現できるようです。参照:https://zenoh.io/docs/getting-started/deployment/

非機能的な要素として

  • Scalable
    • Scalable Routing
    • Low Power Networks
    • Constrained Devices
  • Fast
    • Fast Adoption
    • Low Latency
    • High Throughput

とあり、マイコンからクラウドまでサポートしているとのこと。

C言語で実装されたzenoh-picoもあります。

パフォーマンス

 以下のブログでパフォーマンスについて言及されています。

https://zenoh.io/blog/2023-03-21-zenoh-vs-mqtt-kafka-dds/   MQTTよりも性能があることが示されています。
とはいえ、MQTTは低スペックや不安定なネットワーク向けだから同じ土俵に乗せるのもと思うが。

拡張性

プラグインを使う事により、様々なミドルウェアと接続することが可能になります。

  • mqtt
  • dds
  • rocksdb
  • filesystem
  • influxdb
  • s3
  • webserver
  • sql

などがあり、自作で拡張することが可能です。

動作確認

簡単なpub/subの動作確認の手順です。

  1. z_sub とz_pubをビルド
  $ git clone https://github.com/eclipse-zenoh/zenoh.git
  $ cd zenoh/examples
  $ cargo build --example z_sub
  $ cargo build --example z_pub

subscriberの実行

  $ ../target/debug/examples/z_sub
Opening session...
Declaring Subscriber on 'demo/example/**'...
Press CTRL-C to quit...
>> [Subscriber] Received PUT ('demo/example/zenoh-rs-pub': '[   1] Pub from Rust!')
>> [Subscriber] Received PUT ('demo/example/zenoh-rs-pub': '[   2] Pub from Rust!')
>> [Subscriber] Received PUT ('demo/example/zenoh-rs-pub': '[   3] Pub from Rust!')
>> [Subscriber] Received PUT ('demo/example/zenoh-rs-pub': '[   4] Pub from Rust!')  
  

publisherの実行

$ ../target/debug/examples/z_pub
Opening session...
Declaring Publisher on 'demo/example/zenoh-rs-pub'...
Press CTRL-C to quit...
Putting Data ('demo/example/zenoh-rs-pub': '[   0] Pub from Rust!')...
Putting Data ('demo/example/zenoh-rs-pub': '[   1] Pub from Rust!')...
Putting Data ('demo/example/zenoh-rs-pub': '[   2] Pub from Rust!')...
Putting Data ('demo/example/zenoh-rs-pub': '[   3] Pub from Rust!')...
Putting Data ('demo/example/zenoh-rs-pub': '[   4] Pub from Rust!')...

こんな感じで簡単に動かせます。
routerを使ったサンプルは別の投稿でかければと思います。

使ってみた感想

機能としては未知数なところもあるが、ROS2で使われている&色々拡張できるという面で 非常に期待しているミドルウェアではあります。

uProtocol というミドルウェアでも使われているということなので、そこでも調べてみようかと思います。

参考サイト

以下参考にしたサイトです。

2024年5月6日月曜日

rustup updateした後にcargoを実行したらエラーになった

発生事象

  1. rustup update stableを実行
  2. cargoを実行
$ cargo
error: the 'cargo' binary, normally provided by the 'cargo' component, is not applicable to the '1.72.0-aarch64-unknown-linux-gnu' toolchain

と実行したがエラーが発生した。

解決方法

googleで検索したら、AIが以下コマンド実行してみてと教えてくれたので、実行。

rustup component remove cargo
rustup component add cargo

これで無事cargoが実行できた。

rustup updateだけだとバイナリの不整合が発生する模様なため、cargoの入れ替えも同時に実施したほうが良さそう。

reference

https://github.com/rust-lang/rustup/issues/2704#issuecomment-815062952

2024年5月5日日曜日

instrument_functionで関数トレースをしてみる

モチベーション

ちょっとした動作確認を行った場合、コールツリーなどをチェックしたいということがありました。
わざわざ、関数にDebug Printを入れ込むのも面倒だなと言うことなので、ちょっと調査しました。

実現方法

1 . コンパイルオプション -finstrument-functions を設定してコンパイル 2 . 関数のentry/exitで以下の関数が呼び出される

  void __cyg_profile_func_enter(void *func_address, void *call_site) // func_addressが呼び出される関数アドレス、call_siteは呼び出しもとの関数アドレス
  void __cyg_profile_func_exit(void *func_address, void *call_site)

3 . この関数の中で、アドレスから関数名に変換する仕組みを入れることにより、 コールツリーが確認できる。 ※cyg_profile_func_enter/exit自体はフック対象外とするため、 -finstrument-functionsの対象外とする必要がある。

実装したサンプルコードはこちら サンプルコード

ちなみにstatic関数のコールトレースはできない。    staticを空のマクロ定義にして、読み出せるようにするなどで実現できるが、関数名の衝突などがありそうなため、うまくできるかは微妙。

改善点

  • 関数名までなので、引数も表示できるようにしたい。
    • libdwarfを使うとできるよう。

 

2024年3月6日水曜日

wasm環境構築

https://rustwasm.github.io/docs/book/game-of-life/setup.html

に書いてあることをコピーとちょっとしたエラー時の対応のログです。

必要なパッケージのインストール

npm

npm install npm@latest -g

wasm pack

curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

cargo-generate

$ cargo --version
cargo 1.71.1 (7f1d04c00 2023-07-29)
$ cargo install cargo-generate
  Updating crates.io index
  Installing cargo-generate v0.19.0
  Updating crates.io index
error: failed to compile `cargo-generate v0.19.0`, intermediate artifacts can be found at `/tmp/cargo-installDG3XkQ`
Caused by:
  package `normpath v1.2.0` cannot be built because it requires rustc 1.74.0 or newer, while the currently active rustc v
ersion is 1.71.1
  Try re-running cargo install with `--locked`

エラーの指示の通りrustcのバージョンアップを
する。

$ rustup check
stable-aarch64-unknown-linux-gnu - Update available : 1.71.1 (eb26296b5 2023-08-03) -> 1.76.0 (07dca489a 2024-02-04)
rustup - Up to date : 1.26.0
$ rustup update stable
$ cargo install cargo-generate

無事インストール完了

動作確認

cargo generate --git https://github.com/rustwasm/wasm-pack-template
cd wasm-game-of-life
wasm-pack build
npm init wasm-app www

pkg/package.json を作成し、下記を追加

{
  "name": "wasm-game-of-life",
  "collaborators": [
    "Your Name <your.email@example.com>"
  ],
  "description": null,
  "version": "0.1.0",
  "license": null,
  "repository": null,
  "files": [
    "wasm_game_of_life_bg.wasm",
    "wasm_game_of_life.d.ts"
  ],
  "main": "wasm_game_of_life.js",
  "types": "wasm_game_of_life.d.ts"
}
vi www/package.json

以下ブロックを追加

  "dependencies": {
    "wasm-game-of-life": "file:../pkg"
  },

export NODE_OPTIONS=--openssl-legacy-provider
npm install
npm run start

ブラウザでhttp://localhost:8080 を開くとアラートが表示される

Exercises

lib.rsのgreeting関数に引数をして、jsから引数ありの呼び出しをしてみる。

lib.rs

pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

index.js

wasm.greet("Your Name");