@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Rubyでテトリスを実装する その1

WSL2上にRuby開発環境を構築してテトリスを作ってみた #Ruby - Qiita を見ながらテトリスを動かそうとしています。 読む分には簡単にできそうと思いました。 実際に、写経して動かそうとしてみたら結構大変でした。 特に、Cursesアプリケーションのデバッグがとても大変です。

一気に全部動かすのは諦めます。 マイルストーンを切ることにしました。 まずは、ゲームの盤面を表示します。

ゲームの盤面を表示したスクリーンショット

ここまでのソースコードです。 GitHub - ledsun/tetoris at draw_wall

次は、一種類のテトリミノが落ちてくるのを目指します。

wez.termのテーマを変えた

変更後のテーマのスクリーンショット

matrixという中2っぽいテーマにしました。

ついでに .wezterm.lua に次の設定を追加してフォントを変更しました。

config.font = wezterm.font '源ノ角ゴシック Code JP'
config.font_size = 14.0

もともとは補完途中の文字が暗い赤で表示されて見にくかったのでテーマを変えようとしました。 途中で、wez.termのテーマではなく、fish-shellのcolor-schemeの設定だと気がつきました。 fish-shellの設定をいじっているうちに直ってしまいました。 しかし、どの設定がきいたのかわかりません・・・。

明確にわかっているのは、以前、GitHub - microsoft/inshellisense: IDE style command line auto completeを試したときの設定が .config/fish/config.fishに残っているのを消しました。 でも、inshellisenseを試す前から、色が暗い赤にだったので影響するとは思えないんですよね・・・。

相対URLの解決

URL: URL() コンストラクター - Web API | MDN を使うと、基準になるURLからの相対パスを解決したURLが得られます。 例えば、次のように使います。

// ベース URL:
let baseUrl = "https://developer.mozilla.org";

new URL("ja/docs", baseUrl);
// => 'https://developer.mozilla.org/ja/docs'

このコンストラクターの挙動を試しているときに、次の例を考えました。

new URL('a.rb', 'http://exapmle.com/lib').toString()
// => 'http://exapmle.com/a.rb'

このとき http://exapmle.com/lib/a.rb となって、libディレクトリの中を参照して欲しいのではないでしょうか?

これはJavaScript特有の動作なのでしょうか?

Rubyでも試してみました。 RubyではURLの結合には URI.join を使います。

require 'uri'

URI.join(URI.parse('http://exapmle.com/lib'), 'a.rb')
# => #<URI::HTTP http://exapmle.com/a.rb>

やはり lib が消えます。 統一された動作です。 もしかしてこれはどこかで決まっているのでしょうか?

るりまに以下の説明がありました。

[RFC2396] の Section 5.2 の仕様に従って連結します。

というわけでRFCを見てます。 https://datatracker.ietf.org/doc/html/rfc2396#autoid-33

6) If this step is reached, then we are resolving a relative-path reference. The relative path needs to be merged with the base URI's path. Although there are many ways to do this, we will describe a simple method using a separate string buffer.

 a) All but the last segment of the base URI's path component is
    copied to the buffer.  In other words, any characters after the
    last (right-most) slash character, if any, are excluded.

base URIの最後のスラッシュ以降に何かあれば、その部分はバッファ(解決後のURLを結合するための場所)に入れないそうです。 なるほど、JavaScriptRubyもこの動きをしていそうです。

この記事を書いている途中で気がつきました。 URLは文字列で、ファイルシステムではありません。 http://exapmle.com/lib がファイルかディレクトリかという区別は、文字列から読み取るしかありません。 すると

  • /で終わるのが、ディレクト
  • /の後ろに文字列が続いていたらファイル

みたいな、単純な方法で区別するしかない気がしてきました。 libディレクトリに見えるのは、背景知識があるから人間に判別できているっぽいです。

RubyJavaScript、ファイルとURLの間を行ったり来たりしていると、自分がどこにいるのか、よく見失います。

Playwrightでリダイレクト後のHTTPリクエストをMockできない

ruby.wasmのテストコードを書いていました。

PlaywrightでHTTPリクエストをMockしているスクリーンショット

cdn.jsdriver.netへのリクエストをMockして、レスポンスの内容をローカルファイルに置き換えています。

リダイレクトしたあとはMockできていないスクリーンショット

前述のスクリーンショットと、同じURLに対するリクエストですが、302レスポンスでリダイレクトしたあとはMockできません。

調べてたら、次のコメントを発見しました。

tests for request event and interception with redirects by tjenkinson · Pull Request #3994 · microsoft/playwright · GitHub

The network interception in Playwright is implemented on the Browser -> Network stack boundary. Once the request is in the network stack, it is going to handle the redirects and report them, but not allow intercepting them.

Playwrightが割り込んでいるのは、ブラウザとネットワークスタックの間だそうです。

fetchメソッドはデフォルトで、リダイレクトレスポンスを自動的に追いかけます。 なるほど!この動きはネットワークスタックに含まれていそうです。

Playwrightでリダイレクトの動作をテストで確認する

PlaywrightのNewwork mockingをつかってみる - @ledsun blog ではテスト用のUIを使ってリダイレクトの動作を確認しました。 テストコードで確認してみましょう。

import { test, expect } from '@playwright/test';

test.beforeEach(async ({ context }) => {
  await context.route(/redirect_to/, route => route.fulfill({
    status: 301,
    headers: {
      location: 'https://playwright.dev/'
    }
  }));
});

test('has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  const url = await page.evaluate(async () => {
    const response = await fetch('https://example.com/redirect_to')
    return response.url
  })

  expect(url).toBe('https://playwright.dev/')
});

page.evaluateをつかって、ページ内でJavaScriptを実行します。 fetch('https://example.com/redirect_to')を実行して、Response#urlがリダレクト先のURLになっているか確認します。

前回と比べて次の点が変わっています

  • 意図しないファイルをリダイレクトしないように、リダイレクト用のキーワードをredirect_toに変更
  • httpsのページからfetchするために、fetchの宛先をhttpsに変更

Playwrightでローカルファイルを読み込む

次のようなindex.htmlを用意します。

<html>
  <title>Playwright</title>
</html>

テストは次のように書きます。

import path from 'path';
import { test, expect } from '@playwright/test';

test('has title', async ({ page }) => {
  const htmlPath = `file://${path.join(__dirname, `./index.html`)}`;
  await page.goto(htmlPath);

  // Expect a title "to contain" a substring.
  await expect(page).toHaveTitle(/Playwright/);
});

page.goto の引数に file:// で始まるファイルのパスを指定します。 index.htmlのtitleタグの要素を変えると、テストが失敗します。 期待通りにローカルファイルを読み込めているようです。

参考

Playwrightでローカルファイルを読み込む #TypeScript - Qiita

Amazon Linuxに関する調査メモ

Amazon Linux 2では、Node.jsの新しいバージョンをインストールできなかったり、少しずつ時代遅れ感が出ています。 では、「Amazon Linux 2023にすればいいのか?」というと、どうもそう簡単ではないようです。

これは、アップデートに挑戦して苦戦した話ではなく、アップデートするまえにどんな感じなのか軽く調べてみたメモです。

Amazon Linux 2023を触ってみて質問がありそうなことをまとめてみました。 | ソフトウェア開発のギークフィード

Fedoraの複数バージョンのコンポーネント+アルファをAWSがメンテして一般公開版を出したということになりますので、独自ディストリビューションの公開と維持に、よっぽどの稼働とコストがかかっている

自分の場合は、AWS EC2を使ってもありきたりなWebサーバーを起動したいだけです。 頑張って Amazon Linux を使うべきなのでしょうか? RedHat系のディストリビューションに乗り換えた方が良いのではないでしょうか?

今まで、開発環境では Ubuntu/Debian を使って生ききたので、RedHat系のディストリビューションには詳しくありません。 RedHat系の無料ディストリビューションと言えばCentOSですが、来年で終了するようです。

CentOS Linux のサポート終了について | Red Hat

CentOS Linux 7 のサポート期限は、2024 年 6 月 30 日に終了 (EOL) となります。

後継となるディストリビューションには何があるのか調べてみると、Alma LinuxとRocky Linuxがあるようです。 どうやらAlma Linuxの方が、少し人気があるように思えます。 理由は「企業ではなく非営利団体がメンテナンスしているため、急な方針転換が少ないだろう」でした。 では、Alama Linuxを選んでおけば良いかというと・・・

AlmaLinux と Rocky Linux の比較メモ(2023年8月版) | あぱーブログ

2023年6月 Red Hat は git.centos.org リポジトリの更新を停止したため、このリポジトリからソースコードを取得してビルドしていた AlmaLinux や Rocky Linux など、RHELクローンと呼ばれる Linux ディストリビューションは、ソースコードの取得先の変更を求められています。そのため、今後の各社の動きに注目しておく必要があります。

まだもうちょっと様子を見た方が良いようです。 個人的には急いでいないですし、Amazon Linux 2023でもよいので、もう少し様子を見ます。

参考

PlaywrightのNewwork mockingをつかってみる

Playwrightをつかってみる - @ledsun blog でPlaywrightが最低限動く環境ができました。 リダイレクトレスポンスを扱うテストを書こうと思います。

Playwrightには Netwrok mockingという機能があります。 実行するテストからのHTTPリクエストをモックして書き換えることができます。

サンプルのコードは次のように動きます。

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: https://playwright.dev/
    Server->>Client: 200

これにモックを挟んでみます。

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: Request http://exapmle.com/redirect
    Server->>Client: 301
    Client->>Server: https://playwright.dev/
    Server->>Client: 200 OK

最初の宛先を http://exapmle.com/redirect に変更します。 このリクエストをモックして301 レスポンスを返します。 https://playwright.dev/ にリダイレクトします。

test.beforeEach(async ({ context }) => {
  await context.route(/redirect/, route => route.fulfill({
    status: 301,
    headers: {
      location: 'https://playwright.dev/'
    }
  }));
});

リクエストの宛先に redirect を含むときにモックします。 route.fulfill を使ってレスポンスを書き換えます。

テストも次のように書き換えます。

test('has title', async ({ page }) => {
  await page.goto('http://exapmle.com/redirect');

  // Expect a title "to contain" a substring.
  await expect(page).toHaveTitle(/Playwright/);
});

開くURLを http://exapmle.com/redirect に変更します。 テストを実行してみましょう。 npx playwright test --project=chromium --uiでテストを起動します。

PlaywrightのUIを起動した直後のスクリーンショット

次のように選択してテストを実行します。

  • テスト: has title
  • Networkタブ

テスト実行完了後のスクリーンショット

Networkタブをみます。

  1. /redirect にリクエストして 301が返る
  2. / にリクエストする

と、期待通りに動いたことが確認できました。

よく見ると https://playwright.dev/js/redirection.js へのリクエストもリダイレクトされています。

Playwrightをつかってみる

Installation | Playwrightの手順に従って進めます。

npm init playwright@latest

いまはnpm initにプロジェクトテンプレートをつくる機能が組み込まれているようです。

npm-init | npm Docsによると

initializer in this case is an npm package named create-, which will be installed by npm-exec, and then have its main bin executed -- presumably creating or updating package.json and running any other initialization-related operations.

create-<initializer> というパッケージが実行されるそうです。 つまり、create-playwright - npmインストーラーのようです。

これでプロジェクトが作成されます。 次のコマンドで、サンプルのテストコードが実行出来ます。

npx playwright test

僕の環境ではエラーが出ます。

  4 failed
    [firefox] › example.spec.ts:3:5 › has title ────────────────────────────────────────────────────
    [firefox] › example.spec.ts:10:5 › get started link ────────────────────────────────────────────
    [webkit] › example.spec.ts:3:5 › has title ─────────────────────────────────────────────────────
    [webkit] › example.spec.ts:10:5 › get started link ─────────────────────────────────────────────
  2 passed (2.0s)

どうやらブラウザを実行するためのライブラリーが足りないようです。 Chromiumに限定して実行してみましょう。

npx playwright test --project=chromium

成功しました。

Running 2 tests using 2 workers
  2 passed (1.5s)

To open last HTML report run:

  npx playwright show-report

サンプルのテストケースは2つあるようです。 npx playwright show-report コマンドでテスト結果をHTML形式で見れるようです。 成功時はあんまり必要無いですね。 ちなみに、失敗時はデフォルトでHTML形式で表示されます。

Response.urlの挙動を確認する

ruby.wasmのE2Eテストをデバッグする - @ledsun blog で、Response.urlにリダイレクト後のURLが入っていない現象を観測しました。

MDNには

url プロパティの値は、あらゆるリダイレクトの後に得られる最終的な URL になります。

とあります。矛盾しているように思えます。 問題への理解を深めるために、よりシンプルな環境でRespose.urlの動作を確認します。

次のようにリダイレクトレスポンスを返すサーバーを作成します。

require 'webrick'

server = WEBrick::HTTPServer.new(Port: 3000)

# リダイレクトの設定
server.mount_proc '/redirect' do |req, res|
  res.status = 302
  res['Location'] = 'anywhere'
end

trap 'INT' do
  server.shutdown
end

server.start

起動します。

ruby server.rb

ブラウザで http://localhost:3000 を開きます。

ブラウザで開いて404ページが表示されているスクリーンショット

404ページが表示されます。 今回はHTMLの内容には興味がありません。 これで十分です。

開発コンソールを開きます。 次のようにfetch APIを使って、redirectするURLにリクエストを送り、レスポンスを得ます。

response = await fetch('redirect')

開発コンソールでResposeが表示されているスクリーンショット

Response.url は "http://localhost:3000/anywhere" です。 リダイレクト後のURLが入っています。 前回観測した現象と異なります。 MDNの記述と同じ動作をします。 PlayWrightの実行環境が特殊なのでしょうか? 謎です。

透過pngを作る 2

透過pngを作る - @ledsun blog の続きです。 手元にあるツールで、背景色の透明化を試しましたが上手く行きませんでした。 新しツールをインストールしてみます。

フリーの画像編集ソフトといえばGIMPがあります。 最近はMicrosoft Storeからインストール出来るようです。 GIMP 2.10.34 on Microsoft Store

GIMPには「ファジー選択ツール」が有ります。 「ファジー選択ツール」を使って背景を選択して削除します。

ファジー選択ツールを使って背景を削除

簡単でした。 pngとして保存するには「名前を付けてエクスポート」します。

GIMPで背景を透明化した画像

緑の代わりに白が少し残ります。ファジー選択のしきい値を調整する必要がありそうです。 次の値でやってみます。

しきい値を調整して透明化した画像

綺麗に抜けました。 ただ、装飾も消えました。 もうちょっと調整が必要そうです。

まあ、でも装飾ない方がすっきりしてていいかもしれません。 怪我の功名です。

KPTでトライ狙いすぎ問題

KPTは「チームの力で問題を見つけるふるまい」の養成ギブスです。 ふるまいに慣ていない間は違和感があります。

たとえば次のような問題が起きます。

トライ狙いすぎ問題

KPTの「改善活動」の面に強く期待しすぎて生じる問題です。 無意識に、KPTの成功指標を「TRYの数」にします。

TRYを出すことに意識をとらわれると、慣れている「個人で問題を見つけて解決する」方法を取ることがあります。一つのKPTの場に集まって、参加者がそれぞれ別々に問題を発見して解決します*1

すると、途中のプロセスが無駄に見えると思います。特にKeepに意味を感じないのではないでしょうか?アイスブレイクの一緒だと思ってはいませんか?たとえばKPTの参加者にKeepを出していない人が居ても問題ないと思っていませんか?あるいは、時間短縮のため事前にKeepやProblemを用意していませんか?

KPTをK→P→Tの順に進めることには意味があります。この意味を知るためにKPTを逆順に見て行きましょう。

良いTRYには良いProblemが必要

良いProblemが出せれば、TRYは自然に出ます。問題解決の第一歩は問題発見です。 TRYが出ない、あるいは何度かKPTをやると出なくなってくるのは、良くないProblemを並べているからです。 良くないProblemとはどんなProblemでしょうか?

解決方法がわかっているProblemは良くない

解決方法がわかっているProblemは良くないProblemです。 たとえば「自動テストが導入されていない」は良くないProblemです。

良くないというのは「正しく認識できていない」という意味です。 本当に「自動テストが導入されていない」がProblemならば、導入するというTRYになるはずです。というか、KPTをするまでもなく、業務上のタスクとして導入しているはずです。 導入していないなら、何か理由があるはずです。 それがProblemです。

て、いうと「チームメンバーが・・・」とか「うちの会社では・・・」とか言い出すんですけど、そうじゃないです。 もうこの時点で、良いProblemじゃないんです*2。 これをどんなに掘っても問題は見つかりません。TRYは生まれません。

「解決方法がわかっているProblem」から問題が発見されることはありません。 Keepからやり直しましょう。

KPTでは「チームの力で問題を発見したい」です。 解決方法がわからないProblemの方が良いProblemです。

全員が知っているProblemは良くない

全員が知っているProblemは良くないProblemです。

全員が知っているのに解決していない問題は、重要でない問題です。 例えば「社内システムがSSOに対応していないので、ログインに手間が掛かる」みたいなのは大抵チームで取り組む価値がない重要でない問題です*3

KPTでは「チームの力で問題を発見したい」です。 全員が知らない、一部のメンバーが気がついてるProblemの方が良いProblemです。 もし、1人しか気がついていないとしたら、とても良いProblemです。

良いProblemを出すのは難しい

良いProblemを出すのは難しいです。

解決方法がわからないProblemの場合

「解決方法がわからないProblem」を挙げると、リーダーの機嫌が悪くなることがあります。 リーダーが「チームの問題はリーダーが解決するもの」と、思い込んでいるためです。 責任感の強いリーダーほど、この傾向が強いです。 「問題を抱えているチームのリーダーは無能である」とか「自分のチーム運営に、チームメンバーが不満を感じている」などと思っているはずです。

リーダーの機嫌が悪くなると、メンバーは空気を読みます。「解決方法がわかっているProblem」を挙げるようになります。 あるいは、一見解決できなさそうなProblemを挙げておいて、「実はこういう手段があるんですよ・・・」と、リーダーの機嫌を使って自分の提案が通るように、チームを誘導します。

リーダーがポーカーフェイスをすればいいのか?というと、そうでもありません。 ポーカーフェイスは完璧ではありません。ポーカーフェイスから漏れる感情を読み取る人間が偉くなります。 KPT外の場でリーダーの気にしてる問題を知ろうとしてくるかもしれません。忖度ですね。

簡単な解決方法は、KPTに参加している全員がKeepを挙げることです。詳しくは後述します。

一部のメンバーだけが気がついているProblemの場合

自分しか気がついていないProblemを挙げるのは意外と難しいです。 Problemに気がついたとして、それがチームのProblemなのかわかりません。

もしも自分しか気がついていないProblemだった場合、説明が大変です。 がんばって自分なり説明した結果、上手く伝わらなかったら、 明後日のことを言う変な人になってしまうかもしれません。 恥ずかしくないですか?

こういうとき、人間の脳はいくらでも言い訳が思いつきます。

  • みんなが知っていて当然で、自分だけが知らない情報があるだけ、後になればわかる
  • 気のせい、見間違い
  • 極レアケースで滅多に問題にならない

どんだけ言い訳を並べても、本当にProblemである可能性はなくなりません。 チームのProblemか確かめるには、KPTの場でProblemとして挙げてみるしかありません。

簡単な解決方法は、KPTに参加している全員がKeepを挙げることです。

良いProblem出すには全員がKeepを挙げる必要がある

KPTを良くするための簡単な方法があります。 「全員がKeepを挙げる」です。

仕事に取り組んでいるときの緊張した心理状態では、大抵のProblemが深刻な問題に見えます。 そのため、解決できそうにないProblemを出すこと出されることに、大きな抵抗を感じます。

悪いことを言う前に、良いことを言うと、この抵抗感が消滅します。 なぜか全然わからないんですが、経験則的にマジです*4。 同時に「明後日のことを言ってしまうかも」問題も深刻でないものにしてくれます。 「解決方法がわからないProblem」「一部のメンバーだけが気がついているProblem」どちらを挙げる場合にも効果的な魔法です。

私は、Keepは挙げることが重要で、Keepの内容はあまり重要でないと思っています。 どんな内容でも良いことを口にするだけで、なぜか問題を深刻に感じなくなる効果があります。

例えば、つぎのものでも大丈夫です。

  • 天気が良い
  • 給与が振り込まれた
  • おいしいご飯屋さんをみつけた
  • 楽しみしているゲームが発売された

自分がやった結果でなくても良いです。 チームで取り組んでいる内容でなくても良いです。 単にKPTの場で、脳が「現状は最悪ではない」と認識すれば十分です。

何よりも大事なのは、KPTに参加している全員がKeepを挙げることです。 誰か一人でも最悪の気分の人がいると「解決方法がわからないProblem」を挙げられなくなります。 その人は「自分だけが気がついているProblem」を挙げてくれないでしょう。

Keepが挙げにくい状況

Keepが挙げにくい状況があります。 例えば、次のような場合です。

  • KPTに、チーム外の偉い人が参加する
  • 事前にKeepを考えてくる

こういうとき人間は、かっこいいKeepを挙げたくなります。 「チームで取り組んでいる素晴らしい施策」を挙げたくなります。 こういうKeepの候補は少ないです。 また、前の人が出したKeepと同じでは格好がつきません。 そして、先に格好いいKeepを思いついた人だけがKeepを挙げ、残りの人は「Keepはありません」になります。

また、こういう格好いいKeepを挙げるKPTでは、KPTに急に参加した偉い人は格好いいKeepを挙げられないので「チームの普段の細かい取り組みはよくわからないから」とKeepを挙げるのをサボります。

これはとても良くないです。 偉い人が「現状は最悪ではない」モードでない場で、問題は発見されません。 時間の無駄です*5

Keepが効かない状況

  • 事前にProblemを考えてくる

Problemを考えているとき、参加者全員が「現状は最悪ではない」モードに入っているかわかりません。 事前にProblemを考えてしまうと、良いProblemは出てきません。

ただし、KPT以外の時間でProblemを考えることには意味はあります。 考えたProblemはKPTを待たずにチームに共有しましょう。 KPTの開催日を待つ意味がありません。 共有が遅くなると、チームは損をします。

もし、KPT以外にProblemをチーム内で相談する場がないとしたら、それはそれで別の問題です。 KPTに大きすぎる役割を与えているように思えます。

まとめ

KPTでは「チームの力で問題を発見したい」です。 「良いProblemを挙げられる場を作れるか」の勝負です。 「こんなこと言っても良いのかな?」というProblemが挙げられる会になったら成功です。 良いProblemさえ挙げられればTRYは自然に出てきます。

そのために

  1. 全員がKeepを挙げる
  2. Keepの内容はくだらないことほど良い。特に偉い人ほど

*1:KPTがチームとしてのふるまいを学習する場ではなくなっています。業務と別にサブプロジェクトがあって、KPTがその進捗会議になっているイメージです。

*2:もっというと、これはたぶんKPTを使って「自動テストの導入」に誘導しようとしています。個人の問題を他のメンバーに押しつけているだけで、チームで解決するムーブになっていません。「自動テストは我々の開発の役に立つだろうか?」という問いをすっ飛ばして、個人の意見をいきなり結論として押しつけています。

*3:社内システムを改善するチームだとしたら、重要な問題です。が、その場合はKPTで扱うような問題ではなく。業務上のタスクになっているはずです。

*4:アンカリングの一種なんですかね?理由を知っている人がいたら教えてください。

*5:「偉い人がチームを褒めたたえる」場にしてもいいですが、別にKPTでやる必要ないですよね?

WebAssembly text formatからwasmバイナリをつくる環境を整える

wasmバイナリのカスタムセクションを書いてみる - @ledsun blog でプリアンブルとセクションを持つwasmバイナリを書き出せるようになりました。 最低限のwasmバイナリファイルの構造を理解したといえます。

お題

Writingの次はReadingです。 シンプルなwasmバイナリを作って読んでみましょう。

WebAssemblyにはテキストフォーマットがあります。 たとえば、最小では次の文字列です。

(module)

このテキストフォーマットからどのようなwasmバイナリが生成されるのでしょうか?

  1. プリアンブルだけのバイナリ 0x00 0x61 0x73 0x6D 0x01 0x00 0x00 0x00になる
  2. 何かしらのセクションがつく

どちらになるでしょうか?

手法

WebAssembly text formatからwasmバイナリを生成してみましょう。 Converting WebAssembly text format to Wasm - WebAssembly | MDNによると、wabtというwasmバイナリ用のツールがあるようです。 これを使えばテキストフォーマットからwasmバイナリが作れそうです。

wabtのインストール

わたしの環境はWSL上のubuntuです。

リポジトリのClone

git clone --recursive https://github.com/WebAssembly/wabt
cd wabt
git submodule update --init

ビルドツールのインストール

CMakeninja-buildが必要です。 それぞれインストールします。

CMake

sudo apt install cmake
確認
►cmake --version
cmake version 3.22.1

CMake suite maintained and supported by Kitware (kitware.com/cmake).

ninja-build

sudo apt install ninja-build
確認
►ninja --version
1.10.1

ビルド

make

確認

►bin/wat2wasm --version
1.0.33 (git~1.0.33-31-gbceb2434)

検証

wabtにふくまれるwat2wasmを使って、WebAssembly text formatからwasmバイナリを生成します。

バイナリファイルを生成

echo '(module)' > minimam.wat
~/wabt/bin/wat2wasm minimam.wat -o minimam.wasm

バイナリファイルを確認

►hexdump -C minimam.wasm
00000000  00 61 73 6d 01 00 00 00                           |.asm....|
00000008

正解は 1. プリアンブルだけのバイナリ 0x00 0x61 0x73 0x6D 0x01 0x00 0x00 0x00になる でした。

参考

wasmバイナリのカスタムセクションを書いてみる

最小限のWebAssemblyのバイナリファイルを書く - @ledsun blog でwasmバイナリのプリアンブルの書き出しに成功しました。 続いてセクションを書き出してみます。

Modules — WebAssembly 2.0 (Draft 2023-07-24) によると

セクションの定義
1バイトのID、4バイトのサイズ、サイズで指定した中身のようです。

ID 0はカスタムセクションというデバッグ情報を埋め込むセクションです。 カスタムセクションはnameと呼ばれるUTF-8文字列を埋め込みます。 ということはnameがhogeなカスタムセクションは0x00 0x04 0x00 0x00 0x00 0x68 0x6f 0x67 0x65で良さそうです。

次のRubyスクリプトを使って書き出します。

File.open("custom_section.wasm", "wb") do
  # preamble
  _1.write [0].pack('C')          # 0x00
  _1.write 'asm'.bytes.pack('C3') # 0x61 0x73 0x6d
  _1.write [1].pack('I<')         # 0x01 0x00 0x00 0x00

  # custom section
  _1.write [0].pack('C')           # 0x00
  _1.write [4].pack('I<')          # 0x04 0x00 0x00 0x00
  _1.write 'hoge'.bytes.pack('C4') # 0x68 0x6f 0x67 0x65
end

これをGoogle Chromeコンパイルします。 実行するためのHTMLです。

<html>
  <title>Load wasm binary</title>
  <script>
    fetch('./custom_section.wasm')
      .then(response => response.arrayBuffer())
      .then(buffer => WebAssembly.compile(buffer))
      .then(module => console.log(module))
      .catch(e => console.error(e));
  </script>
</html>

すると次のエラーが起きます。

Google Chromeの出力したコンパイルエラー

(インデックス):8 CompileError: WebAssembly.compile(): section (code 111, "") extends past end of the module (length 103, remaining bytes 1) @+14

理由はよくわかっていません。 結論だけいうと、sizeを7にするとWebAssemblyモジュールとして認識されます。

# custom section
_1.write [0].pack('C')           
_1.write [7].pack('I<')  # 4ではなく7
_1.write 'hoge'.bytes.pack('C4')

この3バイトはどこからでてきたのでしょうか? sizeをセクションの頭から数えるなら、7ではなく8や9になりそうなものです。

最小限のWebAssemblyのバイナリファイルを書く

仕様

WebAssemblyのバイナリファイルはModuleと呼ばれます。 Modules — WebAssembly 2.0 (Draft 2023-07-24) に仕様があります。

  1. プリアンブル
  2. セクション

に分かれています。セクションは空でも良いです。 つまり、最小限のWebAssemblyモジュールはプリアンブルだけあればよいです。

WebAssemblyモジュールのプリアンブル

プリアンブルは0x00 0x61 0x73 0x6D 0x01 0x00 0x00 0x00の8バイトです。 これは前半の0x00 0x61 0x73 0x6DがASCIIのasmです。 後半の0x01 0x00 0x00 0x00はWebAssemblyモジュールのバージョンを表す1です。 1が1バイト目にあるのでリトルエンディアンの32ビット整数です。

バイナリファイルを書く

次のRubyスクリプトで書き出せます。

File.open("preamble.wasm", "wb") do
  _1.write [0].pack('C')          # 0x00
  _1.write 'asm'.bytes.pack('C3') # 0x61 0x73 0x6d
  _1.write [1].pack('I<')         # 0x01 0x00 0x00 0x00
end

hexdumpコマンドで確認してみます。

hexdump -C preamble.wasm
00000000  00 61 73 6d 01 00 00 00                           |.asm....|
00000008

良い感じに出来てそうです。

動作確認

WebAssemblyモジュールとして読み込めるか試してみます。 次のHTMLファイルを用意します。

<html>
  <title>Load wasm preamble</title>
  <script>
    fetch('./preamble.wasm')
      .then(response => response.arrayBuffer())
      .then(buffer => WebAssembly.compile(buffer))
      .then(module => console.log(module));
  </script>
</html>

作ったバイナリファイルを読み込んでWebAssembly.compileします。

ruby -run -e httpd .でHTTPサーバーを起動します。 ブラウザで http://localhost:8080/ を開きます。 開発コンソールを開きます。

開発コンソールのModuleと出力される

無事WebAssemblyモジュールとして認識されました。

不正な場合は?

もしかすると何もチェックされていないかもしれません。 間違ったWebAssemblyモジュールも試してみます。

asmじゃなくてackだったら

マジックワードが不正なWebAssemblyモジュールを読み込ませると

バージョンが2

バージョンにWebAssemblyモジュールを読み込ませると