はじめに
この記事では、Cの構造体におけるいわゆるZero-length Array / Flexible Array Member(このイディオム自体知らなかったわけですが)をRustのFFIから扱う際にstd::mem::transmuteを使って強引にキャストしていました。その後いろいろ試したところ、特にtransmuteが必須というわけでもなかった、ということが分かりましたので、現時点で妥当と思うやり方を書いておきます。
(もっといいやり方がありましたらお教えください)
Zero-length Array / Flexible Array Member
この記事で扱うのは、以下のようなCの構造体をRustのFFIでどう扱うか、です。
struct line {
int length;
char contents[0];
};
// 以下はC99のFlexible Array Memberを使った場合
struct line {
int length;
char contents[];
};
構造体中で可変長の配列を表現したい場合、普通は配列へのポインタにすると思いますが、構造体の末尾に可変長配列そのものを入れてしまうということが、よく行われるようです。このとき、構造体定義上は配列の長さは分からないので何らかの固定値にしておいて、実際の長さは別途構造体メンバで指定します。この仮の固定値は0でもよくて、これがZero-length Arrayということのようです。
また、C99ではFlexible Array Memberとして標準化されたようです。
Rustでのstruct定義
structの定義は以下のような感じが良さそうです。配列サイズを0とする場合はZero Sized Types (ZSTs)ということになって問題ないようです。
# [repr(C)]
pub struct line {
pub length: c_int,
pub content: [c_char;0],
}
// あるいは
# [repr(C)]
pub struct line {
pub length: c_int,
pub content: [u8;0],
}
意味的には素直に可変長配列としてDynamically Sized Types (DSTs)を使いたいところですが、struct全体がUnsizedになってしまう、DSTのメモリレイアウトとZero-length Arrayのそれが一致する保証がなさそう、ということで現時点では使えなさそうです。こちらにも#[repr(C)]におけるDSTは使えなさそうに書いてあります。
また、Cのcharはlibcクレートではi8となっていますが、Rustの文字列はu8がベースになっているのでpub content: [u8;0]とするということもできます。若干気持ち悪いですが、ここでc_char(すなわちi8)としてしまうと後でRustの文字列に変換する際にtransmuteが必要になってしまいます。
構造体メンバへのアクセス
以下のようにスライスを作ってやればアクセス出来ます。
struct定義でpub content: [c_char;0]とした場合、c_charがi8なので変換にはtransmuteが必要になります。
let line: *const line = ...
let len = unsafe { (*line).length as usize };
let ptr = unsafe { std::mem::transmute::<&[c_char;0], *const u8>( (*line).content ) };
let slice = unsafe { std::slice::from_raw_parts( ptr, len ) };
pub content: [u8;0]の場合、型変換は&[u8;0]から*const u8ですが、これはasで変換出来ます。
let line: *const line = ...
let len = unsafe { (*line).length as usize };
let ptr = unsafe { (*line).content as *const u8 };
let slice = unsafe { std::slice::from_raw_parts( ptr, len ) };
この2つのどちらがいいかはよく分かりません。結局Cのcharが8bitでなかった時点で、最終的にslice::from_raw_parts(あるいはこの後文字列に変換する部分)ではおかしくなるわけで、個人的には見た目重視でtransmuteがない後者かなぁと思います。