Vim を WebAssembly に移植した

久々のブログです.

6月ぐらいにWebAssembly の仕様をざっくり読んだので,なんか WebAssembly でやりたいなと思って,Vim を WebAssembly に移植してブラウザで動くようにしてみました,という話です.

github.com

多分実物を見ていただくのが一番早いので,下記のリンクにアクセスしてみてください.

デモページはこちら(下記の注意事項を先にお読みください)

  • 注意
    • デスクトップ版の ChromeFirefoxSafari か Edge を使ってください.どうやら macOS では Safari が一番動きが良いです.
    • デモページは全部で1MBほどのリソースを fetch します.モバイルネットワークなどからアクセスする場合はお気をつけください.
    • keydown でキー入力を取っているので,キー入力を横取りするブラウザ拡張などが有効になっているとうまく動かないかもしれません.その場合はプライベートブラウジング機能などを使ってアクセスしてみてください.
    • 今のところ有効になっているのは 'tiny' features のみです.そのため多くの制限があります.あと描画などにまだいくつかバグがある気がします
    • 入力しても何も起きない時は,一度画面の何処かをクリックしてみてください(Vim が描画されている <canvas/> がフォーカスを失っている可能性があります)
    • Mac でしかまだ試せていないので,他の OS で問題があれば教えてください…

こんな感じにページ全体に Vim の画面が表示されるはずです.これは実際にあなたのブラウザの上で動いている Vim が表示している画面なので,キー入力で自由に操作できます.

f:id:rhysd:20180709053707p:plain

今後について

まだ「とりあえず動いた」段階なので,次のようにして開発を進めていくつもりです

  • 'small' features を有効にしてビルドできるようにする.実行速度に問題が無ければ 'normal' も動かせるようにしたい
  • ブロッキングになっているイベントループを非同期に書き換えて Emterpreter(後述)を使わなくて良くする(かなりチャレンジング)
  • マウスのサポート
  • マルチバイト文字や IME のサポート
  • クリップボードのサポート
  • .vimrc をローカルストレージに保存できるようにする
  • WebComponents として wrap して,npm パッケージや ESM で配布できるようにする
  • PWA としてローカルのデスクトップアプリのように扱うことができるようにする
  • :write で保存ダイアログ経由でローカルに保存できるようにする

どうやって実装したのか

emscripten と Binaryen

そもそも WebAssembly が何かご存じない方は,先に MDN の日本語解説ページを読んでいただけると,技術の概要が分かると思います.

C や C++LLVM IR のコードをブラウザで動かすためのコンパイラツールチェーンとしてemscriptenというものがあります.emscripten は C をコンパイルするためのコンパイラやリンカ,libc などの標準ライブラリなどから成ります. emscripten は以前は C などのコードを asm.jsコンパイルしていたのですが,今ではデフォルトで WebAssembly 形式へコンパイルするようになっており,それには binaryen というコンパイラバックエンドが使われています.

今回は emscripten と binaryen を使って C で実装されている Vim のコードを WebAssembly にコンパイルできるようにしていきます.

ちなみに Mac では brew install emscripten binaryen で一発でインストールできます.

基本的な方針

もちろん,単にコンパイラgcc から emccemscripten が提供するコンパイラgcc インターフェース)に置き換えるだけではうまくいきません.

まず,Vim は ncurses などの何らかの端末ライブラリを用いて CUI の画面を描画しますが,emscripten はどの端末ライブラリもサポートしていません.ncurses を WebAssembly に移植する(描画は <canvas/> などに任せる)のも理論的には可能ですが,膨大な作業量が必要です.

そこで,今回は WebAssembly 向けの UI を (gui_gtkgui_w32, gui_mac のような) GUI gui_wasm として実装し,gui_wasm が有効になった場合には常に GUI 版を使うことにします.これによって端末版の Vim が起動されるパスはなくなるので,ncurses は不要になります.

では,VimGUI 版はどう実装されているかが次の問題になります.これは他の GUI 実装を参考に,gui.h を眺めれば何となく分かります. gui_*.c の中に実装されたいくつかの関数が Vim のコアから呼び出されるので,それらを実装すれば良いです.Vim のコア部分から呼び出される GUI が実装すべき関数は大きく2種類あり,

  1. ユーザからのインプットを待つ系(gui_mch_wait_for_chars()gui_mch_update()
  2. 描画系(特定の row/column に文字を描画したり,画面をスクロールするなど)

に分けられます.なので,WebAssembly でこれらの GUI 実装関数たちをどう実装するかを考えれば良いです.具体的には 1. をユーザからのキー入力を行う DOM イベントを待つことで実装し,2. をブラウザのページ内に置いた <canvas/> などに描画する形で実装すれば良さそうです.

ですが,ここで1つ問題があります.WebAssembly は(少なくとも今は)DOM に直接アクセスするインターフェースを持っていません.なので,上記を C のレイヤーでは直接実装できません. この問題を解決するため,JavaScript で書いたランタイムを用意します.C のレイヤーから JavaScript に描画イベントを送り,JavaScript 側で描画するようにします.また,DOM のキー入力イベントを JavaScript で拾い,それを C のレイヤーに送ることで伝えるようにします. C から JavaScript のコードを呼んだり,逆に JavaScript から C のコードを呼ぶには emscripten の API を利用することができます.

あとは WebAssembly モジュールと JavaScript のランタイムを HTML ファイル内で読み込み,.wasm モジュール内にある _main 関数を呼べば Vim がスタートします.

f:id:rhysd:20180709053830p:plain

詳細な実装

具体的なビルドの流れは下記の図のようになっています.

f:id:rhysd:20180709053904p:plain

C のソースファイルが Clang でそれぞれのソースファイルの LLVM bitcode にコンパイル(&最適化)され,llvm-link で1つの LLVM bitcode ファイル vim.bc にリンクされます.さらにその vim.bc と,エントリポイントとなる HTML ファイルのテンプレート template_vim.htmlJavaScript のランタイム runtime.js,カラースキームなど最低限のファイルを合わせて emscripten が提供するコンパイラ emcc を用いてビルドすると vim.htmlvim.wasmvim.js などの 'executable' がコンパイル結果として吐き出されます.あとはそれらをHTTPサーバでホストして vim.html にアクセスすれば OK です.

実装は configure の修正から始まります.まずは何もしない空の gui_wasm.c を実装します.この時点では実装が必要な関数の中身は空っぽ(もしくは戻り値が必要な場合は単に return FAIL; とだけ書いておく)です. emscriptenコンパイラである emcc を用いてまずは空の GUI 実装が通るところまで持っていきます.emscriptenconfigure を良い感じに emcc 向けに実行してくれる emconfigure という wrapper スクリプトを提供しているのでそれを使って ./configure を走らせます.

前述したターミナルライブラリのチェック(check tlib)を無視するようにしたり,なぜか uint32_t のチェックが通らないので見なかったことにする(手元で emcc を使って再現しようとすると再現しない)など,それなりのワークアラウンドsrc/configure.ac に入れていきます.

./configure が通るようになったところで,今度はビルドが通るように src/Makefile.c.h を修正していきます.emconfigure と同様に emscriptenemmake という emcc 向けにセットアップして make を走らせてくれる wrapper スクリプトがあるのでそれを使います(emmake make -j).

emscriptenLinux ライクな実行環境 (システムコール実装や libc など) を提供してくれるので,基本的には Linux 向けにビルドしてやれば良いですが,修正は必要になります. 例えば,fork (2) や PTY,シグナルは emscripten では利用できません.前者2つの機能は無効にし,シグナルは emscripten が stub を差し込むようにします.他にも WebAssembly では提供できない機能を #ifndef FEAT_GUI_WASM で囲むなどして無効にしていく地道な作業をします.

ビルドが通るようになったら,gui_wasm.c を実装していきます. 先述のとおり,描画イベントを扱う部分とユーザの入力を待つ部分があります.

前者は JavaScript の関数を呼ぶ形で実装します.JavaScript 側で呼びたい関数はプロトタイプ宣言だけしておけば emscripten がつないでくれるので,宣言を src/wasm_runtime.h に書いておきます(vimwasm_ で始まる関数群です).JavaScript 側では emscripten の API で JavaScript ライブラリのかたちで定義しておけば,C から vimwasm_* な関数を呼んだときに自動で JavaScript 側の関数につないでくれます(wasm/runtime.js).

後者のユーザの入力を待つ部分についても DOM の keydown イベントを拾ってユーザが入力した文字の文字コード(またはバックスペースキーなどで使われる2バイトのスペシャルキーコード)を C 側の関数に渡せば,あとは C 側でバッファにそれらの入力を追加する(add_to_buf())ことで実装できます.

ただ,ここで今回最も大きい問題にあたってしまいました.VimGUI 実装に同期的に入力待ちをする関数の実装を要求します.これはブロッキングな処理です. ですが,JavaScript や C からコンパイルされた WebAssembly は(今のところ)ノンブロッキングな処理しか行なえません. 要は,問題にならない範囲で sleep() を挟んでユーザの入力を良い感じに同期的に待つということが WebAssembly や JavaScript では出来ませんでした.emscriptensleep() 関数のコンパイルに対応していますが,なんとその実装はビジーループでした.これではブラウザで Vim を走らせると常にビジーループが回り,CPU をコア1つ専有してしまいます(実際になりました).

この問題を解決するために Emterpreter という emscripten の試験的な機能を使います.Empterpreter は emscripten_sleep() というC関数を提供します.これは C の側では sleep() とほぼ同様に使うことができ,しかもビジーループするようなこともありません. これはかなり強引な(しかしこれを実装し切るのはさすが kripken さんですが)実装になっています.コンパイル時に LLVM 中間表現レベルで emscripten_sleep() など同期的な処理が必要な関数呼び出しの制御フローを直接書き換えます.Emterpreter 有効時の emccコンパイル時に emscripten_sleep() の呼び出しを見つけると,「その呼び出し時点での実行情報をスタックに積んでおき,setTimeout() して一定時間後にそのスタックを resume することで処理を再開するようなコード」に LLVM IR レベルで書き換えます. これによって C 側では同期的に sleep() しているように見える処理が,実際には setTimeout を用いた非同期なコードとして実行されます.

また,emscripten では get_char() (poll() で使われていた) の実装が,なんと window.prompt() を使って実装されているので,謎の入力ダイアログが出まくる問題もありましたが,こちらは emscripten の FileSystem API を使って「もう標準入力からの入力はない」ことを示す(Module.stdin をすげ替える)ことで解決しました.

最後に emscripten の仮想 FileSystem と files preload plugin を使ってカラースキームファイルなどを Vim から読めるようにして,とりあえずの実装を完了しました.

7/1 ぐらいから手を動かし始めたので,今日(7/8)まで,とりあえず動くようになるのに8日ほどかかった感じです.実装行数は今時点で 6832++/2276-- でした.

f:id:rhysd:20180709054115p:plain

その他困ったところ

  • cprotoemcc ではうまく動かない(代わりに gcc を使う)
  • emconfigure おそい
  • select() は実装されているが exceptfds 引数は実装されていないので使えない
  • emscripten の JS library は特殊なプリプロセスを挟んでからビルド結果の JavaScript ファイルに挿入されるので注意が必要
  • emscripten は JS library の最適化に uglifyjs を使うが,uglifyjs は ES5 までの構文しか対応していない(なぜか const は使える).
  • Emterpreter を有効にすると,JavaScript から C の関数を呼ぶ時に文字列が渡せない.JavaScript からの C 関数の呼び出しは Emterpreter がコンパイル時に変換できないので,文字列を渡そうとするとスタックを壊してしまうっぽい.Module.ccall(){ async: true } を指定しても駄目
  • Emterpreter を有効にすると,C のシグネチャJavaScriptシグネチャが合わなくなる関数が発生する
  • Emterpreter を有効にすると,vim.bc からのコンパイルの時間が長くなる
  • Emterpreter を有効にすると,部分的に Emterpreter のインタープリタが実行するバイトコードが必要になるため,バイナリサイズが膨れる

感想

WebAssembly は最近気になっているので,そのツールチェーンである emscripten を使って実際に動くものに触れて良かったです.思ったよりもちゃんと動いているなという印象でした.

また,Vim のコードの GUI 部分についてもかなり理解が深まりました.普段端末の Vim を使っているので,この辺りを読む機会はほぼ無かったのですが,今回良い勉強になりました.

Vim のコード自体は何十万行もあるためそれをブラウザに移植するのはかなり大変ですが,emscripten や WebAssembly などの巨人の肩に乗ることで,わずか数千行で動くところまで持っていけるというのは,巨人の大きさを感じさせられて良い体験でした.

React で Octicon を使うためのコンポーネントライブラリ書いた

OcticonsGitHubオープンソースで配布しているアイコンセットです.

Octicons は以前はウェブフォントを利用してつくられていたのですが,最新のnpm の octicons パッケージの中身を見る限りでは,最近では SVG での配布になったようです. 以前は classocticon octicon-alert などを指定すれば使えていたのですが,それができなくなってしまいました.

React を使って書いているので Octicon の React 向けコンポーネントを探してみると react-octicon がヒットするのですが,

  • ウェブフォント時代の古い Octicon(v4系)のみサポート
  • webpack が必須(CSS ローダを使って Octicon の CSS をロードしている)

という点で私のユースケースに合いませんでした.

というわけで,最新の Octicon(v7.0.1)で動く React コンポーネントライブラリをつくりました.

github.com

特徴は

  • 依存なし: octicons パッケージの SVG を React コンポーネント内に直接埋め込んでいるので依存パッケージなし.よって特定の bundler にも依存していない
  • TypeScript 対応済み.本体も TypeScript で書かれてます

使い方

npm パッケージとして配布しています.

$ npm install --save react-component-octicons

でインストールできます.

import * as React from 'react';
import { render } from 'react-dom';
import Octicon from 'react-component-octicons';

render(
    <div>
        <Octicon name="alert" />
        <Octicon name="star" />
    </div>,
    document.getElementById('root'),
);

のように <Octicon/> コンポーネントとして使えます.ここでは TypeScript で書いていますが,もちろん JavaScript からも使えます.

name プロパティに Octicon のアイコン名を指定するだけです.

追記:アイコンのサイズを変えられるようになりました

import * as React from 'react';
import { render } from 'react-dom';
import Octicon from 'react-component-octicons';

render(
    <div>
        // Normal size
        <Octicon name="alert" />

        // Twice bigger
        <Octicon name="star" zoom="x2" />

        // Size 100px x 100px
        <div style={{width: '100px', height: '100px'}}>
            <Octicon name="flame" zoom="100%" />
        </div>
    </div>,
    document.getElementById('root'),
);

x{N}{N} は整数か小数点数)とすると N 倍のサイズのアイコンになります(例:x4, x1.5). また,{N}%{N} は0〜100)とすると親の要素に対して N% のサイズになります.なので 100% を指定すれば親のサイズに合わせたサイズのアイコンになります.

Typo Safety

name プロパティは 文字列リテラル型で定義しているので, 間違ったアイコン名を指定しているとコンパイルエラーになります.

render(
    <Octicon name="allow-right" />,
    document.getElementById('root'),
);

例えば上記のアイコン名は arrow-rightallow-right にタイポしていますが,これをコンパイルすると

test.tsx(5,17): error TS2322: Type '{ name: "allow-right"; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Octicon> & Readonly<{ children?: ReactNode; }> & R...'.
  Type '{ name: "allow-right"; }' is not assignable to type 'Readonly<OcticonProps>'.
    Types of property 'name' are incompatible.
      Type '"allow-right"' is not assignable to type 'OcticonSymbol'.

のようにエラーになります.

まとめ

React で Octicon を使うためのコンポーネントライブラリ react-component-octicons をつくりました. 自分が使う用途でつくったものですが,もし Octicon を React で使う機会があれば是非使ってみてください.

GitHub のプルリクで blame する ghpr-blame.vim つくった

先日 kazuho さんが git blame でプルリクを表示するスクリプトをつくってらっしゃって,便利そうだったので Vim プラグインをつくってみました. ファイルの各行がどのプルリクで変更されたかを確認し,気になるプルリクはその場で詳細を確認することもできます.

github.com

スクリーンショット

使い方

インストールはお好みの Vim プラグインマネージャを使うなどしてください.

1. ファイルを開いて :GHPRBlame を実行

:GHPRBlame を実行すると裏で git-blame が走り,カレントバッファのファイルの各行のプルリク情報を git blame --line-porcelain で引っ張ってきます. 引っ張ってきた情報を元にカレントバッファの左に細長い一時バッファが開き,そこに各行に紐付いたプルリク番号が表示されます(プルリクに紐付いていない行は何も表示されません). これによって,各行がどのプルリクによって入ったものかが分かります.

2. プルリクの詳細を見たい行で <CR> を押す

プルリクの詳細が見たい位置にカーソルを移動し,<CR>g:ghpr_show_pr_mapping で別のキーにも変更可能)を押すと,カレントバッファのウィンドウの右か下に一時バッファが開き,そのプルリクの情報(タイトル,URL,作成者,マージされた日付,本文)を確認することができます. <CR>を押した際に裏で GitHub API を叩いて対象のプルリクの情報を引っ張ってきているので,表示に少し時間がかかります.一応キャッシュしているので2回目以降同じプルリクは高速に表示できます.

3. 終了する

一番左のプルリク一覧のウィンドウを閉じると自動で右側のウィンドウも閉じ,キャッシュや<CR> マッピングを削除します.もしくは :GHPRBlameQuit で明示的に終了することもできます.

まとめ

ファイルの各行の変更をプルリク単位で調べられる ghpr-blame.vim をつくりました.これ系のは tig みたいに TUI なツールのほうが便利かなと思いつつ,結局コードを読む時は Vim で開いて追いながら読むことが多いので Vim プラグインとしてつくってみました. もし気になったらお試しください.