WebAssemblyを触りたかったのでCASL2 Playgroundの実装で使ってみた

以前このブログでも紹介したWebブラウザでCASL2を動かすCASL2 Playgoundですが、バイトコードインタプリタの実装をTypeScriptからC++へ移植しWebAssemblyでJavaScriptから使うように実装してみました、という日記です。

github.com

実装してみた理由は、ただWebAssemblyを触ってみたかっただけですw

あと、C++についてほとんど書いたことがないのですが読みたいなぁと思うことがあり、文法を学ぶとっかかりが欲しかったのもあります。例えば、一様初期化というのも読めなかったのですよね。 一様初期化 - cpprefjp C++日本語リファレンス

X x {0};

C++については以下の書籍を読みました。良い本なのか判断できるC++の知識は持ち合わせていないのですが、他の言語を触ったことある人がC++に触れるには良かったように感じました。

WebAssemblyについては以下の書籍を読みました。あまり面白くなかった。。けど、Web上から情報を集める手間なくWebAssemblyにとっかかるには良かったかもしれません。

JavaScriptC++とのやりとりについては、Emscriptenの公式サイトが良いです。

Interacting with code — Emscripten 3.1.49-git (dev) documentation

変更前

以前の記事で紹介した通り、アセンブル処理をするassemblerと、その結果であるバイト列を解釈して実行するinterpeterを、分割した作りになっている。

https://i.gyazo.com/178953be2f872979b755dc068647b6dd.png

変更後

バイト列を解釈して実行するinterpreterC++で実装し、JavaScript側でアセンブル処理した結果のバイト列をコピーして渡す方式とする。

https://i.gyazo.com/4ce341ca967e684f9124f4179deb8109.jpg

wasmファイルの生成

C++ソースコードからwasmへのコンパイルはemccというコマンドを使う。CMakeも使ってみて、とりあえずビルドできたものの、CMakeムズカシイ。。

emccによって、wasmファイルとjsファイルが出力される。jsファイルはwasmをダウンロードして使えるようにするためのコードが自動生成されたもの。このjsファイルをHTMLのscriptタグで読み込むようにすればwasmファイルをダウンロードしてくれる。

ブラウザのデベロッパーツールでバイナリであるwasmファイルの内容を見れる。バイナリではなくwat形式と呼ばれるテキストに変換したもので読めるのだけど、これが面白い!なんとS式!

https://i.gyazo.com/195f65c53db10476c8345dad1d8c39ae.png

JavaScriptからC++の関数を呼ぶ

JavaScriptからC++のコードを呼び出す方法については、2通り使ってみた。

後者の方がスマートに呼び出せる。だけど、デバッグのしやすさは前者かもしれない??なぜなら、前者の方式だとJavaScript側から呼べるようにしたC++側の関数名がwat形式のテキストに出力されるのだけど、後者のEmbindを使った方式だとC++側の関数名をwat形式のテキストから探すことができない。もしかしたら何か方法があるのかな?それとも現実的にwat形式のテキストを使ってデバッグすることは滅多にないのかな?

例えば、以下のような makeMachine 関数は、wat形式のテキストを検索すると発見できる。

extern "C" {

  EMSCRIPTEN_KEEPALIVE int makeMachine(uint8_t* bytes, int size, int startAddress) {
    ...
  }

https://i.gyazo.com/ab6ec9ada4f39530b3df650940db5c6e.png

JavaScriptからC++へバイト列を渡す

JavaScriptからC++側の malloc を呼び出しメモリを確保、確保したメモリ領域にアセンブル結果のバイト列をコピーする。そしてC++側の関数を ccall で呼ぶときに引数として確保したメモリのポインタを渡す。

// JavaScript

// assembleの結果のバイト列をwasmのメモリにコピー
const arr = new Uint8Array(assembleResult.buffer);
this.pointer = Module._malloc(Uint8Array.BYTES_PER_ELEMENT * arr.length);
if (this.pointer == null) {
  throw new Error("entry_point :: malloc failed");
}
Module.HEAP8.set(arr, this.pointer / Uint8Array.BYTES_PER_ELEMENT);

// wasmの関数を呼び出して初期化
const setupResult = Module.ccall('makeMachine',
  'number',
  ['number', 'number', 'number'],
  [this.pointer, tmpMemory.content.byteLength, startMemAddress]
);

C++側のバイト列をJavaScriptから参照する

Embindを使ってC++側のバイト列をJavaScript側から参照できるようにする。JavaScriptでは Uint8Array 型になるが、他の部分で処理しやすいように DataView として持つようにした。

// C++

#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>
using namespace emscripten;

unsigned char *buf;
size_t bufSize;

val getMemory() {
  return val(typed_memory_view(bufSize, buf));
}

// JavaScriptから直接参照できるようにする
EMSCRIPTEN_BINDINGS(interpreter_module) {
  function("getMemory", &getMemory);
}
// JavaScript

// wasmのメモリの参照を取得してフィールドに保存
const memoryUint8Array = Module.getMemory();
this.memory = new Memory();
this.memory.content = new DataView(memoryUint8Array.buffer, memoryUint8Array.byteOffset, memoryUint8Array.byteLength);

JavaScript側で malloc を呼び出してメモリを確保したけど、C++側で確保したメモリをEmbindでJavaScript側へ公開して、そこにアセンブル結果を書き込みする方が良いのかな?

C++側では配列ではなく vector で保持しようとしたのだけど、JavaScript側からC++側の関数を呼び出したときに保持していたはずのバイト列がなぜか初期化されてしまって上手くいかなかった。

おしまい

色々試行錯誤して動くものはできたけど、まだまだ不明点が多すぎますね。。深掘りしたい気持ちになったときに改めてチャレンジしよう!

Intel MacのUbuntu上でAndroid OSをビルドした

AOSP (Android Open Source Project) のCodelabがあるのでチャレンジしてみた日記です。

AndroidBluetoothまわりを調べていてOSレイヤーの実装をいじってみたくなり、Android OSをビルドできる環境がないものかと考えてました。ビルド環境に求められる要件は以下に記載がありまして、macはダメ、64ビットCPU、ストレージ512GB、メモリ16GB(推奨64GB)という感じです。

Requirements  |  Android Open Source Project

手元にはちょうどいいマシンがなく、AWSでやるのが良いかなぁと思っていたのですが家でホコリをかぶってるIntel Macの存在に気がつきました!ストレージが不足していますが外付けSSDを接続してUbuntuをインストールすればいける気がする!

ということで無事ビルドできました。やったことは大きく2つです。

Intel MacmacUbuntuデュアルブートできるようにする

ググるとやり方がたくさん出てくる。

外付けSSDパーティション作ってUbuntu 18.04をインストールする。

初回Ubuntuインストール後、Wi-Fiを有効にしようとすると「No Wi-Fi Adapter Found」というエラーになってしまった。Ubuntu再インストールを試みてインストールウィザードの途中にあるWi-Fiドライバを探すオプションをONにすることで解決。

Ubuntu上でAndroid OSをビルドする

下準備

Codelabの手順に従って進めていく。

Android developer codelab  |  Android Open Source Project

必須パッケージをインストールする。問題なし。

repo クライアントをインストールする。問題なし。

repo を使ってソースコードをチェックアウトする。問題なし。

ビルド

ビルドを開始する!

$ source build/envsetup.sh
...

$ lunch aosp_cf_x86_64_phone-userdebug
...

$ m
...
soong bootstrap failed with: exit status 1

エラー!!

調べてみる。多分これと同じ。

stackoverflow.com

メモリが足りないのだな。Codelabのビルド要件に、メモリ推奨64GBとあったのでswapを大きくすることで対応する。

$ sudo swapoff /swapfile
$ sudo fallocate -l 64G /swapfile
$ sudo mkswap /swapfile
$ sudo swapon /swapfile

ビルド再チャレンジ!

$ m

...

2023-10-31 09:19:41 - build_super_image.py - INFO    : Done writing image out/target/product/vsoc_x86_64/super.img
[100% 190384/190384] //frameworks/base/boot:platform-bootclasspath monolithic hi

#### build completed successfully (19:51:23 (hh:mm:ss)) ####

成功!20時間くらい・・・!

m コマンドの直前に実行した lunch aosp_cf_x86_64_phone-userdebug ですが、lunchでビルドする成果物のターゲット指定している。引数は <product_name>-<build_variant>というフォーマットで、<product_name>の部分で成果物であるOSを動かす端末の指定をする。

Building Android  |  Android Open Source Project

今回のCodelabではaosp_cf_x86_64_phoneとなっていて、cfというのはCuttlefishというローカルPC上でも動く仮想Androidバイスを指定しているらしい。

Cuttlefishが起動しない

というわけで、Cuttlefishを動かす。が、起動しない・・・。事象としては以下と同じに見えるのだけど、試行錯誤したけど動かせなかった。

Error running local cuttlefish instance

AVDで動かす

アプローチを変えて、Androidアプリ開発でも使うエミュレータAVDで動かすことにする。

Using Android Emulator virtual devices  |  Android Open Source Project

ドキュメントを読むと<product_name>としてsdk_phone_x86_64を指定する必要があるらしい。<build_variant>が省略して書かれているのだけど開発用なのでengを指定することにした

$ lunch sdk_phone_x86_64-eng
...

$ m
...

[100% 44917/44917] Create system-qemu.img now
  
#### build completed successfully (06:30:13 (hh:mm:ss)) ####

成功!ビルド時間が6時間に縮んだ理由は今のところ謎。

動かしてみる。

$ emulator

キタコレ!!!

おしまい

これでいつでもAndroid OSをいじいじできる...

CASL2 Playgroundの実装ちょっと変えた

以前、WebブラウザでCASL2を動かすCASL2 Playgoundを実装しました。

github.com

その時の日記に書いていたのですが、作り変えたい部分があったので作り変えました、という日記です。

変更前

アセンブラとして機械語を生成する処理と、Commet2上で動きをシミュレートするための関数と、両方をごちゃっと実装してしまっている感じがあって分かりづらい。

Image from Gyazo

これは以前のブログ記事にも貼った画像。入力となるソースコードアセンブルして機械語に変換、同時にCommet2上での動きをシミュレートする関数を生成する。

画面の「Step over」ボタンをクリックする度に1行分シミュレート関数を実行する。

Image from Gyazo

アセンブル時に機械語だけでなく、シミュレート関数も生成してしまっているのがとても分かりづらい。

変更後

Commet2で動かす部分については「言語実装パターン」に書かれている パターン28 レジスタ方式バイトコードインタプリタ を参考しながら機械語を入力とするインタプリタとして作り直すと良さそう。

Image from Gyazo

変更後はこうしました。入力となるソースコードアセンブルして機械語に変換しメモリに保存するassemberと、メモリから機械語を読み取り処理を実行するinterpreterに分離。interpreterは以前のブログ記事にも書いた通り「言語実装パターン」という書籍のパターン28 レジスタ方式バイトコードインタプリタを参考に実装した。

メモリから2byte (CASL2では1語が2byte)ずつ読み取り、読み取った機械語で分岐する。

// 機械語の命令
const LD   = 0x14
const ADDA = 0x24
...

export class Interpreter {
  PR: GeneralRegister
  memory: Memory
  ...

  constructor(
    PR: GeneralRegister,
    memory: Memory,
    ...
  ) {
    this.PR = PR
    this.memory = memory // アセンブルした結果が格納されてる
    ...
  }

  step(): boolean {
    const [opcode, operands] = this.readWord()
    advancePR(this.PR, 1) // 参照するメモリ1語分だけ進める
    switch (opcode) {
      case LD:
        this.ld(operands)
        break
      case ADDA:
        this.adda(operands)
        break
      ...
  }

  private readWord(): [number, number] {
    const word = this.memory.lookup(this.PR.lookup())
    const upperByte = word >> 8
    const lowerByte = word & 0b11111111
    return [upperByte, lowerByte]
  }
}

JavaScriptでバイト列を扱いたい時は、ArrayBufferで領域を確保、読み書きはTypedArrayDataViewでArrayBufferをwrapしてからやる。CASL2では2byteのうち、先頭1byteが命令、それ以降がoperandという仕様であり、ビッグエンディアンで読み書きしたい。エンディアンを明示的に指定する場合はDataViewを使う。

元の実装と比べて実際の挙動に近くなったと思うし、コードも読みやすくなった!

ちなみに「言語実装パターン」というのはこの書籍です。

おしまい

書き換えたいなぁとモヤモヤが残っていたのですっきり!!