Rustのstaticいろいろ

概要: Rustのstaticの亜種をいろいろ挙げる。

ここでは以下のように分類します。

  • 変数
    • ローカル束縛 (引数、 let, match, for, if let, while let)
    • 静的変数/定数
      • コンパイル時定数 (const)
        • アイテム位置の定数
        • トレイトの定数
        • 実装の定数
      • 静的変数 (static)
        • 非スコープ借用される静的変数
          • 組み込み static
          • lazy_static!
          • futures-0.2 task_local!
        • スコープ借用される静的変数
          • thread_local!
          • scoped_thread_local!
          • futures-0.1 task_local!

目次

  • conststatic の違い
  • スコープ借用と非スコープ借用
  • 組み込み static
  • lazy_static!
  • thread_local!
  • scoped_thread_local!
  • futures-0.1 task_local!
  • futures-0.2 task_local!

conststatic の違い

  • constコンパイル時に決まるそのものを定義します。この値はコンパイル時定数が必要な他の場所で使えます。
  • static は(コンパイル時に決まる値が入っている)場所を定義します。値がコンパイル時に決まるのは初期化の都合上にすぎず、それ以上の意味はありません。

たとえば、次のようなコードを考えます。

use std::sync::atomic::{self, AtomicUsize};

const X: AtomicUsize = AtomicUsize::new(0);
static Y: AtomicUsize = AtomicUsize::new(0);

fn main() {
    // どちらもX, Yを5回インクリメントして表示している
    for _ in 0..5 {
        // 0が5回表示される
        println!("X = {}", X.fetch_add(1, atomic::Ordering::SeqCst));
    }
    for _ in 0..5 {
        // ちゃんとインクリメントされる
        println!("Y = {}", Y.fetch_add(1, atomic::Ordering::SeqCst));
    }
}

これを実行すると以下のように出力されます。

X = 0
X = 0
X = 0
X = 0
X = 0
Y = 0
Y = 1
Y = 2
Y = 3
Y = 4

fetch_add&self をとるので、実際には &X&Y が計算されています。この & の動作は以下のように規定されています。

  • X が左辺値なら、そのアドレスをとる。
  • X が右辺値なら、匿名のローカル変数に X を代入して、そのアドレスをとる。
  • ただし、 X が右辺値の場合であっても、以下の条件が満たされる場合は、一時変数ではなく匿名の静的領域に X を代入して、そのアドレスをとる。 (RFC1414 rvalue static promotion)
    • &mut ではなく & 参照をとろうとしている。
    • Xコンパイル時に決定される。
    • X は内部可変性をもたない。
    • X の計算中に内部可変性をもつ関数が使われていない。

ここで X は内部可変性をもつ定数右辺値ですから、2番目の規則が適用されて、一時変数の値が使われます。その結果毎回新しい AtomicUsize が生成されるため、毎回0が出力されます。

このように、状態を管理する場所を作りたい場合は、 static を使います。そのため static が必要なときは 内部可変性をともなうことがほとんどです。

一方、コンパイル時に決まるそのものを定義したい場合は const が適しています。たとえば以下のような場合です。

const X: usize = 0;
// constは他のconst/staticの初期化に利用できる
static Y: usize = X;

fn main() {
    // constは長さのようなコンパイル時定数に使える
    let a = [0; X];
}

以前は、static ではなく const にしてしまうと借用が常に一時的になってしまって煩わしいという問題がありましたが、rvalue static promotionによりこの問題は改善しています。そのため、「値を定義するときは const」「場所を定義するときは static」という原則に近い運用であまり困らないと思います。

スコープ借用と非スコープ借用

Rustの組み込み static 以外にも、 static に準ずる機能を提供するマクロはいくつかあります。これらは使われ方で「スコープ借用」型と「非スコープ借用」型に分類できます。(※本稿独自の分類)

スコープ借用では、借用ごとにクロージャのコールバックが必要になります。コールバックの終了時に借用の終了が強制されます。

X.with(|x| {
  ...
})

いっぽう、非スコープ借用では &XX.get(..) のような方法で借用をとることができます。

組み込み static

言語に組み込みの static です。

use std::sync::atomic::{self, AtomicUsize};

static COUNTER: AtomicUsize = AtomicUsize::new(0);

fn main() {
    // インクリメント
    COUNTER.fetch_add(1, atomic::Ordering::SeqCst);
}

ポイント

  • スコープ: プロセス全体で共有される。
  • 要請: T: Sync + 'static
  • 初期化: コンパイル時定数式でしか初期化できない。 (より高度な初期化には lazy_static! を使う)
  • 借用: &X で借用できる。
  • drop: dropされない。

組み込み static の亜種

組み込みの static は原則として単独で使いますが、以下のような亜種があります。

  • static mut: &mut X での借用が可能になるが、unsafe。排他性を自分で確認する必要がある。 static から借用したものを無理矢理 &mut X として使うとUBになるので static mut にする必要がある。 たいていの場合は static + 内部可変性で事足りる。
  • #[thread_local] static: スレッドローカルになる。不安定機能なので #![feature(thread_local)] が必要。以下の問題点がある。
    • thread_local! と違って動かないプラットフォームがある。
    • dropされない。
  • extern { static }: FFIで使われる。

lazy_static!

初回使用時に初期化される静的変数のためのマクロです。

Mutex::new() 等がコンパイル時定数でないために必要になることが多いです。これは Mutex::new() が内部で Box::new() を必要としているなどの事情があります。軽量同期ライブラリの parking_lot を使って解決する手もあります。

#[macro_use]
extern crate lazy_static;

use std::sync::Mutex;

lazy_static! {
    // 文法上の特徴: "static" ではなく "static ref" を使う
    static ref COUNTER: Mutex<i64> = Mutex::new(0);
}

fn main() {
    // インクリメント
    *COUNTER.lock().unwrap() += 1;
}

ポイント

  • スコープ: プロセス全体で共有される。
  • 要請: T: Sync + 'static
  • 初期化: 初回使用時に初期化するので、コンパイル時定数の必要はない。明示的な初期化も可能。
  • 借用: &X で借用できる。(Derefが実装されている)
  • drop: dropされない。

thread_local!

thread_local! は標準ライブラリマクロで、スレッドローカル変数を作ります。

use std::cell::RefCell;

thread_local! {
    static COUNTER: RefCell<i32> = RefCell::new(0);
}

fn main() {
    COUNTER.with(|counter| {
        // インクリメント
        *counter.borrow_mut() += 1;
    })
}

ポイント

  • スコープ: スレッドごと
  • 要請: T: 'static
  • 初期化: スレッドごとの初回使用時に初期化するので、コンパイル時定数の必要はない。
  • 借用: with でスコープを作って借用する必要がある。
  • drop: 通常はスレッド終了時にdropされるが、保証されているわけではない。

なお、 thread_localクレイトはスレッドごとの静的変数ではなく、スレッドごと×オブジェクトごとの静的領域を提供します。

scoped_thread_local!

scoped_thread_local! は scoped-tlsクレイトが提供するマクロで、参照をそのまま保管できる特殊なストレージを提供します。何らかのコンテキストを暗黙的に引き回す必要がある場合に便利です。 (基本的には明示的に引き回したほうがいいと思いますが)

#[macro_use]
extern crate scoped_tls;

scoped_thread_local!(
    static CONTEXT: i32
);

fn main() {
    let context = 42;
    CONTEXT.set(&context, || {
        CONTEXT.with(|context| {
            println!("context = {}", context);
        })
    })
}

ポイント

  • スコープ: スレッドごと
  • 要請: T: 'static (設計上は T: ?Sized になりそうだけどそれは対応していない様子) ただし、実際には T ではなく &T を保管する。
  • 初期化: set が呼び出されている間だけ値が入っていて、それ以外の期間に with を呼ぶとパニックになる。
  • 借用: with でスコープを作って借用する必要がある。
  • drop: 参照渡しなのでdropは関係ない。

futures-0.1 task_local!

futuresの task_local! はタスク(軽量スレッド) ごとに異なる記憶領域を擬似的に提供しますが、このインターフェースはfutures-0.1とfutures-0.2で異なります。futures-0.1のそれはスコープ借用で、 thread_local! と似た使い心地です。

#[macro_use]
extern crate futures;
extern crate tokio;

use tokio::prelude::*;

use std::cell::RefCell;

task_local!(
    static COUNTER: RefCell<i32> = RefCell::new(0)
);

fn main() {
    tokio::run(future::lazy(|| {
        COUNTER.with(|counter| {
            // インクリメント
            *counter.borrow_mut() += 1;
        });
        Ok(())
    }));
}
  • スコープ: タスクごと
  • 要請: T: Send + 'static (宣言時は Send は要求されない)
  • 初期化: タスクごとの初回使用時に初期化するので、コンパイル時定数の必要はない。
  • 借用: with でスコープを作って借用する必要がある。
  • drop: タスク終了時にdropされる。

futures-0.2 task_local!

futuresの task_local! はタスク(軽量スレッド) ごとに異なる記憶領域を擬似的に提供しますが、このインターフェースはfutures-0.1とfutures-0.2で異なります。futures-0.2のそれは非スコープ借用で、面白い構成になっています。contextを引き回しているおかげで、&mut な借用ができるというのも面白い点といえます。

#[macro_use]
extern crate futures;

use futures::prelude::*;

use futures::executor::ThreadPool;

task_local!(
    static COUNTER: i32 = 0
);

fn main() {
    ThreadPool::new()
        .unwrap()
        .run(futures::future::lazy(|context| {
            // インクリメント
            *COUNTER.get_mut(context) += 1;
            Ok(()) as Result<(), futures::never::Never>
        }))
        .unwrap();
}
  • スコープ: タスクごと
  • 要請: T: Send + 'static (宣言時は Send は要求されない)
  • 初期化: タスクごとの初回使用時に初期化するので、コンパイル時定数の必要はない。
  • 借用: contextのライフタイムだけ借用できるが、同時に1つのtask-local dataしか借用できない。
  • drop: タスク終了時にdropされる。

なお事実上のfutures-0.3にあたるRFC2408の現時点でのドラフトでは、contextからLocalMapを削除することが提案されています。したがってfutures-0.3のtask-local storageはfutures-0.1方式に戻るのではないかと思います。

まとめ

  • const は値、 static は場所を定義する。
  • static っぽいことをするマクロは色々あり、使い分けると幸せになれる。

binfmt_miscの力でBOM shebangをサポートする

概要: binfmt_miscを使うとBOM shebangを擬似的にサポートできる。ただしインタプリタ側がサポートしてないと意味がない。

事の発端

Windowsのメモ帳がLF改行をサポート→でもUTF-8で保存しようとするとBOMがついて悲しい→shebangにBOMがつくと動かないのが悲しい

やったこと

Linuxbinfmt_miscという機能を使って、BOMのついたshebangを動くようにしてみました。

github.com

binfmt_miscはLinux固有の仕組みで、ELFとshebang以外の通常ファイルを実行しようとしたときに、/proc/sys/fs/binfmt_misc 内の一覧にしたがって、ファイルのヘッダまたは拡張子によってインタプリタを選択するというものです。今回は "\xEF\xBB\xBF#!" が来たときにあらかじめ用意しておいた /usr/local/bin/bomshebang というプログラムを起動するように設定しました。このbomshebangはOSの挙動をまねてshebangを解析し、本来のインタプリタを呼び出します。

できたこと

たとえばPerlスクリプトにBOMがついても動くようになります。

Bashインタプリタ側がサポートしていないので駄目みたいです。bomshebang側でソースを書き換えるなどの工夫をすればいけるかもしれませんが、そうするとソースを汚してしまうし、書き込めない可能性もあるので、/tmpに移動する必要がありそうですが、インタプリタによってはソースの位置や名前が重要かもしれないため、この方法では予期しない非互換性が発生しそうです。

まとめ

Linux自体をBOM shebangに対応させるのは簡単でした。しかし、インタプリタ側が対応していないとOS側だけでうまくやるのは難しそうです。

SATySFi for Windows を作った

SATySFiはOCamlのような関数型言語LaTeXのようなマークアップ言語をベースとする新しい組版システムです。

最近SATySFi for Windowsを作ったので紹介します。

SATySFi for Windowsとは

今まではSATySFiを試すにはLinuxMac環境が必要で、Windowsを持っている人はVMかWSLを使って試すのが一般的のようでした。

今回Windows用ビルドを整備したので、これからはWindowsネイティブ環境へのSATySFiの導入が簡単になります。

仕組み

OCamlWindowsとの相性が悪いですが、今回はMinGWターゲットでOPAMを使いたいということで、Linuxからのクロスコンパイルを用いました。

幸いopam-cross-windowsというプロジェクトが既にあったので、これのコンパイラバージョンを上げて、必要なパッケージを追加する作業をするだけで済みました。いくつかPRを出したところwrite権限を貰えたので今後も少しずつ充実させていこうと思っています。

OCamlコンパイラがデフォルトでブートストラップするなど、自己コンパイル大好き言語なので、クロスコンパイラの敵みたいな設計になっていることが多く、そのあたりに対処するのが大変でした。

インストーラ

NSISという有名なインストーラー言語を使いました。NSISはLinuxからのクロスコンパイルが可能で、Ubuntuにもパッケージがあるため便利です。

ただ言語としては微妙で、スタック+レジスタベースの言語をマクロで覆ったような構成なのでマクロ呼び出しの嵐であまり心地良いとは言えません。扱える文字列が最大1024文字でPATHの書き換えに注意が必要など罠も多い感じでした。

SATySFi用Vimプラグインを作った

タイトルの通りふと思い立ってSATySFi用のVimプラグインを作ったので紹介します。 https://github.com/qnighy/satysfi.vim

SATySFi とは

SATySFiはgfn氏が未踏プロジェクトとして開発している組版システムです。LaTeXのような美しい組版を、一から設計しなおしたまともな言語で行うことができるのが特徴です。

SATySFiはプログラムパートにOCaml風の関数型言語組版パートにLaTeX風のマークアップ言語を使い、準クオートのような構文で両者を切り替えながら文書を記述します。組版結果はPDFとして直接出力されます。

satysfi.vimの機能

satysfi.vim は現在、シンタックスハイライトと自動インデントを提供します。

  • SATySFiの持つモードを正しく識別してハイライトします。例えば let はプログラムモードではハイライトされますが水平モードではハイライトされません。
  • 字句エラーをエラーとしてハイライトします。
  • SATySFiの構文に沿ってインデントします。例えば
    • | は対応する |, match, let-rec, type などにあわせてインデントされます。
    • let-rec は直前のトークンに応じて、 let-in式かトップレベル定義か判別され、トップレベル定義ならレベル0にインデントされます。
    • 水平モードでは、 ** のようなバレットを認識し、アスタリスクの個数に応じてインデントされます。

ちなみに

書いてから調べてみたら同名のVimプラグインを既に書いている人がいました

Rustのパニック機構

概要: Rustのパニック機構は異常終了の処理を安全に行うためにある。この動作を詳しくみていく。

パニックとは何か

Rustには2つの異なる例外処理機構があります。

発生源 対処方法
パニック プログラミングエラー 原則として捕捉しない assert!()
境界外参照
Result 例外的な入力 必要に応じて捕捉 I/Oエラー (File::read)
パースエラー (str::parse)

パニックとResultの関係についてはTRPL第2版第9章、未定義動作とパニックの関係についてはRustonomiconのUnwindingの章などが参考になります。

パニックを想定した安全性

Rustではたとえパニック状態でも未定義動作だけは絶対に避ける必要があります。そのため以下の関数は不健全 (unsound)です

use std::ptr;
// この関数はRustではunsound (やってはいけない)
pub fn replace_with<T, F>(x: &mut T, f: F) where F: FnOnce(T) -> T {
    unsafe {
        ptr::write(x, f(ptr::read(x)));
    }
}

このコードは x から一時的に所有権を取り出し、 f にかけてから x に書き戻します。これは一見すると安全ですが、 f がパニックしたときに x が無効な値を指した状態で返されてしまいます。どうしてもこのような関数を用意したい場合には、関数自体をunsafeにして、規約を明記する必要があります。

use std::ptr;
/// `x` の値を `f` で変換します。
///
/// # 安全性
///
/// `f` がパニックした場合、 `x` の指すメモリ領域は未定義になります。
///
/// 呼び出し元は、 `f` がパニックしないよう担保するか、 `f` がパニックした
/// 場合に二重dropが発生しないように追加の処理をする必要があります。
pub unsafe fn replace_with<T, F>(x: &mut T, f: F) where F: FnOnce(T) -> T {
    ptr::write(x, f(ptr::read(x)));
}

二重パニックの抑止

Rustでは二重パニックは特に気をつけて避ける必要があります。Rustのパニックは「原則として捕捉しない」だけで実際には捕捉可能ですが、二重パニックの捕捉はできません。そのため二重パニックは通常のパニックに比べてデバッグが難しくなる可能性があります。

二重パニックは主に誤った drop 実装によって発生しえます。単純化された例では次のようなものが考えられます。

use std::ops::Drop;

struct A;
impl Drop for A {
    fn drop(&mut self) {
        println!("drop(A)");
        panic!("drop(A)");
    }
}

fn main() {
    let _x = A;
    assert_eq!(0, 1);
}

このように Drop::drop 内ではパニックを使うことができませんが、戻り値が () であることから Result を用いたエラー通知もできません。つまり、 Drop::drop 内で発生したエラーを適切に通知する手段はありません。これが困る例として BufWriter があります。

#![feature(termination_trait)] // `io::Result` を main の戻り値として使う

use std::io::{self, BufWriter, Write};
use std::fs::File;

fn main() -> io::Result<()> {
    let mut f = BufWriter::new(File::create("foo.txt")?);
    writeln!(f, "hoge")?;
    f.flush()?; // 明示的にflushしないとエラーを捕捉できない
    Ok(())
}

BufWriter はデータをすぐに書き込まずバッファに蓄えるため、dropされた時点で自動的に残りを書き込みます。しかしこのとき発生したエラーを通知する手段はありません。たとえば、上の例でファイル名を /dev/full にして実行するとエラーが表示されますが、 f.flush()? を消すとエラーは握り潰されてしまいます。

不変条件に気をつける

データに対して、常に保たれていてほしい性質のことを不変条件といいます。たとえば、何らかの理由で単調増加な配列を扱いたい場合、この「単調である」という性質は不変条件の一種です。

ところで不変条件はプログラムの実行中に一時的に壊れる場合があります。たとえば以下の処理を考えます。

// 単調増加な配列に2を足す
fn plus2(a: &mut [i32]) {
    for x in a {
        *x += 2;
    }
}

fn main() {
    let mut a = [3, 5, 6];
    plus2(&mut a);
    assert_eq!(&a[..], &[5, 7, 8]);
}

この plus2 の前後で「単調増加である」という性質は保たれます。ところが、この plus2実行中にはこの不変条件は一時的に壊れています。2項目目まで処理し終えたときの配列は [5, 7, 6] ですから単調増加ではありません。

Rustのパニック機構では、このような論理的な不変条件が壊れる可能性がありますが、その影響が極力波及しないような工夫があります。例えば、先ほどの「単調増加である」という性質を再び考え、以下のようなプログラムを考えます。

// 単調増加な配列を三乗する
fn cube(a: &mut [i32]) {
    for x in a {
        *x = x.pow(3);
    }
}

fn main() {
    let mut a = [3, 5, 6];
    cube(&mut a);
    assert_eq!(&a[..], &[27, 125, 216]);
}

このプログラムに [1290, 1291, 1292] という配列を渡すと、2番目の処理中にオーバーフローでパニックが発生します(デバッグビルドの場合)。このパニック発生時点での配列の中身は [2146689000, 1291, 1292] なので、不変条件が壊れています。しかしこの場合は、パニックを捕捉していないので、不変条件が壊れたデータをユーザープログラムが目にすることはありません。

不変条件が壊れたデータをプログラムが目にする機会には次の2つがあり、どちらも防止策があります。

  • スレッド内でパニックが捕捉された場合。これは UnwindSafe トレイトによって抑止されている。
  • 他のスレッドがデータを参照した場合。これは Mutex/RwLock がもつpoisoningの仕組みによって抑止されている。

なお、どちらの仕組みも転落防止柵くらいの簡易なもので、簡単にオプトアウトできるようになっています。Rustでは論理不変条件の保護を徹底する気はなく、たとえ論理不変条件が壊れていても安全性不変条件さえ守られていれば未定義動作が起こらないようにしなければなりません。

UnwindSafe トレイト

UnwindSafe トレイトは、後述する catch_unwind を使うさいに、不変条件が壊れたデータが漏れないようにする仕組みです。

たとえば、次のように catch_unwind を使うとpanicの巻き戻し処理を一時中断することができます。

use std::panic::{catch_unwind, resume_unwind};

// 単調増加な配列を三乗する
fn cube(a: &mut [i32]) {
    for x in a {
        *x = x.pow(3);
    }
}

fn main() {
    let result = catch_unwind(|| {
        let mut a = [1290, 1291, 1292];
        cube(&mut a);
    });
    
    // 上の処理がpanicしても実行される
    println!("do something");
    
    // panic処理をレジュームする
    result.unwrap_or_else(|e| resume_unwind(e));
}

ここで aクロージャの外に出せば、壊れた a を観測できそうですが、 &mut T: !UnwindSafe であることからコンパイルエラーになります。

use std::panic::{catch_unwind, resume_unwind};

// 単調増加な配列を三乗する
fn cube(a: &mut [i32]) {
    for x in a {
        *x = x.pow(3);
    }
}

fn main() {
    let mut a = [1290, 1291, 1292];
    // ここでコンパイルエラーになる
    let result = catch_unwind(|| {
        cube(&mut a);
    });

    // 上の処理がpanicしても実行される
    println!("a = {:?}", a);

    // panic処理をレジュームする
    result.unwrap_or_else(|e| resume_unwind(e));
}

かわりに AssertUnwindSafe(&mut a) をキャプチャーするようにすればこの仕組みをオプトアウトできます。

use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};

// 単調増加な配列を三乗する
fn cube(a: &mut [i32]) {
    for x in a {
        *x = x.pow(3);
    }
}

fn main() {
    let mut a = [1290, 1291, 1292];
    let result = {
        // &mut a の壊れた値を観測するためにAssertUnwindSafeする
        let aref = AssertUnwindSafe(&mut a);
        catch_unwind(move || {
            cube(aref.0);
        })
    };

    // 上の処理がpanicしても実行される (a = [2146689000, 1291, 1292])
    println!("a = {:?}", a);

    // panic処理をレジュームする
    result.unwrap_or_else(|e| resume_unwind(e));
}

Mutex, RwLock のpoisoning

Mutex のロックや RwLock の書き込みロックを持ったスレッドがパニックすると、その途中の値は不変条件が壊れている可能性があります。これを他のスレッドが間違って読まないようにするため、 MutexRwLock はロック(書き込みロック)を持ったスレッドがパニックした場合、poisonedフラグが立てられ、次回以降のロックが失敗するようになります。

poisonedフラグを元に戻す方法はないようですが(多分)、poisonedフラグによりエラーになった場合、返されたエラーから強制的に壊れた値を読みに行くことはできるようになっています。

パニックの流れ

ここまで、パニックに関連する基本的な注意事項をまとめました。ここからはパニックの仕組みを調べていきます。

まず、ユーザープログラムから見たパニックの流れを説明します。パニックはスレッドごとに処理されます。各スレッドの正常系をここでは次のような図であらわすことにします:

f:id:qnighy:20180218213528p:plain

パニック処理の基本的な流れ (-C panic=unwind) は以下のようになります。

f:id:qnighy:20180218213656p:plain

ただし、 -C panic=abort が指定されているときは以下のようになります。

f:id:qnighy:20180218213741p:plain

パニックハンドラ

パニックハンドラはパニック時にまず呼ばれる処理で、主にスタックトレースの表示などを担当します。デフォルトの処理は以下のようになっています。

  • もしパニックカウントが2なら、常にフルのバックトレースを表示する。
  • パニックカウントが1なら、 RUST_BACKTRACE の値に応じて以下の処理をする。
    • 0 または定義されていないならバックトレースを表示しない。
    • full ならばフルのバックトレースを表示する。
    • それ以外(1など)ならシンプルなバックトレースを表示する。
  • Box<Any + Send>&'static str もしくは String だったときは、これをエラーメッセージとして表示する。それ以外のオブジェクトだったときは、 "Box<Any>" とだけ表示する。

std::panic::set_hook を用いてパニックハンドラを設定できます。なお、パニックハンドラ自体は全スレッドで共通です。

巻き戻し

巻き戻しはLLVMの例外機構 (C++の例外にも使われている) を用いて実装されています。プラットフォームごとにGCC互換やSEHなどとして実装されるようです。

巻き戻しでは基本的にスタックトレースに沿って drop が呼ばれていきます。したがって最終的に以下のいずれかになります。

  • スレッドの先頭まで巻き戻る
  • catch_unwind の呼び出しにぶつかる
  • drop が再びパニックする (二重パニック)

スレッドの先頭まで巻き戻った場合、そのスレッドだけが異常終了したとみなされ、呼び出し元スレッドからの JoinHandle::join 呼び出しに対して Err として報告されます。

catch_unwind の呼び出しにぶつかった場合、通常処理に復帰したとみなされます。しかし前述のように、パニックハンドラによって既にエラーメッセージが表示されてしまっているので、これはエラーを握り潰すのには向いていません。想定されている用途はFFIバウンダリなどで一時的に巻き戻しを止める等で、基本的に resume_unwind と対にして使います。

drop が再びパニックした場合、二重パニックになります。二重パニックの場合はパニックハンドラ呼び出し後にプロセス全体が強制終了になります。

図を見てわかるように、パニックハンドラでパニックが起こる可能性もあり、この場合は三重パニックの可能性があります。

巻き戻さない

-C panic=abort を指定すると巻き戻さずに常にプロセスを強制終了するようになります。RFC 1513に動機が説明されています。

パニック処理の場所

パニックの処理は標準ライブラリとコンパイラの複数の箇所に分散しており追跡が困難です。ここに概要をまとめておきます。

まとめ

パニック機構について説明しました。前半ではパニック機構の注意事項(Resultとの比較、安全性、二重パニック、不変条件)について説明し、後半ではパニック機構の内部構造を説明しました。

Rustの関数ポインタの落とし穴

概要: Rustの関数ポインタの落とし穴について

その1: 関数ポインタはクロージャとは異なる

これはC/C++に慣れている人には当たり前ですが、関数ポインタ型 (fn()) とクロージャ型 (Fn()) には重大な違いがあります。それは、関数ポインタは環境をキャプチャーしないということです。大雑把にいうと、

  • 関数ポインタは、ある機械語コードのアドレス
  • クロージャは、関数ポインタと、キャプチャーした環境の対

なので、関数ポインタは、ひとつのプログラムにつき原則として有限個しかないのに対し、クロージャは、キャプチャーする環境によって無限にたくさんのクロージャを作ることができます。例えば、

fn main() {
    let closures = [3, 7, 1, 5, 8, 9, 2].iter().map(|&i| {
        move |j| i + j
    }).collect::<Vec<_>>();
    println!("{}", closures[3](14));
}

というコードでは、「3を足す関数」「7を足す関数」「1を足す関数」「5を足す関数」…… のようにたくさんの「関数」を動的に生成していますが、こういうのはクロージャでないとできません。

関数ポインタは基本的には fn で定義した関数からしか作ることができません。例外として、キャプチャーしていないクロージャは関数ポインタとして使うことができます。

fn main() {
    let f : fn(i32) -> i32 = |x| x + 1;
    println!("{}", f(3));
}

その2: fn と書いたらそれ自体がポインタ型

C言語では

int (*f)(void) = getchar;

のように、関数ポインタ型には最低1個の * がつきますが、対応するRustの記法では

let f : fn() -> i32 = i32::max_value;

のように、 * が0個で関数ポインタです。したがって、

let f : *const fn() -> i32; // 二重ポインタ!

は、関数ポインタへのポインタになってしまいます。ここを間違えるとFFIで未知のsegfaultに悩まされる可能性があります。

その3: 関数の型は関数ポインタ型ではない

例えば、以下のコードはコンパイルエラーになります。

fn foo() { println!("foo"); }
fn bar() { println!("bar"); }

fn main() {
    let mut f = foo;
    if true {
        f = bar;
    }
    f();
}
   Compiling playground v0.0.1 (file:///playground)
error[E0308]: mismatched types
 --> src/main.rs:7:13
  |
7 |         f = bar;
  |             ^^^ expected fn item, found a different fn item
  |
  = note: expected type `fn() {foo}`
             found type `fn() {bar}`

error: aborting due to previous error

error: Could not compile `playground`.

ここでは fn() ではなく、 fn() {foo}fn() {bar} という型が表示されています。これは関数定義型とよばれ、関数ポインタ型とは異なります

関数ポインタ型と関数定義型のわかりやすい違いとしてバイト数が挙げられます。以下でわかるように、関数定義型は実は0バイトです。

use std::mem;
fn main() {
    println!("{}", mem::size_of_val(&main)); // 0
    println!("{}", mem::size_of_val(&(main as fn()))); // 8
}

関数定義型は、関数ごとに異なる型がついているので、それ自体は情報を持っていなくても呼び出せるようになっています。C++でいえば、各関数ごとにファンクタオブジェクトが一個割り当てられている、以下のような状態とみることができます。

void foo_funptr() {
  printf("foo!\n");
}
struct foo {
  int dummy; //C++にはZSTがないので
  // 直接呼び出し
  void operator()() const {
    printf("foo!\n");
  }
  // 関数ポインタへの変換
  void (*to_funptr())() const {
    return foo_funptr;
  }
};

関数がジェネリクスを持っている場合は、関数定義型にも対応するジェネリクスが与えられます。なお、コンパイラは便宜上 fn() {foo} のような表示をしますが、関数定義型を名指しで指定することはできません。これはクロージャ型 ([closure@src/main.rs:3:20: 3:25] などと表示される) と同様です。

いずれにせよ、通常は必要なタイミングで関数ポインタ型に自動で変換されるので、変なコンパイルエラーなどに遭遇しない限り気にする必要はないでしょう。

その4: 関数ポインタ型にはABIと安全性フラグがつけられる

特に、C/C++とのFFIをするときは、ABIを間違えないように注意が必要です。通常のRust関数は extern "Rust" fn() なのに対して、C/C++と相互呼び出しする関数は extern "C" fn() です。それぞれ fn(), extern fn() と省略できます。また unsafe をつけて unsafe fn() のようにもできます。

なお、 C++extern "C" とは異なり、Rustの extern "C" はABIを指定するだけで、マングリング規則を変更しません。マングリングを無効化するには別途 #[no_mangle] をつけます。

また、これまたC++に慣れているとわかりづらいですが、

extern "C" fn foo() {} // ここに実体がある

extern "C" {
  fn foo(); // 別の場所にある実体とリンクする
}

は意味が異なります。 extern { .. } は、実体が他の場所にあるときに使います。

NonNull安定化記念にInternerを書いてみる

概要: NonNullが安定化され、1.25.0から使えるようになる。そこでNonNullの利用例としてInternerを実装してみた。

NonNull とは

NonNull はRustにある生ポインタ型のひとつです。元々 Unique, Shared という2つの生ポインタ型でしたが、安定化を機に統合・仕様変更が行われ、 NonNull という名前になりました。 (Uniquelibcore 内部には残っていますが、安定化の予定はなくなりました。) 予定通りに進めば、 Rust 1.25.0 から使えるようになるはずです。

Rustのポインタ関連型は仕様の微妙な違いを意識して使い分ける必要があります。以下にそれを列挙しました。

分類 エイリアス 変性 所有 非0 Send Sync
&T 参照 なし※1 共変 × 継承※2 継承
&mut T 参照 なし 非変 × 継承 継承
&Cell<T> 参照 あり 非変 × × ×
Box<T> スマート
ポインタ
なし 共変 継承 継承
*const T 生ポインタ 共変 × × × ×
*mut T 生ポインタ 非変 × × × ×
NonNull<T> 生ポインタ 共変 × × ×
  • ※1 &TTUnsafeCell を含まないときだけエイリアスなしと仮定される
  • ※2 &T: SendT: Sync のときに成立

特に参照型はエイリアスに関する仮定があることに注意してください。ここでの「エイリアス: なし」の意味は、その参照の指すメモリには他からの干渉はないとして最適化してよいという意味で、関数の引数が参照だった場合に適用されます。 Box<T> も特別に、引数や戻り値で渡されたときにエイリアスなしと仮定されます。

NonNull<T> はどんなときに便利か

Rustの型システムでうまく表現できない種類の参照を表す必要がある場合に、自分で不変条件を保ちながら unsafe なコードを書くときは、「ライフタイムがついていないけど、真正な参照」というのを表現する必要がある場合があります。この手のことをするときにはNULLかもしれない *const T よりも NonNull<T> を使うほうが適切です。

Rustの型システムでうまく表現できない参照としては、双方向連結リストがよく知られています。ここでは別の例として、internerと呼ばれるデータ構造を書いてみます。これは以下のようなAPIを備えたデータ構造です。

struct Interner { .. }
impl Interner {
    pub fn new() -> Self { .. }
    // 文字列に対して一意な整数を0から順に振って返す。
    pub fn intern(&mut self, s: &str) -> usize { .. }
    // 振られた番号から文字列を復元する。なかったらpanic。
    pub fn get(&self, idx: usize) -> &str { .. }
}

Internerの設計

Internerは正引きと逆引きのためのデータ構造が必要なので、素朴に書くと次のようになります。

use std::collections::HashMap;
pub struct Interner {
    strings: Vec<String>,
    rev_map: HashMap<String, usize>,
}

しかしこの場合同じ文字列を二重に格納するので無駄があります。そこで今回は危険を承知で、 strings 側で所有しているポインタを rev_map で使い回すことにします。概念的には次のようなものを考えます。

use std::collections::HashMap;
pub struct Interner {
    strings: Vec<String>,
    rev_map: HashMap<&'strings str, usize>,
}

ちなみにこのコードは以前の記事で説明した rental crateのマクロに通すとコンパイルはできますが、 strings に対する変更ができないので意図した通りにはなりません。

ライフタイム問題を回避する

そういうわけなので rev_map のキーを生ポインタで保持することを考えます。つまり以下のようにすることを考えます。

use std::collections::HashMap;
pub struct Interner {
    strings: Vec<String>,
    rev_map: HashMap<*const str, usize>,
}

その上で、このデータ構造は以下の不変条件を満たすようにします。

  • rev_map のキーに使われているポインタは常に strings の要素である。

非ゼロ制約をつける

HashMap がどのような構造になっているかわからないので、パフォーマンス上の利点があると断言はできませんが、非ゼロであるとわかっているものは非ゼロにしたほうがいいはずです。そこで *const str ではなく NonNull を使います。

use std::collections::HashMap;
use std::ptr::NonNull;
pub struct Interner {
    strings: Vec<String>,
    rev_map: HashMap<NonNull<str>, usize>,
}

変性を考える

今回作っている Interner は型パラメータを持たないので、変性については特に考える必要はありません。一般論としては、 NonNull<T> は共変なので注意が必要ですが、Cellのように特殊なことをやっていなければ共変で問題ないことが多いです。

所有を考える

今回作っている Interner は型パラメータを持たないので、所有については特に考える必要はありません。一般論としては、自作の Drop::drop 中で drop_in_place を呼ぶ場合には、 #[may_dangle]PhantomData<T> を組み合わせて使う必要があるかもしれません。これらはdrop checkerを宥めるためのものなので、特に困っていなければ保守的に「 #[may_dangle] をつけない」としてもたいてい安全です。

Send, Sync を実装する

生ポインタはデフォルトでは Send でも Sync でもありません。しかし、今回は他のスレッドに転送したり、複数スレッドから共有参照しても特に問題なさそうなので、 SendSync を明示的に実装します。

unsafe impl Send for Interner {}
unsafe impl Sync for Interner {}

Eq, Hash, Borrow<str> を実装する

おおよそ適切なデータ構造になってきましたが、 HashMap を動作させるにはキーが Eq + Hash を実装している必要があります。また、 &str で検索するには、キーが Borrow<str> を実装している必要があります。

そこで、 NonNull<T> のラッパーを書きます。このラッパーは大変危険なことをしているので、 Interner 内部以外で使わないよう細心の注意を払う必要があります。

pub struct Interner {
    ...
    rev_map: HashMap<StrPtr, usize>,
}
use std::hash::{Hash, Hasher};
use std::borrow::Borrow;

struct StrPtr(NonNull<str>);

impl Borrow<str> for StrPtr {
    fn borrow(&self) -> &str {
        unsafe { self.0.as_ref() }
    }
}
impl Eq for StrPtr {}
impl PartialEq for StrPtr {
    fn eq(&self, rhs: &Self) -> bool {
        unsafe { str::eq(self.0.as_ref(), rhs.0.as_ref()) }
    }
}
impl Hash for StrPtr {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        unsafe { str::hash(self.0.as_ref(), hasher) }
    }
}

実装を書く

最後に intern の実装を書きます。順番に細心の注意を払います。まずハッシュから検索して、あったらそれを返します。そうでなかったら追加する必要があるので &strString にします。ポインタの値を覚えておいて、 String は正引き用のVecに突っ込んでしまいます。覚えておいたポインタを NonNull で包んで、逆引き用のHashMapに追加します。 Entry を使うと検索と追加の二度手間を短縮できるのですが、 &str で検索しつつ String で追加するという手順では Entry が使えないのでここでは使っていません。

impl Interner {
    pub fn new() -> Self {
        Interner {
            strings: Vec::new(),
            rev_map: HashMap::new(),
        }
    }
    pub fn intern(&mut self, s: &str) -> usize {
        if let Some(&idx) = self.rev_map.get(s) {
            return idx;
        }
        let idx = self.strings.len();
        let s = s.to_string();
        let s_ptr = unsafe { StrPtr(NonNull::new_unchecked(s.as_str() as *const str as *mut str)) };
        self.strings.push(s);
        self.rev_map.insert(s_ptr, idx);
        return idx;
    }
    pub fn get(&self, idx: usize) -> &str {
        &self.strings[idx]
    }
}

完成

これでできました。確かに動いていることを確認しました。

use std::hash::{Hash, Hasher};
use std::collections::HashMap;
use std::ptr::NonNull;
use std::borrow::Borrow;

pub struct Interner {
    strings: Vec<String>,
    rev_map: HashMap<StrPtr, usize>,
}
unsafe impl Send for Interner {}
unsafe impl Sync for Interner {}

impl Interner {
    pub fn new() -> Self {
        Interner {
            strings: Vec::new(),
            rev_map: HashMap::new(),
        }
    }
    pub fn intern(&mut self, s: &str) -> usize {
        if let Some(&idx) = self.rev_map.get(s) {
            return idx;
        }
        let idx = self.strings.len();
        let s = s.to_string();
        let s_ptr = unsafe { StrPtr(NonNull::new_unchecked(s.as_str() as *const str as *mut str)) };
        self.strings.push(s);
        self.rev_map.insert(s_ptr, idx);
        return idx;
    }
    pub fn get(&self, idx: usize) -> &str {
        &self.strings[idx]
    }
}

struct StrPtr(NonNull<str>);

impl Borrow<str> for StrPtr {
    fn borrow(&self) -> &str {
        unsafe { self.0.as_ref() }
    }
}
impl Eq for StrPtr {}
impl PartialEq for StrPtr {
    fn eq(&self, rhs: &Self) -> bool {
        unsafe { str::eq(self.0.as_ref(), rhs.0.as_ref()) }
    }
}
impl Hash for StrPtr {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        unsafe { str::hash(self.0.as_ref(), hasher) }
    }
}

fn main() {
    let mut i = Interner::new();
    for s in ["a", "c", "a", "b", "b", "c", "d", "a"].into_iter() {
        println!("intern({:?}) = {:?}", s, i.intern(s));
    }
}

最後にもう一押し: drop の順番

おそらく今回は問題ないですが、このようなコードを書くときには drop の順番にも気をつかう必要があります。この場合は rev_map が先にdropされたほうが安心なので、 rev_mapManuallyDrop を使います。

pub struct Interner {
    ...
    rev_map: ManuallyDrop<HashMap<StrPtr, usize>>,
}
impl Drop for Interner {
    fn drop(&mut self) {
        unsafe {
            ManuallyDrop::drop(&mut self.rev_map);
        }
    }
}

まとめ

NonNull が安定化したので、ライフタイムではちょっと痒いところに手が届かない……というときに生ポインタを使ったコードを書きやすくなりましたが、そういったコードを書くにあたっては細心の注意が必要になります。そこで実際に生ポインタを使ったコードを書きながら、どのような点に注意をしないといけないかを復習してみました。(何か他に見落としている点があったらご指摘ください。)

2018/02/08 追記: NonNullのsinceアトリビュートが1.24から1.25.0に修正されたのを記事に反映した。