wardite 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,624 @@
1
+ ---
2
+ marp: true
3
+ theme: default
4
+ paginate: true
5
+ backgroundColor: #fff
6
+ header: '超入門WebAssembly(のランタイムをRubyで書く方法)'
7
+ # footer: 'Page %page%'
8
+ ---
9
+
10
+ # 超入門WebAssembly<br>(のランタイムをRubyで書く方法)
11
+
12
+ ---
13
+
14
+ # 自己紹介
15
+
16
+ - Uchio Kondo
17
+ - @udzura
18
+
19
+ ---
20
+
21
+ # 最近の生活
22
+
23
+ - 趣味: VM実装になりつつある...
24
+
25
+ ---
26
+
27
+ # mruby/edge
28
+
29
+ - Rustで書いたmrubyランタイム(mruby 3.x のバイトコードを実行可能)
30
+ - 基本的にmruby/cをベースにしている(一部必要な機能はmrubyから移植したい)
31
+
32
+ ---
33
+
34
+ # Wardite
35
+
36
+ - Rubyで書いたWebAssemblyランタイム
37
+
38
+ ---
39
+
40
+ # 今日は Wardite の話をします
41
+
42
+ ---
43
+
44
+ # 前提: WebAssembly とは
45
+
46
+ ---
47
+
48
+ # WebAssembly とは
49
+
50
+ - ブラウザで動くバイナリフォーマット
51
+ - バイトコードをいろいろな言語(C, C++, Rust, ...)からコンパイルして、そのプログラムをブラウザの上で実行できる
52
+
53
+ ---
54
+
55
+ # WebAssembly とは
56
+
57
+ - ~~ブラウザで~~ いろいろなところで動くバイナリフォーマット
58
+
59
+ ---
60
+
61
+ # WebAssembly のメリット
62
+
63
+ - 言語を選ばずにバイナリを作れる
64
+ - そのバイナリは、ブラウザを中心にいろいろなところで動かせる
65
+ - Write Once, Run Anywhere なのじゃよ...
66
+ - 多くのランタイムで高速に動くことが期待できる
67
+ - (安全性等もメリットですが今日は割愛)
68
+
69
+ ---
70
+
71
+ # さて、Wardite
72
+
73
+ ---
74
+
75
+ # Wardite の特徴
76
+
77
+ - Pure Ruby 製
78
+ - 標準添付ライブラリ以外に動作上の依存ライブラリはなし
79
+ - Fully RBS annotated (rbs-inline)
80
+
81
+ <!-- ちなみに標準添付ライブラリもstringioだけです。これも削っちゃおうかな? -->
82
+
83
+ ---
84
+
85
+ `<!--`
86
+
87
+ # 名前の由来
88
+
89
+ - `WA` で始まる鉱石の名前を探して、それを採用した
90
+ - ワード石: NaAl<sub>3</sub>(PO<sub>4</sub>)<sub>2</sub>(OH)<sub>4</sub>2(H<sub>2</sub>O)
91
+ - [Image CC BY-SA 4.0](https://en.wikipedia.org/wiki/Wardite#/media/File:Wardite.jpg)
92
+
93
+ `-->`
94
+
95
+ ---
96
+
97
+ # なぜ Ruby で WebAssembly ランタイムを書くのか
98
+
99
+ - C拡張ベースの組み込みWebAssembly ランタイムなら実際結構ある
100
+ - Wasmtime, Wasmedge, Wasmer, ...
101
+ - 速度面でも強みはあるでしょう...
102
+
103
+ ---
104
+
105
+ # でも...
106
+
107
+ - みなさん、このメッセージ好きですか?
108
+
109
+ ```
110
+ Building native extensions. This could take a while...
111
+ ```
112
+
113
+ ---
114
+
115
+ # cf. [wazero](https://github.com/tetratelabs/wazero)
116
+
117
+ - Pure Go の WebAssembly ランタイム
118
+ - cgo を避けたいGoのコミュニティで受け入れられている
119
+ - wazero + tinygo でGoだけのプラグイン機構があったりする
120
+ - https://github.com/knqyf263/go-plugin
121
+
122
+ ---
123
+
124
+ # Pure Rubyであることのメリット
125
+
126
+ - インストール・セットアップが簡単
127
+ - Rubyが動けば動く、プラットフォームに依存しにくい
128
+ - さらに、Warditeを頑張って高速化することで色々貢献も出てきそう
129
+
130
+ ## とはいえ
131
+
132
+ - 最初は「勉強目的」ではあった。思ったよりちゃんと動くので色んなユースケースを考えている
133
+
134
+ ---
135
+
136
+ # demo
137
+
138
+ ---
139
+
140
+ # こういうCのコードがあります
141
+
142
+ ```c
143
+ /* fibonacci number in C */
144
+ int fib(int n) {
145
+ if (n <= 1) return n;
146
+ return fib(n - 1) + fib(n - 2);
147
+ }
148
+ ```
149
+
150
+ ---
151
+
152
+ # これをWebAssemblyにコンパイルしてみましょう
153
+
154
+ ```
155
+ $ clang --target=wasm32 \
156
+ --no-standard-libraries \
157
+ -Wl,--export-all -Wl,--no-entry \
158
+ -o fib.wasm \
159
+ fib.c
160
+ ```
161
+
162
+ ---
163
+
164
+ # ブラウザで動かす
165
+
166
+ ```html
167
+ <!DOCTYPE html>
168
+ <html>
169
+ <head>
170
+ <script>
171
+ WebAssembly.instantiateStreaming(fetch('./fib.wasm'))
172
+ .then(obj => {
173
+ alert(`fib(20) = ${obj.instance.exports.fib(20)}`);
174
+ });
175
+ </script>
176
+ </head>
177
+ <body>
178
+ </body>
179
+ </html>
180
+ ```
181
+
182
+ ----
183
+
184
+
185
+
186
+ ```javascript
187
+
188
+ // WebAssemblyをフェッチしてインスタンス化する
189
+ WebAssembly.instantiateStreaming(
190
+ fetch('./fib.wasm')
191
+ ).then(obj => {
192
+ // obj.instance にインスタンスがあり、
193
+ // さっきの fib がexportされている
194
+ const value = obj.instance.exports.fib(20)
195
+ alert(`fib(20) = ${value}`);
196
+ });
197
+
198
+ ```
199
+
200
+ ----
201
+
202
+ ![w:800](./image.png)
203
+
204
+ ----
205
+
206
+ # 手元で動作
207
+
208
+ ```
209
+ $ wasmtime --invoke fib fib.wasm 20
210
+ warning: using `--invoke` with a function that takes arguments
211
+ is experimental and may break in the future
212
+ warning: using `--invoke` with a function that returns values
213
+ is experimental and may break in the future
214
+ 6765
215
+ ```
216
+
217
+ ---
218
+
219
+ # そして... Warditeでも動作
220
+
221
+ ```
222
+ $ bundle exec wardite fib.wasm fib 20
223
+ return value: I32(6765)
224
+ ```
225
+
226
+ ---
227
+
228
+ # Wardite を支える技術
229
+
230
+ ---
231
+
232
+ # 最初の実装
233
+
234
+ - ゴリラさんの「[RustでWasm Runtimeを実装する](https://zenn.dev/skanehira/books/writing-wasm-runtime-in-rust)」を参考に実装を開始した
235
+ - 元のコードがRustなので、強烈に型で守られたかったのと、RBS自体経験が浅いので勉強も兼ね、Full RBSを目指して移植していった
236
+
237
+ ---
238
+
239
+ ## 当時のメモを見ながら振り返ると
240
+
241
+ - 最初はとにかくデータ構造を把握しようとした
242
+ - [Wasmバイナリの全体像](https://zenn.dev/skanehira/books/writing-wasm-runtime-in-rust/viewer/04_wasm_binary_structure#wasm%E3%83%90%E3%82%A4%E3%83%8A%E3%83%AA%E3%81%AE%E5%85%A8%E4%BD%93%E5%83%8F)
243
+ - なぜかCの構造体で表現してるメモが...
244
+
245
+ ```c
246
+ struct SectionHeader {
247
+ u8 code;
248
+ u32LEB128 size; //先頭2バイトを除いたセクションデータのバイト数
249
+ union {
250
+ u32LEB128 nr_types;
251
+ u32LEB128 nr_functions;
252
+ u32LEB128 nr_memories; // num memoriesはメモリの個数だが、
253
+ // version 1の仕様ではメモリは1モジュールに1つしか定義できないので、
254
+ // 実質的にこの値は1で固定される。
255
+ u32LEB128 nr_data_segments;
256
+ u32LEB128 nr_imports;
257
+ u32LEB128 nr_exports;
258
+ }
259
+ }
260
+ ```
261
+
262
+ <!-- この発表本当にRubyのコードでないなw -->
263
+
264
+ ---
265
+
266
+ ## > section sizeはLEB128[1]でエンコードされたu32
267
+
268
+ - LEB128っち何??????
269
+ - DWARFの中とかで使われている可変長の数値表現らしい...
270
+
271
+ ---
272
+
273
+ ## Rubyで実装しないといけないので...
274
+
275
+ - 自分で書いた
276
+ - 当時はまだそんなにCopilot使ってなかったんですよ...
277
+
278
+ ```ruby
279
+ def to_i_by_uleb128(bytes)
280
+ dest = 0
281
+ bytes.each_with_index do |b, level|
282
+ upper, lower = (b >> 7), (b & (1 << 7) - 1)
283
+ dest |= lower << (7 * level)
284
+ if upper == 0
285
+ return dest
286
+ end
287
+ end
288
+ raise "unreachable"
289
+ end
290
+
291
+ to_i_by_uleb128 "\xB9\x64".unpack("C*")
292
+ # => 12857
293
+ ```
294
+
295
+ ---
296
+
297
+ ## この辺りの情報をもとにバイナリパーサを書く
298
+
299
+ - StringIO/File どちらも取る想定
300
+ - バイト列を一つ一つ読み取る感じの実装になっている
301
+
302
+ ```ruby
303
+ # @rbs return: Integer
304
+ def self.preamble
305
+ asm = @buf.read 4
306
+ raise LoadError, "buffer too short" if !asm
307
+ raise LoadError, "invalid preamble" asm != "\u0000asm"
308
+ vstr = @buf.read(4)
309
+ version = vstr.to_enum(:chars)
310
+ .with_index
311
+ .inject(0) {|dest, (c, i)| dest | (c.ord << i*8) }
312
+ raise LoadError, "unsupported ver: #{version}" if version != 1
313
+
314
+ version
315
+ end # ...
316
+ ```
317
+
318
+ ---
319
+
320
+ ## 命令を把握する
321
+
322
+ - [Wasm SpecのIndex of Instructions](https://www.w3.org/TR/wasm-core-1/#a7-index-of-instructions)
323
+ - メモには「思ったより多くなかった」って書いてあるが、いや多いでしょ(190個ぐらい)
324
+
325
+ ---
326
+
327
+ ## [WebAssembly Opcodes Table](https://pengowray.github.io/wasm-ops/) が便利
328
+
329
+ ![w:1000](./image-1.png)
330
+
331
+ ---
332
+
333
+ ## 頑張っていた過去の自分
334
+
335
+ - 楽しそうですね
336
+
337
+ ![w:550](./image-2.png)
338
+
339
+ ---
340
+
341
+ ## VM周りの実装
342
+
343
+ - [Runtime Structure](https://webassembly.github.io/spec/core/exec/runtime.html#) も見ながら
344
+ - Runtimeは以下のデータ構造を保持している
345
+ - Store と呼ばれるglobal stateを表現した構造体
346
+ - 3つのスタック
347
+ - Call Stack
348
+ - Value Stack
349
+ - Label Stack
350
+
351
+ ---
352
+
353
+ ## Ruby で書けばこういう感じ
354
+
355
+ ```ruby
356
+ class Store
357
+ attr_accessor :funcs #: Array[WasmFunction|ExternalFunction]
358
+ attr_accessor :modules #: Hash[Symbol, wasmModule]
359
+ attr_accessor :memories #: Array[Memory]
360
+ attr_accessor :globals #: Array[Global]
361
+ attr_accessor :tables #: Array[Table]
362
+ attr_accessor :elements #: Array[[Symbol, Integer, Array[Integer]]]
363
+ #...
364
+ ```
365
+
366
+ ```ruby
367
+ class Runtime
368
+ attr_accessor :store #: Store
369
+ attr_accessor :stack #: Array[wasmValue]
370
+ attr_accessor :call_stack #: Array[Frame]
371
+ attr_accessor :labels #: Array[Label]
372
+ #...
373
+ ```
374
+
375
+ - 実際にはもっとごちゃついた関係になってる。整理したい...
376
+
377
+ ---
378
+
379
+ ## 命令はパースしてこういうクラスに
380
+
381
+ ```ruby
382
+ class Op
383
+ attr_accessor :namespace #: Symbol -- :i32/:i64/...
384
+ attr_accessor :code #: Symbol
385
+ attr_accessor :operand #: Array[operandItem]
386
+ attr_accessor :meta #: Hash[Symbol, Integer]
387
+ end
388
+ ```
389
+
390
+ ---
391
+
392
+ ## VMの命令実行部分
393
+
394
+ - フレームに現在実行しているコード自体とその位置の情報がある
395
+ - それを上からやっていく
396
+
397
+ ```ruby
398
+ # @rbs return: void
399
+ def execute!
400
+ loop do
401
+ cur_frame = self.call_stack.last #: Frame
402
+ if !cur_frame
403
+ break
404
+ end
405
+ cur_frame.pc += 1
406
+ insn = cur_frame.body[cur_frame.pc]
407
+ if !insn
408
+ break
409
+ end
410
+ eval_insn(cur_frame, insn)
411
+ end
412
+ end
413
+ ```
414
+
415
+ ----
416
+
417
+ ## VM名物でっかいcase文
418
+
419
+ ```ruby
420
+ # @rbs frame: Frame
421
+ # @rbs insn: Op
422
+ # @rbs return: void
423
+ def eval_insn(frame, insn)
424
+ # unmached namespace...
425
+ case insn.code
426
+ when :unreachable
427
+ raise Unreachable, "unreachable op"
428
+ when :nop
429
+ return
430
+ when :br
431
+ level = insn.operand[0]
432
+ pc = do_branch(frame.labels, stack, level)
433
+ frame.pc = pc
434
+ #...
435
+ ```
436
+
437
+ ---
438
+
439
+ ## ところで
440
+
441
+ - i32/i64,f32/f64で共通の処理が多い
442
+ - Generatorでまとめて作るようにした
443
+
444
+ ```ruby
445
+ when :i32_add
446
+ right, left = runtime.stack.pop, runtime.stack.pop
447
+ if !right.is_a?(I32) || !left.is_a?(I32)
448
+ raise EvalError, "maybe empty or invalid stack"
449
+ end
450
+ runtime.stack.push(I32(left.value + right.value))
451
+
452
+ # ...
453
+ when :i64_add
454
+ right, left = runtime.stack.pop, runtime.stack.pop
455
+ if !right.is_a?(I64) || !left.is_a?(I64)
456
+ raise EvalError, "maybe empty or invalid stack"
457
+ end
458
+ runtime.stack.push(I64(left.value + right.value))
459
+ ```
460
+
461
+ ---
462
+
463
+ ## こういうDSLで生成するようにした
464
+
465
+ ```ruby
466
+ task :generate do
467
+ GenAlu.execute(libdir + "/wardite/alu_i32.generated.rb", prefix: "i32", defined_ops: [
468
+ :load,
469
+ :load8_s,
470
+ :load8_u,
471
+ :load16_s,
472
+ :load16_u,
473
+ :store,
474
+ :store8,
475
+ :store16,
476
+ :const,
477
+ :eqz,
478
+ :eq,
479
+ :ne,
480
+ :lts,
481
+ # ...
482
+ :rotr,
483
+ ])
484
+ ```
485
+
486
+ - あとは `rake generate`
487
+
488
+ ---
489
+
490
+ ## なぜコード生成?
491
+
492
+ - メタプログラミングは今回はやめとこかと思った
493
+ - 命令はすごい数そのパスを通るので、パフォーマンス面の心配
494
+ - RBS/rbs-inline と相性が悪そうな予感(杞憂かもだが)
495
+
496
+ <!-- GoとRustを結構書いてきたので、別に生成すれば良くね?って自然な気持ちで思ったのが大きいかな... -->
497
+
498
+ ---
499
+
500
+ ## 関数を呼ぶときの命令の解説
501
+
502
+ - 関数インスタンスを取り出してフレームをプッシュする
503
+
504
+ ```ruby
505
+ when :call
506
+ idx = insn.operand[0]
507
+ fn = self.instance.store.funcs[idx]
508
+ case fn
509
+ when WasmFunction
510
+ push_frame(fn)
511
+ # ... call external
512
+ else
513
+ raise GenericError, "got a non-function pointer"
514
+ end
515
+ ```
516
+
517
+ ---
518
+
519
+ ## フレームをプッシュするコード
520
+
521
+ - 初期変数(引数+コードで使う変数)をフレームに積む
522
+ - pcを -1 に初期化
523
+ - フレームをコールスタックに積む
524
+
525
+ ```ruby
526
+ def push_frame(wasm_function)
527
+ local_start = stack.size - wasm_function.callsig.size
528
+ locals = stack[local_start..]
529
+ self.stack = drained_stack(local_start)
530
+ locals.concat(wasm_function.default_locals)
531
+
532
+ arity = wasm_function.retsig.size
533
+ frame = Frame.new(-1, stack.size, wasm_function.body, arity, locals)
534
+ self.call_stack.push(frame)
535
+ end
536
+ ```
537
+
538
+ ---
539
+
540
+ ## returnする時
541
+
542
+ - stackを巻き戻します
543
+
544
+ ```ruby
545
+ def stack_unwind(sp, arity)
546
+ if arity > 0
547
+ if arity > 1
548
+ raise ::NotImplementedError, "return artiy >= 2 not yet supported ;;"
549
+ end
550
+ value = stack.pop
551
+ self.stack = stack[0...sp]
552
+ stack.push value
553
+ else
554
+ self.stack = stack[0...sp]
555
+ end
556
+ end
557
+ ```
558
+
559
+ ---
560
+
561
+ # WASI
562
+
563
+ ---
564
+
565
+ ## > WASI(WebAssembly System Interface)とは何であるのかについて理解が深まる話をします
566
+
567
+ - ...やばい!時間がなくてあんまできない!
568
+ - と言ってもWASIはシンプルだと思う
569
+ - 基本的にはWASM Moduleにインポートする関数群
570
+ - Rubyのような言語でWASIに対応した関数を書く場合
571
+ - 本当にシンプルに、OS側のシステムコールに対応させるだけ
572
+ - 簡単とは言ってないよ!量が多いし!
573
+ - ちなみにpreview1/preview2というのがあるが
574
+ - p2はComponent向け。まず基本p1を実装中
575
+
576
+ ---
577
+
578
+ ## WASI (p1) 周り
579
+
580
+ - ただのimport moduleなのでまずその仕組みを作った
581
+ - あとはこういう関数を地道に実装するだけやで...
582
+
583
+ ```ruby
584
+ class WasiSnapshotPreview1
585
+ # @rbs store: Store
586
+ # @rbs args: Array[wasmValue]
587
+ # @rbs return: Object
588
+ def random_get(store, args)
589
+ buf, buflen = args[0].value, args[1].value
590
+ randoms = SecureRandom.random_bytes(buflen) #: String
591
+ store.memories[0].data[buf...(buf+buflen)] = randoms
592
+ 0
593
+ end
594
+ end
595
+ ```
596
+
597
+ - ちなみに、hello worldだけなら `fd_write` のみでいける
598
+
599
+ ---
600
+
601
+ # という感じで地道に実装中
602
+
603
+ ---
604
+
605
+ # 他、話していないこと
606
+
607
+ - WASM specに対応したテスト実行の仕方
608
+ - [ブログに少し書いた](https://udzura.hatenablog.jp/entry/2024/11/24/210124)
609
+ - 超カバレッジ低い、i32系だけ。
610
+ - パフォーマンスチューニング
611
+ - [ブログ書いた](https://udzura.hatenablog.jp/entry/2024/12/20/173728)
612
+
613
+ ---
614
+
615
+ # 今後の展望
616
+
617
+ - WASI、specカバレッジ、パフォチューを地道に
618
+ - `ruby.wasm` を動かしたいぞん
619
+ - ひとまずWASIサポート関数を増やすなど頑張りが必要
620
+ - 速度は... 我慢で対応(?)
621
+ - Component も読めるようにしたい
622
+ - 簡単なものならまあ...
623
+ - もちろん、松山でWarditeの話をしたいですね
624
+ - mruby/edgeか、どちらかをね。
data/sig/fcntl.rbs ADDED
@@ -0,0 +1,7 @@
1
+ # No fcntl RBS file by default
2
+ module Fcntl
3
+ O_APPEND: Integer
4
+ O_NONBLOCK: Integer
5
+ F_GETFL: Integer
6
+ F_SETFL: Integer
7
+ end
@@ -6,7 +6,7 @@ module Wardite
6
6
  # @rbs buf: File|StringIO
7
7
  # @rbs max_level: Integer
8
8
  # @rbs return: Integer
9
- def fetch_uleb128: (File | StringIO buf, ?max_level: Integer) -> Integer
9
+ def self?.fetch_uleb128: (File | StringIO buf, ?max_level: Integer) -> Integer
10
10
 
11
11
  # @rbs buf: File|StringIO
12
12
  # @rbs return: Integer
@@ -173,10 +173,10 @@ module Wardite
173
173
  self.@buf: File | StringIO
174
174
 
175
175
  # @rbs buf: File|StringIO
176
- # @rbs import_object: Hash[Symbol, Hash[Symbol, wasmCallable]]
176
+ # @rbs import_object: Hash[Symbol, wasmModuleSrc]
177
177
  # @rbs enable_wasi: boolish
178
178
  # @rbs return: Instance
179
- def self.load_from_buffer: (File | StringIO buf, ?import_object: Hash[Symbol, Hash[Symbol, wasmCallable]], ?enable_wasi: boolish) -> Instance
179
+ def self.load_from_buffer: (File | StringIO buf, ?import_object: Hash[Symbol, wasmModuleSrc], ?enable_wasi: boolish) -> Instance
180
180
 
181
181
  # @rbs return: Integer
182
182
  def self.preamble: () -> Integer
@@ -19,16 +19,6 @@ module Wardite
19
19
  # @rbs value: Float
20
20
  # @rbs return: F64
21
21
  def F64: (Float value) -> F64
22
-
23
- private
24
-
25
- # @rbs value: Integer
26
- # @rbs return: Integer
27
- def as_u32: (Integer value) -> Integer
28
-
29
- # @rbs value: Integer
30
- # @rbs return: Integer
31
- def as_u64: (Integer value) -> Integer
32
22
  end
33
23
 
34
24
  extend ValueHelper
@@ -42,6 +32,15 @@ module Wardite
42
32
  # when we want to access signed value, it'd be done via #value_s
43
33
  attr_accessor value: Integer
44
34
 
35
+ @@i32_object_pool: Hash[Integer, I32]
36
+
37
+ # @rbs value: Integer
38
+ # @rbs return: I32
39
+ def self.cached_or_initialize: (Integer value) -> I32
40
+
41
+ # @rbs value: Integer
42
+ def initialize: (?Integer value) -> untyped
43
+
45
44
  # @rbs str: String
46
45
  # @rbs size: Integer|nil
47
46
  # @rbs signed: bool
@@ -126,6 +125,9 @@ module Wardite
126
125
 
127
126
  attr_accessor value: Integer
128
127
 
128
+ # @rbs value: Integer
129
+ def initialize: (?Integer value) -> untyped
130
+
129
131
  # @rbs str: String
130
132
  # @rbs size: Integer|nil
131
133
  # @rbs signed: bool