wardite 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,9 @@ 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
+ # @rbs value: Integer
36
+ def initialize: (?Integer value) -> untyped
37
+
45
38
  # @rbs str: String
46
39
  # @rbs size: Integer|nil
47
40
  # @rbs signed: bool