RustのFFIで他言語との連携を行う方法

先生

Rust FFIで他言語と連携して、最強のクロスプラットフォームアプリを作ろう!🚀

Rust FFIとは?他言語連携の基礎

Rust FFI (Foreign Function Interface) は、Rustで記述されたコードを他のプログラミング言語から利用したり、逆に他の言語で書かれたライブラリをRustから利用したりするための仕組みです。これにより、Rustの安全性とパフォーマンスを活かしつつ、既存の資産や特定の言語でしか利用できないライブラリとの連携が可能になります。

FFIは、異なる言語間の「翻訳者」のような役割を果たします。各言語には独自のメモリ管理、データ型、呼び出し規約がありますが、FFIはこれらの違いを吸収し、言語間のスムーズな通信を実現します。

例えば、Pythonの豊富な機械学習ライブラリをRustの高速な処理能力と組み合わせたり、C言語で書かれた枯れたライブラリをRustの安全な環境で使用したりできます。

FFIの仕組み:C言語との連携を例に

Rust FFI の基本的な仕組みを理解するために、C言語との連携を例に見てみましょう。C言語は、多くの言語と互換性があり、FFIの学習に適しています。

RustでCの関数を呼び出すには、extern ブロックを使用します。このブロック内で、Cの関数シグネチャを宣言します。Rustコンパイラは、この情報をもとに、Cの関数を呼び出すためのコードを生成します。


rust
#[link(name = "my_c_library")]
extern {
    fn my_c_function(arg1: i32) -> i32;
}

fn main() {
    let result = unsafe { my_c_function(10) };
    println!("C function returned: {}", result);
}

#[link(name = "my_c_library")] は、リンクするCのライブラリを指定します。unsafe ブロックは、FFIの呼び出しが安全でない可能性があることをRustコンパイラに伝えます。FFIの呼び出しは、メモリ安全性の問題を引き起こす可能性があるため、注意が必要です。

C言語のライブラリ側のコード例:


c
#include <stdio.h>

int my_c_function(int arg1) {
    printf("C function called with: %d\n", arg1);
    return arg1 * 2;
}

この例では、RustからCの関数my_c_functionを呼び出し、その結果を表示しています。 Cの関数は、引数を2倍にして返します。

データ型の変換:RustとC

RustとCでは、データ型が異なる場合があります。FFIを使用する際には、これらのデータ型を適切に変換する必要があります。

基本的な数値型(i32, f64 など)は、Cの対応する型と直接互換性があります。しかし、文字列や構造体などの複雑な型は、より注意が必要です。

文字列をCに渡す場合、CString 型を使用します。CString は、null終端されたバイト列を表現し、Cの文字列と互換性があります。


rust
use std::ffi::CString;
use std::os::raw::c_char;

extern {
    fn c_function_that_takes_string(s: *const c_char);
}

fn main() {
    let my_string = CString::new("Hello from Rust!").unwrap();
    let c_string = my_string.as_ptr();
    unsafe { c_function_that_takes_string(c_string) };
}

C側で文字列を受け取る例:


c
#include <stdio.h>
#include <string.h>

void c_function_that_takes_string(const char *s) {
    printf("Received string from Rust: %s\n", s);
}

構造体を共有する場合、#[repr(C)] アトリビュートを使用して、Cの構造体と同じメモリレイアウトを持つようにRustの構造体を定義します。


rust
#[repr(C)]
struct MyStruct {
    x: i32,
    y: f64,
}

extern {
    fn c_function_that_takes_struct(s: MyStruct);
}

fn main() {
    let my_struct = MyStruct { x: 10, y: 3.14 };
    unsafe { c_function_that_takes_struct(my_struct) };
}

エラー処理:FFIの安全な利用

FFIの呼び出しは、安全でない操作を含む可能性があるため、エラー処理が重要です。Cの関数がエラーを返す場合、Rust側で適切に処理する必要があります。

CのエラーコードをRustの Result 型に変換することで、安全なエラー処理を実現できます。


rust
use std::os::raw::c_int;

extern {
    fn c_function_that_might_fail() -> c_int;
}

fn main() {
    let result = unsafe { c_function_that_might_fail() };
    if result != 0 {
        println!("C function failed with error code: {}", result);
    } else {
        println!("C function succeeded!");
    }
}

より高度なエラー処理を行うには、Cの関数が返すエラーコードをRustの enum 型にマッピングし、Result 型で使用することができます。

FFIにおけるメモリ管理

メモリ管理はFFIにおいて非常に重要な考慮事項です。Rustと他の言語間でメモリを共有する場合、どちらの言語がメモリの所有権を持つかを明確にする必要があります。

RustからCにメモリを渡す場合、通常、Rustがメモリの所有権を保持し、Cは単にそのメモリへのポインタを受け取ります。Cがメモリを変更する場合、Rustは変更を認識する必要があります。

CからRustにメモリを渡す場合、Box::from_raw を使用して、Cによって割り当てられたメモリをRustの Box 型に変換できます。これにより、Rustがメモリの所有権を取得し、自動的に解放することができます。


rust
use std::ffi::c_void;

extern {
    fn c_function_that_allocates_memory() -> *mut c_void;
    fn c_function_that_uses_memory(ptr: *mut c_void);
    fn c_function_that_frees_memory(ptr: *mut c_void);
}

fn main() {
    let ptr = unsafe { c_function_that_allocates_memory() };

    if !ptr.is_null() {
        unsafe { c_function_that_uses_memory(ptr) };

        // Cで確保されたメモリをCで解放する
        unsafe { c_function_that_frees_memory(ptr) };
    }
}

参考リンク

まとめ

Rust FFI は、Rustのパワーを他の言語と組み合わせるための強力なツールです。C言語との連携を中心に、データ型の変換、エラー処理、メモリ管理など、FFIの基本的な仕組みを理解することで、より安全かつ効率的に他言語との連携を実現できます。 FFIを使いこなして、あなたのRustプロジェクトをさらに進化させましょう。