【禁帯出】stable Rustからunstable featuresを使う

2021/07/04に公開

とても素敵な六月だったので初投稿です。

はじめに、あるコードとその実行結果を紹介します。

use nightly_crimes::nightly_crimes;

nightly_crimes! {
    #![feature(never_type)]
    #![feature(box_syntax)]
    fn hey(x: Result<&str, !>) -> Box<String> {
        match x {
            Ok(x) => box x.to_string(),
            Err(x) => x,
        }
    }
}

fn main() {
    println!("{}", hey(Ok("success!")));
}
$ cargo +stable run
   Compiling yolo-rustc-bootstrap v1.0.2
   Compiling nightly-crimes v1.0.1
   Compiling sand v0.1.0 (/home/manami/ws/pgr)
    Finished dev [unoptimized + debuginfo] target(s) in 2.02s
     Running `target/debug/pgr`
success!

おや、何かがおかしいですね?

Stability on Rust

Rustは後方互換性に特に気を使う言語であり、stable releaseでは破壊的変更をほとんど導入しません[1]
開発途中で仕様が変わる可能性がある(i.e. 不安定な)言語機能やstd APIなどについてはnightly releaseでのみ利用でき、feature gateと呼ばれる仕組みを使って明示的に有効化してあげる必要があります。
これはソースファイルの先頭に#![feature(feature_name)]と書いてあげることで有効化できます。

……お気付きになりましたか?
冒頭の例では、説明に反してstable Rustでbox_syntaxnever_typeといった不安定機能を利用できてしまっています。
コードを見る限り、どうもnightly_crimes!というマクロが一枚噛んでいるようです。

Hacks to use unstable features on stable Rust

今回はnightly_crimes v1.0.1のソースコードを見ていきます。こちらから閲覧可能です。
リポジトリはnightly_crimes crate自身とそれが依存するyolo-rustc-bootstrap crateで構成されています。
まずは前者から見ていくことにしましょう。

nightly_crimes

ソースコードはこちらから閲覧できます。

3行目でyolo_rustc_bootstrap::do_crimes!()を呼び出していますが、これは後から見ていきましょう。

一部を引用します。

#[cfg_attr(yolo_rustc_bootstrap, allow_internal_unstable(allow_internal_unstable))]
macro_rules! nightly_crimes {
    (
        #![feature($($feature:ident),* $(,)?)]
        $($code:tt)*
    ) => (
        #[allow_internal_unstable($($feature,)*)]
        macro_rules! horrible_crimes { () => ( $($code)* ); }
        horrible_crimes! {}
    );
}

allow_internal_unstableはstd/core/allocライブラリやコンパイラ内部にあるマクロをstable Rustでも動くようにするattributeです。例えばお馴染みのassert!マクロはcore_panicという内部実装についての不安定機能を使うためこのattributeを持っています

ややこしいですが、6行目ではallow_internal_unstableという機能それ自体を許可しようとしています。
マクロ内の上の方は複数のattributeがある場合の整形なので割愛します。
下の部分が処理本体で、コードをhorrible_crimesに包んでから展開しています。これはallow_internal_unstableがマクロにのみ効果があるattributeであるためです[2]

しかし、allow_internal_unstableはそれ自体が不安定機能であり、このままstable Rustで実行しようとしてもE0554エラーが発生するだけです。

ということはdo_crimes!マクロが一体何者なのかを探っていく必要がありそうです。

yolo-rustc-bootstrap

ソースコードはこちらから閲覧できます。

実際の処理は30~36行目にあります。以下に引用します。

let mut args = std::env::args_os();
let status = std::process::Command::new(args.next().unwrap())
    .arg("--cfg=yolo_rustc_bootstrap")
    .args(args)
    .env("RUSTC_BOOTSTRAP", "1")
    .status()
    .unwrap();

argsにはこのプログラムの起動時に実行されたコマンドと引数が入っています。
それを--cfg=yolo_rustc_bootstrapという引数とRUSTC_BOOTSTRAP=1という環境変数とともに呼び直しています。
ここで注目すべきはRUSTC_BOOTSTRAPです。これは主にrustcをブートストラップするときに使われるもので、1に設定するとrustcのリリースチャンネルに関わらず不安定機能を利用できるようになります[3]
実はとってもシンプルなタネだったんですね!

実際最初に例示したコードをマクロから取り出してRUSTC_BOOTSTRAP=1を渡しつつcargo +stable runしてみてもコンパイル・実行できます。

ちなみにcfgは条件付きコンパイルのためのもので、初回ビルド時に不安定機能を有効化しないために使われています。

So, what's the hack?

まとめると、nightly_crimes!は以下の流れでコードを実行します。

  1. 初回はcfgで不安定機能を隠しつつ、-cfg=yolo_rustc_bootstrapRUSTC_BOOTSTRAP=1を持たせてnightly_crimesをビルドし直す
  2. 2回目のビルド時にはマクロを実際に露出させつつ不安定機能を使えるようにする
  3. マクロ呼び出し時、allow_internal_unstableの下に渡されたコードを置くことで任意の不安定機能を使ったユーザーコードをコンパイル・実行できるようにする

元々RUSTC_BOOTSTRAP=1自体ユーザーが使うべきでないものなのですが、nightly_crimes!はそれを隠蔽しつつジョークとして仕上げたものになっています[4]

小話として以前はbuild.rsを通してこの環境変数を渡せましたが、1.53.0から禁止されました

nightly_crimesのREADMEに"Please do not use this."と記載されているように、これはジョークに留めておくべきです。

知っておくとちょっと面白いRust小話、あるいはRustのブートストラップ回りに興味を持つきっかけになるなどしていれば幸いです。

脚注
  1. ケースによってはcraterと呼ばれるツールで影響を見つつ導入されることもあります。そのような変更はrelease notes内の"Compatibility Notes"セクションに記載されることになっています。 ↩︎

  2. attributeの付与自体は実装ミスでmatch arm上など一部のアイテムに対してもできるようになっています。今後のeditionか何かで修正されるでしょう。 ↩︎

  3. rustcのセルフホスティング・ブートストラップ事情については、rustc-dev-guideにあるこの章を読むといいと思います。 ↩︎

  4. 発端はこのツイートです。 ↩︎

Discussion