[Rust] FFIでよく使う型変換
自分は最近RustのROS2クライアントを書いていて、間に合えばこれについて記事を書こうと思ったのですが、案の定間に合わなかったので代わりに書いています.
ROS2はROS (Robot Operating System)というPロボット用フームワークの後継で、ROSが抱えるいくつかの問題点を解消するため、互換性を完全に切って新たに作られたものです.問題点の一つに、言語毎のクライアントライブラリの実装の仕様が統一されていないというものがあり、ROS2ではrclc
やrmw
等のC言語ライブラリを用意し、各言語のクライアントライブラリはそれらをラップして実装することが求められています.
こういった事情でRustのROS2クライアントを作るためには、FFIが必須です.
と御託はさておき、FFIでよく使う文字列と配列の変換について書いていきます.
前提知識
FFIについては知っているものとします.知らない方はこのあたりを見てください.
実装
に置いています.C言語ライブラリはbindgen
とcc
を使って、ffi_example_sys
というcrateにしています.
文字列同士の変換
Rustでは文字列として主にString
と&str
を使い、C言語ではchar *
を使います.では変換はどうすればよいでしょうか.
そもそもC言語の文字列はヌル文字 (\0
)が文字列の終端を示しており、RustのString
とは異なるデータ構造をしているわけですが、RustにはFFIに使えるヌル文字終端文字列としてstd::ffi::CString
、std::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::new
はInto<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
という関数がありますが、これを今回の用途で用いてはいけません.
CString
はString
と同様にバッファの所有権を持っているため、破棄するときにメモリを開放しようとしまうわけですが、異なるアロケータが確保したメモリをRust側のアロケータが開放しようとすると未定義動作に繋がってしまいます.
これは色んなところで言えるところなのですが、FFIをする上では以下を肝に銘じておきましょう.
Rust側で確保したメモリはRust側で開放せよ.C言語側で確保したメモリはC言語側で開放せよ.
動的配列同士の変換
RustではVec<T>
や&[T]
あたりを使い、C言語に所謂動的配列はありませんが、ポインタがその役割を担っています.これらの変換について書いていきます.
Rust -> C
以下のような関数を用意して、Rustから呼び出すことを考えてみます.別に関数は足し算ではなくても何でもいいんですが、realloc
やfree
等のメモリ操作はしないという前提はあります.
#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_t
はusize
になって欲しいので、bindgenのsize_t_is_usize
というオプションをtrue
にしています.
例えば、スライスの場合std::slice::as_ptr
やstd::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_parts
やstd::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関数にならないので(この辺りの保証、もうちょっとうまい方法を知りたい).
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);
}
}
}
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);
}
Discussion
ポインタのこと完全に忘れてた.
&T
->*const T
と&mut T
->*mut T
は暗黙に型強制されます.*mut T
->*const T
の型強制もある.逆は
as_ref
やas_mut
を使う.