[Rust] FFIでよく使う型変換

commits8 min read読了の目安(約7600字 1

この記事はRust Advent Calendar 2020の18日目の記事です.

自分は最近RustのROS2クライアントを書いていて、間に合えばこれについて記事を書こうと思ったのですが、案の定間に合わなかったので代わりに書いています.

ROS2はROS (Robot Operating System)というPロボット用フームワークの後継で、ROSが抱えるいくつかの問題点を解消するため、互換性を完全に切って新たに作られたものです.問題点の一つに、言語毎のクライアントライブラリの実装の仕様が統一されていないというものがあり、ROS2ではrclcrmw等のC言語ライブラリを用意し、各言語のクライアントライブラリはそれらをラップして実装することが求められています.
こういった事情でRustのROS2クライアントを作るためには、FFIが必須です.

と御託はさておき、FFIでよく使う文字列と配列の変換について書いていきます.

前提知識

FFIについては知っているものとします.知らない方はこのあたりを見てください.

実装

https://github.com/eduidl/rust-ffi-example

に置いています.C言語ライブラリはbindgenccを使って、ffi_example_sysというcrateにしています.

文字列同士の変換

Rustでは文字列として主にString&strを使い、C言語ではchar *を使います.では変換はどうすればよいでしょうか.
そもそもC言語の文字列はヌル文字 (\0)が文字列の終端を示しており、RustのStringとは異なるデータ構造をしているわけですが、RustにはFFIに使えるヌル文字終端文字列としてstd::ffi::CStringstd::ffi::CStrという型が用意されています.それぞれの対応は以下の表の通りです.

通常 FFI
所有 String std::ffi::CString
借用 &str &std::ffi::CStr

では具体的な変換を含めた呼び出し方法をみていきます.

Rust -> C

まずはRustからCへの変換から.以下のような関数を用意して、Rustから呼び出してみます.

#include <stdio.h>

void print_str(const char *str)
{
    printf("From C: %s\n", str);
}

CString::newを呼ぶだけなので簡単です.以下では&strを渡していますが、CString::newInto<Vec<u8>>を要求しているので他にも色々渡せます.

use std::ffi::CString;

fn main() {
    let c_string = CString::new("Hello FFI").unwrap();
    unsafe {
        ffi_example_sys::print_str(c_string.as_ptr());
	//=> From C: Hello FFI
    }
}

型を明示的に書くと以下のとおりです.

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

fn main() {
    let c_string: CString = CString::new("Hello FFI").unwrap();
    let c_string_ptr: *const c_char = c_string.as_ptr();
    unsafe {
        ffi_example_sys::print_str(c_string_ptr);
	//=> From C: Hello FFI
    }
}

ただし、以下のように書いてしまうと、ffi_example_sys::print_strを呼び出す前にCStringが破棄されてしまい、正しく文字列を渡せないので注意しましょう(以下は呼び出しが破棄の直後なので空文字になっていますが、メモリが再割り当てされたらそうならないこともあります).

use std::ffi::CString;

fn main() {
    let c_string_ptr = CString::new("Hello FFI").unwrap().as_ptr();
    unsafe {
        ffi_example_sys::print_str(c_string_ptr);
	//=> From C: 
    }
}

C -> Rust

以下のような関数を用意して、Rustから利用することを考えます.

const char *hello()
{
    return "Hello FFI";
}

こちらはCStr::from_ptrから得られる&CStrを経て、CStr::to_str&strに変換します.CStr::to_strするとき、正しいUTF-8文字列かどうかのバリデーションはされますが新しくバッファは作られません.メモリの所有権が欲しい場合はさらにto_stringしてStringにします.

use std::ffi::CStr;

fn main() {
    let str_ = unsafe { CStr::from_ptr(ffi_example_sys::hello()) }
        .to_str()
        .unwrap();
    println!("From Rust: {}", str_);
    //=> From Rust: Hello FFI
}

型を明示すると以下の通り.

use std::ffi::CStr;
use std::os::raw::c_char

fn main() {
    let hello: *const c_char = unsafe { ffi_example_sys::hello() };
    let c_str: &CStr = unsafe { CStr::from_ptr(hello) };
    let str_: &str = c_str.to_str().unwrap();
    println!("From Rust: {}", str_);
    //=> From Rust: Hello FFI
}

こちらも一つ注意事項を挙げておくと、CString::from_rawという関数がありますが、これを今回の用途で用いてはいけません.

CStringStringと同様にバッファの所有権を持っているため、破棄するときにメモリを開放しようとしまうわけですが、異なるアロケータが確保したメモリをRust側のアロケータが開放しようとすると未定義動作に繋がってしまいます.
これは色んなところで言えるところなのですが、FFIをする上では以下を肝に銘じておきましょう.

Rust側で確保したメモリはRust側で開放せよ.C言語側で確保したメモリはC言語側で開放せよ.

動的配列同士の変換

RustではVec<T>&[T]あたりを使い、C言語に所謂動的配列はありませんが、ポインタがその役割を担っています.これらの変換について書いていきます.

Rust -> C

以下のような関数を用意して、Rustから呼び出すことを考えてみます.別に関数は足し算ではなくても何でもいいんですが、reallocfree等のメモリ操作はしないという前提はあります.

#incldue <stddef.h>

int sum(const int *arr, size_t size)
{
    int total = 0;
    for (size_t i = 0; i < size; ++i)
    {
        total += arr[i];
    }
    return total;
}

Cのsize_tusizeになって欲しいので、bindgenのsize_t_is_usizeというオプションをtrueにしています.

例えば、スライスの場合std::slice::as_ptrstd::slice::as_mut_ptrが使えます.

fn main() {
    let slice = &[1, 13, 5];
    assert_eq!(
        unsafe { ffi_example_sys::sum(slice.as_ptr(), slice.len()) },
        19
    );
}

Vec<T>[T; N]&[T]を取得可能な型については同様の処理が可能です.

fn main() {
    let vec = vec![1, 13, 5];
    assert_eq!(
        unsafe { ffi_example_sys::sum(vec.as_ptr(), vec.len()) },
        19
    );
}

C -> Rust

以下のような連番を作る関数を用意して、Rustから利用することを考えます.配列の解放用関数も用意しておきます.

int *get_sequential_array(size_t size)
{
    int *arr = (int *)malloc(size * sizeof(int));
    for (size_t i = 0; i < size; ++i)
    {
        arr[i] = i;
    }

    return arr;
}

void free_array(int *arr)
{
    free(arr);
    printf("free\n");
}

Vec<T>にしたいと思った場合、Vec::from_raw_partsという関数に辿り着いてしまうかもしれませんが、CString::from_rawと同様の理由により使ってはいけません.

最も単純なものとしては、std::slice::from_raw_partsstd::slice::from_raw_parts_mutを使ってスライスにする方法があります(どうしてもVec<T>が欲しい場合はto_vecすればよい).

fn main() {
    let size = 101;
    let ptr = unsafe { ffi_example_sys::get_sequential_array(size) };
    let slice = unsafe { std::slice::from_raw_parts(ptr, size) };
    assert_eq!(slice.iter().fold(0, |sum, a| sum + a), 5050);
    drop(slice);  // 安全のため.
    unsafe {
        ffi_example_sys::free_array(ptr);
    }
}

ただ、渡されたポインタが静的でない場合、スライスとポインタの寿命の整合性を取るのが若干面倒かもしれません.先の例もわざとらしいdrop(slice)が目につきます.そういったときは次に述べる方法を取るとよいでしょう.

まず、c_vecというcrateのCVecという構造体を使うという手があります.デストラクタを外から渡せるのも地味に便利です.

fn main() {
    let size = 101;
    let cvec =
        unsafe { c_vec::CVec::new_with_dtor(ptr, size, |ptr| ffi_example_sys::free_array(ptr)) };
    assert_eq!(cvec.iter().fold(0, |sum, a| sum + a), 5050);
}

よく使う場合は新しく構造体を定義するというのも手でしょう.
モジュールを明示的にわけているのは、ptrを直接書き換えさせないためです.この前提がないとsafe関数にならないので(この辺りの保証、もうちょっとうまい方法を知りたい).

sequence.rs
pub struct Sequence {
    ptr: *mut c_int,
    size: usize,
}

impl Sequence {
    pub fn new(size: usize) -> Self {
        let ptr = unsafe { ffi_example_sys::get_sequential_array(size) };
        Self { ptr, size }
    }
}

impl Deref for Sequence {
    type Target = [c_int];

    fn deref(&self) -> &Self::Target {
        unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
    }
}

impl DerefMut for Sequence {
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.size) }
    }
}

impl Drop for Sequence {
    fn drop(&mut self) {
        unsafe {
            ffi_example_sys::free_array(self.ptr);
        }
    }
}
main.rs
mod sequence;
use sequence::Sequence;

fn main() {
    let size = 101;
    let seq = Sequence::new(size);
    assert_eq!(seq.iter().fold(0, |sum, a| sum + a), 5050);
}