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を作るためになりそう 。

参考