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