RustのREPL "evcxr" を使ってみた

概要

evcxrはRustのパーサーとコンパイラを外部から呼び出すことでREPLを実現している。セミブラックボックス的なアプローチにも関わらずなかなかの完成度で、今後が期待できる。

evcxr

evcxrは最近公開されたRustのREPL (GitHub, redditのpos)である。また、Jupyterのカーネルもあるため、Jupyter Notebook上でRustを書くこともできる。

Evaluation Context for Rust とのことで、 ev + cx + r と思えば覚えられる。

インストール方法

コマンドラインは以下で入る。

cargo install evcxr_repl

Jupyter版については公式サイトを参照するとよい。

基本的な動作を試してみる。

$ evcxr
Welcome to evcxr. For help, type :help
>> :help
:vars             List bound variables and their types
:opt              Toggle optimization
:explain          Print explanation of last error
:clear            Clear all state, keeping compilation cache
:dep              Add dependency. e.g. :dep regex = "1.0"

Mostly for development / debugging purposes:
:last_compile_dir Print the directory in which we last compiled
:timing           Toggle printing of how long evaluations take
:last_error_json  Print the last compilation error as JSON (for debugging)
:internal_debug   Toggle various internal debugging code
>>

: がメタコマンド。今のところ終了コマンドはないが、Ctrl+CやCtrl+Dで終了できる。

>> let s = String::from("Hello, ");
>> s
"Hello, "
>> s.push_str("world!");
   ^ cannot borrow mutably
cannot borrow immutable local variable `s` as mutable

エラーに色がついて、年季のわりに凝っている。

>> let mut s = s;
>> s.push_str("world!");

シャドーイングも普通にできる。

1行ずつコンパイルしているため体感がかなり遅い。

>> s
"Hello, world!"
>> s
"Hello, world!"

式はborrowして表示しているっぽい。

>> drop(s);
>> s
   ^ not found in this scope
cannot find value `s` in this scope

dropするとなくなる。

>> let s = String::from("ABC");
>> if false { drop(s) };
>> s
   ^ not found in this scope
cannot find value `s` in this scope

実際にはdropされていなくても、同じコンパイルエラーの条件に落ちるときは、なくなった扱いになる。

>> let s = String::from("foo");
>> let z = &s;
            ^ borrowed value does not live long enough
`s` does not live long enough
Values assigned to variables in Evcxr cannot contain references (unless they're static)
>> { let z = &s; }
()

行をまたいで参照を渡すことはできないが、スコープを切ってやればできる。

>> use std::io::{self, Read, Write, BufRead};
>> use std::fs::File;
>> use std::io::BufReader;
>> let f = File::open("/proc/cpuinfo").unwrap();
>> let mut f = BufReader::new(f);
>> let mut line = String::new();
>> f.read_line(&mut line).unwrap();
>> line
"processor\t: 0\n"

インポートは行をまたいで有効。

シリアライズできない値も持ち越せる。

>> extern crate rand;

extern crate を宣言すると、いい感じに依存関係を追加してくれる。依存関係のコンパイルをするので、時間はかかる。

ここで、手元に evcxr-test というlibrary crateを作ってみる。内容はこんな感じ。

pub fn fib(n: u32) -> u32 {
    (0..n).fold((0, 1), |(a, b), _| (b, a + b)).0
}

これは以下のようにして追加できた。evcxrのtemporary directory上にある Cargo.toml に依存を追加するような仕組みになっているっぽい。

>> :dep evcxr-test = { path = "/home/qnighy/workdir/evcxr-test" }
>> evcxr_test::fib(40)
102334155

ただ、 path はevcxrのtmpdirからの相対パスのため、現状ではほぼ絶対パスしか使えないようだ。

さっき作ったライブラリを書き換えてみる。

pub fn fib(n: u32) -> u32 {
    (0..n).fold((0, 1), |(a, b), _| (b, a + b)).0
}

pub fn foo(n: u32) -> u32 {
    n
}

:dep でリロードしてみる。

>> :dep evcxr-test = { path = "/home/qnighy/workdir/evcxr-test" }
>> evcxr_test::foo(40)
40

リロードできた。

>> struct Foo(u32);
Items currently need to be explicitly made pub along with all fields of structs.
>> pub struct Foo(u32);
Items currently need to be explicitly made pub along with all fields of structs.
>> pub struct Foo(pub u32);
>> let x = Foo(42);
>> let y: Foo = x;

アイテム定義は持ち越されるが、プライベートな定義が含まれてはいけない。(フィールドも含めて)

>> pub struct Foo(pub u32);
>> let z: Foo = y;
                ^ expected struct `user_code_29::Foo`, found struct `user_code_26::Foo`
mismatched types

同じ名前をもう一度定義したときは、別のものとして扱われる。シャドーイングしたかのような動作になる。

仕組み

HOW_IT_WORKSという文書があった。Rustで外部REPLを作ろうと思ったらこうなるだろう、という妥当な設計になっている。

  • シリアライズできない値も持ち越したい。また、パニックやunsafeによるsegfaultなどもある程度ハンドリングしたい。
    • REPLコードを実行するための子プロセスをひとつ立ち上げて、そこに順次ライブラリをロードしていく形になる。
    • 親プロセスは新しい行が来たら syn でパースして、必要なスタブをくっつけてからコンパイルする。
    • コンパイルされた行 (単独のライブラリになっている) を子プロセスにロードさせて、実行させる。式だったら結果を Debug で表示する。
  • 各行を、あたかも前の行の続きであるかのように実行させるには、インポートとグローバル定義、ローカル定義を引き継ぐ必要がある。
    • インポートは単に、過去に実行した extern crateuse を全部覚えておいて、毎回リプレイすればよい。
    • グローバル定義は、過去に定義したものを覚えておいて、前に実行したときの定義を引き継ぐような use を生成する。
      • 行をまたぐと別のモジュールになってしまうため、privateな定義は直感的に振る舞わない。今はそういう定義を丸ごと禁止している。
    • ローカル定義の型定義は、変数名と型名を String の対として (親プロセスが) 持っておく。
    • ローカル定義の実体は、 HashMap<String, Box<dyn Any + 'static>> という評価文脈として (子プロセスが) 持っておく(おそらくこれが名前の由来)。
      • そのため、参照は持ち越せない。
    • 実際に生成されるコードは、文脈を受け取って文脈を返す関数になる。
      • 文脈を受け取る部分は、親プロセス側が知っている型定義に基づいてdowncastする let が挿入される。
      • 文脈を返す部分は、その行の let に基づいて、構文的に返しうる変数を全て挿入するような処理が生成される。これはコンパイルエラーになる可能性があるが、とりあえずコンパイルしてから判断する。
      • その変数が 'static でないためにエラーになった場合は、その旨をユーザーに報告して以上。
      • その変数がmove outされている可能性があるという趣旨のエラーだった場合は、その変数の挿入処理を削除して再試行する。

rustcにはコンパイル結果をJSONで返す仕組みがあるため、コンパイルエラーに基づく処理は意外と堅牢に動作するらしい。

感想

思ったよりよく出来ている。REPLの用途の一つはライブラリ制作時のちょっとした確認用だが、そのための機能は少々足りていないと感じた。具体的にはtmpdirを使うと、起動するたびに依存ライブラリの再コンパイルが必要なのと、手元のライブラリの参照が面倒なのがネックになる。まさにその問題がTODOに書かれているので今後に期待。

redditの作者コメントによると2ヶ月ちょいでここまでできたとのことなので、今後の発展でかなり良くなる可能性はありそう。

Rustコンパイラをクラウドでビルドする備忘録(改良版)

概要

前回の改良版。 Rustコンパイラをいじっているとビルドの遅さのせいで作業に支障をきたすため、クラウドでビルドするようにしてみる。

EC2を立ち上げる

Ubuntu 16.04 (HVM) の c5.4xlarge を使うことにする(結局、手元とあわせたくなったので18.04に上げた)。使うインスタンスの強さは財布と相談して決めることにする。ディスクは結構必要なので上げておく。

何度も再起動して使いたいのでEIPを割り当てておく。(デフォルトのグローバルIPは再起動のたびに変わるっぽい)

高くて強いインスタンスを使っていくので、使わないときは落とし忘れないように気をつける(停止時はEBSやEIPだけ課金される)。このあたりも必要なら自動でシャットダウンする仕組みを考えるといいかもしれない。

IAMユーザーを作る

当該インスタンスを立ち上げられるだけの専用のユーザーを作る。以下のようなインラインポリシーを作る。 (i- ではじまる部分に当該インスタンスインスタンスIDを入れる)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ec2:StartInstances",
            "Resource": "arn:aws:ec2:*:*:instance/i-0123456789abcdef0"
        }
    ]
}

このユーザーのコンソールへのログインを不許可にしてクレデンシャルを発行する。手元のマシンで以下を実行してクレデンシャルを入れる。 (~/.aws/credentials を直接編集してもOK)

$ aws configure --profile rust-builder

セットアップ

手元から rust-builder という名前でSSHできるようにする。依存関係として以下を入れておく。

$ sudo apt update && sudo apt upgrade && sudo apt install build-essential python-minimal cmake curl git

自動シャットダウン

以下のファイルを /home/qnighy/shutdown-timer.sh として置き、実行権限をつける。 (ユーザー名は適宜置き換えて) (ログイン状態のチェックのために w の出力を雑にgrepしているので、そこの置き換えを忘れないように)

#!/usr/bin/env bash
set -ue

count=0
sleep=600
max_count=6

while true; do
  sleep "$sleep"
  date >&2
  if w | grep -q qnighy; then
    echo "logged in; reset" >&2
    count=0
  else
    count="$((count + 1))"
    echo "not logged in; count=$count" >&2
    if [[ $count -ge $max_count ]]; then
      echo "shutting down..." >&2
      shutdown -h now
    fi
  fi
done

また以下のsystemd設定ファイルを /etc/systemd/system/shutdown-timer.service に置く。

[Unit]
Description = shutdown timer

[Service]
ExecStart = /home/qnighy/shutdown-timer.sh
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target

以下で有効化

sudo systemctl enable shutdown-timer
sudo systemctl start shutdown-timer

これにより、10分に1回の頻度でログイン状態をチェックし、60分間非ログインだったらシャットダウンするようになる。

スクリプトの配置

Rustコンパイラを手元にcloneして、 x.py があるのと同じ位置に以下の y.sh を置いてexec permissionをつける。

#!/usr/bin/env bash
set -ue

# Remote side:
# sudo apt update && sudo apt upgrade && sudo apt install build-essential python-minimal cmake curl git gdb

ec2_profile=rust-builder
ec2_region=ap-northeast-1
ec2_instance=i-0123456789abcdef0

builder_host=rust-builder
local_host="$(hostname)"
dirpath="$(realpath --relative-to "$HOME" "$(pwd)")"

while ! aws --profile "$ec2_profile" --region "$ec2_region" ec2 start-instances --instance-ids "$ec2_instance" | grep -q running; do
  echo "waiting for EC2 wakeup..." >&2
  sleep 1
done

git submodule update --init --recursive
rsync -avz --delete --exclude '.git' --exclude '/build' "$HOME/$dirpath/" "$builder_host:$local_host/$dirpath"
ssh -t "$builder_host" "cd $local_host/$dirpath; $@"

やっていることは以下。

  • まずEC2インスタンスを立ち上げる、replyに "start" が含まれるようになるまで待つ。
  • rsyncで送って向こうでビルドを叩く。
  • gitを全部転送したくないので .git は省く。ただしsubmoduleが必要なのでsubmodule updateを自動で叩く。 (x.pyは.gitがあるときはsubmodule updateを自動でやってくれる)
  • build も当然省く。手元のbuildと衝突しうるし、 --delete に巻き込まれないようにする意図もある。ただしbuildというディレクトリがソースツリーの中にあるので、 /build と指定する必要がある。
  • tmuxを立ち上げたりはしない。ビルドが短くなることが期待されているので途中で切れてもよい。むしろ自動アタッチとか考えるのが面倒。

ビルド

./remote.sh ./x.py test --stage 1 のように通常のビルドコマンドに ./remote.sh をつけるとリモートでのビルドになる。

設定

クラウドとは関係ないが、コンパイラを開発するときは config.toml を絶対にいじったほうがよい。

  • compiler-docs = true。ほとんどの関数はdoc-comment (///) を持たないが、シグネチャが一覧できるだけでもありがたい。
  • debug-assertions = true。 これがないとログがほとんど出ない。また、これを有効にすると一部の追加のチェックが行われる。
  • debuginfo = true, debuginfo-lines = true。 これがないとICE(コンパイラ内部エラー)の時のバックトレースがちょっと貧弱で困る。
  • incremental = true。 効果は計測してないけど効率がちょっと上がりそう

ISUCON8にRustで参加するためにしたこと

背景

  • ISUCONとは、与えられたWebサービスをいい感じにスピードアップするコンテストである。
  • いい感じに最適化すさえすれば言語は問われない。ということは、最初に提供される参照実装の書かれた言語を使うのが得策である。
  • 複数の言語で参照実装が提供されることはあるが、Rust版が提供される可能性は高くない。

まず、ISUCONではないコンテストをする覚悟を持つ

http://isucon.net/archives/52012898.html

競技の成り立ちにも由来しますが、ISUCONは参加して学ぶことではなく「勝つこと」が前提となっているイベントです。

とある。↑に書いたように参照実装でない言語を好んで使うということは割とこれに反しているし、ISUCONで本来得られる経験の一部を「IShokU CONtest」の経験に変換することになる。(オペレーション担当が必死にログを見ている傍らでひたすらコードを右から左に移植する)

「そうは言っても、私はこの言語が好きなんだから仕方ない」と思うしかない。

いずれにせよ、本来の勝利条件を捨てているので勝利条件を設定する必要はある。ハッキリと宣言したわけではないが、今回は「Rustを一部でも使って点数に貢献する」といったところが暗黙の勝利条件になっていた気がする。ここは凡そ達成されたと思う。

チームメンバー集め

社内にRustに興味があるエンジニアがいたので、その人達に組んでもらった。とはいっても、Rustでアプリケーションを書けるレベルまで練習する余裕はなかったので、一人で書くことになってしまった。

フレームワークの選定

フレームワークをくまなく漁ったわけではないが、昨今のRustではActix-Webを使うのが正解っぽい雰囲気がある。

5月にあった社内ISUCONでもRustで参加し、このときはGothamを使ったが、これはあまり良くなかった。Gotham自身の設計はよくできているが、ミドルウェアのインターフェースがあっても肝心の実装がないみたいな状況が多く、Actix-Webに比べると心許ない。

逆にActix-WebはRustでサーバーを書くにあたっての最低限(クッキーセッションミドルウェア、redisセッションミドルウェア、グレースフルシャットダウン、JSON APIのためのアダプタなど)が揃っていてよく出来ているが、個人的には以下のような不安点もある。

  • Actix-Webはtokioベースの非同期処理を採用しているが、実はその上にもう1枚Actixというレイヤが挟まっている。Actixはアクターフレームワークで、正直このレイヤがどんな仕事をしているのかはあまり詳しくない。Actix-WebはActixのレイヤをそこまで露出していないが、たまに困ることがある。
    • 例えば、Actix-Web用のライブラリだと思ったらActix用のライブラリだったときにがっかりする(Actix-Webしか使わない人は脳内でActix-WebをActixと省略しがちなので)。
    • また、Actixはexecutor(futureを回すお仕事をする人)を独自に実装しており、tokioのデフォルトであるtokio-threadpoolを使っていない。これはいくつか問題がある。
      • たとえば最近のRust非同期界隈ではFutureがSend (有り体にいえばスレッドセーフ) であることが割と前提になりつつある。しかし、Actixはまだそうではないらしく、Actix-Web関連のデータ型にはSendでないものが結構あり、tokioとの相互運用で困ることがある。
      • tokio-threadpoolに特有の処理 (tokio_threadpool::blocking など) が使えない。
  • 歴史的な事情からか、ハンドラのインターフェースの決定版がなく、APIごとに少しずつ異なっている。たとえば Responder トレイトの他に AsyncResponder トレイトもあるが、 Responder トレイトでも非同期処理を書けるし、現在の標準的なハンドラ挿入インターフェースである route メソッドでは Responder が想定されている。

テンプレートエンジンの選定

5月の社内ISUCONではHandlebarsを使ったが、これは失敗で、コンパイルに時間がかかるのにHandlebarsまわりのミスは実行時までわからないことが多かった。(そもそもこいつはJavaScript用のテンプレートエンジンの移植なのでさもありなん)

今回はAskamaというテンプレートエンジンを採用した。これはJinja2風の記法で、Rustコードにコンパイルしてから使用する。テンプレートパラメーターも、「テンプレートごとに別々の構造体にマップしてから渡す」というRustイディオマティックな方法でできるので圧倒的に型に守られる。また、Actix-Web連携機能もあり、有効にしておくとテンプレート構造体がそのまま Responder になってくれるので、とても便利だった。

ちょっと運が悪かった点としては、今回対象となっているHTMLがVuejs用のやつで、Jinja2記法と衝突して面倒だった。

DB・コネクションプール

まず、不本意なことにISUCONではPostgreSQLではなくMySQL(またはMariaDB)ばかり使われるそうなので(日本のWebサービスの傾向がそうらしい?)、基本的にMySQLを想定して準備する。

MySQL用のDBライブラリとしては最もポピュラーな mysql を使い、コネクションプールにも最もポピュラーな r2d2 を用いることにした。

この組み合わせなら、さらに著名なORMの diesel を載せることもできるが、ISUCONではORMを使わずクエリを直接書く傾向にある (参照実装もそうだし、改善する上でもクエリをいじる方が便利) ということで、使わないことにした。

この部分の選定では、ORMを使うか否かのほかに難しい選択がひとつある。それは、非同期に対応させるかどうかである。特にコネクションプールの r2d2 とORMの diesel には非同期の代替品がない。それらの作者が非同期に対応しない理由はissueから伺い知ることができ、以下のように推測される。

  • Rustの非同期まわりの基礎部分のインターフェースが確立していない。
    • たとえば、 Future のインターフェースは0.1から0.2までで大きく変わった。単にAPIが変わっただけではなく、コンテキストを陽に持ち回すようになったので、全体的な設計変更が必要になる可能性がある。
    • その上、async/awaitの対応のため、(0.2をほとんど見捨てる形で0.3が導入され) 0.2から0.3まででさらに変化した。Pinという特殊な参照型が導入された上に、 Future が標準ライブラリに移管された。
    • futures は汎用非同期ライブラリだが、その上の非同期I/Oライブラリである tokio にも大きな変化があった。元々のランタイムであった tokio-core が廃止され、新しいランタイムライブラリである tokio に移行されるようになった。特に大きな変更として、シングルスレッドからマルチスレッドへの移行がある。 tokio-core は1:Nの非同期モデルなのに対して、 tokio はN:Mの非同期モデルである。そのため tokio への移行では関連するデータ型をスレッドセーフなものにアップグレードする必要がある。
  • 現状で実装しようにも、現状の非同期エコシステムではコネクション系のAPIを適切に設計する準備ができていない。
    • 特に、現状の非同期エコシステムは参照との相性が悪い。しかし、コネクションで conn: &mut Conn を受け渡すようなパターンを回避しようとすると、Conn を受け取って (Result, Conn) を返すといったパターンになりがちで、これは皮肉にもRustイディオマティックではないし利便性も低い。
      • async/awaitの導入により非同期における参照の問題はかなり解決される見込みなので、あと少しの辛抱かもしれない。

そもそも、RDBに貼るコネクションが同時10000接続とかそういうことはしない。 (postgresとかだと1コネクション1プロセスらしいので、それをやるとサーバー側が大変なことになりそう) たとえば10接続くらいだったら、同じ数のOSスレッドを用意してやればイベントループをブロックせずに捌けることになる。

さて、そういうとき (OSスレッドを使うことで、非同期処理の一部で同期的な処理をする) には以下の2つの選択肢がある。

  • futures-cpupool 。専用のCPUプール(OSスレッドのプール)を持っておき、そこに同期処理をオフロードする。今回はこちらを使った。
  • tokio_threadpool::blockingtokioのランタイムが所有するスレッドプールのスレッドをロックする。別スレッドに移動せず、その場で同期処理を行うことができる。イベントループはロックされていないスレッドに移管されるので、非同期処理が間違ってブロックされることはない。
    • Actix-Webが tokio-threadpool を使っていないため、こちらは使えなかった

ちなみにDBクライアントレベルでは非同期に対応しているものもある。たとえば mysql ライブラリの作者は mysql-async というライブラリも作っていて、同じ作者だけあってインターフェースも似通っている。(5月の社内ISUCONのときはこれを使った)

(もう1点、非同期とかとは関係なく困った点として、手元のMacBookに入れたmysql8のサーバーと相性が悪く接続するとpanicになるという問題があった。結局mysql5系列を入れたら治った)

redis

redisなどのオンメモリキャッシュストレージもISUCONでは頻出とのことでredisのことも考えた。基本的な方針は↑と同じだけど、オンメモリキャッシュの目的上、もっと非同期で繋いでバシバシ捌けるほうがいいのかなと思って、迷っているうちに当日が来てしまった。結果、redisが本質じゃないっぽい問題で、redisを使う機会自体が来なかったので結果オーライ。

ちなみに状況としては、事実上の標準である redis が最近非同期接続をサポートしたので、その部分の選定では困らなさそう。ただ、非同期接続のほうは r2d2 とかには対応していないのでやはり悩ましい。自分でプーリングする?同期的に使う?それともプーリングをしないで戦う?

非同期対応のクライアントとしては他に redis-async というのもあり、5月の社内ISUCONではこっちを使っていた。

セッションミドルウェア

ここが肝で、今回はRack::Session::Cookie互換のクッキーセッションミドルウェアを自作した。別にわざわざRubyのMarshalをデコードしてやる義理はないので、JSONフォーマットの互換を書いた。

なぜ互換である必要があるか?それは、Rubyで書かれた参照実装から少しずつRustに移植していくためである。「一気に移植して、移植しきれなかったらドボン」みたいな怖い真似をわざわざする必要はないので、このようにした。こんな互換ミドルウェアは需要が少ない(真面目に作るとN2種類必要になる……)ので、諦めて自作するのがよい。

ISUCONではRuby実装があってSinatraで書かれているのが相場のようなので、そういう想定で互換ミドルウェアを書いた。↑にあるようにJSONフォーマットにする必要があるが、これはRuby側を1,2行修正すればすむ。

「一部だけ移植なんて甘え」という声はあるかもしれない。ちなみに「ちょっとずつ移植するなら、セッション共有の必要があるじゃん」というのは5月の社内ISUCONの当日に気付いた話で、その時はかろうじて全部移植した。(その時は2人チームで相方のagatanはRustが書ける)

過去問を解く

過去問を解くといい感じにノウハウが溜まるし、何よりもいい感じのプロジェクトテンプレートができてよい。上記のセッションミドルウェアも当然、過去問を解きながら作った。

気合で移植する

どうせ、サーバーサイドのための便利関数もミドルウェアデザパタも出揃ってないので、冗長でもとにかく書くしか勝利への道はない。

移植しながら最適化するか、移植してから最適化するかは悩ましい。今回はRustでアプリを書けるメンバーが自分しかいなかったので、「Rubyで最適化してから移植する」とか「Rubyで最適化して、それとは別にRustに移植して、Ruby側の最適化を参考にRust側も最適化する」とかになってしまったので、そこは反省が必要だった。

それはそれとしてRustで最適化した部分で点数が上がったりしたのでちょっと嬉しかった。しかし、点数がガチャであることが後に発覚してちょっと悲しかったりした。 /api/events の内容を一通り移植してN+1を潰すのはできたので、あとはapiのつなぎ込みという感じだったが、まあそのつなぎ込みが結局大変という……

その他の話

今回のISUCONではRustの参照実装が出るかもという話があったが、「どちらかというと出ないでしょう」という仮定のもと、チーム全員書けるRubyから移植することを始めから確定させていた。結果、やっぱり参照実装は出なかった。

まとめ

  • ISUCONは頻出パターンがあるので、それにあわせてフレームワークミドルウェアの選定をする
  • 選定ミスると結構つらいが、使ってみないとわからない点もあるので過去問を走るとよい
  • セッション共有は可能なので、変な言語でやる人にはオススメ
  • その言語でアプリを書ける人間が2人はいたほうが良い

Rustコンパイラをクラウドでビルドする備忘録

概要

Rustコンパイラをいじっているとビルドの遅さのせいで作業に支障をきたすため、クラウドでビルドするようにしてみる。

EC2を立ち上げる

Ubuntu 16.04 (HVM) の c5.4xlarge を使うことにする。使うインスタンスの強さは財布と相談して決めることにする。

高くて強いインスタンスを使っていくので、使わないときは落とし忘れないように気をつける(停止時はEBSやEIPだけ課金される)。このあたりも必要なら自動でシャットダウンする仕組みを考えるといいかもしれない。

セットアップ

手元から rust-builder という名前でSSHできるようにする。依存関係として以下を入れておく。

$ sudo apt update && sudo apt upgrade && sudo apt install build-essential python-minimal cmake curl git

スクリプトの配置

Rustコンパイラを手元にcloneして、 x.py があるのと同じ位置に以下の y.sh を置いてexec permissionをつける。

#!/usr/bin/env bash
set -ue

# Remote side:
# sudo apt update && sudo apt upgrade && sudo apt install build-essential python-minimal cmake curl git

builder_host=rust-builder
local_host="$(hostname)"
dirpath="$(realpath --relative-to "$HOME" "$(pwd)")"

git submodule update --init --recursive
rsync -avz --delete --exclude '.git' --exclude '/build' "$HOME/$dirpath/" "$builder_host:$local_host/$dirpath"
ssh -t "$builder_host" "cd $local_host/$dirpath; ./x.py $@"

やっていることは以下。

  • rsyncで送って向こうでビルドを叩く。
  • gitを全部転送したくないので .git は省く。ただしsubmoduleが必要なのでsubmodule updateを自動で叩く。 (x.pyは.gitがあるときはsubmodule updateを自動でやってくれる)
  • build も当然省く。手元のbuildと衝突しうるし、 --delete に巻き込まれないようにする意図もある。ただしbuildというディレクトリがソースツリーの中にあるので、 /build と指定する必要がある。
  • tmuxを立ち上げたりはしない。ビルドが短くなることが期待されているので途中で切れてもよい。むしろ自動アタッチとか考えるのが面倒。

ビルド

./x.py のかわりに ./y.sh をつけてビルドする。(例: ./y.sh test --stage 1)

設定

クラウドとは関係ないが、コンパイラを開発するときは config.toml を絶対にいじったほうがよい。

  • compiler-docs = true。ほとんどの関数はdoc-comment (///) を持たないが、シグネチャが一覧できるだけでもありがたい。
  • debug-assertions = true。 これがないとログがほとんど出ない。また、これを有効にすると一部の追加のチェックが行われる。
  • debuginfo = true, debuginfo-lines = true。 これがないとICE(コンパイラ内部エラー)の時のバックトレースがちょっと貧弱で困る。
  • incremental = true。 効果は計測してないけど効率がちょっと上がりそう

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の書き換えに注意が必要など罠も多い感じでした。