入門WebAssemblyの読書メモ

読んだ。

本書では、WATという人が読み書きしやすいテキストフォーマットを通して、WebAssemblyを解説している。 実務でWATを直接書くのは想像できないので、教養的な位置付けになるかと思っていたが、結構よかったと思う。

ちなみに、8章で、衝突判定をwasmとjsで比較していて、wasmの方が速いという結果になっていたのだけれど、本書のjsではclassを使ったOOP的な書き方になっていていかにも遅そうだったので、jsで線形メモリを使って書きなおしてみたところと、O3で最適化したwasmより圧倒的に速い結果となってしまった。(書き直したコードはこの記事の末尾に貼っておく。)

こうなると、大きめの数値演算処理をwasmに切り出せれば早くなるとも言えなさそう。 いずれにしても、WASMを実投入するには、十分に検証が必要必要になりそうだなーという印象。 (js版とwasm版を並行開発するして速い方を選ぶくらいやらないとつらそう。。。)

以下はメモ。順不同です。

WebAssembly

  • ダウンロード、コンパイル、実行がJavascriptよりも高速(ということになっているが、個別に検証はいると思う)
  • ブラウザだけでなく、Node.jsなどのJavascriptエンジンからも実行できる。
  • まだJavascriptほど汎用的ではない。

エンディアンはリトル。

スタックマシン

レジスタマシン上(x86)出来実行しなければならない、仮想スタックマシン。

スタックマシンの利点はバイトコードのサイズが小さいことと、利用できる汎用レジスタの数についての前提を設けなくてよいこと。(実行するハードウェア側で選択できる)

ページ

ページのサイズはすべて64KB。アプリケーションは合計で最大2GBのページを確保できる。 一度確保したページは解放できない。 サーバーサイドでは不十分。組み込みでは64KBでも大きすぎることがある。

記述スタイル

WAT: WebAssembly Text

疑似アセンブリ言語アセンブリ言語ではない)

WASI: WebAssembly System Interface

ランタイム仕様でOSとやりとりするための規格。

i32やi64などの整数型でsignedとunsignedは、型ではなく関数で区別する。 区別が必要な関数にのみsとuの両方が用意されている。

なお、Javascriptの数値は64bitの浮動小数点型。 (64bit浮動小数点では32bit整数をすべて表現できる。)

JavascriptではES2021まで64bit整数を扱えなかったが、ES2021ではBigInt型が追加されている。

文字列

  • null 終端
  • 文字列長を表すprefix

とのことだけど、これは文字列表現の一般論であって、自力で実装する必要があると理解した。

メモは以上。

jsのコード

jsとwatの速度に疑問があったので、 衝突判定のjsを線形メモリを使うように修正したものは以下。watとだいたい同じアルゴリズムになるように気を付けている。

function set_pixel(x, y, c) {
    if (x >= cnvs_size) return;
    if (y >= cnvs_size) return;

    const i = y * cnvs_size + x;
    mem_i32[i] = c;
}

function draw_obj(x, y, c) {
    for (let xi = 0; xi < obj_size; ++xi) {
        for (let yi = 0; yi < obj_size; ++yi) {
            set_pixel(x + xi, y + yi, c);
        }
    }
}

function clear_canvas() {
    for (let i = 0; i < pixel_count; ++i) {
        mem_i32[i] = 0xff_00_00_00;
    }
}

function animate_by_js() {
    clear_canvas();

    const stride_i32 = stride_bytes / 4;
    const end = obj_cnt * stride_i32;
    for (let i = 0; i < end; i += stride_i32) {
        let x = mem_i32[obj_start_32 + i];
        let y = mem_i32[obj_start_32 + i + 1];
        let vx = mem_i32[obj_start_32 + i + 2];
        let vy = mem_i32[obj_start_32 + i + 3];

        mem_i32[obj_start_32 + i] = (x + vx) % cnvs_size;
        mem_i32[obj_start_32 + i + 1] = (y + vy) % cnvs_size;
    }

    for (let i = 0; i < end; i += stride_i32) {
        let x1 = mem_i32[obj_start_32 + i];
        let y1 = mem_i32[obj_start_32 + i + 1];

        let hit = false;
        for (let j = 0; j < end; j += stride_i32) {
            if (i == j) {
                continue;
            }

            const x2 = mem_i32[obj_start_32 + j];
            const xdist = Math.abs(x1 - x2);
            if (xdist >= obj_size) {
                continue;
            }

            const y2 = mem_i32[obj_start_32 + j + 1];
            const ydist = Math.abs(y1 - y2);
            if (ydist >= obj_size) {
                continue;
            }
            hit = true;
            break;
        }

        if (hit) {
            draw_obj(x1, y1, hit_color);
        } else {
            draw_obj(x1, y1, no_hit_color);
        }
    }
}

React自作

少し前に話題になっていたReact自体の自作をやってみた。 一日かからないくらいの時間で、実装できるし、Reactの動作をかなりこまかく理解できるようになったので、React書く人にはお勧めしたい。

zenn.dev

自分が書いたのは以下。ほぼ写経。 github.com

気づき

やってみての気づきはいくつかある。

まず、DOMに対応するFiberというデータ単位で、仮想的なDOMツリーを構築し、更新を管理していることを理解できた。 Fiberが具体的にどんなデータを持っていて、何を管理しているのかがある程度わかった。

次に、DOMツリー構築の順序というかアルゴリズムも理解できた。 Render PharseとCommit Pharseの2Pharseそれぞれの具体的な役割がわかった。 (例えば、DOMの作成は、Render Pharseで、親のDOMへのappendChildはCommit Pharseでやっているというような。)

さらに、Hooksがどう動くかも理解できた。具体的には、setStateを呼ぶと処理自体はfiber内のqueueに積まれて、Render Pharseでのfiber自体の差分更新処理時に反映される、といった細かい振る舞いの理解ができた。

あとはrequestIdleCallbackというJavascriptの関数を知った。これ裏で重い処理したい場合はとても重宝する。

そんなところ。

tsx/jsxの変換

ちょっとしたメモの共有。

Reactの前にjsx/tsxをjsに変換できるように解釈が必要だが、そのあたりのやり方がわからず、自分はちょっと詰まった。

viteでやったので、tsconfigをちょっと編集して対応した。(MyReactという名前にした。)

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "MyReact.createElement",
    "allowJs": true,
    ....
  },
  ...
}

そうではなくて単純に、以下のbabel用のアノテーションをつけるだけでもよかったかも。

/** @jsx MyReact.createElement */
const element = <div>Hello</div>;

vite使わずに、素のtypescriptだけでも、jsxをjsに簡単に変換できる。

tsc --jsx react --allowJs hello.jsx

ちなみに、自作するReact自体はTypescriptで書いたが、ただし、自作したReactを使って動くコードをtsxやtsで加工とすると、TSXの型定義で苦労しそうだったので、そこはjsxで書いた。

nand2tetris

一通り終えたので、感想などを書き残しておきたい。

1~5章は一週間かからずにやれたと思うけど、 6章から12章のソフトパートは、3週間くらいはかかったのではないかと思う。 (手が止まっていた期間もそこそこあるけれど。)

1章

この章ではNANDからNOT、AND、ORとか基本的な素子を作った。 大学のころ授業でやって気がするが、ド・モルガンのNOTを二つつけて変形するテクニックとか忘れていて、思い出して楽しめた。

2章

この章では全加算器までを作った。これは淡々と進めた。

3章

この章ではフリップフロップからレジスタ、そしてRAMを作った。

時間があるときにフリップフロップを物理的に作ってみたいなーと思っているので、そのうちやってみたい。 (水晶振動子とかも。)

4章

この章は、アセンブリ言語に慣れる章だった。 独自のアセンブリ言語の仕様をちゃんと読めばすぐ終わるのに、適当に読み飛ばしていたのでちょっと詰まった。

5章

この章は、CPUを組み立てる章。

これまでの章の内容をちゃんと理解できてないとCPUを作るのが難しいと感じた。自分は、2章で作ったALUの仕様をちゃんと理解してなかったり、4章で課題をこなす程度にしかアセンブラの仕様を読み込んでいなかったのが原因で、ちょっと詰まった。

逆にこの章でCPUを作り終えると、4章までの内容は大体理解できたな、と実感できる。

ハードはソフトと違って、各クロックで複数の処理を平行して実行して、必要な処理だけを選択して使うという実装になるのが感覚的にわかって面白かった。

6章

この章では、アセンブリ言語機械語に変換するアセンブラを作る。 忘れかけていたRustを思い出しながら書いたので、基本のパーサを書くあたりでも苦労した。

7章

この章では、VMとして作ったCPU上で動作するスタックマシンを作る。

popの実装のときに、値の入れ替えが必要になったけど、レジスタ2つしかなくて無理では?となってちょっと詰まった。 RAMの固定アドレスをレジスタR5-R12として扱うので、ここがレジスタ代わりに使えることに気づければ、あとはコツコツやればできた。 過去にもなんどかアセンブラを書いたことはあるのだが、スタックのpushとpopを実装するのは初めてだったので、結構新鮮だった。

ずっとJVMで仕事してきたのに、あまりVM自体については考えたことがなかったので、今回の実装を通して多少でも理解できたのは大きな収穫かもしれない。

8章

制御構文は簡単に実装できるし、関数フレームの作り方も疑似コードが示されているので、こつこつやればできた。レジスタは3つあれば大体なんとかなる。

9章

この章は、詰まってから読み返せばいいので、流し読みして終わり。12章の課題を一部先に実装してもよいかもしれない。

10章

コードから構文木を生成する。 公式のテストを通せればよいわけではなく、ちゃんと木構造を作らないといけない。 構文木の生成自体が別に難しいというよりも、 コンパイラをさほど慣れていないRustで書いてしまったせいで、Rustの使いこなしで結構苦労したりもした。

本書はかなり上長に構文木を構築するけれども、ほとんどのキーワードは除外した方が楽なので、後で手を入れた。

11章

10章で生成した構文木をもとに、VMのコードを生成する。 なかなかどこから手を付けるかピンと来ずに苦労したが、JackCompilerが吐き出すVMコードを見ればわかった。 ここで、VMで実装したthisとthatの意味を理解した。thisはともかく、thatがなぜ必要なのかわかってなかった。

12章

OSとして、動的なメモリ確保や、基本的な描画などをJack言語で実装する。

12章はこれまでの章とは違ってアルゴリズムが示されているので、Jack言語で実装するだけである。 とはいえ、Jack言語の演算子は一文字のみなので、2文字の演算子が使えなかったりとか不自由した。あとは、アドレス指定してデータ操作する方法もわからず迷子になったりはした。 いっそ言語を拡張しようかと思ったが、面倒なので結局やってない。

演算子の順序や、種類を増やしたり、制御構文を拡張したりといったことは、やればできると思う。 (が、まぁ、やり方もだいたいわかることに時間を使うのもなぁという気分になったので。。。)

全体を通して。

いろいろ物足りなさがあるけれども、コンパクトに一通り学べる教材だと感じた。

これまでもリアルタイムシェーダー、コンパイラ、OSなど、それぞれ個別にもう少し踏み込んだ内容を扱う教材で学んできたが、 今回のハードからOSまで一貫して実装してみるというのは、ほかに代えがたい内容だと感じた。

手だけでなく頭も動かして、結構悩んだり、再設計しなおしたりもしたので、理解が深まったと思う。 特にVM周りの仕様は、最初疑問だったことが、後の章で納得できることも多く、勉強になった。

ただ、テストスクリプトや期待する出力は事前に示されているので、 本当に自力で実装するのと比べるとずいぶん楽をさせてもらったような気はする。

CPUの設計から自分でやってみれば、本当の意味で理解が深まるのかもしれない。そのうちやってみたい。

これまでにやってみた自作

参考まで。

  • 大学4回のころに、リアルタイムシェーダーを実装したけど記事に残してない。

ma38su.hatenablog.com

ma38su.hatenablog.com

ma38su.hatenablog.com

ma38su.hatenablog.com

ma38su.hatenablog.com

記事にしてなかったが、Ray Tracing in One Weekend Seriesもやった。 raytracing.github.io

ma38su.hatenablog.com

Revit/DynamoのPythonでAPI callしたりブラウザを開いたりする。

Revit + Dynamoでは、CPythonが動くので、pythonでできることは一通りできると考えてよさそう。 サンプルなどで見かけたことはないのだが、PythonノードではWebAPIをコールして、結果を次のノードに渡すとかもできた。

例を二つほど上げておく。

PythonでPOSTでWeb APIをコールする。

import requests

url = 'http://localhost:3000/api/layout'
res = requests.post(url, json={
    key: 'Hello',
    value: 'World'
})

ただし、requestsは、pipで事前にインストールしておき、パスも通す必要はある。 詳しくは以下を参考に。

ma38su.hatenablog.com

Pythonでブラウザを開く。

Windows環境を想定。 なにかデータ送信した結果をブラウザで確認するような用途を想定している。 もしくは、responseを受けて、次の処理を実行してもよい。

import subprocess

subprocess.Popen([r'C:\Program Files\Google\Chrome\Application\chrome.exe','https://ma38su.hatenablog.com/'])

「Pythonでいかにして暗号を破るか」の読書メモ

一通り終えた。一週間くらいはかかったと思う。

少なくとも最初の数章結構平易なので、基本的な数学に抵抗がなければ、Python初心者でもある程度頑張れるかもしれない。 ある程度Pythonになじみがある人にとっては前半は物足りないかもしれない。

ただ、後半は、辞書や、単語パターン、頻度分析などを用いて、暗号を解読していくので、新鮮だった。

原著(英語)ならオンラインで公開されているようです。

Cracking Codes with Python inventwithpython.com

C#でBase64エンコード

Base64の=はパディングだったことを知りました。

まったく意識していなかったのですが、WebAPIなどではパディングなしが求められることもあります。 無意識にBase64エンコードするとパディングありの結果が出力されていることもあるので、少し注意が必要です。

C#でno paddingなBase64エンコードしたい場合は以下でできました。

string input = "....";
var base64encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(input)).TrimEnd('=');

TrimEndは末尾の文字が連続してもぜんぶ両方Trimしてくれるので、これでno paddingにできます。

今回、inputはマルチバイトではない文字を想定していたので、UTF8エンコードでバイト列に変換しましたが、 マルチバイト文字の場合は正しいエンコードを選択する必要がありそうです。

C#のstringの文字コードってどこで決まるんでしょうか。。。

C#をラフに試すために

C#を書かないといけないことがよくあるのだが、いちいちVisual Studio立ち上げてプロジェクト開くのは結構めんどくさい。 特にプラグイン開発などは、コードを実行までが長い。。。 (ユニットテストとかでさくっと確認すればよいのかもしれない。)

特にVisual Studioでプロジェクト作ったりするのが億劫なので、VSCodeとWSLの実行環境について調べてみたら、簡単にできた。 Ubuntu 20.04 + WSLだと、パッケージリポジトリを追加すればaptでインストールも更新もできる。 手順は全部公式に書いてある通りでOK。 learn.microsoft.com

できたら、以下でプロジェクトを作って実行できる。

dotnet new console -o Sample
cd Sample
dotnet run

c#のエントリーポイントはclassか構造体の中のMainメソッド。 書き捨てならMainメソッド省略してもいいらしい。 競プロとかだと捗りそうではある。(使うかどうかは別)

ちなみに、c#パスカルケースなので先頭は大文字でないとだめ。