union: RubyでRustがunsafeになった話

この記事は[Rust Advent Calendar 2021](https://qiita.com/advent-calendar/2021/rust)の7日目の記事です。

# TLDR
Rustのunionについて、自作Ruby処理系の宣伝を交えつつ紹介します。結論としては、unionは危ないのであんまり使わない方がよい。

# 自己紹介
こんにちは、Rustで **ruruby** と称するRubyインタプリタを作っているmonochromeといいます。言語処理系が好きで、普段は[プログラミング言語処理系が好きな人の集まりSlack](https://prog-lang-sys-ja-slack.github.io/wiki/)あたりでウロウロしております。

**ruruby** のGitHubレポジトリは下記です。
https://github.com/sisshiki1969/ruruby

**プログラミング言語処理系が好きな人の集まりSlack** の入り口は下記。プログラミング言語処理系が好きな方ならどなたでも参加できます。
https://prog-lang-sys-ja-slack.github.io/wiki/
 
**ruruby** はRuby（ここではC言語で書かれたCRuby =「みんなが使っているRuby」を指します）並みの高速性を目標として開発しています。処理系の種別としては、CRubyと同様にRubyコードを仮想マシンコードへコンパイルして実行する、仮想マシンインタプリタ（VMインタプリタ）です。現時点でのベンチマークの結果を下記に掲載しています。
https://github.com/sisshiki1969/ruruby/wiki/Benchmarks

今回はインタプリタ作成の上で必要となったRustのunsafeな機能の一つとして`union`を紹介し、実際にどのように使っているかを説明します。

# union in Rust
`union`はRust 1.19.0で安定化された機能で、`struct`や`enum`のようなRust組み込みのデータ構造です。
[Rust Blog: What's in 1.19.0 stable](https://blog.rust-lang.org/2017/07/20/Rust-1.19.html#whats-in-1190-stable)
[unionのRFC](https://github.com/rust-lang/rfcs/blob/master/text/1444-union.md#motivation)

`union`は`enum`に似ていますが、内部にバリアントの種類を示すタグを持ちません。

定義は

```Rust
union MyUnion {
    f1: u32,
    f2: f32,
}
```
こんな形をしています。大文字始まりのバリアントを列挙する`enum`と違い、`struct`と同様の文法です[^3]。違うのはC言語の`union`と同様、全てのフィールドが同一のメモリ領域に割り当てられることです。上の例だと`f1`と`f2`は同じメモリ領域を指します[^2]。

```Rust
let u = MyUnion { f1: 1 };
u.f1 = 5;
let value = unsafe { u.f1 };
```
１行目のコンストラクタ・２行目の代入はsafeですが、フィールドの読み出しはunsafeな操作になります。`union`ではデータの内部にタグがないため、どのフィールドとして扱うのが正しいのかという情報がコンパイラから見えないからです[^1]。
上記の例だと`u.f2 = 0.0`と代入した後、誤って`u.f1`で読み出そうとすると意図しない値になります[^6]。数値であれば「意図しない値」で済みますが、`Box`や`Vec`など、ポインタとして読み出した場合には大変よろしくない事態になります。

また、Rust特有の注意点としてフィールド書き換えの際のデストラクタ処理（drop）の問題があります。`enum`や`struct`では値を書き換える際、Rustコンパイラが古い方の値を破棄（drop）するコードを自動的に挿入します。`union`の場合は古い値がどのフィールドなのかをコンパイラが知ることができないため、自動的なdropが安全に行えません。このため、現状では`union`のフィールドは`Copy`な値か、そうでない場合には`ManuallyDrop`構造体でラップして、自動的なdropを行わないことをコンパイラに明示する必要があります。

[std::mem::ManuallyDrop](https://doc.rust-lang.org/stable/std/mem/struct.ManuallyDrop.html)
[Tracking issue for RFC 2514, "Union initialization and Drop"](https://github.com/rust-lang/rust/issues/55149)

以上の事情で`union`内のデータのデストラクタ処理はプログラマの責任となり、`Drop`トレイトを自分で実装するか、手動でfreeするなどして適切に処理する必要があります。

# で、何が嬉しいのか
Rustの特徴は堅固な静的型システムを持ち、所有権の概念でメモリ安全性を確保しつつ、高速に動作するコードを生成できることだと思っています。`enum`は上記の特徴をすべて備えている一方、`union`のようなunsafeだらけの機能はRustの長所を損なうもののように見えます。`union`導入の動機としては、上記の[RFC](https://github.com/rust-lang/rfcs/blob/master/text/1444-union.md#motivation)によれば、

1. C言語の`union`との相互運用性
2. enumに比べ、よりメモリ効率の良いデータ構造を実装できる

といった点にあります。以下、**ruruby** でどう使っているかみていきます。

# 使用例

Rubyは静的型システムを持たないオブジェクト指向言語です。**ruruby** ではRubyオブジェクトは`RValue`という64バイトの構造体に格納されています[^4]。`RValue`は多岐にわたる種類のオブジェクトを表現するため、全オブジェクト共通の`class`・`var_table`の他、オブジェクトの種類（≒クラス）固有の内部データを保持する`kind`フィールドを持ちます。

```Rust
/// ヒープアロケートされたRubyオブジェクト
pub struct RValue {
    // 8バイト　下から２バイト目に、kindフィールドがどのObjKindかを示すタグ
    //         が格納される
    flags: RVFlag,
    // 8バイト　オブジェクトのクラス　今回は関係なし
    class: Module,
    // 8バイト　インスタンス変数テーブル　今回は関係なし
    var_table: Option<Box<ValueTable>>,
    // 40バイト　オブジェクトの内部データ
    pub kind: ObjKind,
}

pub union RVFlag {
    // 生きているオブジェクトの場合：kindフィールドがどのObjKindかを示すタグ
    flag: u64,
    // 回収されたオブジェクトの場合：ガベージコレクタがリンクリスト用に使用　
    next: Option<NonNull<RValue>>,
}

#[repr(C)]
pub union ObjKind {
    pub float: f64,
    pub module: ManuallyDrop<ClassInfo>,
    pub string: ManuallyDrop<String>,
    pub array: ManuallyDrop<ArrayInfo>,
    // 以下略
}
```

以下、RValueの構造をみていきます。
まず、最初の`flags`フィールドが`RVFlag`という名前の`union`になっています。

1. `RValue`がインタプリタから到達可能な場合（つまり今後も参照される可能性がある場合）には`RVFlag`の`flag`フィールドが使用され、`kind`フィールド用の1バイトのタグや、その他のフラグ類が格納されます。最下位ビットは常に１となります。

1. `RValue`がインタプリタからたどれない場合（つまり今後参照されることがない場合）にはガベージコレクタが回収してデストラクタ処理を行い、`RVFlag`の`next`フィールドとして次の回収済み`RValue`へのポインタを書き込みます。この値は最下位ビットが必ず0になります。[^5]
1. 最下位ビットをみることで、上記の1.か2.かが判定できます。

次に、`kind`フィールドも`union`です。このフィールドの識別に使用されるタグ（1バイト）は下記のように定義され、`flags.flag`に格納されます。

```Rust
impl ObjKind {
    pub const FLOAT: u8 = 3;
    pub const MODULE: u8 = 5;
    pub const CLASS: u8 = 20;
    pub const STRING: u8 = 6;
    pub const ARRAY: u8 = 7;
    // 以下略
}
```

例えば、`RValue`をタグに応じてStringオブジェクトとして扱い、内部のStringの参照を取得する関数は下記のようになっています。

```Rust
    // 安全にアクセスするためにまずkind()関数でタグを取得して分岐する
    match self.kind() {
        ObjKind::STRING => {
            // 中のデータを取り出す
            let s: &String = unsafe { &*self.kind.string };
            // なんか処理する
        }
        // 以下略
    }

impl RValue {
    // タグを取り出す関数
    fn kind(&self) -> u8 {
        let flag = unsafe { self.flags.flag };
        // うっかりGC用のポインタを読まないように最下位ビットが1か確認
        assert!(flag & 0b1 == 1);
        (flag >> 8) as u8
    }

    // ガーベジコレクタから呼ばれるデストラクタ
    fn free(&mut self) {
        unsafe {
            match self.kind() {
                ObjKind::MODULE => ManuallyDrop::drop(&mut self.kind.module),
                ObjKind::CLASS => ManuallyDrop::drop(&mut self.kind.module),
                ObjKind::STRING => ManuallyDrop::drop(&mut self.kind.string),
                ObjKind::ARRAY => ManuallyDrop::drop(&mut self.kind.array),
                // 以下略
                _ => {}
            }
            self.flags.next = None;
            self.var_table = None;
        }
    }
}
```
# まとめ

Rustの`union`を概説し、自作処理系でどのように使っているかを例として挙げました。`union`は危険なうえに使うのが面倒なので、よほどのことがない限りは`enum`を使いましょう。


[^1]: 例示したコードは正しく動作するが、一般的には`union`のデータがどのフィールドとして書き込まれたかは静的に確定できず、誤ったフィールドとして読み込んでしまうと大惨事になる。

[^2]: 厳密には`union`のメモリレイアウトは"unspecified"なので`f1`と`f2`が同一のメモリアドレスに割り当てられる保証はない（ような気がする）。レイアウトを明示したい場合は`#[repr(C)]`アトリビュートを使用する。

[^3]: Rustでは`struct`や`enum`はいわゆる予約語（Strict keywords）で変数名や関数名には使えない。が、`union`は"Weak" keywordsとなっていて、変数名にも関数名にも使える。参考：[The Rust reference: Keywords](https://doc.rust-lang.org/reference/keywords.html)

[^4]: `RValue`という構造体名はCRubyに合わせて名付けた。ちなみにCRubyの`RValue`は（64ビットアーキテクチャの場合）40バイト。なお`RValue`は常にヒープ上に確保されるが、`Vec`や`Box`とは異なりRustのアロケータではなく、**ruruby** 本体にある独自実装アロケータにより生成される。不要となった`RValue`のデストラクタ処理は独自実装のガベージコレクタが行う。

[^5]: `Option<NonNull<RValue>>`では`NonNull<RValue>>`がnullにならないことが保証されているので、nullが`Option::None`に割り当てられ、`Option<NonNull<RValue>>`は`*mut RValue`と同じメモリサイズになる。そして`RValue`の先頭アドレスは常に8バイトアラインされているので、最下位3ビットは常に0になるはず……　しかしよく考えるとnullのビット表現の最下位ビットが0である保証が仕様上はない気がする…　詳しい人誰か教えてください。

[^6]: 意図的にf32で書き込んだものをu32で読み出す、という場合もある。