Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

CellとRefCell

CellRefCell はどちらも「内側のミュータビリティ」を実現するものだが、実現方法が違う。

RefCell については、ランタイムでボローチェックしていることはよく知られている(と思う)。

    let ref_cell = RefCell::new(20i32);
    println!("ref_cell (1): {}", ref_cell.borrow());

    println!("ref_cell (2): {}", ref_cell.borrow());

    *ref_cell.borrow_mut() = 200;
    println!("ref_cell (3): {}", ref_cell.borrow());
ref_cell (1): 20
ref_cell (2): 20
ref_cell (3): 200

ランタイムでとは、文字通りコンパイラではなくランタイムで、つまりプログラムで手動で所有権を管理しているということである。

rust/cell.rs at dd4851d503f3fae0c0c742a19e0d8e6e2140bd2a · rust-lang/rust · GitHub

UnsafeCell というのは、この値は「内側のミュータビリティ」として使うよという意思表示であり、要は生ポインタで処理しますっていうこと。 borrow というのが借用の数で、正の数の場合は immutable な参照の個数で、負の数の場合は mutable な参照の個数(通常は借用ルールから 1 個まで)と管理されている。

rust/cell.rs at dd4851d503f3fae0c0c742a19e0d8e6e2140bd2a · rust-lang/rust · GitHub

じゃあ、 Cell はどうかというと、こちらは copy または move によって「内側のミュータビリティ」を実現する。 immutable で定義した値について、 copy または move することによって保持する値を無理矢理変更することができる。

    let cell = Cell::new(10i32);
    println!("cell (1): {}", cell.take());

    println!("cell (2): {}", cell.take());

    cell.set(100);
    println!("cell (3): {}", cell.take());
cell (1): 10
cell (2): 0
cell (3): 100

cell (2) の出力が 0 になっていることに着目したい。これは take() されることにより、 Cell の保持する値がデフォルト値に戻ったことを意味する。変数 cell はイミュータブルで定義されているが、 println! する度に値が変わっており、きちんと「内側のミュータビリティ」を実現できていることがわかる。

実装は RefCell より遥かにシンプルなので、興味がある人は読んでみよう。保持している値が Copy なのか Default なのかで、2つの異なる API get()take() が実装されているのが面白い。

参考: Rust Cell and RefCell

Data.Vaultの非GHC実装

ふと、 Data.Vault ってどうやって実装してるんだろと中身を覗いていたら、なんかすごいものを見つけた。

vault/IORef.hs at 39cf64b47c24b83c24924d47d2385f8213a3f322 · HeinrichApfelmus/vault · GitHub

今はほぼ GHC 一択なのでこの実装を使っている人はほとんどいないんだろうけど、 issue を読む限り UHC 向けに実装されたもののようだ。

Vault の定義を見ると、一瞬ぎょっとなる。

data Locker s = Locker !Unique (IO ())
newtype Vault s = Vault (Map Unique (Locker s))

まさかの IO () なんだが。どうしてこれで値を保存できるかというと、 Key の方に IORef があるからである。

data Key s a  = Key    !Unique (IORef (Maybe a))
unlock (Key k ref) (Locker k' m)
    | k == k' = unsafePerformIO $ do
        m
        readIORef ref     -- FIXME: race condition!
    | otherwise = Nothing
lookup key@(Key k _)   (Vault m) = unlock key =<< Map.lookup k m

lookup すると、 Vault に保存されている IO ()unsafePerformIO で実行した上で IORef から値を読むことになる。ここまで来るともう察しが付くように、 insert はこのように IORef に値を書き込む IO アクションを保存している。

lock (Key u ref) x = Locker u $ writeIORef ref $ Just x
insert key@(Key k _) x (Vault m) = Vault $ Map.insert k (lock key x) m

こうすることで、 Vault の値の型を IO () 単一に固定しつつ、 Key a 側の型 a の値を読めるようになっている。この発想はなかった。ただ、だいぶ雑に IORef を読み書きしているので、まあ、レースコンディションになるわなって感じではある。

ちなみに、 GHC 向けの実装は GHC.Exts.Any を使った平和な実装になっている。知りたかったのはこっちだ。

vault/GHC.h at 39cf64b47c24b83c24924d47d2385f8213a3f322 · HeinrichApfelmus/vault · GitHub

謹賀新年

明けましておめでとうございます。本年もよろしくお願い致します。

新型コロナで在宅勤務が始まってからは、自然と育児にコミットする時間が激増していて、まあ、間近で子供の成長を見られるのは他では体験できないとても幸せなことではあるのですが、一方で、本当にこんな人生を歩んでいいんだっけと日々自問自答しております。

一言で言えば、数学やHaskellやRustなどきちんと興味を持てるものにどっぷりと浸かりたいので、新年を機に時間を作っていこうと思います。

cargo test --jobs N -- --test-threads=M

redis-rs の cluster のテストが何度やっても通らなくてハマった。ただし、テスト名を指定して一個ずつであれば成功する。該当するテストは以下。

redis-rs/test_cluster.rs at bd8dc731ded564329d74717dfacc5cce748d891f · mitsuhiko/redis-rs · GitHub

このテストは redis server を立ち上げてクラスタを組んでテストを実行している。このとき、各サーバには 7000 番ポートから順番に割り当てている。

redis-rs/cluster.rs at bd8dc731ded564329d74717dfacc5cce748d891f · mitsuhiko/redis-rs · GitHub

そして、デフォルトでは cargo test 1 は複数のテストを並列に実行する。直列な世界に生きているので、ここに気がつくまでに時間がかかってしまった。

テストの実行ファイルでヘルプを表示させると、デフォルトでは並列実装すると書いてある。

$ target/debug/deps/test_cluster-cdbee22b93a5c3e7 --help
Usage: --help [OPTIONS] [FILTER]                                                                                                                                                                                                                        
Options:
..snip..
        --test-threads n_threads
                        Number of threads used for running tests in parallel
..snip..
By default, all tests are run in parallel. This can be altered with the
--test-threads flag or the RUST_TEST_THREADS environment variable when running
tests (set it to 1).
..snip..

実際、 rustc 側で実装を見ると、デフォルトは CPU コア数であることがわかる。

rust/concurrency.rs at 4ae328bef47dffcbf363e5ae873f419c06a5511d · rust-lang/rust · GitHub

並列で実行すると test_cluster.rs が落ちるのは明確だ。 7000 番 port が競合しているので、それぞれのテストで使う redis のサーバが建てられなくなってしまう。直列で実行するしかない。

redis-rs のリポジトリには、 Makefile があり、そこで --test-threads が指定されている。これはテストの実行ファイルが持つオプションなので cargo に直接渡すことはできず、 -- --test-threads=1 というように -- の後ろに指定しなければならない。

redis-rs/Makefile at bd8dc731ded564329d74717dfacc5cce748d891f · mitsuhiko/redis-rs · GitHub

ところで、 --test-threads=1 とは別に、 cargo には --jobs というオプションがある。

$ cargo test --help
..snip..
    -j, --jobs <N>                   Number of parallel jobs, defaults to # of CPUs
..snip..

ドキュメントは以下にある。紛らわしいが、実行ファイルのビルドに使う CPU 数を指定するオプションらしい。

doc.rust-lang.org

The --jobs argument affects the building of the test executable but does not affect how many threads are used when running the tests.

今回のように「単体テストが逐次実行しかできない」という場合には --jobs ではなく --test-threads を使わねばならない。指定先も cargo に指定するのか単体テストの実行ファイル側に指定するのかも別れていてわかりにくい。

そもそも、「単体テストが逐次実行しかできない」という状況がイケてないなあとは思う。


  1. 正確には、テストの実行ファイルである。

Haskellの古いmkSocketの引数のfamily

Linuxネットワークプログラミングの勉強をきちんとしましょうということなんだけど、とりあえず調べたことをメモっておく。

HaskellmkSocket という関数がある。

https://hackage.haskell.org/package/network-3.1.2.0/docs/Network-Socket.html#v:mkSocket

今は引数がファイルディスクリプタ CInt だけとなっているが、 2.x 系の頃の引数はもっと複雑だった。

https://hackage.haskell.org/package/network-2.2.3/docs/Network-Socket.html#v:mkSocket

この中に Family という引数があって、これは AF_INET とか AF_INET6AF_UNIX と言ったようなソケットの種類を表す値なんだけど、 Server::Starter のようにファイルディスクリプタが渡されている場合にどうやってこれらの値を決めるかが今回の疑問点。

実際、拙作の hs-server-starter では、 Server::Starter から渡された環境変数の値 0.0.0.0:80=3 というような値を雑にパースしてこれらの値を推測していたのだけど、 IPv6 アドレスの判定が雑過ぎて完全にバグっていた。

https://github.com/hiratara/hs-server-starter/blob/f47d198aab92135e6546191f82f44bec60ccaf5f/src/Network/ServerStarter/Socket.hs#L83-L101

じゃあ、どうやって直せばいいかってところなんだけど、 Starlet のコードを見ると、渡ってきたファイルディスクリプタはすべて IPv4 であると見なしてコードが組まれているように見えた。これに従えば、 AF_INET 決め打ちにしても差し支えないように思える。

https://metacpan.org/source/KAZUHO/Starlet-0.31/lib/Plack/Handler/Starlet.pm#L26-30

現に、以下のように改修をしても、テストやサンプルアプリは問題なく動いた(もちろん、 network-2.x 系を使って試している)。

https://github.com/hiratara/hs-server-starter/pull/10/files

じゃあ、この family は何に使っているのかということで、例えば以下にある getSocketName を見てみる。

https://hackage.haskell.org/package/network-2.2.3/docs/src/Network-Socket.html#getSocketName

withNewSockAddr を見ると、 sizeOfSockAddrByFamily でサイズを求めるのに使っている。直前の定義を見ると AF_INET = 16 AF_INET6 = 28 AF_UNIX = 110 などと family ごとにサイズが違っている。

https://hackage.haskell.org/package/network-2.2.3/docs/src/Network-Socket-Internal.html#withNewSockAddr

そして、このサイズを元に struct sockaddr を確保して c_getsockname を呼ぶことになる。この getsockname を調べると、

https://man7.org/linux/man-pages//man2/getsockname.2.html

The returned address is truncated if the buffer provided is too small;

つまり、渡した struct sockaddr のサイズが足りなくても、渡した文のサイズで切られて返されるだけということである。そういう意味では AF_INET は一番サイズが短いので、 AF_UNIX などを採用しておいたほうが害が少ないのかもしれない。

family はこのようにシステムコールを呼ぶのに必要な struct sockaddr のサイズを決めるためにしか使われておらず、実際はシステムコールによって返された struct sockaddr を以下の関数 peekSockAddr で解釈して使うことになる。 struct sockaddrfamily によって形式が違うが、先頭は sa_family_t となっているので、これを見ることで family を判別できる。

https://hackage.haskell.org/package/network-2.2.3/docs/src/Network-Socket-Internal.html#peekSockAddr

ちなみに、 network-3.x 系では family などを渡さなくて良くなっているが、それは十分に大きい struct sockaddr を確保しているからである。

https://hackage.haskell.org/package/network-3.1.2.0/docs/src/Network.Socket.Types.html

-- sizeof(struct sockaddr_storage) which has enough space to contain
-- sockaddr_in, sockaddr_in6 and sockaddr_un.
sockaddrStorageLen :: Int
sockaddrStorageLen = 128

Rustでdocker build時に依存ライブラリの再ビルドを避ける

Rust で書かれたアプリをビルドするための docker image は以下で公開されている。

hub.docker.com

しかし、ここに出ているように COPY . . なんてやっていると、 src/main.rs を触るだけで毎回 cargo install 時に依存するライブラリをすべてダウンロードしてきてコンパイルしてしまい、恐ろしく効率が悪い。

これを避けるには、アプリをビルドする前に依存だけをビルドするレイヤを作っておき、その上に自分のアプリをビルドするレイヤを載せられるといい。しかし、残念なことに、現在の cargo には依存クレートだけをビルドする仕組みがない。じゃあどうするかというのが、以下のエントリに書かれている。

whitfin.io

要点だけ説明すると、まず cargo new --bin my_project とこれからビルドするプロジェクトと同名のダミーの空のプロジェクトを用意し、そこに Cargo.tomlCargo.lock のみを配置して cargo build --release する。その後 src/*.rs を本物に差し替えて、もう一度 cargo build --release すれば良い。ただし、自分で試してみたところなぜか 2 度目の cargo build が動いてくれなかったので、 touch src/main.rs をするなどの処理をおまけで入れておいた。

さて、手元での実行はこれで良いが、 Cloud Build: serverless CI/CD platform  |  Google Cloud を使うと、前回ビルド済のレイヤを参照できるわけではないのでキャッシュが効いてくれない。 --cache-from を使う方法もあるが、マルチステージビルドとだいぶ相性が悪い。というのも、ビルドに使ったイメージも破棄せず docker push しなければならないので。じゃあどうするかというのは、以下のエントリに書かれている。

qiita.com

kaniko を使えば、 cloudbuild.yaml は本当に大丈夫かいなってくらい恐ろしくシンプルになる。それでも動かしてみるときちんとキャッシュをする動きになっていて、初回実行が 18M52S かかる cargo build が2度目の実行が 1M25S で済んだ。すばらしい。

&Tがそのまま&dyn Traitとして使えるわけではない

トレイトオブジェクトを返す次の関数 fコンパイルできる。

trait X {}

fn f(x: &dyn X) -> &dyn X {
    x
}

ところが、次のように書くとコンパイルできない。

fn f<T: X + ?Sized>(x: &T) -> &dyn X {
    x
}
   Compiling playground v0.0.1 (/playground)
error[E0277]: the size for values of type `T` cannot be known at compilation time
 --> src/lib.rs:4:5
  |
3 | fn f<T: X + ?Sized>(x: &T) -> &dyn X {
  |      - this type parameter needs to be `std::marker::Sized`
4 |     x
  |     ^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `T`
  = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
  = note: required for the cast to the object type `dyn X`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

T はトレイト X を実装しているのだから、何もしなくても &dyn X なのではと思われるかもしれないが、この書き方をした f では x が直接返されるわけではなく、トレイトオブジェクトに変換される。トレイトオブジェクトについては Trait object types - The Rust Reference に記述がある。 Exploring Rust fat pointers でもトレイトオブジェクトの構造 (fat pointers) についてわかりやすく説明されている。

Each instance of a pointer to a trait object includes: a pointer to an instance of a type T that implements SomeTrait a virtual method table, often just called a vtable, which contains, for each method of SomeTrait and its supertraits that T implements, a pointer to T's implementation (i.e. a function pointer).

この変換時になぜ Sized が必要なのかは、以下の issue で説明されている。 [u8] のサイズの格納位置が &&dyn の場合で違うため、単純にポインタを流用してトレイトオブジェクトを作ることができないようだ。なるほど。

github.com

A legitimate example of why the Sized bound is needed is for e.g. Self = [u8]. You cannot cast &[u8] to &dyn Trait because there would be nowhere to store the length of the slice (&[u8] stores it in the same place where &dyn Trait puts its vtable).

ただ、この issue がなぜクローズされていないかはよくわからず。将来挙動が変わる見込みがあるんだろうか?