WAT (WebAssembly Text Format) と Common Lisp で遊ぶ ~準備編~

lisp Advent Calendar 2020 7日目の記事です。

WebAssemblyを真面目に試そうと思ったらCやらRustやら他の言語からコンパイルして生成するものかと思いますが、せっかくそのテキスト形式 = WAT (WebAssembly Text Format) がS式なので直接書いて(書かせて)ちょっと遊んでみようという記事です。

2, 3回ぐらいに分けて書いていこうかと思いますが、今回は遊ぶ前の準備編で、静的なWATとそれを呼び出すJavaScriptファイルを用意して、(WATはバイナリ形式のWASMに変換した上で)配信するだけのCommon Lispサーバを作るあたりまでを扱います。

色々遊んでいるリポジトリは下記になります。

github.com

今回は下記ぐらいのコミット時点の内容になります(所々余分なコードがあるので、本文では適宜整理したものを記載しています)。

https://github.com/eshamster/try-wasm-with-cl/tree/f36397c70bfb8dd3a39d70bffb70e7370a6a87b4

また、下記あたりの記事を参考にしています。


目次


WABT (WebAssembly Binary Toolkit) のインストール

テキスト形式のWATはそのままではブラウザで解釈できないので、バイナリ形式のWASMにする必要があります。そのためのツールである wat2wasm コマンドが含まれる WABT (WebAssembly Binary Toolkit) をインストールしてパスを通します。

個人的にRoswellとEmacsを入れて開発に使っているDockerイメージ eshamster/cl-devel2 では下記のようにインストールできました。なお、素のAlpine上でも同じようにmakeできました(念のため、gitがないので apk add git は追加で必要になります)。

FROM eshamster/cl-devel2:latest

RUN git clone --recursive https://github.com/WebAssembly/wabt && \
    apk add --no-cache cmake clang binutils gcc libc-dev clang-dev build-base && \ 
    cd wabt && \
    make

ENV PATH /root/wabt/bin:${PATH}

配信するファイル(など)の用意

HTML

HTMLははなから cl-markup を利用して Common Lisp で書いていますが、見れば分かるとは思うのでそのまま貼っておきます。

            (html5 (:head
                    (:title "try-wasm-with-cl")
                    (:script :src "js/main.js" nil))

ソース

  • <script> タグで js/main.js をロードする...だけの内容です

JavaScript

JavaScript側は次のようなファイルを用意します。

/* static/js/main.js */
var importObject = {
    console: {
        log: console.log
    }
};

WebAssembly.instantiateStreaming(fetch('wasm/main.wasm'), importObject)
    .then(results => {
        results.instance.exports.exported_func();
    });

ソース

  • WASM側から console.log を呼び出せるように importObject に詰めて受け渡す
  • WebAssembly.instantiateStreaming を利用してサーバから指定のWASMをロードする
  • ロードできたらWSAM側で定義された exported_func を呼び出す

ちなみに、次のコミットJavaScriptファイルは消滅して、Parenscriptと拙作ps-experiment(quiecklispリポジトリ未登録)を利用して Common Lisp コードから生成するようになりますが、本題ではないのでおいておきます。

WAT (WebAssembly Text Format)

実際に配信するのはWATではなくWASMですが、wat2wasm を呼び出してWASMにする処理はCommon Lisp側でやるので、ここではWATまで用意します。

;; static/wasm/main.wat
(module
  (import "console" "log" (func $log (param i32)))
  (func (export "exported_func")
    i32.const 100
    call $log))

ソース

  • JavaScript側から渡される console.log 関数を $log という名前でimportする
  • exported_func という名前で関数をexport & 定義する
  • exported_func 内で $log 関数に定数100を渡す
    • JavaScriptconsole.log(100) とするのと同じ

ここからコンソール上でWASMを生成したい場合は wat2wasm main.wat -o main.wasm のようにすれば main.wasm が生成されます。

ちなみに、何コミットかするとリポジトリからWATファイルが消滅してCommon Lispから書き出すようになりますが、その辺りの話が次回になります。

サーバ側の用意

lack application の作成

https://github.com/eshamster/try-wasm-with-cl/blob/f36397c70bfb8dd3a39d70bffb70e7370a6a87b4/server.lisp あたりの話です。

サーバ側は、まず軽量Webアプリケーションフレームワークである ningle を利用して、ルートへのアクセス時に先程のHTMLを返すような lack application を作成します。

(defvar *app* (make-instance 'ningle:<app>))

(setf (ningle:route *app* "/" :method :GET)
      (lambda (params)
        (declare (ignore params))
        (with-output-to-string (str)
          (let ((cl-markup:*output-stream* str))
            (html5 (:head
                    (:title "try-wasm-with-cl")
                    (:script :src "js/main.js" nil)))))))

これに lacklack:builder を利用して2つ程ミドルウェアをくっつけて最終的な lack application を構成します。そして、clackclack:clackup で起動して、無事ブラウザからアクセできるようになります(下記の start-server)。

;; --- 諸々の変数 --- ;;

(defvar *app* (make-instance '<app>))

(defvar *server* nil)

(defvar *script-dir*
  (merge-pathnames "static/"
                   (asdf:component-pathname
                    (asdf:find-system :try-wasm-with-cl))))

(defvar *wat-path*
  (merge-pathnames "wasm/main.wat" *script-dir*))

(defvar *wasm-path*
  (merge-pathnames "wasm/main.wasm" *script-dir*))

;; --- lack application の build & 起動する関数 --- ;;

(defun start-server (&key (port 5000) (address "0.0.0.0"))
  (setf *server*
        (clack:clackup
         (lack:builder ;; ミドルウェア1: 本文へ
                       (lambda (app)
                         (lambda (env)
                           (let ((res (funcall app env))
                                 (path (getf env :path-info)))
                             (when (scan "\\.wasm$" path)
                               (wat2wasm *wat-path* *wasm-path*)
                               (setf (getf (cadr res) :content-type)
                                     "application/wasm"))
                             res)))
                       ;; ミドルウェア2:
                       ;; .js, .wasm へのアクセスは static フォルダ以下の静的ファイルを返す
                       (:static :path (lambda (path)
                                        (when (scan "^(?:/js/|/wasm/$)"
                                                    path)
                                          path))
                                :root *script-dir*)
                       ;; 先程作った lack appalication
                       *app*)
         :port port
         :address address)))

ミドルウェア2は単に静的ファイルを返すように設定しているだけです。

ミドルウェア1の方は *.wasm へのアクセスがあった場合に下記のようなことをします。

  1. 後述の wat2wasm を利用して main.watmain.wasmコンパイルする
    • 中身は wat2wasm コマンドを呼び出しているだけです
    • 本来は XXX.wasm へのアクセスなら XXX.wat -> XXX.wasmコンパイルをするようにすべきでしょうが、そこは手抜きしてます
  2. HTTPヘッダの content-typeapplication/wasm に設定する
    • JavaScript側の WebAssembly.instantiateStreaming が同 content-type を要求するためです

WASMファイルの書き出し

https://github.com/eshamster/try-wasm-with-cl/blob/f36397c70bfb8dd3a39d70bffb70e7370a6a87b4/compiler.lisp

ファイルが異なるので一応項を分けましたが、WAT->WASMへの変換は下記の通り wat2wasm コマンドを呼び出し、wat-path で指定されたWATファイルをWASMに変換し、wasm-path で指定されたパスにアウトプットするだけです。

(defun wat2wasm (wat-path wasm-path)
  (uiop:run-program (format nil "wat2wasm ~S -o ~S"
                            (namestring wat-path)
                            (namestring wasm-path))))

結果

ということで、下記のようにしてサーバを起動し、ブラウザから localhost:5000 にアクセスすれば、開発者コンソールに 100 が表示されるようになります。

> (ql:quickload :try-wasm-with-cl)
> (try-wasm-with-cl:start-server :port 5000)

なお、最新版ではquicklispリポジトリ未登録のps-experimentに依存しています。qlfileを置いているのでqlotで取ってくるのが簡単かと思います。

> (ql:quickload :qlot)
> (qlot:quickload :try-wasm-with-cl)
> (try-wasm-with-cl:start-server :port 5000)

次回

eshamster.hatenablog.com