Rust の Cranelift で逆ポーランド記法を JIT コンパイル

Cranelift のコンパイラ基盤を使ったハローワールドメモ。

Cranelift is a low-level retargetable code generator. It translates a target-independent intermediate representation into executable machine code.

Wasmtime の JIT 基盤として使われているプロダクトで、Rust から Crainelift IR を構築して各 CPU マシン語コードにコンパイルして実行することができる。オリジナルのコンパイラ言語を作る時にも活用できそうです。

逆ポーランド記法をパースして JIT で計算させる

Cargo.toml

[package]
name = "cranelift-example"
version = "0.1.0"
edition = "2021"

[dependencies]
cranelift = "0.117.0"
cranelift-codegen = "0.117.0"
cranelift-frontend = "0.117.0"
cranelift-jit = "0.117.0"
cranelift-module = "0.117.0"
libc = "0.2"

bin/rpn.rs

use cranelift::codegen::ir::{types, AbiParam, Function, InstBuilder, Value};
use cranelift::frontend::{FunctionBuilder, FunctionBuilderContext};
use cranelift_codegen::ir::UserFuncName;
use cranelift_jit::{JITBuilder, JITModule};
use cranelift_module::Module;
use std::collections::VecDeque;

fn main() {
    // RPN expression input
    let rpn_expression = "5 1 2 + 4 * + 3 -";

    // Create a JIT builder and module.
    let jit_builder = JITBuilder::new(cranelift_module::default_libcall_names()).expect("Failed to create JITBuilder");
    let mut module = JITModule::new(jit_builder);

    // Create the main function signature.
    let mut ctx = module.make_context();
    let mut sig = module.make_signature();
    sig.returns.push(AbiParam::new(types::I32));
    let main_func = module.declare_function("main", cranelift_module::Linkage::Export, &sig).unwrap();

    ctx.func = Function::with_name_signature(UserFuncName::user(0, 1), sig);
    let mut builder_ctx = FunctionBuilderContext::new();
    let mut builder = FunctionBuilder::new(&mut ctx.func, &mut builder_ctx);

    // Create the entry block.
    let entry_block = builder.create_block();
    builder.switch_to_block(entry_block);
    builder.seal_block(entry_block);

    // Stack to hold values
    let mut stack: Vec<Value> = Vec::new();

    // Parse and evaluate the RPN expression
    let tokens: VecDeque<&str> = rpn_expression.split_whitespace().collect();
    for token in tokens {
        match token {
            "+" => {
                let b = stack.pop().expect("Stack underflow");
                let a = stack.pop().expect("Stack underflow");
                let result = builder.ins().iadd(a, b);
                stack.push(result);
            }
            "-" => {
                let b = stack.pop().expect("Stack underflow");
                let a = stack.pop().expect("Stack underflow");
                let result = builder.ins().isub(a, b);
                stack.push(result);
            }
            "*" => {
                let b = stack.pop().expect("Stack underflow");
                let a = stack.pop().expect("Stack underflow");
                let result = builder.ins().imul(a, b);
                stack.push(result);
            }
            "/" => {
                let b = stack.pop().expect("Stack underflow");
                let a = stack.pop().expect("Stack underflow");
                let result = builder.ins().sdiv(a, b);
                stack.push(result);
            }
            _ => {
                let value: i32 = token.parse().expect("Invalid token");
                let value = builder.ins().iconst(types::I32, value as i64);
                stack.push(value);
            }
        }
    }

    // The final result should be the only value left on the stack
    let result = stack.pop().expect("No result on stack");
    builder.ins().return_(&[result]);

    // Finalize the function.
    builder.finalize();

    println!("rpn_expression: {}", rpn_expression);
    println!("Compiled function: ");
    println!("{}", ctx.func.display());

    // Compile and run the function.
    module.define_function(main_func, &mut ctx).unwrap();
    module.clear_context(&mut ctx);
    module.finalize_definitions().expect("Failed to finalize definitions");

    let code_ptr = module.get_finalized_function(main_func);
    let code_fn = unsafe { std::mem::transmute::<_, fn() -> i32>(code_ptr) };
    let result = code_fn();

    println!("Result: {}", result); // Expected output: Result: 14
}

生成される Cranelift IR と実行ログ:

$ cargo run --bin rpn
   Compiling cranelift-example v0.1.0 (/Users/hk2a/devel/rust/cranelift-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.20s
     Running `target/debug/rpn`
rpn_expression: 5 1 2 + 4 * + 3 -
Compiled function:
function u0:1() -> i32 system_v {
block0:
    v0 = iconst.i32 5
    v1 = iconst.i32 1
    v2 = iconst.i32 2
    v3 = iadd v1, v2  ; v1 = 1, v2 = 2
    v4 = iconst.i32 4
    v5 = imul v3, v4  ; v4 = 4
    v6 = iadd v0, v5  ; v0 = 5
    v7 = iconst.i32 3
    v8 = isub v6, v7  ; v7 = 3
    return v8
}

Result: 14

逆ポーランド記法をパースして JIT で計算させる(スタックスロット)

bin/rpn.rs

use cranelift::codegen::ir::{types, AbiParam, Function, InstBuilder, StackSlot, StackSlotData, StackSlotKind, Value};
use cranelift::frontend::{FunctionBuilder, FunctionBuilderContext};
use cranelift_codegen::ir::UserFuncName;
use cranelift_jit::{JITBuilder, JITModule};
use cranelift_module::Module;
use std::collections::VecDeque;

fn main() {
    // RPN expression input
    let rpn_expression = "5 1 2 + 4 * + 3 -";

    // Create a JIT builder and module.
    let jit_builder = JITBuilder::new(cranelift_module::default_libcall_names()).expect("Failed to create JITBuilder");
    let mut module = JITModule::new(jit_builder);

    // Create the main function signature.
    let mut ctx = module.make_context();
    let mut sig = module.make_signature();
    sig.returns.push(AbiParam::new(types::I32));
    let main_func = module.declare_function("main", cranelift_module::Linkage::Export, &sig).unwrap();

    ctx.func = Function::with_name_signature(UserFuncName::user(0, 1), sig);
    let mut builder_ctx = FunctionBuilderContext::new();
    let mut builder = FunctionBuilder::new(&mut ctx.func, &mut builder_ctx);

    // Create the entry block.
    let entry_block = builder.create_block();
    builder.switch_to_block(entry_block);
    builder.seal_block(entry_block);

    // Stack slot to hold values
    let stack_slot = builder.create_sized_stack_slot(StackSlotData::new(StackSlotKind::ExplicitSlot, 16, 0));
    let mut offset = 0;

    // Parse and evaluate the RPN expression
    let tokens: VecDeque<&str> = rpn_expression.split_whitespace().collect();
    for token in tokens {
        match token {
            "+" => {
                let b = pop(&mut builder, stack_slot, &mut offset);
                let a = pop(&mut builder, stack_slot, &mut offset);
                let result = builder.ins().iadd(a, b);
                push(&mut builder, stack_slot, &mut offset, result);
            }
            "-" => {
                let b = pop(&mut builder, stack_slot, &mut offset);
                let a = pop(&mut builder, stack_slot, &mut offset);
                let result = builder.ins().isub(a, b);
                push(&mut builder, stack_slot, &mut offset, result);
            }
            "*" => {
                let b = pop(&mut builder, stack_slot, &mut offset);
                let a = pop(&mut builder, stack_slot, &mut offset);
                let result = builder.ins().imul(a, b);
                push(&mut builder, stack_slot, &mut offset, result);
            }
            "/" => {
                let b = pop(&mut builder, stack_slot, &mut offset);
                let a = pop(&mut builder, stack_slot, &mut offset);
                let result = builder.ins().sdiv(a, b);
                push(&mut builder, stack_slot, &mut offset, result);
            }
            _ => {
                let value: i32 = token.parse().expect("Invalid token");
                let value = builder.ins().iconst(types::I32, value as i64);
                push(&mut builder, stack_slot, &mut offset, value);
            }
        }
    }

    // The final result should be the only value left on the stack
    let result = pop(&mut builder, stack_slot, &mut offset);
    builder.ins().return_(&[result]);

    // Finalize the function.
    builder.finalize();

    println!("rpn_expression: {}", rpn_expression);
    println!("Compiled function: ");
    println!("{}", ctx.func.display());

    // Compile and run the function.
    module.define_function(main_func, &mut ctx).unwrap();
    module.clear_context(&mut ctx);
    module.finalize_definitions().expect("Failed to finalize definitions");

    let code_ptr = module.get_finalized_function(main_func);
    let code_fn = unsafe { std::mem::transmute::<_, fn() -> i32>(code_ptr) };
    let result = code_fn();

    println!("Result: {}", result); // Expected output: Result: 14
}

// Helper functions for stack operations
fn push(builder: &mut FunctionBuilder, stack_slot: StackSlot, offset: &mut i32, value: Value) {
    builder.ins().stack_store(value, stack_slot, *offset);
    *offset += 4;
}

fn pop(builder: &mut FunctionBuilder, stack_slot: StackSlot, offset: &mut i32) -> Value {
    *offset -= 4;
    builder.ins().stack_load(types::I32, stack_slot, *offset)
}

生成される Cranelift IR と実行ログ:

$ cargo run --bin rpn
   Compiling cranelift-example v0.1.0 (/Users/hk2a/devel/rust/cranelift-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.74s
     Running `target/debug/rpn`
rpn_expression: 5 1 2 + 4 * + 3 -
Compiled function:
function u0:1() -> i32 system_v {
    ss0 = explicit_slot 16

block0:
    v0 = iconst.i32 5
    stack_store v0, ss0  ; v0 = 5
    v1 = iconst.i32 1
    stack_store v1, ss0+4  ; v1 = 1
    v2 = iconst.i32 2
    stack_store v2, ss0+8  ; v2 = 2
    v3 = stack_load.i32 ss0+8
    v4 = stack_load.i32 ss0+4
    v5 = iadd v4, v3
    stack_store v5, ss0+4
    v6 = iconst.i32 4
    stack_store v6, ss0+8  ; v6 = 4
    v7 = stack_load.i32 ss0+8
    v8 = stack_load.i32 ss0+4
    v9 = imul v8, v7
    stack_store v9, ss0+4
    v10 = stack_load.i32 ss0+4
    v11 = stack_load.i32 ss0
    v12 = iadd v11, v10
    stack_store v12, ss0
    v13 = iconst.i32 3
    stack_store v13, ss0+4  ; v13 = 3
    v14 = stack_load.i32 ss0+4
    v15 = stack_load.i32 ss0
    v16 = isub v15, v14
    stack_store v16, ss0
    v17 = stack_load.i32 ss0
    return v17
}

Result: 14

システムコールする例

libc の printf をコールして Hello, World を出力。

bin/hello.rs

use cranelift::codegen::ir::{types, AbiParam, Function, InstBuilder};
use cranelift::frontend::{FunctionBuilder, FunctionBuilderContext};
use cranelift_codegen::ir::UserFuncName;
use cranelift_jit::{JITBuilder, JITModule};
use std::ffi::CString;
use cranelift_module::{Module, Linkage};
use libc;

fn main() {
    // Create a JIT builder and module.
    let mut jit_builder = JITBuilder::new(cranelift_module::default_libcall_names()).expect("Failed to create JITBuilder");
    jit_builder.symbol("printf", printf as *const u8);
    let mut module = JITModule::new(jit_builder);

    // Create a function signature for `printf`.
    let mut ctx = module.make_context();
    let mut sig = module.make_signature();
    let pointer_type = module.target_config().pointer_type();
    sig.params.push(AbiParam::new(pointer_type)); // フォーマット文字列の引数
    sig.params.push(AbiParam::new(pointer_type)); // 可変引数のためのダミー引数
    sig.returns.push(AbiParam::new(types::I32));
    let printf = module.declare_function("printf", Linkage::Import, &sig).unwrap();

    // Create the main function signature.
    let mut sig = module.make_signature();
    sig.returns.push(AbiParam::new(types::I32));
    let main_func = module.declare_function("main", Linkage::Export, &sig).unwrap();

    ctx.func = Function::with_name_signature(UserFuncName::user(0, 1), sig);
    let mut builder_ctx = FunctionBuilderContext::new();
    let mut builder = FunctionBuilder::new(&mut ctx.func, &mut builder_ctx);

    // Create the entry block.
    let entry_block = builder.create_block();
    builder.append_block_params_for_function_params(entry_block);
    builder.switch_to_block(entry_block);
    builder.seal_block(entry_block);

    // Create the string data.
    let hello_world = CString::new("Hello, World!\n").unwrap();
    let hello_world_ptr = hello_world.as_ptr() as i64;

    // Call the `printf` function.
    let printf_func = module.declare_func_in_func(printf, builder.func);
    let format_str = builder.ins().iconst(types::I64, hello_world_ptr);
    let zero = builder.ins().iconst(types::I64, 0);
    let call = builder.ins().call(printf_func, &[format_str, zero]);

    // Return the result of the call.
    let result = builder.inst_results(call)[0];
    builder.ins().return_(&[result]);

    builder.finalize();
    println!("{}", ctx.func.display());

    // Compile and run the function.
    module.define_function(main_func, &mut ctx).unwrap();
    module.clear_context(&mut ctx);
    module.finalize_definitions().expect("Failed to finalize definitions");

    let code_ptr = module.get_finalized_function(main_func);
    let code_fn = unsafe { std::mem::transmute::<_, fn() -> i32>(code_ptr) };
    code_fn();
}

// Dummy printf function to link with.
extern "C" fn printf(fmt: *const i8, _dummy: i64) -> i32 {
    unsafe {
        libc::printf(fmt)
    }
}

生成される Cranelift IR と実行ログ:

$ cargo run --bin hello
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/hello`
function u0:1() -> i32 system_v {
    sig0 = (i64, i64) -> i32 system_v
    fn0 = u0:0 sig0

block0:
    v0 = iconst.i64 0x6000_0258_0180
    v1 = iconst.i64 0
    v2 = call fn0(v0, v1)  ; v0 = 0x6000_0258_0180, v1 = 0
    return v2
}

Hello, World!

Zellij ターミナルワークスペースが持つ Wasm プラグインシステム

WebAssembly Advent Calendar 2024 の 10 日目の記事です。

この記事では、WebAssembly をリアルワールドで実用的に活用している Zellij について紹介したいと思います。

Zellij について

Zellij はいわゆる tmux や screen などのターミナルマルチプレクサの機能をもつ Rust でかかれた “terminal workspace” です。

https://zellij.dev

A terminal workspace with batteries included

ターミナルエミュレータ上で端末セッションの管理やタブ・ペインといった操作を加え、コマンドライン環境を強力にサポートしてくれます。

同様の機能を有する tmux は時間を使ってコンフィグレーションし使い方を身につける必要がありますが、Zellij は操作ガイドも常に表示することもでき、瞬間的に導入できるお勧めのターミナルマルチプレクサとなっています。

Zellij と WebAssembly

Zellij は一般的なターミナルマルチプレクサの機能に加え、WebAssembly によるプラグインシステムを持っており、任意の Wasm 対応のプログラミング言語で機能の拡張が可能です。具体的には、

  • ホットキーからペインを開き UI 含めた任意の処理を実行。
  • レイアウトに常に表示する情報ペインを実装。
  • パイプから入力された文字列処理して戻す。

などなどの処理を Wasm でかいたプログラムをもって実行できます。

プラグインは.wasm バイナリとして配布され、Zellij から実行時にダイナミックに読み込まれ内部の wasmtime ランタイムで実行されます。

このことから、amd64 や aarch64、riscv64(つまり各種 Linux や macOS)などのプラットフォームを気にせず、同一のプラグインバイナリを安全に動作させることができて非常に便利です。

プラグインの API は次のようになっています。

Rust の例:

use zellij_tile::prelude::*;

#[derive(Default)]
struct State {
}
register_plugin!(State);

impl ZellijPlugin for State {
    fn load(&mut self, configuration: BTreeMap<String, String>) {
        // ...snip...
    }

    fn update(&mut self, event: Event) -> bool {
        let mut should_render: bool = false;
        // ...snip...
        should_render
    }

    fn render(&mut self, rows: usize, cols: usize) {
    }
}

WebAssembly プラグインであるのに関わらず、引数のシグネチャが非常にリッチな形になっているのが確認できますが、これは Protocol Buffers の仲介をもって ABI の安定化がされています。

https://github.com/zellij-org/zellij/tree/main/zellij-utils/src/plugin_api

将来的には Component Model / WIT になるのかもしれませんが、現在用いることができるインターフェース手法のひとつとして個人的にとても参考になりました。

Zellij プラグインはお気に入りの言語でプラットフォームを気にせず思いついたアイディアをすぐ実装できて楽しいのではないかと思いますので、Wasm プレイとしても良ければ…!

https://github.com/zellij-org/awesome-zellij

A list of resources for Zellij workspace: plugins, tutorials and configuration settings.

というわけで、Wasm でお手軽に拡張できる Zellij の紹介でした。- 最後にいくつか自分がつくったプラグインを貼り付けて記事を終わりたいと思います。

zellij-datetime – WASI から現在時刻をシステムインターフェースし、タイムゾーンを選択して表示

zellij-imagebox – パイプから画像バイナリをもらって、いい感じにリサイズ・減色して sixel graphics でペイン上に出力

Ubuntu 22.04 LTS から Ubuntu 24.04 LTS にアップグレードメモ

Ubuntu 24.04 LTS リリースからしばらく経ちましたが、ようやくメインで使っている ThinkPad P14s (AMD) を Ubuntu 22.04 から 24.04 にアップグレードしました。導入メモ。

アップグレード:

sudo do-release-upgrade

30分ほどで特に問題なく終了。

uname -a
Linux thinkpad-p14s 6.8.0-48-generic #48-Ubuntu SMP PREEMPT_DYNAMIC Fri Sep 27 14:04:52 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

導入後の Ubuntu 24.04 LTS (Wayland セッション):

Good work! 😀

無効になったサードパーティリポジトリを再有効化:

以下に手順があります。

Firefox は標準の snap 版ではなく Mozilla 提供のリポジトリのがお勧めです。

GNOME Shell Extention 再有効化:

sudo apt install gnome-shell-extension-manager
  • Clipboard Indicator (クリップボードマネージャ)
  • Current Monitor Window/App Switcher (マルチモニタや仮想画面で Alt(Super) + Tab の出力をカレント画面のアプリだけに抑止)

ふと今気がついたのですが、クリップボードマネージャが画像にも対応していました。グッド。

オーディオ系:

Pipewire の設定で USB-DAC の 96KHz をデフォルトとかにする場合。

  • /usr/share/pipewire/pipewire.conf
  • /usr/share/pipewire/pipewire-pulse.conf
context.properties = {
    ## ..snip..
    default.clock.rate          = 96000
    default.clock.allowed-rates = [ 48000, 96000 ]
    ## ..snip..
}

接続確認。

pw-dump | jq '.[] | select(.type == "PipeWire:Interface:Node" ) | .info.props'

Audacious 出力サンプリングレート設定:

プラグイン – エフェクト – サンプリング周波数コンバータより。(96KHz とかにアップサンプリングする場合)

Wayland 系メモ:

Firefox のピクチャーインピクチャーがデフォルトで最前面表示にならない。右クリックして「最前面に維持する」でうまくいく。

  • OBS Studio の自画面キャプチャはまだうまくいかない。(ので使う場合は X.org セッションに切り替えて使用)
  • 画面キャプチャの Frameshot もキャプチャできずに停止してしまうような動作。いったん GNOME Shell 標準のものでがんばる。
  • Shotcut などアプリ起動のスプラッシュ画面(ウインドウ枠なし)が画面センターではなく、左上とかに表示されるようだ。

Command Line Interface:

fzf が 0.44.1 (debian) となり、罫線が右に伸び切らない表示になったため以下を設定して解消。

export RUNEWIDTH_EASTASIAN=0

GoのTUIで表示が崩れる場合

gnome-terminalを使用している場合は、設定の「曖昧幅の文字(W)」と環境変数RUNEWIDTH_EASTASIANを一致させよう。

OneDrive

Ubuntu 22.04 LTS では onedriver を使っていましたが、24.04 LTS から Ubuntu 標準のオンラインアカウント接続を使うように変更しました。

オンラインアカウント設定時に謎の Client ID などを聞かれますが、ブランクのままサインイン押下後に通常の Microsoft アカウントでログインすれば OK のようです。

関連