見出し画像

RustからC言語の関数をコールする方法(連載22)

前回、SOLID-OSの割り込み関連関数をRustからコールして使いました。

この割り込み関連関数、C/C++で書かれています。
という事は、RustからC/C++の関数を呼んでいる、という事になります。

solidクレート側で、SOLID OSの持つC/C++関数を、Rustからコールする方法が準備されていて、それを使っていたという事ですが、、、あまり意識していませんでした。

今回は、どうやってRustからC/C++の関数を呼ぶことを実現しているか、について考えていきたいと思います。

1.autocxxクレートという橋渡し役

以下Rust公式サイトに、autocxxというクレートについて記載されています。

https://docs.rs/autocxx/latest/autocxx/index.html

どうやらこのクレートは、RustとC/C++の間で橋渡しの交通整理をする役目であるようです。

引用:「Think of autocxx as glue which plugs bindgen into cxx.」
glueって、接着剤ですね。
どのように接着しているのでしょうか。

autocxx::include_cpp! {
    #include "url/origin.h"
    generate!("url::Origin")
    safety!(unsafe_ffi)
}

fn main() {
    let o = ffi::url::Origin::CreateFromNormalizedTuple("https",
        "google.com", 443);
    let uri = o.Serialize();
    println!("URI is {}", uri.to_str().unwrap());
}

https://docs.rs/autocxx/latest/autocxx/index.html からのソースコード引用です。

autocxxクレートのinclude_cpp!マクロによって、
・C/C++のヘッダファイルを読み込んで、
・そこに定義されている関数を「generate!()」マクロでRustから呼べるようバインディングし
・safety!(unsafe_ffi)で、これらの関数は安全ですよと宣言する
という流れに(ざっくりと)なっています。

※ ffi:Foreign Function Interface で、異なる言語へのアクセスをするI/Fです。

コールするときは、先ほど定義したインタフェース関数から呼び出す、という流れのようです。

2.autocxxクレートを使っているabi.rs

autocxxについて見たところで、次にsolidクレートのabi.rsを見てみましょう。

https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/bd024446d2e7693f3cb91bf3b5884713bb865c42/common/solid/src/abi.rs

3行目で、autocxxクレートをuseしています。

use autocxx::prelude::*;

8, 9行目で、C/C++部のヘッダファイルをインクルードしています。

include_cpp! {
   #include "src/abi.hpp"

ちょっとここでヘッダファイル”abi.hpp”を見てみましょう。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/bd024446d2e7693f3cb91bf3b5884713bb865c42/common/solid/src/abi.hpp

2行目から9行目にわたり、OS機能のAPIらしきヘッダファイルがインクルードされています。

例えば前回使った割り込み関連関数に関係するヘッダファイルとしては、solid_intc.hをインクルードしています。

#include "solid_intc.h"

このヘッダファイルには、以下のURLに説明があるとおり、C/C++で記述されたSOLID OSの割り込みコントローラ関連APIが定義されています。

https://solid.kmckk.com/SOLID/doc/latest/os/cs/intc.html

一応これでつながりました。

これでどうつながっているのか、まだ腑に落ちないので、具体的に見ていきます。

3.具体例で追ってみる(1) - 関数コール

前回使用したSOLID_INTC_GetPriorityLevel関数に的を絞って見ていくことにします。
今度はC/C++側から追っていきます。

① solid_intc.hにSOLID_INTC_GetPriorityLevel関数が定義されている。
<=ここが出発点です。

② solidクレート内のabi.hppファイルで、solid_intc.hがインクルードされているので、SOLID_INTC_GetPriorityLevel関数が使用できる状態になっている。

③ solidクレート内のRustソースコードであるabi.rsで、②のabi.hppをインクルードしている。さらにここで、generate!("SOLID_INTC_GetPriorityLevel")を行いバインディングしている。このことにより、Rustコードからsolid::abi::SOLID_INTC_GetPriorityLevel()と呼ぶことができるようになる。

このように、abi.rsで、autocxxクレートによりC/C++関数とバインディングされていることがわかりました。

ところで補足ですが、solidクレートでは、もう一つ上のレベルのWrapperモジュールもあります。
例えば、interruptモジュールです。

https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/bd024446d2e7693f3cb91bf3b5884713bb865c42/common/solid/src/interrupt.rs

この中で、例えば割り込みのEnable/Disableをより便利に操作できるNumberモジュールがあります。

例えばNumber.enable でSOLID_INTC_Enable関数をコールします。

筆者のソースコードでは、以下のように使用しています。

interrupt::Number(150).enable().expect("unable to enable interrupt line");

このコードは、最終的にはC/C++のSOLID_INTC_Enable関数を呼んで、割り込みをEnableにしています。

SOLID OSのAPIを直接コールせずにすむよう、配慮がなされているという事ですね。

4.具体例で追ってみる(2) – 構造体

C/C++と橋渡しして欲しいのは、関数だけではありません。

その関数が使用する構造体も橋渡しする必要があります。

前回使用した、割り込み構造体について見ていきましょう。

SOLID OSでは以下の割り込み構造体が必要でした。

typedef struct _SOLID_INTC_HANDLER_ {
    int intno;
    int priority;
    int config;
    int (*func)(void*, SOLID_CPU_CONTEXT*);
    void* param;
} SOLID_INTC_HANDLER;

この構造体をどのようにRustから橋渡しされているのでしょうか。

前回、以下のコードで、割り込み構造体を準備しました。
pin_singleton!マクロで、ずっと同じメモリに留め置く(ピン)型としてこの構造体を準備していました。

let spi_handler = pin_singleton!(: Handler<> = interrupt::Handler::new(|: CpuCx<'_>| spi_trans_handler())).unwrap();

interrupt::Handler::newで実行されるコードの実体は、solidクレートのinterruptモジュール内にあり、以下となっています。

impl Handler {
    /// Construct an unregistered `Handler`.
    #[inline]
    pub const fn new(handler: T) -> Self {
        Self {
            inner: abi::SOLID_INTC_HANDLER {
                // zeroed to allow placing it in .bss
                intno: 0,
                priority: 0,
                config: 0,
                func: null_mut(),
                param: null_mut(),
            },
            line: Line(0),
            handler: UnsafeCell::new(handler),
            _pin: core::marker::PhantomPinned,
        }
    }

abi::SOLID_INTC_HANDLER構造体をnewしています。

abi.rsを見てみると、SOLID_INTC_HANDLERは以下のように書かれています。

extern_cpp_opaque_type!("SOLID_INTC_HANDLER", super::SOLID_INTC_HANDLER)

ここで、extern_cpp_opaque_type!マクロを使っています。

C/C++で定義された型がautocxxで生成できない場合に使用するようです。
https://docs.rs/autocxx/latest/autocxx/macro.extern_cpp_opaque_type.html

このままではサイズやアライメントの保証がされないことになります。

この対処方法は、abi.rsの下の方に記載されています。

/// Layout check of `SOLID_INTC_HANDLER`
const _: () = {
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_OFFSET0 == offset_of!(SOLID_INTC_HANDLER, intno));
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_OFFSET1 == offset_of!(SOLID_INTC_HANDLER, priority));
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_OFFSET2 == offset_of!(SOLID_INTC_HANDLER, config));
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_OFFSET3 == offset_of!(SOLID_INTC_HANDLER, func));
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_OFFSET4 == offset_of!(SOLID_INTC_HANDLER, param));
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_SIZE == size_of::<SOLID_INTC_HANDLER>());
};

サイズとオフセットを検証しています。

とはいえ、SOLID_RS_SOLID_INTC_HANDLER_OFFSET0等、ここで初めて見る定義が出てきましたが、どこで定義しているのでしょうか。

実はこれは、abi.hppにあります。

static constexpr size_t _SOLID_RS_SOLID_INTC_HANDLER_OFFSET0 = offsetof(SOLID_INTC_HANDLER, intno);
static constexpr size_t _SOLID_RS_SOLID_INTC_HANDLER_OFFSET1 = offsetof(SOLID_INTC_HANDLER, priority);
static constexpr size_t _SOLID_RS_SOLID_INTC_HANDLER_OFFSET2 = offsetof(SOLID_INTC_HANDLER, config);
static constexpr size_t _SOLID_RS_SOLID_INTC_HANDLER_OFFSET3 = offsetof(SOLID_INTC_HANDLER, func);
static constexpr size_t _SOLID_RS_SOLID_INTC_HANDLER_OFFSET4 = offsetof(SOLID_INTC_HANDLER, param);
static constexpr size_t _SOLID_RS_SOLID_INTC_HANDLER_SIZE = sizeof(SOLID_INTC_HANDLER);

という事で、C/C++での割り込み構造体の各メンバの配置、サイズについて継承できていることもわかりました。

5.もっと簡単にC/C++関数を呼ぶ方法

ここまで、solidクレートを使用して秩序を守った方法でのOS関数コールを行いました。

ですが、LOG関数だけ呼びたい、というような場合、簡単にC/C++関数を呼ぶこともできます。
よいサンプルプログラムがありましたのでご紹介します。

SOLID_LOG_printf関数を呼ぶプログラムです。(同様にdly_tsk関数も呼んでいます)

https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/rust-blinky-raw-rtos/rustapp/src/lib.rs

① C/C++のSOLID_LOG_printf関数をexternで取り込む宣言を持ったモジュールを定義する。

/// FFI declarations used in this application
#[allow(non_camel_case_types)]
mod ffi {
    :
    extern "C" {
        pub fn SOLID_LOG_printf(format: *const c_char, ...);
        pub fn dly_tsk(dlytim: RELTIM) -> ER;
    }
}

② unsafeでくくって、①で作成したモジュール経由でC/C++関数をコールする

unsafe { ffi::SOLID_LOG_printf(b"Starting LED blinker\n\0".as_ptr().cast()) };

これでOKです。

6.どの方法を選択するか?

autocxxを使用する方法、手動で簡単に書く方法、を見てきました。

その他、bindgen を使う手段もあるそうです。

どういう基準で、どの方法を選択しているのか、意見を聞いてみたくなりました。

まず、solidクレートのabi.rsでautocxxを使っている理由について、作成者の方に聞いてみました。

autocxxを使用しているのはヘッダーファイルからFFI宣言 (extern "C" { ... }) を自動的に生成することで、情報の重複を無くし、言語解釈の誤り、記述ミス、コンパイルオプション、ヘッダファイルの更新等によるシグニチャの不一致が生じないようにするとともに、関数の追加を容易にするためです。

ちなみに今はautocxxを使っていますが、様々な要件を考慮した上で、将来的にbindgen等に切り替える可能性があります。(手動記述も選択肢にあります。)

作成者の方の見解

autocxxを使うときと、手動記述にする時の目安はについても意見を聞いてみました。

一般に、autocxxはautocxxが定める作法に従ったC++ APIであれば真価を発揮し、C++コードも同時に開発するケースではcxxも強力な選択肢になると思われます。ツールの使用による開発オーバヘッドが大きい、ヘッダーファイルが無くても独立してビルドできるようにしたい、といった要件があれば、手動記述も有効です。

作成者の方の見解

との事でした。


以上、今回はC/C++で書かれたSOLID OS関数をRustから呼ぶ仕組みについて書いてきました。
C/C++で書かれた既存機能をRustからコールできるのであれば、新機能からRustで実装しよう、という意欲も湧いてくるかもしれません。

次回は、逆に、C/C++からRust関数をコールすることを試してみたいと思います。
せっかくSOLID-IDEで、C++にmain関数を配置でき、かつ、Rustのライブラリが作れるようになっているので、片方だけ使うのではなく両方使えるのではないかと思います。
基本はC/C++だけど一部Rustで書きたいなー、という事ができると、個人的に嬉しいので。。。

この記事が気に入ったらサポートをしてみませんか?