RustとC++の相互運用性による安全なアプリケーション開発
Emily Parker
Product Engineer · Leapcell

はじめに
ソフトウェア開発が急速に進化する中で、さまざまなプログラミング言語の長所を組み合わせる能力は、堅牢で効率的なアプリケーションを構築する上でしばしば不可欠です。比類なきメモリ安全性とパフォーマンスで称賛されているRustは、クリティカルなシステムでますます採用されています。しかし、世界中で高度に最適化され、時間によってテストされたソフトウェアインフラストラクチャのかなりの部分は、CまたはC++で書かれています。これには、高性能数値ライブラリ、オペレーティングシステムAPI、組み込みシステムファームウェアなど、あらゆるものが含まれます。
ここで課題が生じます。Rustアプリケーションは、Rustのコアとなるメモリ安全性とデータ整合性の保証を犠牲にすることなく、これらの既存の強力なC/C++ライブラリを活用するにはどうすればよいでしょうか?そこで、外部関数インターフェース(FFI)が登場します。FFIは、Rustプログラムが他の言語で書かれた関数を呼び出すことを可能にし、その逆も同様です。特にC/C++の場合、RustのFFI機能によりブリッジが実現され、数十年にわたる確立されたC/C++コードベースにアクセスし、パフォーマンスが重要なセクションを最適化し、C APIを通じて主に公開されているシステムレベルの機能とインターフェースすることができます。この記事では、Rustアプリケーションから安全にC/C++コードを呼び出すためにRust FFIを使用する実践的な側面を掘り下げ、Rustの安全性原則を維持しながらこれを達成する方法を示します。
Rustで既存のC/C++ライブラリを活用する
FFIを使用してRustとC/C++の間のギャップを効果的に埋めるためには、いくつかのコアコンセプトと用語を理解することが不可欠です。
コアコンセプト
- 外部関数インターフェース (FFI): あるプログラミング言語で書かれたプログラムが、別の言語で書かれたルーチンまたは関数を呼び出すことができるメカニズム。Rustでは、
ffi
は主にCアプリケーションバイナリインターフェース(ABI)と対話するために使用されます。 - ABI (アプリケーションバイナリインターフェース): パラメータがどのように渡されるか、戻り値がどのように受信されるか、スタックフレームがどのように管理されるかを含む、低レベルで関数がどのように呼び出されるかを定義します。C ABIは、Rustを含む多くの言語が相互運用できる事実上の標準です。
unsafe
キーワード: Rustのメモリ安全性の保証を侵害する可能性のあるコードブロックを注意深く使用しない場合にマークするための方法。FFI操作は、Rustコンパイラが他の言語で書かれたコードの安全性を保証できないため、unsafe
ブロックを必要とすることがよくあります。extern
ブロック: Rustでは、外部ライブラリ(例:C/C++ライブラリ)で定義された関数を宣言するために使用されます。これらのブロックは、関数シグネチャと、オプションで、使用するABIを指定します。no_mangle
属性: Rustコンパイラが関数の名前を「マングル」(変更)するのを防ぐために使用されるRust属性。これにより、コンパイルされたバイナリでC互換の名前が保証されます。Rust関数がC/C++から呼び出される場合に重要です。libc
クレート: 一般的なCライブラリ関数、データ型、定数への生のFFIバインディングを提供するRustクレート。有用ですが、カスタムC/C++コードの場合、直接のextern
ブロックがより一般的です。
統合の原則
根本的な原則は、C/C++コードのC互換インターフェースを作成することを中心に展開します。これには通常、次のことが含まれます。
- C互換関数の公開: C++関数には、C FFIが直接消費できない名前マングルやクラス構造がある場合があります。Rustとインターフェースするには、
extern "C"
関数でC++ロジックをラップする必要があります。これにより、C++コンパイラインストラクチャがC呼び出し規則を使用して関数をコンパイルし、名前マングルを防ぐように指示されます。 - Rustでの対応するシグネチャの定義: Rust側では、
extern "C"
ブロックを使用してこれらのC互換関数を宣言し、関数シグネチャ(パラメータタイプ、戻り値タイプ)がRustとC/C++の間で正確に一致することを保証します。 - データ型の処理: 基本型(整数、浮動小数点数)は通常直接マッピングされます。文字列、配列、カスタム構造体などのより複雑な型は、一貫したメモリレイアウトと所有権セマンティクスを確保するために慎重な処理が必要です。Rustの
std::ffi
モジュールは、安全なC文字列操作のためのCStr
やCString
などの役立つ型を提供します。ポインタは通常、Rustの生のポインタ(*const T
、*mut T
)にマッピングされます。
実践例:CからRustを呼び出す
簡単な例で説明しましょう。基本的な算術演算を実行するCライブラリがあるとします。
Cコード (my_math.h
および my_math.c
)
まず、Cヘッダーファイルに関数シグネチャを定義します。
// my_math.h #ifndef MY_MATH_H #define MY_MATH_H // 2つの整数を加算する簡単な関数 int add_integers(int a, int b); #endif // MY_MATH_H
そして実装:
// my_math.c #include "my_math.h" #include <stdio.h> int add_integers(int a, int b) { printf("C function: Adding %d and %d\n", a, b); return a + b; }
これを静的ライブラリ(Linux/macOSでは .a
、Windowsでは .lib
)にコンパイルするには、gcc
を使用します。
gcc -c my_math.c -o my_math.o ar rcs libmy_math.a my_math.o
これにより libmy_math.a
が作成されます。
Rustコード (src/main.rs
)
次に、RustプロジェクトでC関数を宣言し、呼び出します。
// src/main.rs // このブロックは、関数 'add_integers' が // C互換ライブラリで外部で定義されていることをRustに伝えます。 // 'link_name' はライブラリファイルの名前を指定します('lib'プレフィックスと'.a/.so/.dll'サフィックスなし) // 'link' は静的リンクか動的リンクかを指定します(指定しない場合は一般的なケースで静的がデフォルトです) #[link(name = "my_math", kind = "static")] // libmy_math.aに対してリンク extern "C" { // C関数のシグネチャを宣言します。 // Rustのi32のCのintと正確に一致することを確認してください。 fn add_integers(a: i32, b: i32) -> i32; } fn main() { let x = 10; let y = 20; // FFI呼び出しは、Rustが外国語で実行されるコードの安全性を保証できないため、 // 本質的に安全ではありません。 // プログラマーはC関数呼び出しが安全であることを保証する必要があります。 let sum = unsafe { add_integers(x, y) }; println!("Rust: The sum from C is: {}", sum); }
このRustコードをコンパイルして実行するには、rustc
にCライブラリの場所を伝える必要があります。通常は、Cargo.toml
のルートディレクトリに libmy_math.a
を配置するか、ビルドスクリプトでパスを指定することでこれを行うことができます。簡単なケースでは、cargo build
を使用し、手動でリンクするのが一般的です。 libmy_math.a
がRustプロジェクトの同じディレクトリにある場合、検索パスを指定する必要がある場合があります。
Rustプロジェクトの build.rs
スクリプトでこれを自動化できます。
// build.rs fn main() { println!("cargo:rustc-link-search=native=/path/to/your/lib"); // このパスを調整してください! println!("cargo:rustc-link-lib=static=my_math"); }
または、libmy_math.a
がプロジェクトのルートにある場合、コンパイル時に直接リンクできます。
# Rustコードをコンパイルし、Cライブラリをリンクする rustc src/main.rs -L . -l static=my_math -o rust_app # アプリケーションを実行する ./rust_app
期待される出力:
C function: Adding 10 and 20
Rust: The sum from C is: 30
C++コードの処理
C++関数をRustから直接呼び出すことは、C++の名前マングル、仮想関数、およびオブジェクトモデルのために、より複雑になります。標準的なアプローチは、C++コードの周りにC互換APIレイヤー(「Cラッパー」と呼ばれることが多い)を作成することです。
C++ラッパーの例
C++クラスがあるとします。
// my_cpp_class.hpp #ifndef MY_CPP_CLASS_HPP #define MY_CPP_CLASS_HPP #include <string> class MyCppClass { public: MyCppClass(int value); void greet(const std::string& name); int get_value() const; private: int m_value; }; #endif // MY_CPP_CLASS_HPP
// my_cpp_class.cpp #include "my_cpp_class.hpp" #include <iostream> MyCppClass::MyCppClass(int value) : m_value(value) { std::cout << "C++: MyCppClass constructed with value: " << m_value << std::endl; } void MyCppClass::greet(const std::string& name) { std::cout << "C++: Hello, " << name << "! My internal value is " << m_value << std::endl; } int MyCppClass::get_value() const { return m_value; }
これをRustから呼び出せるようにするために、extern "C"
ラッパーを作成します。
// my_cpp_class_wrapper.cpp #include "my_cpp_class.hpp" #include <cstring> // strlen、strcpy用 #include <string> // std::string用 extern "C" { // MyCppClassインスタンスの不透明なポインタ // これにより、C++クラスの詳細がRustから隠されます typedef void MyCppClassOpaque; // コンストラクタラッパー MyCppClassOpaque* my_cpp_class_new(int value) { return reinterpret_cast<MyCppClassOpaque*>(new MyCppClass(value)); } // メソッドラッパー: greet void my_cpp_class_greet(MyCppClassOpaque* ptr, const char* name_ptr) { MyCppClass* instance = reinterpret_cast<MyCppClass*>(ptr); if (instance && name_ptr) { instance->greet(std::string(name_ptr)); } } // メソッドラッパー: get_value int my_cpp_class_get_value(MyCppClassOpaque* ptr) { MyCppClass* instance = reinterpret_cast<MyCppClass*>(ptr); if (instance) { return instance->get_value(); } return -1; // または適切にエラーを処理する } // デストラクタラッパー void my_cpp_class_free(MyCppClassOpaque* ptr) { MyCppClass* instance = reinterpret_cast<MyCppClass*>(ptr); delete instance; } } // extern "C"
C++コードとラッパーをライブラリにコンパイルします。
g++ -c my_cpp_class.cpp my_cpp_class_wrapper.cpp -o my_cpp_class.o -o my_cpp_class_wrapper.o ar rcs libmy_cpp_class.a my_cpp_class.o my_cpp_class_wrapper.o
C++ラッパー用のRustコード
// src/main.rs use std::ffi::{CStr, CString}; use std::os::raw::c_char; // C++ラッパーからC互換関数を宣言します #[link(name = "my_cpp_class", kind = "static")] extern "C" { // C++クラスインスタンスの不透明なポインタ型 type MyCppClassOpaque; fn my_cpp_class_new(value: i32) -> *mut MyCppClassOpaque; fn my_cpp_class_greet(ptr: *mut MyCppClassOpaque, name_ptr: *const c_char); fn my_cpp_class_get_value(ptr: *mut MyCppClassOpaque) -> i32; fn my_cpp_class_free(ptr: *mut MyCppClassOpaque); } fn main() { let initial_value = 42; let instance_ptr = unsafe { // C++オブジェクトを構築する my_cpp_class_new(initial_value) }; if instance_ptr.is_null() { eprintln!("Failed to create C++ object."); return; } let rust_name = "Rustacean"; let c_name = CString::new(rust_name).expect("CString conversion failed"); unsafe { // C++オブジェクトのメソッドを呼び出す my_cpp_class_greet(instance_ptr, c_name.as_ptr()); // C++オブジェクトから値を取得する let value = my_cpp_class_get_value(instance_ptr); println!("Rust: Retrieved value from C++ object: {}", value); // C++オブジェクトを解放する my_cpp_class_free(instance_ptr); } }
libmy_cpp_class.a
がリンクされることを確認して、同様にコンパイルして実行します。
安全性に関する考慮事項
Rustの unsafe
キーワードは、FFIを伴う責任を果たすための強力なリマインダーとして機能します。 unsafe
コードを呼び出すときは、Rustの不変条件を手動で維持する必要があります。
- メモリ安全性: C/C++に渡されるポインタが有効であり、C/C++によって割り当てられたメモリ(ある場合)が、リークや解放後の使用エラーを回避するために適切に解放されることを確認してください。
- データ整合性: RustとC/C++の間で共有されるデータ構造が互換性のあるレイアウトを持っていることを確認してください。
- スレッド安全性: C/C++コードがスレッドセーフでない場合、適切な同期なしに複数のRustスレッドから同時に呼び出されないことを確認してください。
- エラー処理: C/C++関数は、エラー報告のためにリターンコード、
errno
、または例外を使用することがよくあります。これらをRustのResult
型または適切なエラー処理メカニズムにマッピングします。 - null ポインタ: C/C++関数から返されたnullポインタを安全に処理します。Rustの
Option<T>
は、nullチェックを提供するために生のポインタをラップするために使用できます。
bindgen
のようなツールは、CヘッダーファイルからRust FFIバインディングを自動的に作成でき、定型的なコードや潜在的なエラーを削減します。C++プロジェクトの場合、autocxx
は、RustバインディングとC++ブリッジを自動的に生成するためのより高度なソリューションを提供します。
アプリケーションシナリオ
C/C++とのRust FFIは、いくつかのシナリオで非常に価値があります。
- OS APIとのインターフェース: ほとんどのオペレーティングシステム機能は、C API(例:Win32 API、POSIX関数)を通じて公開されています。
- 高性能ライブラリの活用: 科学計算、グラフィックス、機械学習、暗号化は、高度に最適化されたC/C++ライブラリ(例:BLAS、LAPACK、OpenCV、TensorFlow、OpenSSL)に依存することがよくあります。
- レガシーシステムのリポート: レガシーC/C++コードベースの一部をRustに段階的に移行し、新しいRustコンポーネントが既存のC/C++ロジックと対話できるようにします。
- 組み込みシステム開発: Cで書かれた低レベルハードウェアドライバとのインターフェース。
- パフォーマンスが重要なセクション: アプリケーションの大部分をRustで安全性と保守性のために維持しながら、パフォーマンスのボトルネックをC/C++で書き直します。
結論
RustのFFI機能は、RustアプリケーションとC/C++コードの広大なエコシステムとの間のギャップを埋めるための堅牢で驚くほど安全な方法を提供します。メモリ管理とデータ表現に関する細心の注意が必要ですが、高度に最適化された既存のC/C++ライブラリを活用するメリットは計り知れません。extern "C"
ラッパーと unsafe
ブロックを戦略的に使用し、慎重な型マッピングを組み合わせることで、Rust開発者はRustの最新の安全保証と、C/C++コードベースの比類なきパフォーマンスと確立された機能性を組み合わせた強力なアプリケーションを構築できます。これにより、Rustの範囲と有効性が新しいエキサイティングなドメインに拡張されます。