Rustコンパイラはalignedかつpackedな構造体を表現できない

実世界では微妙に出てくるにもかかわらず、Rustコンパイラはalignedかつpackedな構造体を表現できないためBindgenでコードを生成出来ないライブラリがある

Cでは構造体に対して__attribute__を用いてアライメントやフィールドの詰め込みを指定できる。アライメントは__attribute__(aligned(4))とかで、詰め込みは__attribute__(packed)で指定可能。Rustも同様に#[repr(packed)]#[repr(align(n))]で指定できる。Rustの構造体のpackに関しては公式ドキュメントが分かりやすい。

repr(Rust)doc.rust-jp.rs

要約すると、構造体のフィールドをどのように配置するかを指定するかで、以下の三種類があるということだ。

  • #[repr(C)]:フィールドをC言語の順序で配置する。
    • これはフィールドを愚直に書かれた順序に詰め込む。なのでソースコードから(コンパイラ依存ではあるが)基本的にバイナリレイアウトがそのままわかる。
  • #[repr(rust)]: Rust標準
    • アライメントを跨がないようにしつつ、フィールドの順序を並べ替えてバイナリサイズを最適化する。
  • #[repr(packed(n))]:フィールドを詰め込みで配置する。ただし、アライメントをnとしてそれは守る。nを指定しない場合は1とみなされる。

#[repr(rust)]でわざわざアライメントを考慮した詰め込みを行うのは実行効率が関係している。多くのCPUではuint64_tの場合は8の倍数のアドレスに配置されていないと直接メモリからLOAD命令を実行できない。メモリからの読み込み回路やキャッシュ回路の設計を考えると別に不自然な制約ではないだろう。なので#[repr(packed)]を指定した場合は一部ずつ読み取り、bit shiftしてbit orを計算することで元の値をレジスタに入れる必要がある。また、packedの場合フィールド内部のアライメントが小さくなっているので構造体それ自体もアライメントを守る必要は(基本的には)ない。なので構造体自体の配置もpacked(n)で指定されたnのアライメントのみを守るようになる。

また、先に述べたようにドキュメントに記載されていないものの#[repr(align(n))]も安定化されている。こちらはアライメントを手動で上げることが可能な属性になる。SIMD命令やDirect I/O APIなどでは8バイトアライメント以上のアライメントがSIMD命令やLinuxページング機構の都合上必要になってくる。

RFC: Add #[repr(align = "N")] by alexcrichton · Pull Request #1358 · rust-lang/rfcs
RFC: Add #[repr(align = "N")] by alexcrichton · Pull Request #1358 · rust-lang/rfcsExtend the existing #[repr] attribute on structs with an align = "N" option to specify a custom alignment for struct types. Renderedgithub.com

これを指定すると構造体自体はnのアライメントを守って配置されるようになるし、内部のフィールドも当然この指定されたアライメントを跨がないように配置される(大きくなるので別に動作が変わることはない)。

この2つの機能はある意味アライメントを下げる指定と上げる指定と考えることが出来る。これは基本的に相反するものであるためRustでは#[repr(packed)]#[repr(align)]を両方同時に指定することは禁止されている。両方指定された場合は#[repr(packed)]を構造体内部に、#[repr(align)]を構造体自体のアライメントとする案もあったが、これだと#[repr(align)]が追加されると#[repr(packed)]の意味が変更されるややこしいことになるので簡単のために禁止されたようだ。

Cの仕様

ところでGCCとClangでは__attribute__((packed))__attribute__((aligned(n)))両方同時に 指定出来る。両方指定した場合の動作としては、packedで構造体内部のフィールドを配置してalignedで構造体自体を配置する動作になる。元々GCC拡張だがClangでもサポートされているのでまあ割と動くと思っていいだろう(C標準ではないので常にうごくわけではないが)。

こんなん何に使うねんという感じがするし、実際Rustでもこうしたケースは重要だとは考えられなかったが、実際は使われるケースが稀にある。Memory Mapped I/Oなどである程度アラインされた単位でデータの塊を送受信するが、そのデータの塊の内部を構造体に直接マッピングして扱うコードを書きたい場合がこれに該当する。

notitle
struct something_packed_t {
  uint16_t a;
  uint16_t b;
  uint32_t c
} __attribute__((packed)) __attribute__((aligned(8)));

このように指定すると構造体自体は8バイトで並ぶがその内部のビット配列を構造体で分解出来る。便利ですね。そうかな……?普通はビットシフトで読み書きする関数を作るところだが、1秒で3000万リクエストとかを捌く高速NICとかではここまでしたいケースがあるらしい。実際にNVIDIAが自社NICの専用ライブラリとして配布しているmlx5_dv.hではこうしたコードがマジで存在する。こうしたコードと同じようなことは実はRustでは出来ないため、bindgenとかでコード生成を行うときには自明な変換が出来なくなり結構困る。