梅雨入り前の一日
風鈴がちりん、となった。我が家には風鈴があるのであった。
たしか川崎大社で買い求めたものだ。存在を忘れていたが風情があって良い。
午後にかけてよく晴れた日。ふらっと散歩をして気を良くした。
WebAssemblyのanyref
WebAssembly にreference typesがあるとどううれしいのか、を理解しようとして書いたメモ。参照型として二つ提案されているが、ここではanyref
に着目する。
(注: Reference types は提案段階なので各種ブラウザではまだデフォルトで有効にはなっていない。Chrome で試すにはコマンドライン引数に--js-flags=--experimental-wasm-anyref
をつける必要がある。)
任意の JS の値を受け取って、それをそのまま返すidentity()
という関数を wasm で作ろう。とってつけた例題だけど骨子を最小限のコードで示すために許してほしい。こういうやつ。
identity(value) === value; // => true
anyref
が存在しない現在の仕様だとこれは wasm だけでは定義できない。なぜなら wasm の関数は引数/戻り値に整数型か浮動小数点型しか指定できないから。JS には数値以外にも文字列型やオブジェクト型がある。文字列やオブジェクトを数値に変換する仕組みが JS 側に必要だ。
任意の JS の値を数値に変換する単純な方法としてぱっと思いつくのは、グローバルな配列をひとつ用意して、そこを値の格納先として使うやり方だろう。JS/wasm 間での値の受け渡しはこの配列のインデックスを使う。要はヒープみたいなもの。必要最小限の実装はこんな感じ。
let heap = [];
function toWasmValue(value) {
heap.push(value);
return heap.length - 1;
}
function fromWasmValue(index) {
return heap[index];
}
Wasm の関数を呼ぶときの流れは、toWasmValue()
でインデックスを取得、wasm で定義された関数の呼び出し、戻り値をfromWasmValue()
で JS の値に戻す、という感じになる。
(async function() {
const stream = fetch("identity.wasm");
const { instance } = await WebAssembly.instantiateStreaming(stream);
// Wrapper function of `identity`
function identity(value) {
const index = toWasmValue(value);
const ret = instance.exports.identity(index);
return fromWasmValue(ret);
}
console.log(identity(42)); // => 42
console.log(identity(window)); // => window
})();
Wasm 側のコードはただ受け取ったインデックスを返すだけ。
(module
(func (export "identity") (param i32) (result i32)
;; Just return the argument
get_local 0))
これでidentity()
を定義できたが、値を単純に JS → wasm → JS と引き回したいだけなのになんだか面倒だ。しかも今の実装は実用に耐えない。toWasmValue()
を呼ぶたびにヒープが大きくなるし、オブジェクトへの参照が消えないのでリークが起きる。きちんと使えるものを作ろうと思うと、不要になった値をヒープから削除しなければならないし、必要に応じてフラグメンテーションも解消しないといけない。
要はメモリ管理が必要になるのだけど、GC がある JS の上にメモリ管理機構を作るのは冗長だと思える。wasm 側で値を操作しないなら(単純に JS に引き渡すだけなら)、その値への参照を wasm に渡すのを許可してもいいのではないか。そうすれば値の生存期間の管理は JS 側の GC に任せられるし、値の変換も不要になる。
…こんな感じでanyref
が提案されたんだろう、と自分は理解した。
実際にanyref
を使って書き直すと以下のようにだいぶすっきりする。JS 上にヒープを作る必要はない。JS 側の GC が参照の生存管理をする。
(async function() {
const stream = fetch("identity_anyref.wasm");
const { instance } = await WebAssembly.instantiateStreaming(stream);
console.log(instance.exports.identity(42)); // => 42
console.log(instance.exports.identity(window)); // => window
})();
Wasm 側のコードは引数と戻り値の型を変えるだけ。wat2wasm
で以下をコンパイルするときは--enable-reference-types
フラグをつける。
(module
(func (export "identity") (param anyref) (result anyref)
;; Just return the argument
get_local 0))
anyref
が解きたい課題は分かった。では実際の問題に対してanyref
はどううれしいのか。多分ほとんどの開発者には特段メリットはないんじゃないか。というのも、こういう低レイヤの変換やメモリ管理はすでに Emscripten や wasm-bindgen が面倒を見てくれているから。これらのフレームワーク自体にとってはanyref
があるとラッパー関数なんかを削減できてうれしいと思う。開発者にとってもランタイムのサイズが減って間接的にうれしいかもしれない。
提案の概要にはここで説明した以外の動機も書いてある。むしろ主眼はそちらかもしれなくて、reference types はほかの提案の土台としての側面が強そう。
補記
anyref
はWebAssembly.Global
の型やWebAssembly.Table
の要素型としても指定できる。
参考
再帰的なデータ構造のイテレータを手書きする
amos.me - Recursive iterators in Rust が面白かった。以前 AST をたどるイテレータを書いたときに同じような問題に遭遇した記憶がある。そのときは既存のイテレータやアダプタを組み合わせてうまい具合にやる方法が思いつかず、結局自分でイテレータを実装した。イテレータの内部では状態を管理する列挙型と、今どのノードをたどっているのかを記憶するスタックを使用する。
上記のエントリで出てくる例題に対して実装するとこんなかんじ → Rust Playground
うまく既存のイテレータやアダプタを使って関数型言語っぽい書き方をできるようになりたいなあと思う一方、自分にはどうも手続き的なコードのほうが理解しやすい。イテレータを組み合わせて書いてあるコードはすっきりしていて賢いと思うんだけど、理解が追いつかないことが多い。Rustを使い始めてそこそこ経つので慣れの問題だけじゃないかもしれない。
ニューヨークでベーグルを食べた
4 月に出張でトロントとニューヨークを訪れた。渡航前に引いた風邪を引きずってしまったせいで体調が悪く、しんどい旅となった。
トロントではあまり観光できなかったが、後半少し回復したのでニューヨークを少し観光することができた。ニューヨークを訪れるのは二度目。最初に訪れたときに食べたベーグルが美味しかったので、今回もベーグルを食べようとお店を巡った。
最初に訪れたのは Tompkins Square Bagels。界隈でベストなベーグル屋さんはどこ?と現地の同僚に聞いたら教えてくれたお店である。

ドライトマトが入ったクリームチーズを挟んでもらった。翌日まで顎がつかれるぐらい噛みごたえのある生地で自分好みの食感であった。

別の日にはアッパーウエストサイドにある Aboslute Bagles を訪問した。ここは前回ニューヨークに来たときに一番印象に残っていたので再訪しようと思っていたお店だ。朝 9 時ぐらいについたのだけど、けっこうな行列ができていた。ただ、店内で食べていく人はあまりいないので待ち時間はそれほどでもない。どれを食べようかと悩んでいる間に行列は掃けた。

定番のサーモンが入ったスプレッドにしよう思っていたのだけど、陳列されていたブルーベリーのやつが美味しそうだったので少し悩んだ結果ブルーベリーに変更した。ベーグルはそれに合いそうなプレーンを選択。

これが正解で、とても美味しかった。写真ではわからないかもしれないが、ここのベーグルはかなり大きい。一食分としては半分でも多いくらい。それでも完食してしまった。おかげでこの日はお昼が入らなかった。再度ニューヨークを訪れる機会があればここのベーグルはまた食べたい。
最終日の空港に向かう前に訪れたのはBrooklyn Bagel & Coffee Company。ここではスプレッドを頼まずベーグル単体で購入した。種類はプレーンと全部入り(Everything)。

これは結局食べる余裕がなくて帰国後冷凍保存した。これも一つが大きいので食べごたえがありそうであった。
TypeScriptの型チェックと仲良くする
TypeScript のコンパイルエラーを一時的に抑止したい場面は多々ある。この記事では、自分が型エラーを回避するのに便利だなと思っている機能を状況に応じて 3 つ紹介したいと思う。想定している文脈は、趣味プロジェクトで、フレームワークを使わない素のフロントエンド開発。
Type Guards
状況: このエレメントは<foo>
なんだからbar
っていう属性があるのに TypsScript はそれを分かってくれない。
例えばオーディオを再生するページを静的に記述したとする。
<audio id="my-audio"></audio>
このオーディオに対して再生位置をリセットするスクリプトを書きたい。書いている側からするとmy-audio
はHTMLAudioElement
であることが分かっている。getElementById('my-audio')
の返り値はHTMLAudioElement
だからと思って以下のように書くとコンパイラに怒られる。
const audioEl = document.getElementById("my-audio");
// NG: `[ts] Property 'currentTime' does not exist on type 'HTMLElement'. [2339]`
audioEl.currentTime = 0;
この場合は Type guards に頼る。if 文で型の整合性をチェックすると、コンパイラがコントロールフローを解析して型を限定してくれる。以下では if 文以降audioEl
はHTMLMediaElement
であることが保証される。
const audioEl = document.getElementById("my-audio");
if (!(audioEl instanceof HTMLMediaElement)) {
throw new Error("#my-audio is not an HTMLMediaElement");
}
// OK: At this point TS compiler knows `audioEl` is an HTMLMediaElement.
audioEl.currentTime = 0;
冗長だけど if 文以降に型チェックの恩恵を受けられることを考えるとトレードオフとしては悪くない。as
を使う方法もあるけれど、 Type Guards を使ったほうがより安全になる。
参考: Advanced Types · TypeScript
Non Null Assertion Operator
状況: 関数foo()
はnull
やundefined
じゃない値を返すのが分かってるのに TypeScript がそれを分かってくれない。
2D の絵を描きたいとする。ブラウザ上で 2D の絵を描くにはHTMLCanvasElement
を用意してそれに対してgetContext('2d')
を呼んで描画コンテキストを取得する。だけど TypeScript はgetContext()
はnull
を返すかもしれないと文句を言ってくる。
// NG: [ts] Type 'CanvasRenderingContext2D | null' is not assignable to type 'CanvasRenderingContext2D'.
// Type 'null' is not assignable to type 'CanvasRenderingContext2D'. [2322]
const ctx: CanvasRenderingContext2D = canvas.getContext("2d");
この場合は!
を末尾につけている。これはNon Null Assertion Operatorというやつで、式の末尾に!
をつけるとコンパイラはその式がnull
やundefined
を返すことがないと仮定するようになる。null
やundefined
を返す式にしか使えないけど、Type guards を使うよりも簡潔に型を限定できる。
// OK
const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!;
ctx.clearRect(0, 0, width, height);
ただ Type guards と違ってコンパイラが null や undefined にならないことを保証してくれるわけでは無い、という点に注意。
参考: TypeScript 2.0 · TypeScript
@ts-ignore
状況: window
に一時的にデバッグ用のプロパティを追加したいけど TypeScript がそれを許してくれない。
開発の初期段階ではブラウザのデベロッパーツールを使っていろんな検証をしたい。例えば自作のApp
オブジェクトの状態をデベロッパーツールで確認したいとする。そんなときには手っ取り早くアクセスできるオブジェクト、例えばwindow
にそのオブジェクトをぶら下げるのが簡単だろう。でも単純にそれをやろうとするとコンパイラが怒る。
const app = new App(...);
// NG: [ts] Property 'app' does not exist on type 'Window'. [2339]
window.app = app;
こういう状況では@ts-ignore
を使っている。@ts-ignore
をコメントとして書くと、以後の一文だけはコンパイラは何もエラーを出さなくなる。tsconfig.json などで一括にエラーを抑止するのは避けたいけど、この一文だけ見逃して欲しい場面で重宝する。
const app = new App(...);
// @ts-ignore
window.app = app; // OK
...
console.log(window.app); // NG
あくまで@ts-ignore
の直下の行だけエラーを出力しないようになるだけなので、ほかの場所でapp
を使おうとするとコンパイラに怒られる。自分は@ts-ignore
をDevToolsを使ったデバッグや調査をしたいときや、トリッキーなimport
をしている場所のエラーを抑止したいときなんかに使っている。
参考: TypeScript 2.6 · TypeScript
参考文献
Revised Revised 型の国のTypeScript は非常に良い入門書。一日ぐらいかけて目を通しておくと TypeScript の気持ちがわかるようになると思う。自分は技術書典3で紙の本を購入した。
TypeScriptのunsafeな操作まとめ では TypeScript の型検査が常に有効ではないことを議論している。
Rustでwasm用カスタムアロケータを書く
Wasm とホスト間での関数呼び出しでは数値しか受け渡すことができないので、文字列や配列をやり取りするときには wasm インスタンスのメモリを経由する必要がある。例えば文字列をホスト(JavaScript)から wasm に渡したいときは一度 wasm インスタンスのメモリ領域に文字列を書いてから、書いた場所のアドレスとサイズを関数に渡す。これは数値以外の情報をやり取りする場合は wasm 側にメモリアロケータが必要であることを意味している。
Rust が生成する wasm がメモリ割り当てをどうやっているのかを調べて、自分でナイーブなアロケータを作るところまでやったので、その過程で理解したことをエントリにしておこうと思う。
ここではホスト環境としてブラウザ(JavaScript)を想定している。また wasm のインスタンス化の際にはメモリを import せず wasm 内で管理することを前提とする。
WebAssembly のメモリ
WebAssembly は単純なメモリモデルを採用していて、メモリは連続したバイト列として表現される。初期化時にある一定のサイズが確保されて、必要に応じて領域を広げることができる。ただし一度確保した領域を縮小することはできない。メモリ領域の伸張は 64KB のページ単位で行う。
Wasm インスタンスのメモリは JavaScript からは ArrayBuffer として見える。例として[1,2,3]
という配列を JS から wasm に&[u32]
として渡したいとしよう。JS 側のコードはこんな感じになる。
// `wasm` is an WebAssembly.Instance.
const arg = [1, 2, 3];
const arr = new Uint32Array(wasm.memory.buffer);
const addr = wasm.allocBytes(arg.length * 4);
arr.set(arg, addr / 4);
wasm.someFunc(addr, arg.length);
wasm.freeBytes(addr);
wasm.memory.buffer
から見える ArrayBuffer が wasm インスタンスのメモリ。wasm 側のアロケータの仕事は、wasm.memory.buffer
をヒープ領域として扱い、そのヒープを適宜伸張しながら malloc()/free()相当の機能を提供すること。上記ではそれらをwasm.allocBytes()
やwasm.freeBytes()
として定義している。
wasm からメモリ領域を伸張するにはmemory.grow
命令を使う。この命令は引数を二つとる。一つは新たに確保するページの数。もう一つは対象となるメモリ領域を指定するインデックス。このインデックスには常に 0 を指定する。仕様の上では wasm インスタンスは複数のメモリ領域を参照できるようになっているが、これは将来の拡張のために用意されているもの。現時点では一つの wasm インスタンスにつきメモリ領域は一つしか存在しない。
アロケータの実装
Rust 側での実装に話を進めていく。Rust から WebAssembly のmemory.grow
命令を呼ぶにはwasm32::memory_grow()
を使う。以下の Rust のコードをコンパイルすると、
#![cfg_attr(target_arch = "wasm32", feature(stdsimd))]
use core::arch::wasm32;
fn f() -> usize {
const MEMORY_INDEX: usize = 0;
unsafe { wasm32::memory_grow(MEMORY_INDEX, 1) }
}
以下の wasm が生成される。
(func (;1;) (type 1) (result i32)
i32.const 1
memory.grow)
memory_grow()
は成功すると伸張前のメモリのサイズをページ単位で返す。言い換えれば、新規に確保されたメモリの開始アドレスは戻り値に 64KB を掛けることで求められる。また伸長に失敗したときはusize::max_value()
を返す。以下のようなラッパーを書くと malloc()に近い使い勝手になる。
unsafe fn grow_pages(num_pages: usize) -> *mut u8 {
let num_pages = wasm32::memory_grow(MEMORY_INDEX, num_pages);
if num_pages == usize::max_value() {
process::abort();
}
(num_pages * PAGE_SIZE) as *mut u8
}
下準備ができたのでアロケータを書いていく。今回はアロケータを作るうえで必要なパーツを説明するのが主眼なので、Memory Allocators 101という記事を参照にして単純な first-fit のアロケータを作成した。コードは Github にあげているのでここではインタフェースのみ載せておく。
struct Heap {
// ...
}
impl Heap {
unsafe fn alloc(&mut self, size: usize) -> *mut u8 { /* ... */ }
unsafe fn dealloc(&mut self, ptr: *mut u8) { /* ... */ }
}
ポインタを安全でない方法でキャストしたりするので割り切って全体を unsafe にしている。もっと Rust 力があれば安全に書けるかもしれないが、後述する GlobalAlloc が unsafe なのであまり頑張っても意味がないかもしれない。
GlobalAlloc
さらに、さきほど作成したアロケータを Rust のVec<T>
やString
などの標準ライブラリのメモリ割り当てにも使うようにする。ホスト環境とのデータのやり取りのみに自作のアロケータを使う場合はこの作業は不要だが、デフォルトのアロケータはバイナリサイズが大きく(10KB ぐらいになる)、使用するアロケータは一つに統一しておいたほうが良い。
標準ライブラリのアロケータを自前のものに差し替えるには以下の二つを行う。
- GlobalAllocトレイトを実装する型を作る
- その型の static なグローバル変数を
#[global_allocator]
属性をつけて定義する
GlobalAlloc
を実装するには以下の二つのメソッドを実装する。Layout
はサイズとアライメントを保持する構造体。
unsafe fn alloc(&self, layout: Layout) -> *mut u8;
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);
これら二つのメソッド、&mut self
ではなく&self
を取る。これは GlobalAlloc を実装する型が static なグローバル変数として使われることからくる制約だろう。
とはいえメモリの割り当てと解放にはアロケータの状態を変更しなければならない。こういう場合の Rust のイディオムは内部可変性を使うことだろう。ここではUnsafeCell<T>
を使う。内部可変性を提供する型を書く際は排他制御をして各種の競合に注意しなければならないが、Rust は WebAssembly 向けのスレッドはまだサポートしていないので今回はシングルスレッドを前提にする。
struct CustomAlloc {
heap: UnsafeCell<Heap>,
}
impl GlobalAlloc for CustomAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
(*self.heap.get()).alloc(layout.size())
}
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
(*self.heap.get()).dealloc(ptr)
}
}
unsafe impl Sync for CustomAlloc {}
static ALLOC: CustomAlloc = CustomAlloc {
heap: UnsafeCell::new(Heap { /* ... */ }),
};
UnsafeCell<T>
はSync
を実装しないので、シングルスレッドで使う前提でSync
を明示的に実装している。これを忘れるとCustomAlloc
を static な変数として使おうとしたときにコンパイラに怒られる。
これで実装は一通り完了したのであとは実際に使ってみる。
wasm-bindgen
冒頭に説明したように wasm とホスト環境で文字列や配列をやり取りする際には面倒な手順を踏む必要がある。文字列を渡す場合を例に具体的なステップを見てみると、(1) ホスト側からメモリ領域を確保するよう wasm インスタンスに要求する、(2)ホスト側で wasm のメモリ領域に文字列の内容を書く、(3)データを書いたアドレスとサイズを wasm の関数に渡す、(4)wasm の関数では渡されたアドレスとサイズから String なり &str に変換する、という作業が必要になる。
この部分も自作しようかと考えたが、結構煩雑になるのでこの変換部分は wasm-bindgen に頼ることにした。wasm-bindgen を使うとホスト(JavaScript)と wasm 側両方のラッパーを作ってくれるのでとても楽になる。
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn to_uppercase(s: &str) -> String {
s.to_uppercase()
}
あとはビルドして wasm-bindgen の CLI で後処理をすれば最終的な wasm/JS ができる。wasm-bindgen はデフォルトだと Webpack で処理する前提の ES modules 形式で出力するが、オプションを指定することでブラウザで直接読み込める形式にすることもできる。
$ cargo build --release --target wasm32-unknown-unknown
$ wasm-bindgen --browser --no-modules target/wasm32-unknown-unknown/release/wasm_custom_allocator_example.wasm --out-dir public/dist
wee_alloc
アロケータを差し替える主な動機はバイナリサイズの削減だろう。今回は理解を深める目的で自分でアロケータを書いてみたが、実用的には独自アロケータを書くよりもwee_allocを使うのが良いと思う。アロケータは unsafe なコードがどうしても多くなるし、unsafe なコードをきちんと書くのは相当難しい。
参考
正月休み
実家に帰ったり、おせち食べたり、三社参りしたり、わりと普通の年末年始を楽しんだ。年末に崩した体調も回復した。ToDoリストの進捗は芳しからず。辞書ビューアをひと段落させたかったがコーディングはあまり進まなかった。
年をまたいだことだしサイトのトップページのリストを年単位でグループ分けすることにした。日記的なエントリを書きやすくなると思う。
ひそかに期待していたsteps to phantasienの更新はないらしい。休みの期間にちょくちょくチェックしていたのだけど残念。
2018年の振り返り
今年は Github の草を生やすのを日課にしていた。内容は二の次で些細な変更、例えばタイポの修正とかでもいいから毎日コードを書く。対象はほぼプライベートリポジトリ。

旅行してたり体調不良の時以外は埋まっている。習慣化できたのは良かった。ただ草をはやすのを目標にすると、難しいコーディングを避けて簡単なバグ修正をしたり、小さい検証コードを書いたりしがちであった。十分な時間を確保できないと、まとまったコードを書く気にならないのが問題だったと思う。来年はもうちょっと意味のあるコードを書くようにしたい。
あと学生だった時以来のブログを再開した。文章を書く練習をしたい、というのと学習したことを記憶に定着させたい、というのが動機。勉強したことを忘れないように記事にする、というのは nhiroki さんが言っていて、実際に効果あるなあと感じている。技術的な内容に限定する気はないので今後は日常の事とか趣味のこととか書いていきたい。shinh さんの日記みたいな、思ったことをふらっと書くスタイルとかやってみたいし、森田さんのブログみたいなちょっと詩的な感じのやつとか憧れたりする。
Rust
技術的なところでは今年一番時間を使ったのが Rust の勉強。本が 2 冊出たのと、Raph Levienが最近凝っているらしいというので勉強を続けている。自作の辞書ビューアを Rust で書き直したりしているが、まだ手になじまない。実際のアプリを作ろうとしたときに制約がきつくてつらい、イディオムがわからない、みたいな気持ちになる。Rust を学習すること自体は楽しいので来年も続けると思う。
LeetCode
コーディング面接対策で有名なサイト。面接で聞かれそうな問題が豊富にある。競技プログラミングは早々に挫折した身なのだけど、こちらは基礎的なアルゴリズムやデータ構造を知っていれば自分にも解ける問題が多い。動的計画法が苦手だという意識があったので 4 月 5 月あたりに集中的に解いていた。これまでのところ Solved は 113 個。今後もちょっとづつ解いていこうと思っている。
英語
昨年の 12 月から英会話スクールに通っている。ちょうど一年過ぎたぐらい。この間スクールが定めるレベルを一つクリアした。割と効果を実感していて、壊れた英語でもしゃべること自体の抵抗感がなくなった。受講料が高いのでコストパフォーマンスに見合うかどうかは正直わからないが、来年も続けようと思っている。あとひとつレベルをクリアしたい。
散歩とポッドキャスト
健康と趣味(町散策)を兼ねてできるだけ歩くようにしている。ここ数年は一日 10km ぐらい歩くのを目標にしていて、今年もだいたい達成できた。散歩しているときはいつもポッドキャストを聞いている。最近はポッドキャストの番組が増えてうれしい。今年よく聞いた番組たち:
- Misreading Chat
- mozaic.fm
- backspace.fm
- Freakonomics
- omoiyari.fm
- EM . FM
- ajitofm
- Rebuild
- 愚者の宮殿
- dex.fm
- engineer meeting podcast
- yatteiki.fm
写真
長いこと買うか悩んでいた Sony α7R III を冬前に購入した。写真撮るのが一段と楽しくなった。街撮りメインだけどポートレートとかも練習したい。
RustのSend
疑問: RustのライブラリAPIの定義を見ていたら制約にT: Send
と書いてあった。なぜこのAPIはSend
を要求するのか。
短い答え: そのAPIはスレッドをまたいで値を渡す。その値の所有権をスレッド越しに渡してもデータ競合を起こさない、ということを要求するため。Send
でない型はたいていRc<T>
かポインタ。
これはAPIドキュメントを見て疑問の思ったことを調べて、使う側からどう解釈すべきか、という視点から理解しようとするシリーズの二番目の記事となる。前回に引き続き、お題はthread::spawn
。自分の浅い理解から書いているので間違いを含んでいるかもしれない。
thread::spawn()
に渡すクロージャの制約はFnOnce() + Send +'static
なので前回の記事で言及したFnOnce()
に加えてSend
を実装していないといけない。APIドキュメントには値を安全に別スレッドに送るためにSend
が必要だ、と書いてある。でもRustには所有権の概念があるから値を渡せばそれで安全になるんじゃないのか。逆に言うとスレッド越しに渡すと安全じゃない値ってなんなんだろうか。調べた感じだと、大体二つに分類される。
ひとつはRc<T>
。Rc<T>
な値は参照カウンタとともにヒープ上に確保される。レイアウトはこんな感じ:
+--------------+------------+-------+
|strong ref cnt|weak ref cnt| value |
+--------------+------------+-------+
Rc::clone()
を呼ぶと値を複製できる。つまり所有権を持つ変数を複数作れる。Rc::clone()
はヒープ上の参照カウントを増やすのだけど、この参照カウンタはアトミック変数じゃないし、排他制御も行わない。これらの値を異なるスレッドで生成したりドロップしたりしたら参照カウンタのデータ競合が起こる。なのでRc<T>
はSend
ではない。
一方、Rc<T>
と同じように内部可変性(interior mutability)を使っているCell<T>
やRefCell<T>
は、T
がSend
であればCell/RefCellもSend
になる。Cell/RefCellがスレッド安全でなくなるのは、Cell/RefCellへの参照をスレッド間で渡したときであって、所有権を渡すこと自体は安全な操作となる。
もうひとつのSend
でない型はポインタ。*const u8
とか*mut u8
とか。これらがSend
ではない理由はRc<T>
ほど自明ではない。ポインタをスレッド間で渡すこと自体はデータ競合を起こさない。安全でなくなるのはポインタをdereferenceするときだけど、そのときはどのみちunsafeでくくらなければいけない。だったらポインタをSend
である、としても安全性は損なわないような…などと思いつつ調べていたらNomiconに言及があった。主に書き手に注意喚起する目的らしい。言語設計上の選択だったんだろう。
ちなみに参照&T
のほうはSend
を実装している。理由はコンパイラが借用ルールを使って安全でない参照の利用を検知できるから。
自分で定義した型はどうか。型がSend
であるかどうかは基本的にコンパイラがよしなに判断してくれる。内包する型が全部Send
を実装していれば、その型もSend
になる。内包する型のうち一つでもSend
でない型があれば、定義した型もSendを満たすことができない。
コンパイラにお任せできないこともある。メソッドをスレッド安全でない方法で実装したときなどがこれに該当する。この場合はimpl !Send for T
みたいにして明示的にSendを実装しない、と表明する必要がある。
thread::spawn()
に話を戻す。thread::spawn()
はクロージャを受け取る。API定義はそのクロージャがSend
を満たしていないといけない、と言っている。ではクロージャがSend
かどうかはどう決まるのか。それはクロージャの作り方によって決まる。クロージャがSend
でない値をキャプチャしていたら、そのクロージャはSend
ではない。ただし、Send
ではない値をクロージャの内部で使っていても、クロージャ自体はSend
になることができる。
// Error
fn f1() {
let v = Rc::new(42);
thread::spawn(move || {
let _v2 = Rc::clone(&v);
});
}
// OK
fn f2() {
thread::spawn(|| {
let _v = Rc::new(42);
})
}
API定義でSend
を見かけたときの心構えとしてはどう考えればいいか。ほとんどの型がSend
であることを考えれば、スレッド越しに値を渡すよということぐらいで特に気にしなくても良さそう、というのが今の理解。
余談だけど、スレッド安全に関するもう一つのトレイトとしてSync
がある。こちらはthread::spawn()
と直接関係がなかったのであまり追っておらず、理解があやふやなので言及を避けた。
Rust の Fn, FnMut, FnOnce の使い分け
Rustでスレッドを起動するにはthread::spawn()を使う。この関数は引数にクロージャをとるのだけど、引数の定義を見ると、 F: FnOnce() -> T + Send + 'static
となっていて初見では理解するのが難しい。ここではFnOnce()
、つまりクロージャの型について、どういう風に使い分けるのか、といった視点で見ていこうと思う。
Rustにはクロージャを表す型が3つある。
- Fn()
- FnMut()
- FnOnce()
型が3つ必要なのはRustが値の所有権や借用、生存期間をきちんと把握するためである。
Fn()
は使う側、引数として受け取る側としては使い勝手が一番よい。何度でも呼び出せるし、クロージャ自体が可変じゃなくてもいい。これは立場を変えると、クロージャを渡す側としてはFn()
は制約が一番厳しいことを意味する。Fn()
として渡されるクロージャは、何度実行されても所有権や借用のルールを破らないようにしなければならない。例えば、値の所有権をとってそれを消費するクロージャはFn()
としては渡せない。一度値を消費してしまったら二度は使えないから。以下のコードはコンパイルエラーで通らない。
fn do_something<F: Fn()>(f: F) {
f();
f();
}
fn main() {
let nums = vec![1,2,3];
do_something(move || {
for n in nums { ... } // Consume `nums`
});
}
FnOnce()
の使い勝手はFn()
の逆と言える。使う側としての制約は一番厳しい。一度しか実行できないし、実行にはそのクロージャの(借用ではなく)所有権が必要となる。一方、渡すクロージャの自由度は大きい。クロージャは一度しか呼ばれないことが保証されているので、そのクロージャ内で値を消費してもいいし、生存期間さえ守っていれば可変参照を取って値を変更することもできる。
fn do_something<F: FnOnce()>(f: F) {
f();
// Can't use `f` more than once
}
fn main() {
let nums = vec![0,1,2];
let mut greeting = "Hello".to_string();
let greeting_ref = &mut greeting;
do_something(move || {
for n in nums { ... } // Consume `nums`
greeting_ref.push_str(", World"); // Mutate value via mutable ref
});
println!("{}", greeting);
}
この流れで行くとFnMut()
はFn()
とFnOnce()
の中間的な立ち位置になる。値の消費はしないけれど可変参照経由で変更はする、みたいなクロージャはFnMut()
として取り扱うことができる。FnMut()
は何度も呼べるけれど、内部に可変参照をもっているのでそのクロージャを実行するにはmutを必要とする。
fn do_something<F: FnMut()>(mut f: F) {
f();
f();
}
fn main() {
let mut greeting = "Hello".to_string();
let greeting_ref = &mut greeting;
do_something(move || {
greeting_ref.push_str(", World");
});
println!("{}", greeting);
}
ここでthread::spawn()
に立ち戻ってみよう。ライブラリ関数としては利用方法にできるだけ制限をかけたくない。ライブラリ利用者側、つまりクロージャを渡す側にとって一番制約がゆるいのはFnOnce()
である。おそらくこれがspawn()
がFnOnce()
なクロージャを受け付けるようになっている理由だろう。spawn()
は渡されたクロージャを一度実行すればよいだけなので、利用者側の制約が厳しいFn()
を要求する必要はない。
自分がAPIを提供する立場だとしたら、引数として受け取るクロージャの型には注意しておきたい。ライブラリの中身を実装している最中に、コンパイラを通すためだけに安易にFn()
を要求するAPIを作ってしまうと、あとで利用する側で困る、みたいなことになりかねない。
ただ、thread::spawn()
と似たようなAPIを自作して提供する場合にはひとつ落とし穴がある。公式ドキュメントで言及されているので、自分で作ってみようとする前に目を通しておくとよい。自分はこれに悩んで時間を浪費してしまった。
追記
Send
について記事を書いた。