SVX日記
2023-12-01(Fri) WebAssemblyのstackで開発がstuck
「WebAssemblyのひとつだけの使い道」ということで、ボチボチと開発を進めていたのだが、どうにも不可解な動きがあって、理解するまでにだいぶかかってしまった。
main = ->
importObjects = {
console: { log: (arg) => console.log(arg) },
}
obj = await WebAssembly.instantiateStreaming(fetch('test.wasm'), importObjects)
console.log('call test(0)')
obj.instance.exports.test(0)
console.log('call test(1)')
obj.instance.exports.test(1)
main()
(module
(import "console" "log" (func $log (param i32)))
(func (export "test") (param $val i32)
push val
if
i32.push 10
call log
else
i32.push 20
call log
end
)
)
call test(0)
20
call test(1)
10
push val
if
i32.push 10
else
i32.push 20
end
call log
test.wat:10:4: error: type mismatch in if true branch, expected [] but got [i32]
i32.const 10
^^^^^^^^^
test.wat:13:3: error: type mismatch in if false branch, expected [] but got [i32]
end
^^^
test.wat:14:3: error: type mismatch in call, expected [i32] but got []
call $log
^^^^
push val
if (result i32)
i32.push 10
else
i32.push 20
end
call log
push val
if (result i32) (result i32)
i32.push 10
i32.push 10
else
i32.push 20
i32.push 20
end
call log
call log
test.wat:9:3: error: multiple if results not currently supported.
if (result i32) (result i32)
^^
i32.push 100
push val
if (result i32)
i32.push 10
i32.add
else
i32.push 20
i32.add
end
call log
test.wat:12:4: error: type mismatch in i32.add, expected [i32, i32] but got [i32]
i32.add
^^^^^^^
test.wat:15:4: error: type mismatch in i32.add, expected [i32, i32] but got [i32]
i32.add
^^^^^^^
test.wat:17:3: error: type mismatch in function, expected [] but got [i32]
call $log
^^^^
こうなることがどうにも理解できなくて、長らくグダグダしていた。MDNのifの項を読んでも、特段なにも触れられていない。が、これは「ifは関数コール」のようなものだ、と理解するべきだという結論にたどり着いた。
ifブロックに入ったら「スタックの内容は持ち込めない」し「スタックの内容は持ち出せない(ただし返値としてひとつだけは許容される)」ということで、これは関数コールの特性そのものである。これまでのアセンブラ知識が邪魔になって必要以上に理解するのに時間がかかってしまった。
i32.push 100
push val
if (result i32)
i32.push 10
else
i32.push 20
end
i32.add
call log
i32.push 100
blk1: block (result i32)
i32.push 10
push val
br_if blk1
drop
i32.push 20
end
i32.add
call log
以下は、引数が0だった場合に、100をログに出力させるプログラムだが、非0だった場合はスタックに100が残ることになる。が、それは許容されるらしい。関数コールと考えれば、それが捨てられるだろうことは、まぁ理解できなくもないのだが。
blk1: block
i32.push 100 ;; [ 100
push val ;; [ 100 val
br_if blk1 ;; [ 100
call log ;; [
end
;; ;; [ ???
i32.push 99 ;; [ 99
blk1: block (result i32)
i32.push 100 ;; [ 99 100
i32.push 12 ;; [ 99 100 12
i32.push 11 ;; [ 99 100 12 11
i32.push 10 ;; [ 99 100 12 11 10
push val ;; [ 99 100 12 11 10 val
br_if blk1 ;; [ 99 100 12 11 10
call log ;; [ 99 100 12 11
drop ;; [ 99 100 12
drop ;; [ 99 100
end
call log ;; [ ???
call log ;; [ ???
call test(0)
10
100
99
call test(1)
10
99
2023-11-10(Fri) CoffeeScript2にてMixinを試す
ここのところ、ものスゴい学習欲と、それを越えるほどの課題が湧き上がってきて頭を休める間もない。WebAssemblyからSIMD命令(MMX/SSE/AVX)、浮動小数点演算、CPUキャッシュに飛び火し、なぜか線形代数の学び直しから、3DCG、AIに至るまで。
しかし最終的にアニメーションさせたいなら、以前に作ったシューティングゲームの既存のフレームを使ったほうがいい、ということになり、久々に引っ張り出してくると……動かねぇ。
どこが引っかかっているのかと思ったらMixinだ。なぜか以前に書いたMixinの仕組みが動かない。まぁJavaScriptとしても、CoffeeScriptとしても、Mixinが正式な言語仕様として組み込まれているわけではないからな、と思いつつ、調べていくと意外と根が深い。
そもそもJavaScriptにはクラスが存在しない。いや、最近まで存在しなかった。CoffeeScriptで書いていたので気づかなかったが、クラスが正式な言語仕様として組み込まれているわけではないところに、prototypeとかいう仕組みを通じてCoffeeScriptによりコネあげられていたのだ。ところが、最近になってJavaScriptにクラスの仕組みが組み込まれたので、CoffeeScript2からはコネあげをやめ、その仕組みをそのまま利用するようになった。
それだけならよかったのだが、クラスでメソッドを定義するとprototypeに登録されないようなのだ。先のMixinの仕組みはprototypeを通じてコネあげられていたので、それができなくなってしまった、と。
// コネあげ版クラス(JavaScript)
const Animal = (function() {
function Animal(name) {
this.name = name;
}
Animal.prototype.walk = function() {
console.log(this.name, 'walking.');
};
return Animal;
})();
// ネィティブクラス(JavaScript)
const Animal = class Animal {
constructor(name) {
this.name = name;
}
walk() {
console.log(this.name, 'walking.');
}
};
// どちらも同様に動くのだが
console.log(Animal.prototype);
const pochi = new Animal('Pochi');
pochi.walk();
// コネあげ版クラスだとprototypeにwalkがあるのに
$ node diff_func.js
Animal { walk: [Function] }
Pochi walking.
// ネィティブクラスだとprototypeがカラ
$ node diff_class.js
Animal {}
Pochi walking.
const Flying = (base) => class extends base {
fly() {
console.log(this.name, 'flying.');
}
}
class Bird extends Flying(Animal) {};
以前は「mixOf base, mixin」という以下のような書き方だったが「Flying(Animal)」って書き方も悪くはないなぁ。従来のmixOf関数の定義は必要なくなる。上記の定義自体がMixinを行う関数になっている。
class Bird extends mixOf Animal, Flying
'use strict'
class Animal
constructor: (name) ->
@name = name
walk: ->
console.log(@name, 'walking.')
class Human extends Animal
talk: ->
console.log(@name, 'talking.')
Flying = (base) -> class extends base
fly: ->
console.log(@name, 'flying.')
Ejecting = (base) -> class extends base
eject: ->
console.log(@name, 'ejecting.')
class Bird extends Flying(Animal)
class Pilot extends Ejecting(Flying(Human))
pochi = new Animal('Pochi')
pochi.walk()
taro = new Human('Taro')
taro.walk()
taro.talk()
pichan = new Bird('Pi-chan')
pichan.walk()
pichan.fly()
tom = new Pilot('Tom')
tom.walk()
tom.talk()
tom.fly()
tom.eject()
$ node mixin.js
Pochi walking.
Taro walking.
Taro talking.
Pi-chan walking.
Pi-chan flying.
Tom walking.
Tom talking.
Tom flying.
Tom ejecting.
2023-10-23(Mon) WebAssemblyでイメージを操作
というわけで「WebAssemblyのひとつだけの使い道」のプログラミングを始めた。やりたいのは要するにイメージの操作だ。単純な計算を山ほどループで繰り返す。実にWebAssembly向きの処理である。
いきなりだが、以下が機械語サブルーチン部分。イメージを表すRGBAの羅列を渡すと、GとBを抜いてくれるというもの。だいぶカリカリにチューン済み。結局、ループの最適解は減算&非ゼロ判定だな。それはそうと、見慣れないニーモニックが入っている。
(module
(import "js" "mem" (memory $mem 1))
;; filter(ソースの末尾 + 1 のアドレス)
(func (export "filter") (param $src_adr i32)
(local $dst_adr i32)
push src_adr ;; dst_adr = src_adr << 1
i32.push 1
i32.shl
pop dst_adr
loop1: loop
push dst_adr ;; dst_adr -= 4
i32.push 4
i32.sub
pop_push dst_adr ;; [ dst_adr
push src_adr ;; src_adr -= 4
i32.push 4
i32.sub
pop_push src_adr ;; [ dst_adr src_adr
i32.load ;; [ dst_adr color
i32.push 0xFF0000FF ;; [ dst_adr color mask
i32.and ;; [ dst_adr color
i32.store ;; [
push src_adr
jp_nz loop1 ;; src_adr != 0 loop
end
)
)
つうか、ちょっとは努力をしたつもりなのだが「i32.const」とか「local.set」とか「local.get」とか……ダメだわ。まったく頭に入ってこない。まったくスタックに出し入れしている感じが湧かない。結局、だいぶ前にZ80ライクニーモニックからPICニーモニックに変換するRubyスクリプトを書いた時と同じく、Z80ライクニーモニックからWebAssemblyのwat形式に変換するRubyスクリプトを書いてしまった。PIC用に作ったそれに比べれば、恐ろしく単純な変換しかしていないが、自分にはそれで十分にわかりやすく書ける。はて、自分は頭が固いのか柔らかいのか、どっちなのだろう……ゼッパチの魂百まで。怖い。
#!/usr/bin/env ruby
# coding: utf-8
wat80src = ARGV[0]
file_in = open(wat80src, 'r')
file_out = open(wat80src.gsub(/\.[^.]+$/, '') + '.wat', 'w', 0444)
file_in.each {|line|
break if(line =~ /^__END__$/)
unless(line =~ /^#/)
line.chomp!
if(line =~ /^(\w+):\s*(\w+)(\s*.*)/) # loop1: loop
line = "\t\t%s\t\t$%s%s" % [$2, $1, $3]
end
if(line =~ /^(\s+br\w*)\s+(\w+)(\s*.*)/) # br_if loop1
line = "%s\t\t$%s%s" % [$1, $2, $3]
end
if(line =~ /^(\s+)jp_nz\s+(\w+)(\s*.*)/) # jp_nz loop1
line = "%sbr_if\t\t$%s%s" % [$1, $2, $3]
end
if(line =~ /^(\s+\w+)\.push\s+([\d-]+)(\s*.*)/) # i32.push 10
line = "%s.const\t\t%s%s" % [$1, $2, $3]
end
if(line =~ /^(\s+)pop\s+(\w+)(\s*.*)/) # pop i
line = "%slocal.set\t\t$%s%s" % [$1, $2, $3]
end
if(line =~ /^(\s+)push\s+(\w+)(\s*.*)/) # push i
line = "%slocal.get\t\t$%s%s" % [$1, $2, $3]
end
if(line =~ /^(\s+)pop_push\s+(\w+)(\s*.*)/) # pop_push i
line = "%slocal.tee\t\t$%s%s" % [$1, $2, $3]
end
if(line =~ /^(\s+)call\s+(\w+)(\s*.*)/) # call log
line = "%scall\t\t$%s%s" % [$1, $2, $3]
end
line += $/
end
file_out.write line
}
<HTML>
<HEAD>
<TITLE>WebAssembly Graphics Test</TITLE>
</HEAD>
<BODY>
<CANVAS id='canvas1' width='512' height='384'></CANVAS>
<SCRIPT type='text/javascript' src='graphics.js'></SCRIPT>
</BODY>
</HTML>
で、例によって、JavaScriptはCoffeeScriptに書き直した。CoffeeScriptでasyncやawaitはどう書くのかと思ったら、awaitが含まれる関数は、自動的にasyncを付けてくれるらしい。CoffeeScript 1.xではダメで、2.0以上が必要なようだが。
'use strict'
# ソースイメージを読み込む
image_element = new Image
image_element.src = 'gra2.png'
# ソースイメージの読み込み完了を待つ
prep = ->
if(!image_element.complete)
setTimeout(prep, 100)
else
main()
main = ->
screen_canvas_element = document.getElementById('canvas1')
screen_context = screen_canvas_element.getContext('2d')
# 枠描画、ソースイメージを描画
screen_context.fillStyle = 'lightgray'
screen_context.fillRect( 0, 0, image_element.width + 32, image_element.height + 32)
screen_context.fillStyle = 'gray'
screen_context.fillRect(16, 16, image_element.width, image_element.height)
screen_context.drawImage(image_element, 16, 16)
console.log('image_element:', image_element)
# ソースイメージをデータ化
work_canvas_element = document.createElement('canvas')
work_canvas_element.width = image_element.width
work_canvas_element.height = image_element.height
work_context = work_canvas_element.getContext('2d')
work_context.drawImage(image_element, 0, 0)
# source_image = work_context.getImageData(0, 0, image_element.width, image_element.height)
source_bytes = work_context.getImageData(0, 0, image_element.width, image_element.height).data
source_longs = new BigUint64Array(source_bytes.buffer, 0, source_bytes.length >> 3) # コピーの高速化のために共用体化
# console.log('source_image:', source_image)
console.log('source_bytes:', source_bytes)
# ワークメモリを確保
work_memory = new WebAssembly.Memory({ initial: 1, maximum: 1 }) # 1 PAGE = 64 KB
work_bytes = new Uint8ClampedArray(work_memory.buffer, 0, source_bytes.length)
work_longs = new BigUint64Array(work_memory.buffer, 0, source_longs.length) # コピーの高速化のために共用体化
# ソースイメージデータをワークメモリにコピー
for p in [0...source_longs.length]
work_longs[p] = source_longs[p]
# wasmをロード、メモリ操作(イメージデータの加工生成)を実行
importObjects = {
js: { mem: work_memory },
console: { log: (arg) => console.log(arg) },
}
obj = await WebAssembly.instantiateStreaming(fetch('graphics.wasm'), importObjects)
obj.instance.exports.filter(source_bytes.length)
# 生成データをイメージ化
filtered_bytes = new Uint8ClampedArray(work_memory.buffer, source_bytes.length, source_bytes.length)
filtered_image = new ImageData(filtered_bytes, image_element.width, image_element.height)
console.log('filtered_bytes:', filtered_bytes)
console.log('filtered_image:', filtered_image)
# スプライト(=キャンバス要素)を生成
sprite_canvas_element = document.createElement('canvas')
sprite_canvas_element.width = image_element.width
sprite_canvas_element.height = image_element.height
sprite_context = sprite_canvas_element.getContext('2d')
sprite_context.putImageData(filtered_image, 0, 0)
# 枠描画、スプライトを描画
screen_context.fillStyle = 'lightgray'
screen_context.fillRect( 0, 64, image_element.width + 32, image_element.height + 32)
screen_context.fillStyle = 'gray'
screen_context.fillRect(16, 80, image_element.width, image_element.height)
screen_context.drawImage(sprite_canvas_element, 16, 80)
console.log('sprite_canvas_element:', sprite_canvas_element)
prep()
結構、長い処理になってしまった。仕様上、何度も変換する必要があるのが面倒くさい。
- 元となるpngを内部CANVASに描き、getImageDataでUint8ClampedArrayの形で取り出す。
- それをWebAssembly.Memoryの領域にコピーする。
- WebAssemblyの側でフィルタ処理を行う。
- WebAssembly.Memoryの領域にUint8ClampedArrayの枠を被せて、ImageDataとして取り込む。
- それを新たにスプライトとして扱うCANVASにputImageDataで描く、までが準備作業。
最後に、表示されているCANVASに、drawImageでスプライトCANVASを重ねて完成だ。結果はこんな感じ。
2023-10-15(Sun) WebAssembly $00
;; https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Control_flow
(module
(func $log (import "console" "log") (param i32))
(func (export "ifTest") (param $val i32)
local.get $val
if (result i32)
i32.const 456 ;; not 0: True
else
i32.const 123 ;; 0: False
end
call $log
)
(func (export "loopTest") (param $times i32) (result i32)
(local $i i32)
(local $sum i32)
i32.const 0 ;; 初期値
local.tee $i
local.set $sum
loop $loop1
local.get $i ;; 処理1
call $log
local.get $sum ;; 処理2 $sum += $i
local.get $i
i32.add
local.set $sum
local.get $i ;; $i += 1
i32.const 1
i32.add
local.tee $i
local.get $times
i32.lt_s ;; $i < $times
br_if $loop1
end
local.get $sum
)
(func (export "whileTest") (param $times i32)
(local $i i32)
local.get $times ;; 初期値
local.set $i
block $loop1
loop $loop2
local.get $i ;; $i == 0
i32.const 0
i32.eq
br_if $loop1 ;; break
local.get $i ;; 処理1
call $log
local.get $i ;; $i -= 1
i32.const 1
i32.sub
local.set $i
br $loop2 ;; continue
end
end
)
)
<!-- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Control_flow -->
<HTML>
<HEAD>
<TITLE>WebAssembly Control Flow Test</TITLE>
</HEAD>
<BODY>
<SCRIPT>
async function main() {
const importObjects = {
console: { log: (arg) => console.log(arg) },
};
const obj = await WebAssembly.instantiateStreaming(fetch('controlflow.wasm'), importObjects);
obj.instance.exports.ifTest(0);
obj.instance.exports.ifTest(1);
obj.instance.exports.ifTest(2);
let sum = obj.instance.exports.loopTest(11);
console.log('total:', sum);
obj.instance.exports.whileTest(5);
}
main();
</SCRIPT>
</BODY>
</HTML>
123
456
456
0
1
2
3
4
5
6
7
8
9
10
total: 55
5
4
3
2
1
まずは条件分岐(if)。WebAssemblyのifはスタックの先頭の値がゼロか非ゼロかで処理を分岐するものだ。JavaScriptから0を渡すとelseの123が返り、非ゼロを渡すと456が返っている。ちょっとクセがあるのは、アセンブラにはスタックのズレを検出する機能があり、if構造を抜けた際にスタックが高くなって(ズレて)いると、それが検出されてエラーになってしまうところだ。そういう場合「スタックに返り値を積んだんのですよ」と明示する必要がある。ifの後の「(result i32)」がそれだ。思い返せば、Z80の頃にプログラムを暴走させてしまう一番の原因はPUSHとPOPのズレだったよなぁ。
次は繰り返し(loop)。カウンタは0から加算され、指定した回数だけ実行される。中身に処理がないのもアレなんでカウンタの累計を計算して返すようにしてみた。11回のループで0〜10が加算され結果の55が返っている。
2023-10-14(Sat) WebAssembly $43
さて「グローバル変数」によりJavaScriptとWebAssembly間で情報を共有する方法をテストできたが、WebAssemblyは計算やメモリ操作しかできないので、どちらかというと大量のデータを処理する用途に向くわけだから、大きなメモリを共有してこそ真価が発揮されるわけだ。そこで次はJavaScriptとWebAssembly間でメモリ空間を共有できる「共有メモリ」をテストしてみることにする。
;; https://github.com/mdn/webassembly-examples/blob/main/js-api-examples/memory.wat
(module
(memory $mem (import "js" "mem") 1)
(func $log (import "console" "log") (param i32))
(func (export "memsize")
memory.size
call $log
)
(func (export "memStoreTest")
i32.const 0 ;; adr
i32.const 518 ;; val 0x_0000_0206
i32.store
i32.const 8 ;; adr
i32.const 524 ;; val 0x_0000_020C
i32.store
i32.const 5 ;; adr
i32.const 0x55
i32.store8
i32.const 6 ;; adr
i32.const 0xAA
i32.store8
i32.const 0x0d ;; adr
i32.const 0x55AA
i32.store16
i32.const -1
call $log
)
(func (export "memLoadTest") (param $adr i32)
local.get $adr
i32.load
call $log
local.get $adr
i32.load8_u
call $log
)
)
<!-- https://developer.mozilla.org/ja/docs/WebAssembly/JavaScript_interface/Memory -->
<HTML>
<HEAD>
<TITLE>WebAssembly Memory Test</TITLE>
</HEAD>
<BODY>
<SCRIPT>
async function main() {
const memory = new WebAssembly.Memory({ initial: 3, maximum: 8 }); // 1 PAGE = 64 KB
const importObjects = {
js: { mem: memory },
console: { log: (arg) => console.log(arg) },
};
const obj = await WebAssembly.instantiateStreaming(fetch('memory.wasm'), importObjects);
obj.instance.exports.memsize();
memory.grow(2); // +2 pages
obj.instance.exports.memsize();
const i8 = new Uint8Array(memory.buffer);
const i32 = new Uint32Array(memory.buffer);
function dump(i8) { // memory dump
const dump = [];
for(let p = 0; p < 16; p++) {
dump.push(i8[p].toString(16));
}
console.log('dump:', dump.join(' '));
}
dump(i8);
for(let p = 0; p < 16; p++) { // 8bit store by JavaScript
i8[p] = 0x80 + (p << 1);
}
dump(i8);
for(let p = 0; p < 16; p++) { // 32bit store by JavaScript
i32[p] = 0x80 + (p << 1);
}
dump(i8); // little endian
obj.instance.exports.memStoreTest(); // store by WebAssembly
dump(i8);
obj.instance.exports.memLoadTest(0); // load by WebAssembly
obj.instance.exports.memLoadTest(8);
}
main();
</SCRIPT>
</BODY>
</HTML>
3
5
dump: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
dump: 80 82 84 86 88 8a 8c 8e 90 92 94 96 98 9a 9c 9e
dump: 80 0 0 0 82 0 0 0 84 0 0 0 86 0 0 0
-1
dump: 6 2 0 0 82 55 aa 0 c 2 0 0 86 aa 55 0
518
6
524
12
その後、メモリの先頭16バイトをダンプ表示。JavaScript側では、Cの共用体のようにメモリをchar配列とint32配列とで共用する形にして、順に数字を格納していって、バイトオーダがリトルエンディアンであることを確認している。
2023-10-13(Fri) WebAssembly $42
「一般的なブラウザでRubyが動くなら実に喜ばしい」などと思って始めたWebAssemblyだが、だんだんRubyはどうでもよくなってきた。Z80やX68000でやってたようなアセンブラ遊びをまたやれる。これだけCPUパフォーマンスが上がってきた今日この頃、ちゃんと価値を伴ったアセンブラ遊びが再びできるとは思ってもみなかった。調べるとWebAssemblyは、スタックマシンでレジスタの数に制限はないようだ。絶妙にネイティブアセンブラと違うところが面白いではないか。
さて、WebAssemblyにおける「hello, world」に当たる足し算が終わったところで、次はJavaScriptとWebAssembly間で情報を共有できる「グローバル変数」をテストしてみることにする。
;; https://github.com/mdn/webassembly-examples/blob/main/js-api-examples/global.wat
(module
(import "js" "global" (global $g (mut i32)))
(func (export "getGlobal") (result i32)
global.get $g
)
(func (export "incGlobal")
global.get $g
i32.const 1
i32.add
global.set $g
)
)
<!-- https://developer.mozilla.org/ja/docs/WebAssembly/JavaScript_interface/Global -->
<HTML>
<HEAD>
<TITLE>WebAssembly Global Test</TITLE>
</HEAD>
<BODY>
<SCRIPT>
const global = new WebAssembly.Global({ value: 'i32', mutable: true }, 8);
console.log('a:', global.value);
global.value = 16;
console.log('b:', global.value);
global.value += 8;
console.log('c:', global.value);
const importObjects = {
js: { global },
};
WebAssembly.instantiateStreaming(fetch('global.wasm'), importObjects).then(
(obj) => {
let g = obj.instance.exports.getGlobal();
console.log('d:', g);
obj.instance.exports.incGlobal();
console.log('e:', global.value);
},
);
console.log('f:', global.value);
</SCRIPT>
</BODY>
</HTML>
a: 8
b: 16
c: 24
f: 24
d: 24
e: 25
WebAssembly.Globalでglobalというグローバル変数インスタンスを作り、JavaScript内で演算する様子をa, b, cで、WebAssembly内で演算する様子をd, e, fで確認する……と、意外な結果に。fの結果がdより前に出ていて、WebAssembly内の演算結果が反映されていない。
どうも、参考にしたサンプルの「WebAssembly.instantiateStreaming」というwasmを読み込むメソッドに、並列実行を行う性質があることが原因のようだ。いやしかしこんな挙動は扱いにくいだけで、完全に余計なお世話なんだが……。
だいぶアレコレと調べ上げウンウンと数多くの試行を繰り返した挙句、落ち着いたのが以下のJavaScriptコードだ。「Streaming」なのにワザワザ「await」するという冗談みたいなコードだが、これが一番シンプルで機械語サブルーチンを扱いやすい形にできる方法だった。
<!-- https://developer.mozilla.org/ja/docs/WebAssembly/JavaScript_interface/Global -->
<HTML>
<HEAD>
<TITLE>WebAssembly Global Test</TITLE>
</HEAD>
<BODY>
<SCRIPT>
async function main() {
const global = new WebAssembly.Global({ value: 'i32', mutable: true }, 8);
console.log('a:', global.value);
global.value = 16;
console.log('b:', global.value);
global.value += 8;
console.log('c:', global.value);
const importObjects = {
js: { global },
};
const obj = await WebAssembly.instantiateStreaming(fetch('global.wasm'), importObjects);
let g = obj.instance.exports.getGlobal();
console.log('d:', g);
obj.instance.exports.incGlobal();
console.log('e:', global.value);
console.log('f:', global.value);
}
main();
</SCRIPT>
</BODY>
</HTML>
a: 8
b: 16
c: 24
d: 24
e: 25
f: 25
2023-10-12(Thu) WebAssembly $41
偶然「RubyがWebAssemblyで動く」という記事を目にした。「WebAssembly」と聞くとブラウザ上で機械語が動くらしい印象を受けるが、具体的にはサッパリわからない。一般的なブラウザでRubyが動くなら実に喜ばしいことだが。しかし、Rubyを動かすためにはファイルシステムが……みたいな話になっている。よく理解できない。フムン。まずはRuby抜きでWebAssembly自体について学んでみたくなってきたな。
そもそも、自分は8bitの頃にコンピュータを始めたクチで、その頃はBASICを理解したら、次は機械語というのが定番のルートだった。自分はSHARP党だったので、X1ではZ80、X68000ではMC68000、Oh!XキッカケでPICマイコンのアセンブラにも手を染めたことがあるが、そもそもWebAssemblyの「Assembly」はアセンブラの意味で正しいのだろうか?
調べ始めると、どうもそれで正しいらしい。JavaScriptよりも高速な処理を目指したもので、そのバイナリは仮想マシンで処理されるものの、数倍の性能が得られるらしい。数倍……か。Z80の頃には、BASICと機械語では軽く数十倍以上の性能が得られたことに比べればずいぶん控えめな値ではあるが。
しかし驚いたのは、あらゆるI/O機能が「ない」ことだ。つまり計算やメモリ操作しかできない。そんなもん何に使ったらいいのか。せっかく懐かしくアセンブラに手を染められるかと思ったのに、使い道のないものを学んでも仕方がない……ん? いや、あるッ! ひとつだけ思いついたぞ。うむ、じゃ、やってみるか。
ということで、Web上でWebAssemblyの記事を探すと、CやRustから使う記事が多い。いや、そうじゃないんだ。せっかくなんだから、直接にニーモニック(mnemonic)を書きたいんだ。で、しつこく探すとそういう記事も多少は見つかる。
;; https://developer.mozilla.org/ja/docs/WebAssembly/Understanding_the_text_format
(module
(func (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add
)
)
# dnf search webassem
wabt.x86_64 : The WebAssembly Binary Toolkit
# dnf install wabt
# wat2wasm add.wat
# ndump add.wasm
#Address +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 'Character
#00000000 = 00 61 73 6D 01 00 00 00 01 07 01 60 02 7F 7F 01 '.asm....:...`....
#00000010 = 7F 03 02 01 00 0A 09 01 07 00 20 00 20 01 6A 0B '........:.. . .j.
# wasm2wat add.wasm
(module
(type (;0;) (func (param i32 i32) (result i32)))
(func (;0;) (type 0) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))
しかし、なにしろ動かしてみないことには面白くない。なんでも、wasmバイナリを動かすには、ブラウザでJavaScriptを実行し、そこから呼び出す形式になるらしい。まさに昔、BASICプログラムから機械語サブルーチンを呼び出す場合の「CALL」や「USR」と同じ形式ということだな。
;; https://developer.mozilla.org/ja/docs/WebAssembly/Understanding_the_text_format
(module
(func (export "add") (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add
)
)
<!-- https://developer.mozilla.org/ja/docs/WebAssembly/Understanding_the_text_format -->
<HTML>
<HEAD>
<TITLE>WebAssembly Add Test</TITLE>
</HEAD>
<BODY>
<SCRIPT>
WebAssembly.instantiateStreaming(fetch("add.wasm")).then(
(obj) => {
console.log(obj.instance.exports.add(1, 2));
}
);
</SCRIPT>
</BODY>
</HTML>
JavaScriptを含むhtmlと、機械語サブルーチンであるwasmは、/var/www/htmlの下に配置し、手元でApacheを上げ、ブラウザからhttp://localhost/add.htmlでアクセスする。何も画面に表示されなくても焦ってはいけない。「Chrome」なら右クリックからの「検証」「Console」し、ログに結果である「3」が……アレ? 出てないな。代わりにエラーが出ている。
Uncaught (in promise) TypeError: Failed to execute 'compile' on 'WebAssembly':
Incorrect response MIME type. Expected 'application/wasm'.
TypeError: Response has unsupported MIME type
よく読めばChromeの方が(ちょっと過剰にw)わかりやすい。Apacheがwasmを返す時の「MIME type」は「application/wasm」じゃないの? って言ってるワケだ。これは、Apache側に設定追加してやる必要がある。
/etc/httpd/conf/httpd.conf
<IfModule mime_module>
:
AddType application/wasm .wasm
:
</IfModule>
2023-09-11(Mon) クレギオン、ロケットガール、ダブルリーチ
6月の中頃、ポイントが余っていることもあり、何か面白い小説でもないかと探していると目に止まったのが、野尻抱介氏の「クレギオン」シリーズ。軽い気持ちで電子書籍版を買ってみたところ、これが近年マレに見る面白さ。スペースオペラで、ダーティペアに近い雰囲気だが、それに比べハードSF成分が高いのに、展開はソフトという感じ。
元は富士見ファンタジア文庫での出版だったが、今はハヤカワ文庫で再販されているという状況が驚くほど自然に思える内容だ。ライトノベルとハードSFの見事な融合。なんというかライトでハードってアルミノベル!?
野尻氏の作品は、過去に「南極点のピアピア動画」「沈黙のフライバイ」「太陽の簒奪者」「ふわふわの泉」のいずれも読んでいて好印象だったが、それにも増して面白い。「クレギオン」は平成4年出版開始だが、まったく古さを感じさせない。
1冊目の「ヴェイスの盲点」を読了したところで、躊躇なく2冊目の「フェイダーリンクの鯨」へ進む。が、ひとつ問題が。挿絵がまったくないので、いまひとつ宇宙空間の状況が入ってこない。主役メカであるアルフェッカ号の形もピンと来ていない。
どうも、ハヤカワ文庫に移籍した時に挿絵が捨てられてしまったようだ。いや、それはあったほうがいいのだがな。まだ先に5冊もあるのに、このまま挿絵のない電子書籍版を買い進めるのは、特にこの作品においてはもったいなさすぎるように思える。
しかし、一方で紙の本は食事中に読み進められないという問題がある。非常に不真面目な読書態度で申し訳ないのだが、最近の自分は「細切れの数分の時間を使って、数ページずつ読み進める」というスタイルなのだ。自分の場合、それでも十分に内容は頭に入るし、存分に楽しめるのだ。というわけで、それが解決になるのかはわからないものの、とりあえず「書見台」というものを買ってみることにした。
で、3冊目の「アンクスの海賊」から紙の本で読み始めた……のだが、期待に反し、挿絵は人物画ばかりでSF的な情景を描いたものは皆無だった。ま、そうなるか。とはいえ、マージもメイも、オマケにロイドも、顔はあったほうがいい。結局、7冊目の「ベクフッドの虜」まで紙の本で調達してしまい、その勢いで、間違いなく「ロケットガール」シリーズも面白いハズだから読むべきと思い、これも紙の本を調達。当然、ハヤカワ版ではなく、富士見の再販版である。ついでに、さかのぼってクレギオンの1,2冊目も調達。
こんな気持ちになるのは珍しいが、7冊目の「ベクフッドの虜」はもったいなくて読まずに残し「ロケットガール」に進む。これまた、無闇に面白い。笹本祐一氏の「星のパイロット」シリーズと内容も面白さもだいぶ被るが、リアルな宇宙開発を舞台にしてるだけなのに、なんでこんなに面白いのか。
2023-09-05(Tue) Rubyで端末画面に画像を表示する
一応、CUIコンソール画面に画像を出力できたんだが、Maveに実装することを考えれば、縮小(ついでに拡大)機能と、右側のトリミング機能が必要だ。ついでに透過処理もできるとカッコいい。
さて、透過処理はどうすれば……と考え込んだところで「▀」という「上半分塗りつぶし」文字と「▄」という「下半分塗りつぶし」文字を使い分ける必要があることに気づく。そういや、参考に読んだcatimgのコードで両方を使い分けていたのはそういうことだっとのか。
# ./grouping.rb
["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R"]
1: A,B,C,D,E
2: F,G,H,I,J
3: K,L,M,N,O
4: P,Q,R
#!/usr/bin/env ruby
x = '@'; xs = []; 18.times {
xs << x.succ!.dup
}
puts(xs.inspect)
n = 0; xp = []; xs.each {|x|
xp << x
if(xp.size == 5)
puts('%d: %s' % [n += 1, xp.join(',')]) # same!
xp.clear
end
}
puts('%d: %s' % [n += 1, xp.join(',')]) # same!
__END__
ここで「same!」とある行を2度書くのがすごくイヤなんだよな。こういうのに対応する構文を持つ言語ってあるのかしらん……とか考えつつ、その部分はさておき、他はどうにかこうにか納得できるコードが書けた。
#!/usr/bin/env ruby
require 'rmagick'
if(ARGV.size == 0)
abort(<<USAGE)
Usage: rcatimg image_file [width] [trim]
USAGE
end
image = Magick::Image.read(ARGV[0])
w = image[0].columns
h = image[0].rows
tgt_w = `tput cols`.to_i
tgt_w > w and tgt_w = w
(it = ARGV[1]) and tgt_w = it.to_i
trm_x = false
(it = ARGV[2]) and trm_x = it.to_i
tgt_h = h * tgt_w / w
sstep = (h << 8) / tgt_h
# +---------+ tdms = [ [ 0b00, 'Ar;Ag;Ab', 'Wr;Wg;Wb' ],
# | A C D | [ 0b10, nil, 'Xr;Xg;Xb' ],
# | W X Z | [ 0b01, 'Cr;Cg;Cb', nil ],
# +---------+ [ 0b00, 'Dr;Dg;Db', 'Zr;Zg;Zb' ] ]
ess = [ "\e[38;2;%2$sm\e[48;2;%3$sm\u2580\e[m", # 0b00: f_col, b_col, [▀]
"\e[38;2;%2$sm\u2580\e[m", # 0b01: f_col, [▀]
"\e[38;2;%3$sm\u2584\e[m", # 0b10: f_col, [▄]
' ' ] # 0b11: [ ]
y8 = 0; tdms = []; tgt_h.times {
x8 = 0; tgt_w.times {|cx|
trm_x and (cx < trm_x or break)
c = image[0].pixel_color(x8 >> 8, y8 >> 8)
tdms[cx] ||= [0]
tdms[cx][0] <<= 1
if(c.opacity < 64)
tdms[cx] << '%d;%d;%d' % [c.red >> 8, c.green >> 8, c.blue >> 8]
else
tdms[cx][0] += 1 # 透明
tdms[cx] << nil
end
x8 += sstep
}
if(tdms[0].size == 3)
tdms.each {|tdm|
print(ess[tdm[0]] % tdm)
}
puts; tdms.clear
end
y8 += sstep
}
if(tdms.size != 0)
tdms.each {|tdm|
print(ess[(tdm[0] << 1) + 1] % tdm)
}
puts
end
if(ENV['INFO'])
puts('#' * trm_x) if(trm_x)
puts('#' * tgt_w)
puts('[%d, %d] -> x%d/%d -> [%d, %d]' % [w, h, 256, sstep, tgt_w, tgt_h])
end
__END__
2023-09-04(Mon) スイッチ、ングハブ、交換
無線LANルータのオマケの4ポートをスイッチの代わりに、最低限のマシンだけ有線接続する応急対処の後、100Mで5ポートの手持ちのスイッチで増設、なんてことをやっているうちに、ポチっておいたハブが到着したので交換。NETGEARは最安ではないが、特段の何の問題も起こしてこなかった実績があるんだから、そりゃ続投だ。
思い返せば、自宅の新築に伴い24ポートを導入したが、ファンが異音を発するようになってきたので、ファンレスの16ポートに交換したところ、今回の破損だ。3年はちょっと短いがしゃーないな。