Open4

nccc: GC言語に統合するためのプリミティブの準備

okuokuokuoku

いくつかNCCCのレベルで用意しておいた方が良いプリミティブがあるので設計/実装する。

  • Dispatch -- GCオブジェクトをpinして他のNCCC関数を呼ぶ仕組
  • Chime -- イベントキューに他のスレッドからイベントを挿入する仕組

NCCC単体では関数に数値しか渡せないため、言語内のGCオブジェクト -- 要するに Buffer -- を関数に渡すためには専用の仕組みが必要になる。 Dispatch は通常のNCCCの呼び出しに加えて、命令列のリストといくつかのGCオブジェクトを受け取り、それらのポインタを操作した上で本命のNCCC呼び出し操作を行う。

NCCCは意図的にスレッドプリミティブは仕様から外している。しかし、複数のイベントキューを統合する必要性から、必要最低限の "wakeup" 処理だけは標準ライブラリとして備えることにした。

okuokuokuoku

Chime

Chime は様々なライブラリで以下のような関数の組合せとして定義される。

void* miniio_chime_new(void* ctx, void* userdata);
int miniio_chime_trigger(void* ctx, void* handle);
void miniio_chime_destroy(void* ctx, void* handle);

Chimeオブジェクトはイベントキューに関連付けられ 何度でもトリガできるがトリガした回数だけイベントが配送されることは保証しない (UNIXの非同期シグナルと同じ)。 trigger はイベントキューへのイベントの追加に成功した場合にゼロを返却する。

別のライブラリのchimeを起こすには、NCCCでwrapされた trigger 関数を持ちまわして使うことになる。

... 要するにChimeは単なる構造であり、NCCCのランタイムとして何か特殊なものを持っているわけではない。

okuokuokuoku

Dispatch

Dispatchは ちょっとしたVM であり、外部から与えたパラメタを元にNCCC関数の呼び出しを行う。このVMに持たせる機能性はめっちゃ悩みどころだが一旦 MOVCALL JMP JNZ (Jump if not zero) RETURN だけにした。 (要するに演算を省略した)

通常のNCCC関数にいくつか void* ポインタを足したものがdispatch関数となる。例えば4つのポインタを持つdispatchは、

void nccc_dispatch_4(const uint64_t* opc, uint64_t* out, void* p0, void* p1, void* p2, void* p3);

のようなシグネチャとなる。大抵のプログラミング言語はGCオブジェクトである Buffer 等をポインタとして渡す機能があるので、それを使うことを想定している。

命令列

命令列は 64bit 長 x複数ワードとなる。

[RETURN]
[MOVPTR <idx> <dest>]
[MOV <src> <dest>]
[CALL <func> <inptr> <outptr>]
[JMP <opptr>]
[JNZ <inptr> <opptr>]

ポインタは常に絶対番地となる 。ただ、この仕様だとJavaScriptやJavaのような絶対番地を持たないシステムでは使えないことになるので、どうするべきかは後で考えたい。。

MOV はメモリ同士の転送となる MOV と、引数に指定されたポインタをストアするための MOVPTR の2種となる。間接ジャンプは今のところ必要性を思いついていない。MOVはNCCCが表現できる最大のワード長である64bitsを常に転送する。

CALL は他のNCCC関数を呼び出す。つまり、dispatchに与える命令列によってNCCC関数の引数列を作成し、何か呼び出すというのが基本的な使いかたになる。

okuokuokuoku

やっぱレジスタマシンに替える

いくつかプログラムを書いてみて気付いたけど、入力バッファを書き換えながら動く仕様にすると、リエントラントにならないというドデカい問題がある。。

レジスタは32個スタック上に確保し、 r0 は命令/データバッファ、以降引数として渡したバッファで初期化するものとする。また、レジスタを指すスタック上のアドレスを得る命令 REGADDR を用意し、CALLで呼ぶ引数は基本的にレジスタ上に組み立てることにする。

... スタックマシンにするのとどっちが良いんだろうね。。今回の場合は命令密度は要らないのでわかりやすさでレジスタマシンを採った。

[RETURN]
[REGADDR rBASE rOUT]
[ADD rIN1 rIN2 rOUT]
[LDC const rDEST] ;; 定数ロード
[LD rBASE rOFFS rDEST]
[MOV rFROM rTO]
[CALL rFUNC rINBUF rOUTBUF]
[JMP rBASE imm]
[JNZ rIN rBASE imm]

... というかコレ巧妙に設計すればかなり汎用性有る気がするな。。

例題 memcpy

NCCCでラップした memcpy を bytevector同士のコピーとして利用してみる。

void
nccc_memcpy(void* to, const void* from, uintptr_t size){
    (void)memcpy(to, from, size);
}   
(define (memcpy to to-offs from from-offs k)
  (dispatch cmd to to-offs from from-offs k))

cmd に収める命令列は引数の加工(オフセット加算)を行ってから nccc_memcpy を呼ぶ:

;; r0 = cmd
;; r1 = to
;; r2 = to-offs
;; r3 = from
;; r4 = from-offs
;; r5 = k
;; 以降はcmd内で生成する
;; r6 = &nccc_memcpy
;; r7 = (memcpyに渡す) to
;; r8 = (memcpyに渡す) from
;; r9 = (memcpyに渡す) size
;; r10 = 0 (返値が無いのでダミー)
;; r11 = [r6] (CALLに渡すin)
;; r12 = [r10] (CALLに渡すout)
[LDC &nccc_memcpy r6] ;; CALLのfunc
[ADD r1 r2 r7] ;; to (CALLのin)
[ADD r3 r4 r8] ;; from-offs
[MOV r5 r9] ;; size
[LDC 0 r10] ;; CALLで使うダミーのout
[REGADDR r6 r11]
[REGADDR r10 r12]
[CALL r6 r11 r12]
[RETURN]