Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8825

Node.js でつくる WASMコンパイラー - Extra1:WASIを使ってWASMを動かす

$
0
0

はじめに

Node.jsで小さなプログラミング言語を作ってみるシリーズを、「ミニコンパイラー」「ミニインタープリター」とやってきました。そして三部作(?)の最後として、 ミニNode.jsからWASMを生成する小さなコンパイラーに取り組んでいます。

今回の目的

前回で目標としていたNode.js-WASMコンパイラーの最低限の実装が終わりました。今回は生成したWASMをいろいろな環境で動かすべく WASI(WebAssembly System Interface)に対応させたいと思います。

WASI とは

WASIはWebAssemblyをウェブ以外の場所(ブラウザやNode.js以外の環境)で動かせる様にする取り組みです。

WASMのコードを、いろいろなプラアットフォーム上で動かせる様にシステムコールに相当するAPIを標準化する試みです。様々なランタイムが実装されていて、CDNのエッジサーバーや組み込みデバイスで動かす試みもあります。

  • wasmtime ... Rustで作られた、リファレンス的なランタイム環境
  • lucet ... Fastlyが取り組んでいる、CDNエッジ上でWASMを実行することを目指したランタイム
  • WebAssembly Micro Runtime ... 組み込みでも使えることを目指した、軽量ランタイム(JITコンパイラーなし、インタープリターのみ)

WASIを使えば、将来的にCDN上や組み込みデバイス上でWASMを実行できるはずです。ワクワクしますね。

WASIで使える関数

WASIではOSを抽象化して、ファイルやネットワークなどの入出力にアクセスできるようになります。実際にサポートされるAPIはこちらにまとめられています。

これを見ると、「System Interface」と言うだけあってC言語のprintf()やputs()などは存在せず、よりプリミティブな関数がサポートされています。今回のミニWASMコンパイラーの組み込み関数putn()/puts()を実現するために、次の関数を利用することにします。

Hello, WASI

WASI の実行環境

今回はWASIの実行にwasmtimeを使います。ビルドにはRustとcargoが必要です。

wasmtimeのビルド
$ git clone --recurse-submodules https://github.com/bytecodealliance/wasmtime.git
$ cd wasmtime
$ cargo build --release
$ ./target/release/wasmtime --version
0.7.0

文字列の出力

さっそくWASIを使った文字列出力にチャンレンジしてみます。次のWATファイルを用意しました。

hello_wasi.wat
(module
    ;; -- WASIの fd_write()をインポイートするため宣言 -- 
    ;;    fd_write(File Descriptor, *iovs, iovs_len, nwritten)
    ;;      -> Returns number of bytes written
    (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))

    (memory 1)
    (export "memory" (memory 0))
    (data (i32.const 16) "Hello WASI\n") ;; 'Hello WASI\n' をメモリ上に確保 (offset 16 bytes, length 11 bytes)

    ;; -- メイン関数は _start() としてエクスポート --
    (func $main (export "_start")
        ;; iov (バッファーのアドレスと、長さのセット)をメモリ上に用意
        (i32.store (i32.const 0) (i32.const 16))  ;; バアッファーの先頭アドレス(=offset)
        (i32.store (i32.const 4) (i32.const 11))  ;; バッファーの長さ

        (call $fd_write
            (i32.const 1) ;; ファイルでスクリプタ - 1:stdout
            (i32.const 0) ;; iovのセットへのアドレス
            (i32.const 1) ;; iovのセットの長さ - [buffer, length]のセットの数
            (i32.const 8) ;; *nwritten - 出力されたバイト数を受け取るポインター
        )
        drop ;; 戻り値として出力されたバイト数が帰ってきているので、それを破棄
    )
)
  • fd_write()関数をインポート
  • エントリーポイントとなるメイン関数を _start() という名前でエクスポート
  • 出力する文字列をメモリ上に確保
  • 「文字列バッファーの先頭アドレスと、その長さ」のセットをメモリ上に確保 ... iov
  • 先のiovのアドレスと、そのセット数を指定して、fd_write()を呼び出す

実行結果はこちら

$ wasmtime hello_wasi.wat
Hello WASI

無事出力されました。

WASI対応コンパイラー

これまで作った Node.js-WASMコンパイラー mininode_wasm_08.jsを、WASI向けに改造します。

WASI関数のインポート

いままでは呼び出し側(Node.js)でputn(), puts()の実体を用意したものをWASM内部でインポートしていました。その代わりにWASIのランタイムからfd_write()をインポートします。

// ---- compile simplified tree into WAT ---functioncompile(tree,gctx,lctx){// ... 省略 ...letblock='(module'+LF();// -- builtin func (imports) --block=block+TAB()+';; ---- builtin func imports ---'+LF();// --- normal WASM ---//block = block + TAB() + '(func $putn (import "imports" "imported_putn") (param i32))' + LF();//block = block + TAB() + '(func $puts (import "imports" "imported_puts") (param i32))' + LF();// --- WASI ---block=block+TAB()+'(import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))'+LF();// ... 省略 ...}

メイン関数の宣言

メイン関数のエクスポート宣言部分も_start()に変更します。

// ---- compile simplified tree into WAT ---functioncompile(tree,gctx,lctx){// ... 省略 ...block=block+TAB()+';; ---- export main function  ---'+LF();// --- normal WASM ---//block = block + TAB() + '(export "exported_main" (func $main))' + LF();// --- WASI ---block=block+TAB()+'(export "_start" (func $main))'+LF();// ... 省略 ...}

整数の出力 putn()

インポートしたfd_write()を内部で呼び出して、符号付32ビット整数を表示するputn()関数を作ります。あらかじめWATで記述した別ファイルを用意した関数を用意しておき、コンパイラでWATを生成する際に連結する方式ににします。

putn() は内部で次の処理を行います。

  • (1) 整数値を文字列で表現した時の桁数を算出する
    • この時、マイナス値の場合はマイナス記号分もカウントする
  • (2) 整数値がマイナスの場合は絶対値をとる
  • (3) 一桁ずつ取り出し、1文字のASCIIキャラクターに変換、メモリー上に格納する
    • 例) 1 --> 49 (0x31)
  • (4) 最後に改行文字(`\n')を入れる、マイナス値だったら先頭にマイナス記号を格納する
  • (5) fd_write()を呼び出すためのパラメーターをメモリー領域に準備する
    • パラメーターは、あらかじめメモリー上の決まった位置にダミーの値で確保
    • 毎回値を書き換えて使う
  • (6) fd_write()を呼びだす

これを全てWATで手書きするのはなかなか骨が折れます。そこで、一部をJSで記述して、前回までのコンパイラーを使ってWATを生成することにしました。直接メモリーをいじったり関数を呼び出すところはコンパイラーでサポートしていないので、手書きすることになります。

整数を文字列に変換する部分を _convI32ToString() として抜き出してJSで実装します。担当する処理は上記(1)~(3)の範囲です。

_convI32ToString()
function_convI32ToString(n){letrestValue=n;letisMinus=0;letdummy;if(_isMinus(n)){restValue=-n;isMinus=1;dummy=_storeChar(0,45);// minus mark '-'}letlen=_calcLength(restValue);letidx=len-1;letdigitChar=0;while(idx>=0){digitChar=_getOneDigit(restValue);_storeChar(idx+isMinus,digitChar);restValue=_div10(restValue);idx=idx-1;}returnlen+isMinus;}

実際にはさらに次の内部関数を呼び処理を行っています。
- _calcLength() ... 整数が文字列にした場合に何桁になるかを算出
- _getOneDigit() ... 整数の一の位をASCIIコードに変換
- _div10() ... 整数を1/10にする
- _isMinus() ... 整数がマイナス値かどうかを判定
- _storeChar() ... 1文字分をメモリーに格納するダミー
- 実際にはWASMのメモリーに値を格納する処理に後で置き換える

// calc char length of int32//  NOT support minusfunction_calcLength(n){letrestValue=n;letlen=1;while(restValue>=10){restValue=restValue/10;len=len+1;}returnlen;}// get 1 digit char codefunction_getOneDigit(n){constr=n%10;constc=48+r;// '0' + rreturnc;}// div 10function_div10(n){constd=n/10;// calc as intreturnd;}// --- for node direct ---//let _strBuf = '....................';function_storeChar(idx,charCode){puts(' _storeChar() called. idx, charCode bellow');putn(idx);putn(charCode);/* --- for Node.js direct --- *///let ch = String.fromCharCode(charCode);//_strBuf = _strBuf.slice(0, idx) + ch + _strBuf.slice(idx + 1);return0;}function_isMinus(n){if(n<0){return1;}return0;}

前回のWASMコンパイラー mininode_wasm_08.jsでコンパイルした結果の抜粋はこちらです。実際にはこれを手で修正して利用しています。

watの抜粋
 (func $_calcLength (param $n i32) (result i32)
    (local $restValue i32)
    (local $len i32)
    get_local $n
    set_local $restValue

    i32.const 1
    set_local $len

    loop ;; --begin of while loop--
      get_local $restValue
      i32.const 10
      i32.ge_s
      if
        get_local $restValue
        i32.const 10
        i32.div_s

        set_local $restValue

        get_local $len
        i32.const 1
        i32.add

        set_local $len

        br 1 ;; --jump to head of while loop--
      end ;; end of if-then
    end ;; --end of while loop--
    get_local $len
    return

    i32.const 88
    return
  )

  (func $_div10 (param $n i32) (result i32)
    (local $d i32)
    get_local $n
    i32.const 10
    i32.div_s

    set_local $d

    get_local $d
    return

    i32.const 88
    return
  )

  (func $_convI32ToString (param $n i32) (result i32)
    (local $restValue i32)
    (local $isMinus i32)
    (local $dummy i32)
    (local $len i32)
    (local $idx i32)
    (local $digitChar i32)
    get_local $n
    set_local $restValue

    i32.const 0
    set_local $isMinus

    get_local $n
    call $_isMinus

    if
      i32.const 0
      get_local $n
      i32.sub

      set_local $restValue

      i32.const 1
      set_local $isMinus

      i32.const 0
      i32.const 45
      call $_storeChar

      set_local $dummy

    end

    get_local $restValue
    call $_calcLength

    set_local $len

    get_local $len
    i32.const 1
    i32.sub

    set_local $idx

    i32.const 0
    set_local $digitChar

    loop ;; --begin of while loop--
      get_local $idx
      i32.const 0
      i32.ge_s
      if
        get_local $restValue
        call $_getOneDigit

        set_local $digitChar

        get_local $idx
        get_local $isMinus
        i32.add

        get_local $digitChar
        call $_storeChar

        get_local $restValue
        call $_div10

        set_local $restValue

        get_local $idx
        i32.const 1
        i32.sub

        set_local $idx

        br 1 ;; --jump to head of while loop--
      end ;; end of if-then
    end ;; --end of while loop--
    get_local $len
    get_local $isMinus
    i32.add

    return

  )

この生成した関数を使って、putn()を実現します。

putn()
  (func $putn(param $n i32)
    (local $strLen i32)
    get_local $n
    call $_convI32ToString ;; ret=Lenght
    set_local $strLen

    ;; write tail LF
    i32.const 12 ;; head of string buffer
    get_local $strLen
    i32.add
    i32.const 10 ;; LF
    i32.store8 

    ;; +1 length for tail LF
    get_local $strLen
    i32.const 1
    i32.add
    set_local $strLen

    ;; iov.iov_base 
    i32.const 4
    i32.const 12
    i32.store

    ;; iov.iov_len
    i32.const 8
    get_local $strLen
    i32.store

    ;; $fd_write
    i32.const 1 ;; file_descriptor - 1 for stdout
    i32.const 4 ;; *iovs - The pointer to the iov array, which is stored at memory location 0
    i32.const 1 ;; iovs_len - We're printing 1 string stored in an iov - so one.
    i32.const 0 ;; nwritten - A place in memory to store the number of bytes writen
    call $fd_write

    drop ;; Discard the number of bytes written from the top the stack
  )

文字列の出力 puts()

同様に、固定文字列を表示するputs()関数も作ります。puts() は内部で次の処理を行います。

  • (1) 出力する文字列のアドレスを受け取る
  • (2) 別のメモリー領域に文字列をコピーする
  • (3) 最後に改行文字(`\n')を入れる
  • (4) fd_write()を呼び出すためのパラメーターをメモリー領域に準備する
    • パラメーターは、あらかじめメモリー上の決まった位置にダミーの値で確保
    • 毎回値を書き換えて使う
  • (5) fd_write()を呼びだす

これを全てWATで手書きするのはなかなか骨が折れます。そこで、一部をJSで記述して、前回までのコンパイラーを使ってWATを生成することにしました。直接メモリーをいじったり関数を呼び出すところはコンパイラーでサポートしていないので、手書きすることになります。

今回のputs()の例では「(2)別のメモリー領域に文字列をコピーする」部分をJSファイルで書いてからコンパイラーで生成したものを参考にし、残りは手書きで作りました。

puts()
  (func $puts (param $n i32)
    (local $srcIdx i32)
    (local $destIdx i32)
    (local $len i32)
    (local $c i32)
    get_local $n
    set_local $srcIdx

    i32.const 0
    set_local $destIdx

    i32.const 0
    set_local $len

    get_local $srcIdx
    call $_loadChar

    set_local $c

    loop ;; --begin of while loop--
      get_local $c      
      if
        get_local $destIdx
        get_local $c
        call $_storeChar

        get_local $len
        i32.const 1
        i32.add

        set_local $len

        get_local $srcIdx
        i32.const 1
        i32.add

        set_local $srcIdx

        get_local $destIdx
        i32.const 1
        i32.add

        set_local $destIdx

        get_local $srcIdx
        call $_loadChar
        set_local $c

        ;; check lenght 255
        get_local $destIdx
        i32.const 255
        i32.lt_s
        br_if 1

        ;; br 1 ;; --jump to head of while loop--
      end ;; end of if-then
    end ;; --end of while loop--

    ;;get_local $len
    ;;call $putn

    ;; tail LF
    get_local $destIdx
    i32.const 10 ;; LF
    call $_storeChar

    get_local $len
    i32.const 1
    i32.add
    set_local $len

    ;; iov.iov_base 
    i32.const 4
    i32.const 12
    i32.store

    ;; iov.iov_len
    i32.const 8
    get_local $len
    i32.store


    ;; $fd_write
    i32.const 1 ;; file_descriptor - 1 for stdout
    i32.const 4 ;; *iovs - The pointer to the iov array, which is stored at memory location 0
    i32.const 1 ;; iovs_len - We're printing 1 string stored in an iov - so one.
    i32.const 0 ;; nwritten - A place in memory to store the number of bytes writen
    call $fd_write

    drop ;; Discard the number of bytes written from the top the stack 
  )

WASI対応コンパイラーの拡張

テンプレートの用意

用意したビルトイン関数putn(), puts()はこちらの別ファイルに保存しておき、コンパイラーで読み込んで使います。

テンプレート読み込みモジュール

今回のミニNode.js-WASMコンパイラーでは、最初に作っていた「ミニインタープリター」で動かす、という縛りを設けています。ミニインタープリターではファイルの読み書きを直接はサポートしておらず、外部モジュールとして準備しています。なので今回のテンプレートファイルも外部モジュールを用意してそちらで読み込みます。

// -------------------------// module_wasibuiltin.js - WASM builtin for WASI// - puts()// - putn()// -------------------------'use strict'constfs=require('fs');constprintln=require('./module_println.js');constabort=require('./module_abort.js');constprintWarn=require('./module_printwarn.js');constbuiltinTamplateFile='wasi_builtin_template.watx';// === exports ===// --- parser ----module.exports=wasiBuiltin;functionwasiBuiltin(){constbuiltinFuncs=fs.readFileSync(builtinTamplateFile,'utf-8');//println(builtinFuncs);returnbuiltinFuncs;}

fd_write()呼び出し用のパラメータ領域

fd_write()の呼び出しで使うパラメータをメモリ上に確保しておきます。

  • オフセット位置 0バイト目から、4バイト分 ... 実際に出力したバイト数を受け取るための領域
  • オフセット位置 4バイト目から、4バイト分 ... 出力するバイト列の組の最初のアドレスを格納する領域
  • オフセット位置 8バイト目から、4バイト分 ... 出力するバイト列の組の数
  • オフセット位置 12バイト目から、255バイト分 ... 出力するバイト列を格納する領域
functiongenerateMemoryBlock(){letblock='';block=block+TAB()+'(memory 1)'+LF();block=block+TAB()+'(export "memory" (memory 0))'+LF();block=block+TAB()+'(data (i32.const 0) "\\00\\00\\00\\00") ;; placeholder for nwritten - A place in memory to store the number of bytes written'+LF();block=block+TAB()+'(data (i32.const 4) "\\00\\00\\00\\00") ;; placeholder for iov.iov_base (pointer to start of string)'+LF();block=block+TAB()+'(data (i32.const 8) "\\00\\00\\00\\00") ;; placeholder for iovs_len (length of string)'+LF();block=block+TAB()+'(data (i32.const 12) "hello world\\n")  ;; 4--> iov.iov_base = 12, 4--> iov_len = 8, 12-->"hello ...":len=13'+LF();returnblock;}

この領域をputn(), puts()で利用しています。

テンプレートの連結

コンパイラーでWATファイルを生成する際に、ユーザ定義関数に引き続きテンプレートとして用意しておいたputn(), puts()のWATコードを連結して出力します。

functioncompile(tree,gctx,lctx){// ... 省略 ...// ---- global user_defined functions ---block=block+generateGlobalFunctions(gctx);// ---- builtin function for wasi ---block=block+wasiBuiltin();// --- close all ---block=block+')';returnblock;}

WASI向けのコンパイル&実行

今回作ったコンパイラーはこちらです。

これを使って、これまでのサンプルをコンパイル、wasmtimeを使って実行してみましょう。(wasmtimeはテキスト形式の.wat、バイナリ形式の.wasmの両方を実行することができます)

FizzBuffの例

$ node mininode_wasm_wasi.js sample/fizzbuzz_func.js
$ wasmtime generated.wat
1
2
Fizz
4
Buzz
Fizz
7
... 省略 ...
94
Buzz
Fizz
97
98
Fizz
Buzz
$

WASIランタイム上で、無事FizzBuzzを実行できました!

ここまでのソース

GitHubにソースを上げておきます。


Viewing all articles
Browse latest Browse all 8825

Trending Articles