ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

BCCでTCにアタッチし、egressのIPを表示する

サーバではいろいろな通信があるけれど、実際どんな通信が行われているか全貌を確認するのはなかなか難しい。

blog.ssrf.in

上記のソリューションはかっこいいが、KRSIなんで、「うちの環境のカーネルじゃちょっと...」となりがち。BPFあるあるだと思う。

一方でBPF自体は、カーネルでサポートされて随分時間が経ってきており、徐々に使えるカーネルが普及してきた。

とりあえず通信のログを残したい、というだけであればTCにBPFをアタッチする方法も使えて、これはUbuntu 20.04のカーネル(5.4 〜 5.8)なら十分使える。

// よく考えたらTCなのでブロックもできるのかな?

まあ今度調べるとして、egressの通信をキャプチャしてdst IP(今回はv4だけです)を集計するサンプルを残す。

動作環境(主にカーネルバージョン)について

今回は別の環境カーネル5.4でも動作確認しま。もう少し低くても行けるかもしれない。

しかしこのブログの環境は以下。

ubuntu@udzura.local:~$ uname -a
Linux kondo-ke-focal 5.8.0-63-generic #71~20.04.1-Ubuntu SMP Thu Jul 15 17:46:44 UTC 2021 aarch64 aarch64 aarch64 GNU/Linux

BCC の環境を作る

BCCといえば自作ビルド... という時代もあったが、今回はパッケージマネージャー経由のもので十分。

$ apt install libbpfcc python3-bpfcc python3-pyroute2

Ubuntu 20.04 では libbcc 0.12.0 が入ってくる。

自分の環境は全然違うディストリです!という場合はいっそ --host=netdebianを立ち上げてもいいと思う。

$ docker run -ti --rm -v /lib/modules:/lib/modules -v /usr/src:/usr/src --privileged --net=host debian:11-slim
~# apt update && apt install libbpfcc python3-bpfcc python3-pyroute2

多分動く。debian 11 では libbcc 0.18.0 が入ってくる。

Pythonスクリプト

いろいろな実装のつぎはぎだが、これを動かす。

from bcc import BPF, libbcc
from struct import unpack
import pyroute2
import time
import sys

code = """
// SPDX-License-Identifier: GPL-2.0+
#define BPF_LICENSE GPL

#include <linux/pkt_cls.h>
#include <uapi/linux/bpf.h>
#include <bcc/proto.h>

BPF_HASH(ip_counter, u32, u64);

int counter(struct __sk_buff *skb) {
    u32 key = 0;
    u8 *cursor = 0;

    struct ethernet_t *ethernet = cursor_advance(cursor, sizeof(*ethernet));
    // 0x0800 = IPv4
    if (ethernet->type != 0x0800)
        return TC_ACT_OK;

    struct ip_t *ip = cursor_advance(cursor, sizeof(*ip));
    key = ip->dst;

    if (key == 0)
        return TC_ACT_OK;

    // 192.168.0.0 ~ 192.168.255.255 のログは取らない
    // 他のプライベートIPレンジは宿題とします。
    if (3232235520 <= key && key <= 3232301055)
        return TC_ACT_OK;

    u64 zero = 0;
    u64 *value = ip_counter.lookup_or_try_init(&key, &zero);
    if (value)
        *value += 1;

    return TC_ACT_OK;
}
"""

# ライブラリを知らないので頑張って自作した
def int_to_ip(nr_):
    nr = unpack('I', nr_)[0]
    return '{}.{}.{}.{}'.format(
                (nr >> 24 & 0b11111111),
                (nr >> 16 & 0b11111111),
                (nr >> 8  & 0b11111111),
                (nr       & 0b11111111)
            )

def count(nr_):
    return unpack('L', nr_)[0]

def count_packet(iface):
    bpf = BPF(text=code, cflags=["-w"])
    func = bpf.load_func("counter", BPF.SCHED_CLS)
    counter = bpf.get_table("ip_counter")
    
    print("map fd: {}".format(counter.map_fd))
    ip = pyroute2.IPRoute()
    ipdb = pyroute2.IPDB(nl=ip)
    idx = ipdb.interfaces[iface].index
    ip.tc("add", "clsact", idx)

    # ffff:fff3 というのが、 egress のことらしい。
    # ingress なら ffff:fff2 らしい。
    ip.tc("add-filter", "bpf", idx, ":1", fd=func.fd, name=func.name,
          parent="ffff:fff3", classid=1, direct_action=True)

    print("Hit CTRL+C to stop")
    while True:
        try:
            print("outgoing dest ips in 1s:")
            for k, v in counter.items():
                print(f'k: {int_to_ip(k)}, v: {count(v)}')
            counter.clear()
            time.sleep(1)
        except KeyboardInterrupt:
            print("Removing filter from device")
            break
    ip.tc("del", "clsact", idx)
    ipdb.release()

if __name__ == '__main__':
    iface = sys.argv[1]
    count_packet(iface)

※ ところで拙作 RbBCC ではこれと同じことができない。libbccのFFIなので本当はできるはずだが、pyroute2のRuby版がないのでBPFプログラムを作れてもアタッチできない。pyroute2は巨大なので流石に僕は移植する時間が...。

pyroute2がRubyにあると嬉しいなあという願いだけ残そう。

tcにアタッチする

上記をsudoで実行するとデバイスにアタッチできる。

$ sudo python3 filter.py enp0s1
map fd: 4
Hit CTRL+C to stop
outgoing dest ips in 1s:
outgoing dest ips in 1s:
outgoing dest ips in 1s:
outgoing dest ips in 1s:

この状態で、 enp0s1 を通って外に行くような通信をすると、ちゃんと記録されることがわかる。

$ ping hatena.ne.jp
PING hatena.ne.jp (54.65.114.10) 56(84) bytes of data.
64 bytes from ec2-54-65-114-10.ap-northeast-1.compute.amazonaws.com (54.65.114.10): icmp_seq=1 ttl=232 time=36.1 ms
64 bytes from ec2-54-65-114-10.ap-northeast-1.compute.amazonaws.com (54.65.114.10): icmp_seq=2 ttl=232 time=32.1 ms

...

outgoing dest ips in 1s:
k: 54.65.114.10, v: 1
outgoing dest ips in 1s:
k: 54.65.114.10, v: 1
...

今回は雑然とMapに保存しているが、定期的に取り出して監査ログにするなり、怪しい通信を確認するなりやろうと思えばできそうかな?

トラブルシュート

tc にアタッチする前に、以下のコマンドで現在何もクラスがないことが確認できる。

$ tc qdisc show dev enp0s1 clsact
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn

上のスクリプトの実行が失敗すると、中途半端なクラスやフィルターが残る。

$ tc qdisc show dev enp0s1 clsact
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn 
qdisc clsact ffff: parent ffff:fff1 <- これ

削除は以下のコマンドで。

$ sudo tc qdisc del dev enp0s1 clsact

(実はこの記事の主題こと)宣伝

RubyKaigi 2022 で BPF の話をします。と言いつつこの記事でしているネットワーク関係の機能の話はしなくて、Observability中心です。

rubykaigi.org

思えば2年以上前にとりあえず完成させたものですが、RbBCCの話中心に色々やっていくような気がします。正直、現場で運用にも携わる人としては、CO-REが普通に使えるカーネルに全部置き換えよう! ってすぐにはいかないので、全然BCC使っていこうと思っていますから...。

udzura.hatenablog.jp

今年はハイブリッド開催だそうです。できる形で是非ご参加くださいませ。赤福

(今気づいたけど1行もRubyのコードがない記事で宣伝してるw)

参考にしたページ

atmarkit.itmedia.co.jp

yunazuno.hatenablog.com

github.com

RustによるRubyアプリケーションサーバー Flamboyant: version 0.1.0.rc

「なんとか動かせた」という世界の話なんですが、途中経過をメモしておきます。

先日Rustでgemをなるべくイマい形で書く方法をメモっていたんですが、一つは実用的なものを作ろうと思ってサーバを書いています。

udzura.hatenablog.jp

Flamboyant という名前をつけました。

github.com

RubyGems 3.4.0.dev (今のgithub masterにあるやつ)を使っていればRuby自体は3系であれば動くようです。インストールは:

gem install flamboyant --pre

あるいはGemfileに:

# 2022/06/11 現在
gem "flamboyant", "0.1.0.rc2"
続きを読む

Fukuoka.rb #256 LTの補足: RustでRubyGemを書く話

fukuokarb.connpass.com

docs.google.com

キリがいい回で話しました。およそLTで収まる話ではなかった(5分、一番難しい)ため、最も肝心の「どうやってコードを書くか」の部分を全部端折ってこのブログで補足することにしました。


サンプルプロジェクト

github.com

ここを読み込めば大体書けるようになるのでは、という感じなのですが、書く際の留意点をつらつら残します。

(5月初旬現在)事前にそもそも最新のRubyGemsをインストールする必要がある

色々方法があるんですが、10年前の方法を思い出すために ruby setup.rb を叩く方法を使いました。逆にRubyGemsさえ最新ならRubyを3.2まで上げなくて良い? と思う。

rbenv shell 3.2.0-preview1
git clone --depth=1 https://github.com/rubygems/rubygems.git
cd rubygems
ruby setup.rb

プロジェクトの初期化

gemとしてもlib crateとしてもvalidなディレクトリ構成にすればOKです。あと、拡張gemの名前で - を含めると難儀します(自動で読み込むC関数名に - を含もうとするため)ので気をつけましょう。

bundle gem oye_rust
cd oye_rust
cargo init --lib

ちなみにこれで生成されたプロジェクトは Cargo.lock がgitignoreされていますが、gemとしてはこのファイルは必要なのでgitの管理対象にしましょう。

このあと Cargo.toml を更新する必要があり、パッチ作者が作ったプロジェクトが参考になりますが、罠があって rb-sys を使うときは features = ["link-ruby"] を指定していないと私の環境(Apple Silicon の Macです)では読み込み時に関数が見つからないなどと言われました。この辺は怪しい(これだとlibrubyを二重にリンクしてね?とか)感じもしますが、きっと誰かが精査してくれるということで...。

crate-type = ["cdylib"] になるというのもポイントです。

[package]
name = "oye_rust"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
libc = "0.2.125" # どうせ後で使うので宣言しています
rb-sys = { git = "https://github.com/ianks/rb-sys", branch = "main", features = ["link-ruby"] }

一度 cargo build して Cargo.lock を更新しましょう。

Rust側実装

rb-sys は基本的にruby.hからbindgenで生成した関数群なので、ほぼ全部unsafeです。頑張りましょう。逆にいうとRustでただのFFIのコードを書くだけとも言えます(補完も効きますし)。

コードを見てあとはコンパイラ耳をすませば書けると思うんですが、渋いかもポイント*1をいくつか書いておくと、

マクロが基本使えないので、 Qnil/Qtrue/QfalseRSTRING_LEN() やとにかく色々使えない

ここはbindgenの制限だと思います。抽出されている定数もあるのでbindgenでの運用がどうだったかもうちょい調べようとは思いますが...。

Qnil について言えばは実態はenumなのでそっちを直接使えば大丈夫です。

#[repr(u32)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub enum ruby_special_consts {
    RUBY_Qfalse = 0,
    RUBY_Qtrue = 20,
    RUBY_Qnil = 8,
    RUBY_Qundef = 52,
    RUBY_IMMEDIATE_MASK = 7,
    RUBY_FIXNUM_FLAG = 1,
    RUBY_FLONUM_MASK = 3,
    RUBY_FLONUM_FLAG = 2,
    RUBY_SYMBOL_FLAG = 12,
}

しかしこれはこれで別の渋いポイントが。

型のサイズに厳しい

上記のように Qnil/Qtrue/Qfalse の大きさは本来 32bit ですが、筆者の環境(M-1 Mac)では VALUE は64bitです。なので以下のコードはコンパイルできません。

use rb_sys::ruby_special_consts;
let qnil = ruby_special_consts::RUBY_Qnil;
std::mem::transmute::<ruby_special_consts, VALUE>(qnil);

ひとまずこういう構造体を挟んで回避しました。これは as u64 でもいいのかな、試していませんが、エンディアンのことを考えると言い切れない(試そうね)...。

#[repr(C)]
struct WrappedValue {
    value: u32,
    padding: u32,
}

let qnil = WrappedValue {
    value: ruby_special_consts::RUBY_Qnil as u32,
    padding: 0,
};
std::mem::transmute::<WrappedValue, VALUE>(qnil)

VALUEruby_special_constsのサイズは当然フラグや環境で変わるということでしょうので*2、厳密にはRust側でも #[cfg] などで切り替えるべきでしょう。

メソッドに対応する関数は、引数を持つ場合基本的に std::mem::transmute() で置き換えることになる

Rustにおける関数ポインタ型は、引数と戻り値の型がセットでそれぞれ別の型として認識されます。

しかしCRubyでは引数に何個VALUE型を取っても問題なくメソッド定義として利用できる関数ポインタ型として扱われます。

Rustの表現で言うと、 fn() -> VALUE 型でも fn(VALUE) -> VALUE 型でも fn(VALUE, VALUE) -> VALUE 型でもvalidであり、 rb_define_method() の引数に使えます。

従ってC言語のノリで関数を書いていた場合、Rustではこれもtransmute()が必要になります。

#[no_mangle]
unsafe extern "C" fn test_meth(rbself: VALUE) -> VALUE {
    ....
}

#[no_mangle]
pub extern "C" fn Init_test() {
    // ...
    let function_name = CString::new("test").unwrap();
    let callback = unsafe {
        std::mem::transmute::<unsafe extern "C" fn(VALUE) -> VALUE, unsafe extern "C" fn() -> VALUE>(
            test_meth,
        )
    };

    unsafe { rb_define_method(klass, function_name.as_ptr(), Some(callback), 0) }
}

引数は原則 rb_get_args() のような仕組みで取り扱うのも手かと思います*3

雑感

Rustで拡張gemを書くにしても libruby の関数を使う必要があるわけで、それを現状提供されている rb-sys crate 経由で利用しようとすると unsafe 祭りになるのが現状です。Rustの安全性とは...という気持ちになる。

しかしそれでも静的検査してくれるところは強力だし、他ツールやライブラリ(ことパッケージマネージャ)が整っているなど生産性はかなり上がるのですが。

一方でストレスなくRustでgemを書きたいなら、 nix crate のようなノリでRubyGemを書くための薄いラッパーcrateがあるといいと思います。OSSチャンスだ!

まだほぼ中身はないですが名前を思いついたので押さえておくテスト。一応最低限は書きました。

このcoffretを使っていないとこういう感じで、

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_oye_rust() {
    let name = CString::new("Rust").unwrap();
    let object = unsafe { rb_cObject };
    let klass = unsafe { rb_define_class(name.as_ptr(), object) };

    let function_name = CString::new("mostrarme").unwrap();
    let callback = unsafe {
        std::mem::transmute::<unsafe extern "C" fn(VALUE) -> VALUE, unsafe extern "C" fn() -> VALUE>(
            test_show_self,
        )
    };

    unsafe { rb_define_method(klass, function_name.as_ptr(), Some(callback), 0) }
}

coffretを使うととりあえずこういう感じにできました。もっと綺麗にできると思います。

use coffret::class;
use coffret::exception;

fn init_oye_rust_internal() -> Result<(), Box<dyn Error>> {
    let object = class::object_class();
    let klass = class::define_class("Rust", object);

    let callback = class::make_callback(&test_show_self);

    class::define_method(klass, "mostrarme", callback, 0);
    Ok(())
}

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_oye_rust() {
    match init_oye_rust_internal() {
        // これはRustのErrorをRubyの例外として上げるというかっこいい関数
        Err(e) => exception::rustly_raise(e.as_ref()),
        Ok(_) => {}
    }
}

雑感

個人的にはRubyGemsが公式対応したことで、妙なハックをせずにgemとしてもcrateとしてもvalidなプロジェクトを作ればそれが使えるようになるので、楽になったなあと思いました。色々アイデアが思い浮かびます。

*1:LTでも話すかもですが

*2:その辺のルールは流石に把握していないんですが

*3:c.f. mrubyは完全にそっちに寄せてる

archof: リモートのままイメージのarchitectureを確認するコマンド

背景

Apple SiliconのMacが販売されてそれなりの時間も経ち、エコシステムもできてきてお仕事の開発に使っている人も増えてきたように思う。

ところで、colimaのようなツールを使い、Docker環境をMacに作成した時、Apple Silicon上ではデフォルトで arm64 版のDocker*1がインストールされる。

$ docker info | grep Archi
 Architecture: aarch64

この上で何も考えずにイメージをビルドしたら arm64 のイメージができる。

この時、このイメージをリモートのレジストリGCRGHCRなどを想像してほしい)にpushにしたら、もちろん arm64 のイメージがpushされる。

ここで、このイメージをx86_64の本番サーバにpullして走らせるような運用だったら、困ったことになるだろう...。

このような違うアーキテクチャのイメージをうっかりプッシュしてしまう事故は、意外と防ぐ仕組みが簡単ではない*2

さらに言うと、pushされたイメージのアーキテクチャがなんであるかを確認するのも意外と骨が折れる。pullしてしまえば docker inspect なりで確認すればすぐわかるが、何度もプッシュしていてそのうちいくつかに混ざってしまったような場合とか、そもそもイメージが大きい場合とかは難しいこともありそう。

と言うことで Docker Registory API を叩いてリモートから確認するコマンドを作った。

github.com

インストール

$ go install github.com/udzura/archof

Go 1.17 で動作確認しています。多分1.18などでも問題なさそう。

様子

$ archof docker.io/amd64/ubuntu:latest
amd64

$ archof docker.io/arm64v8/ubuntu:latest 
arm64

認証が必要なやつはBearer認証に対応しているので、たとえばGCRだとこう言う感じで使えます。

$ archof gcr.io/udzura-dev/sample:latest --bearer "$(gcloud auth print-access-token)"                                                                     
amd64

内部

マニフェストを取得して、そこからConfigのDigestを取得してBlobを取得すればいいんですが、今回利用した go-containerregisrty はBlobの方のAPIをどう叩けばいいかよくわからず、URLを自分で構築して直接叩くことになった。いい方法があるんだろうか。

ref, _ := name.ParseReference(target)
desc, _ := remote.Get(ref, remote.WithAuth(&authn.Bearer{Token: token}))

reg := ref.Context()
url := fmt.Sprintf("%s://%s/v2/%s/blobs/%s",
    reg.Scheme(),
    reg.RegistryStr(),
    reg.RepositoryStr(),
    sha,
)
res, _ := desc.Client.Get(url)

type BlobResponse struct {
    Architecture string `json:"architecture"`
}
b := BlobResponse{}

if err := json.NewDecoder(res.Body).Decode(&b); err != nil {
    panic(err)
}

fmt.Println(b.Architecture)

雑感

Go の思い出しも兼ねて作ったが、特に変わったことをしていないので思い出せたのかどうか。

あとコマンドの名前の主語が大きいかも?(対象はコンテナイメージだけなのに) と思ったが、覚えやすいしえいやと付けました。ご了承ください。

とりあえず不安になった時にお使いください。というかpush防ぐ方法ってないんですかね...。

*1:正確にはLinuxVMイメージ

*2:いい方法があれば知りたい、というのもこのブログを書いたモチベーションの一つ

How to build ruby.wasm on Apple Silicon Mac

ここまでのあらすじ

<読み飛ばし推奨>

子どもたちのためにJava版マインクラフトを買い、よーしパパ*1MOD作って「威厳*2」を見せちゃうぞ〜 と思ったが全くやる気配がない。

一方で、子どもは無限の時間をScratchに費やしていた。Scratch 3.0はReactで作られていることが知られており、いわゆるSPA(もしかして、この用語もう古い?)であり、基本的にはGitHub Pagesのようなバックエンドレスな環境にもデプロイできる。

また、TypeScript/JavaScript+Reactで拡張を書くことが出来る。自作の拡張は残念ながら公式のScratchでは利用できないが、自分でビルドしてデプロイしたScratchインスタンスで使うことができる。一種のMODである。例えば機械学習に触れることが出来る拡張を有効にしたMODが知られている。

champierre.github.io

ということで、サーバサイドやカーネルHackをいくらやっても子どもたちに相手をしてもらうのは難しい*3ため、フロントエンドをやっていくことにした。


しかしフロントエンド、ここまでのキャリアにおいてほぼ、全く触れていない、せいぜいVueとReactのチュートリアルをなぞってTODOアプリをマスターした程度のレベルだし、それも忘れてしまった。

ひとまずとっつきやすいところから始めよう、ならWebAssemblyだな、なんかRubyも対応したし、と思ったのでそこからやることにした。前途多難であることは言うまでもない。

www.ruby-lang.org

</読み飛ばし推奨>

要するにRuby 3.2 preview1 のソースコードから WASM 版Rubyをビルドすることにした。

必要な情報は以下のリポジトリにある。

github.com

が、ここでLinux環境であればDockerfileが提供され、どう言うツールが必要でどうインストールするかが全部まとまっており、このイメージを作ればビルドできる...

はずなのだが見れば分かるとおり x86_64 環境のみを想定している。これは関連ツールがLinuxの場合x86_64のバイナリしかないとか、色々仕方ない面がある。すなわちApple Siliconの上で生活してる人の場合、Linux環境、ことDockerイメージを作ってその上で作業というのは逆に難しい。

この辺は以前znzさんが試した際にも言及されている。

blog.n-z.jp

今回はmacOS + Apple Siliconな環境でのビルド手順をまとめる。


ひとまず今回必要なツールキットは以下のような感じである。

順番にインストール方法を解説する。

wasi-sdk

公式に配られているバイナリ、Mac版はアーキテクチャが明示されておらず、不安なので自分でビルドする。

github.com

ビルドの依存は cmake、clang、ninja らしい。いかついが、clangはXCodeのアレで入ってるはずだし、cmakeとninjaはHomebrewなどで入れれば問題ない。

あとはチェックアウトして以下でOK。

git checkout wasi-sdk-14
NINJA_FLAGS=-v make package

build/wasi-sdk-14.0 に関連ファイルが作られるので ~/wasi など適当なディレクトリにコピーする。

cp -vr build/wasi-sdk-14.0/* ~/wasi/

Binaryen

wasi-sdkはWASIのSDKか! って感じだが、Binaryen が何者か調べてみても難しい...。

こんな感じだと理解した。

こちらはarm64のMac向けバイナリがある。

wget https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-arm64-macos.tar.gz
tar xzf binaryen-version_105-arm64-macos.tar.gz
cp -vr binaryen-version_105/* ~/wasi/

ruby.wasm

いよいよruby.wasmリポジトリに何があるかの話になる*4

github.com

Emscriptenで作ったやつとWASIの上に作るやつがあるらしく、WASI向けの情報は このディレクトリ にまとまっているようだが、ひとまず rake コマンドで色々ビルドできる。

$ rake -T
rake build:head-wasm32-unknown-emscripten-full        # Build head-wasm32-unknown-emscripten-full
rake build:head-wasm32-unknown-emscripten-minimal     # Build head-wasm32-unknown-emscripten-minimal
rake build:head-wasm32-unknown-wasi-full              # Build head-wasm32-unknown-wasi-full
rake build:head-wasm32-unknown-wasi-full-debug        # Build head-wasm32-unknown-wasi-full-debug
rake build:head-wasm32-unknown-wasi-full-js           # Build head-wasm32-unknown-wasi-full-js
rake build:head-wasm32-unknown-wasi-full-js-debug     # Build head-wasm32-unknown-wasi-full-js-debug
rake build:head-wasm32-unknown-wasi-minimal           # Build head-wasm32-unknown-wasi-minimal
rake build:head-wasm32-unknown-wasi-minimal-debug     # Build head-wasm32-unknown-wasi-minimal-debug
rake build:head-wasm32-unknown-wasi-minimal-js        # Build head-wasm32-unknown-wasi-minimal-js
rake build:head-wasm32-unknown-wasi-minimal-js-debug  # Build head-wasm32-unknown-wasi-minimal-js-debug
rake deps:libyaml-wasm32-unknown-emscripten           # build libyaml 0.2.5 for wasm32-unknown-emscripten
rake deps:libyaml-wasm32-unknown-wasi                 # build libyaml 0.2.5 for wasm32-unknown-wasi
rake fetch_artifacts[run_id]                          # Fetch artifacts of a run of GitHub Actions
rake npm:all                                          # Build all npm packages
rake npm:configure_prerelease[prerel]                 # Configure for pre-release
rake npm:ruby-head-wasm-emscripten                    # Build npm package ruby-head-wasm-emscripten
rake npm:ruby-head-wasm-emscripten-check              # Check npm package ruby-head-wasm-emscripten
rake npm:ruby-head-wasm-wasi                          # Build npm package ruby-head-wasm-wasi
rake npm:ruby-head-wasm-wasi-check                    # Check npm package ruby-head-wasm-wasi
rake publish[tag]                                     # Publish artifacts as a GitHub Release
rake wapm:irb-build                                   # Build wapm package irb
rake wapm:irb-publish                                 # Publish wapm package irb
rake wapm:ruby-build                                  # Build wapm package ruby
rake wapm:ruby-publish                                # Publish wapm package ruby

wasm32-unknown みたいなのはRustのターゲット名に合わせた呼び方だと思う。で、suffixがいくつかあって

  • minimal: 拡張(Cコンパイルが必要ということだと思う) standard library 抜きでビルドする(like json, yaml, or stringio など)
  • full: 拡張ライブラリ含め全部ビルドする
  • *-js: JavaScript で使いやすいような形でビルドし、Glue的なファイルも用意する。npmパッケージにするみたい
  • *-debug: DWARFの情報や、WASMのnameセクション? を含む。明らかにバイナリサイズがデカくなりそう。

今回はとにかくシンプルに rake build:head-wasm32-unknown-wasi-minimal を試す。通常のRubyのビルドよろしくconfigureから走る。

$ rake build:head-wasm32-unknown-wasi-minimal
warning: vfs feature is not enabled due to no LIB_WASI_VFS_A
/opt/ghq/github.com/ruby/ruby.wasm/build/src/head/configure --host wasm32-unknown-wasi --build arm64-apple-darwin21 --with-static-linked-ext --with-ext="" --with-libyaml-d
ir="/opt/ghq/github.com/ruby/ruby.wasm/build/deps/wasm32-unknown-wasi/opt/libyaml/usr/local" LDFLAGS="-Xlinker -zstack-size=16777216" XLDFLAGS="/opt/ghq/github.com/ruby/ru
by.wasm/build/ext-build/head-wasm32-unknown-wasi-minimal/extinit.o" debugflags="-g0" --disable-install-doc
checking for ruby... /Users/udzura/.rbenv/versions/3.1.1/bin/ruby
tool/config.guess already exists                                                                                                                                           tool/config.sub already exists
checking build system type... aarch64-apple-darwin21
checking host system type... wasm32-unknown-wasi
checking target system type... wasm32-unknown-wasi
checking for wasm32-unknown-wasi-wasm-opt... no
checking for wasm-opt... wasm-opt
configure: WARNING: using cross tools not prefixed with host triplet
checking wheather $WASI_SDK_PATH is set... yes
checking for wasm32-unknown-wasi-cl.exe... /Users/udzura/wasi/bin/clang
checking for /Users/udzura/wasi/bin/llvm-ar... /Users/udzura/wasi/bin/llvm-ar
checking for /Users/udzura/wasi/bin/clang++... no
checking for /Users/udzura/wasi/bin/llvm-nm... no
...

WASMなRubyrubies ディレクトリにできる。

$ file rubies/head-wasm32-unknown-wasi-minimal/work-wasm/usr/local/bin/ruby 
rubies/head-wasm32-unknown-wasi-minimal/work-wasm/usr/local/bin/ruby: WebAssembly (wasm) binary module version 0x1 (MVP)
$ cd rubies/head-wasm32-unknown-wasi-minimal/work-wasm/usr/local/bin
$ wasmtime ./ruby -- -e 'p RUBY_VERSION'
`RubyGems' were not loaded.
`error_highlight' was not loaded.
`did_you_mean' was not loaded.
"3.2.0"

という感じ。この先は... あまり考えていない...。

*1:このコピペがもう古い

*2:本当は全く威厳を示すつもりはなく、単に共通の話題が欲しいという話だが、単語の響きが面白いのでここで使ってしまった

*3:諸説あります

*4:READMEとエスパーで書いた内容なので間違っていたら優しく指摘してください