🛡️

軽量 VMM の Hyperlight と Hyperlight-wasm を触ってみる

に公開

2025/3/26 に Azure Core 開発チームが Hyperlight-wasm を OSS としてリリースしたブログが公開されました。

https://opensource.microsoft.com/blog/2025/03/26/hyperlight-wasm-fast-secure-and-os-free/

そこで Hyperlight-wasm とそのベースとなっている Hyperlight について簡単に触ってみます。

Hyperlight について

Hyperlight はホストから隔離したセキュアな Sandbox 環境を高速に作成し、その中でアプリケーションコードを安全に実行するための Rust 製 Virtual Machine Manager (VMM) です。hyperlight 自体は hyperlight-wasm リリース前から既に Microsoft から OSS として公開されており、2025/3/4 には CNCF の Sandbox プロジェクト に採択されています。Github repo は以下。

https://github.com/hyperlight-dev/hyperlight

ホストから隔離された軽量なゲスト VM 環境 (Sandbox 環境) でアプリケーションを高速かつセキュアに実行するというのは Firecracker以前記事にした Kata Container と同じようなコンセプトになっています。ただ hyperlight では kernel やネットワーク、ファイルシステムなど他の一般的な VMM が持っている機能を省き、Sandbox 環境上でアプリケーションコードを実行するために必要な最小限のみの機能を提供することで類似の VMM と比較して非常に低いレイテンシや起動時間を実現している点が特徴的になっています。詳細は https://github.com/hyperlight-dev/hyperlight/blob/main/docs/hyperlight-execution-details.md を参照。

以下のブログによると、ゲスト VM を作成するのにかかる時間は 1 ~ 2 ms 程度とのことです。
https://opensource.microsoft.com/blog/2024/11/07/introducing-hyperlight-virtual-machine-based-security-for-functions-at-scale/

1-2 milliseconds: The time it takes to spawn a new Hyperlight micro-VM

一方で類似の VMM にある仮想ネットワークやファイルシステムといった機能は持たないので、VM 上で大規模なワークロードを実行するよりは小規模で短時間実行のアプリケーションコードを実行するような用途に向いています。AWS Lambda のようなサーバレスアプリケーションや FaaS ワークロードをセキュアな環境で高速に実行するための一連の hyperlight ライブラリを提供することがコンセプトになっています。

Hyperlight のライブラリ

hyperlight 上でアプリケーションを動作させる際は、ゲスト環境 (Sandbox 環境) を構築するためのホストライブラリを使用したホスト側コードと、Sandbox 環境内で実際に実行されるゲスト関数を定義するゲスト側コード(ゲストアプリケーション)を用意する必要があります。hyperlight リポジトリ ではホスト側で参照するライブラリやゲスト側で参照するライブラリ、療法で共通で使用するライブラリが提供されているため、それぞれのコード内で各ライブラリを使用して Sandbox 環境を構築し、ゲストアプリケーションを実行することができます。

また、ホスト側コードでゲスト側から call する関数を定義してゲスト側コード内で実行したり、ゲスト側で定義した関数をホスト側から call することもできます。hyperlight のライブラリを利用した API により双方向の call が可能となっています。


Hyperlight での VM 作成の順序とホスト・ゲストの関係性。Introducing Hyperlight: Virtual machine-based security for functions at scale より引用

サンプルコードを動かしてみる

Readme にサンプルコードの実行手順が記載されているので、これに沿って動作を見てみます。
実行環境の要件は以下。

  • Linux with KVM.
  • Windows with Windows Hypervisor Platform (WHP). - Note that you need Windows 11 / Windows Server 2022 or later to use hyperlight, if you are running on earlier versions of Windows then you should consider using our devcontainer on GitHub codespaces or WSL2.
  • Windows Subsystem for Linux 2 (see instructions here for Windows client and here for Windows Server) with KVM.
  • Azure Linux with mshv (note that you need mshv to be installed to use Hyperlight)

ここでは nested KVM を有効化した openstack 上に構築した VM 上で試します。スペックは以下。

  • OS: ubuntu 24.04 LTS
  • メモリ: 8GB
  • vCPU: 4
  • cpu virtualization 有効化

まずは以下の前提条件をインストールしておきます。

  • build-essential
  • rust
  • just
  • clang and LLVM

リポジトリを clone して手順通りにライブラリのビルドなどを実行します。

git clone https://github.com/hyperlight-dev/hyperlight.git
cd hyperlight

just build  # build the Hyperlight library
just rg     # build the rust test guest binaries

cargo run --example hello-world を実行するとサンプルコードの hello-world が実行され、hyperlight の Sandbox 環境上で実行された Hello, World! I am executing inside of a VM :) が出力されます。

$ cargo run --example hello-world
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/examples/hello-world`
Hello, World! I am executing inside of a VM :)

サンプルコードの動作としてはこれだけですが、これだけではあまり面白くないので実行される HelloWorld の中身について簡単に見てみます。

ホスト側コード

まず hyperlight で Sandbox を作成するためのホスト側コードは src/hyperlight_host/examples/hello-world/main.rs に対応しています。中身を見ると以下の環境で sandbox を作成し、ゲスト側の関数 simple_guest_as_string をロードしています。

 // Create an uninitialized sandbox with a guest binary
    let mut uninitialized_sandbox = UninitializedSandbox::new(
        hyperlight_host::GuestBinary::FilePath(
            hyperlight_testing::simple_guest_as_string().unwrap(),
        ),
        None, // default configuration
        None, // default run options
        None, // default host print function
    )?;

以下ではホスト側関数として 5 sec sleep する関数 sleep_5_secs を定義して sandbox 環境に登録しています。サンプルコードでは使用されていませんが、ホスト側で登録した関数はゲスト側から call できるようになります。

// Register a host functions
    fn sleep_5_secs() -> hyperlight_host::Result<()> {
        thread::sleep(std::time::Duration::from_secs(5));
        Ok(())
    }

    let host_function = Arc::new(Mutex::new(sleep_5_secs));

    host_function.register(&mut uninitialized_sandbox, "Sleep5Secs")?;
    // Note: This function is unused, it's just here for demonstration purposes

サンプルコード実行時に表示される Hello, World! I am executing inside of a VM はここでゲスト関数の PrintOutput の引数に渡して call しています。

// Call guest function
    let message = "Hello, World! I am executing inside of a VM :)\n".to_string();
    let result = multi_use_sandbox.call_guest_function_by_name(
        "PrintOutput", // function must be defined in the guest binary
        ReturnType::Int,
        Some(vec![ParameterValue::String(message.clone())]),
    );

ゲスト側コード

sandbox 環境作成時に hyperlight_testing::simple_guest_as_string をロードしていますが、そのソースコードは src/hyperlight_testing/src/lib.rs に対応しています。

hyperlight/src/hyperlight_testing/src/lib.rs
/// Get a fully qualified OS-specific path to the simpleguest elf binary
pub fn simple_guest_as_string() -> Result<String> {
    let buf = rust_guest_as_pathbuf("simpleguest");
    buf.to_str()
        .map(|s| s.to_string())
        .ok_or_else(|| anyhow!("couldn't convert simple guest PathBuf to string"))

ゲスト関数として call されている PrintOutput は以下に対応。これにより PrintOutput という関数名で simple_print_output がホスト側で call できるようになります。

hyperlight/src/tests/rust_guests/simpleguest/src/main.rs
#[no_mangle]
pub extern "C" fn hyperlight_main() {

    let simple_print_output_def = GuestFunctionDefinition::new(
        "PrintOutput".to_string(),
        Vec::from(&[ParameterType::String]),
        ReturnType::Int,
        simple_print_output as usize,
    );
    register_function(simple_print_output_def);

simple_print_output の処理内容としては print_output を呼び出し、これはホスト関数の HostPrint を call しています。

fn print_output(message: &str) -> Result<Vec<u8>> {
    call_host_function(
        "HostPrint",
        Some(Vec::from(&[ParameterValue::String(message.to_string())])),
        ReturnType::Int,
    )?;
    let result = get_host_return_value::<i32>()?;
    Ok(get_flatbuffer_result(result))
}

fn simple_print_output(function_call: &FunctionCall) -> Result<Vec<u8>> {
    if let ParameterValue::String(message) = function_call.parameters.clone().unwrap()[0].clone() {
        print_output(&message)
    } else {
        Err(HyperlightGuestError::new(
            ErrorCode::GuestFunctionParameterTypeMismatch,
            "Invalid parameters passed to simple_print_output".to_string(),
        ))
    }
}

HostPrint が具体的にどのような処理を行っているのかはよくわかりませんでしたが、関数名的に単にホスト側で文字を出力するための関数であることが推測されます。

以上より、サンプルコードの実行時にはゲスト側コード内で定義された PrintOutput が Sandbox 環境内で実行される構成になっています。
サンプルでは PrintOutputsrc/tests/rust_guests/simpleguest/src/main.rs で事前に定義済みですが、独自のアプリケーションを実装するときも同様のやり方で実行できるようになります。

  1. ゲスト側コードでホスト関数から call する実行する関数を定義
  2. ホスト側コードでゲスト関数を定義し、Sandbox を作成、登録
  3. 実行可能なバイナリをビルド。

ベンチマークの測定

hyperlight リポジトリでは criterion を使ってベンチマークが測定できるようになっています。

https://github.com/hyperlight-dev/hyperlight/blob/main/docs/benchmarking-hyperlight.md

測定結果は実行環境のハードウェア構成等に依存するためあくまで参考程度になりますが、hyperlight では VM 環境を高速に作成できることを押し出しているので実際にどのくらいになるか試してみます。
ベンチマーク測定には昔からある plot ソフトの gnuplot が必要なので sudo apt install gnuplot でインストールし、just bench を実行してベンチマークを測定します。

$ just bench

Benchmarking guest_functions/guest_call: Analyzing
guest_functions/guest_call
                        time:   [125.53 µs 126.45 µs 127.47 µs]
Found 12 outliers among 100 measurements (12.00%)
  4 (4.00%) high mild
  8 (8.00%) high severe
slope  [125.53 µs 127.47 µs] R^2            [0.8471226 0.8457152]
mean   [128.74 µs 135.63 µs] std. dev.      [7.5629 µs 27.372 µs]
median [126.67 µs 127.93 µs] med. abs. dev. [2.5872 µs 5.0575 µs]
Benchmarking guest_functions/guest_call_with_reset
Benchmarking guest_functions/guest_call_with_reset: Warming up for 3.0000 s
Benchmarking guest_functions/guest_call_with_reset: Collecting 100 samples in estimated 6.5403 s (20200 iterations)
Benchmarking guest_functions/guest_call_with_reset: Analyzing

実行すると項目毎に計測結果が出力されるのでこちらで確認してもいいですが、測定が完了すると target/criterion 以下に測定結果のアーティファクト一式が作成され、target/criterion/report/index.html をブラウザで開くとグラフ付きの詳細結果を確認することができます。

ベンチマークでの測定項目は https://github.com/hyperlight-dev/hyperlight/blob/main/src/hyperlight_host/benches/benchmarks.rs で定義されており、VM 環境 (sandbox) の作成やゲストライブラリの call にかかる時間などを測定しているようです。例えば sandbox 作成の結果 (sandboxes/create_sandbox) を見てみると、平均で 50 ms 程度で sandbox が作成されていることがわかります。

その他 guest_call_with_call_to_host_function ではゲストアプリケーション内でホスト関数を call する処理となっています。

    // Benchmarks a guest function call calling into the host.
    // The benchmark does **not** include the time to reset the sandbox memory after the call.
    group.bench_function("guest_call_with_call_to_host_function", |b| {
        let mut uninitialized_sandbox = create_uninit_sandbox();

        // Define a host function that adds two integers and register it.
        fn add(a: i32, b: i32) -> hyperlight_host::Result<i32> {
            Ok(a + b)
        }
        let host_function = Arc::new(Mutex::new(add));
        host_function
            .register(&mut uninitialized_sandbox, "HostAdd")
            .unwrap();

        let multiuse_sandbox: MultiUseSandbox =
            uninitialized_sandbox.evolve(Noop::default()).unwrap();
        let mut call_ctx = multiuse_sandbox.new_call_context();

        b.iter(|| {
            call_ctx
                .call(
                    "Add",
                    ReturnType::Int,
                    Some(vec![ParameterValue::Int(1), ParameterValue::Int(41)]),
                )
                .unwrap()
        });
    });

この実行時間は平均 416 μs となりました。

また、公式のベンチマークが github release で公開されているので手元の結果と比較してみます。release から自分の環境にあった bench の tar を見つけてダウンロードするか、Justfile 内で定義されたコマンドより just 経由でダウンロードできます。gh コマンドを使うので事前にインストールが必要。

Justfile
# Warning: can overwrite previous local benchmarks, so run this before running benchmarks
# Downloads the benchmarks result from the given release tag.
# If tag is not given, defaults to latest release
# Options for os: "Windows", or "Linux"
# Options for Linux hypervisor: "kvm", "mshv"
# Options for Windows hypervisor: "hyperv"
# Options for cpu: "amd", "intel"
bench-download os hypervisor cpu tag="":
    gh release download {{ tag }} -D ./target/ -p benchmarks_{{ os }}_{{ hypervisor }}_{{ cpu }}.tar.gz
    mkdir -p target/criterion {{ if os() == "windows" { "-Force" } else { "" } }}
    tar -zxvf target/benchmarks_{{ os }}_{{ hypervisor }}_{{ cpu }}.tar.gz -C target/criterion/ --strip-components=1

今回は Linux 環境で KVM を使っているので、以下のコマンドでダウンロードします。

just bench-download Linux kvm amd

公式のベンチマークでは sandbox 作成は 5.95 ms, ゲスト関数内のホスト関数の呼び出しは 231 μs となっており、手元の環境と比較してもよりよいパフォーマンスとなっています。

hyperlight を紹介している ブログ によれば micro VM の作成は 1 ~ 2 ms 程度と表記されているためオーダーで見ればそれほど乖離はない結果となっています。

< 0.03 milliseconds: The time it takes to start a new Wasmtime sandbox
1-2 milliseconds: The time it takes to spawn a new Hyperlight micro-VM

120 milliseconds: The time it takes to spawn an optimized, traditional VM

公式のベンチマークはリリース毎に Benchmarks.yml による Github workflow 内で計測されているようなので、この実行環境に近い環境で測定を行えばより近い結果が得られるかもしれません。

その他

Hyperlight-wasm

hyperlight-wasm では hyperlight によって構築された Sandbox 環境で wasm モジュールを動作させるためのコンポーネントとなっています。

https://github.com/hyperlight-dev/hyperlight-wasm

hyperlight ではホスト側から call するゲスト側コードは rust や c 言語で記述する必要がありましたが、hyperlight-wasm リリース時のブログ によると hyperlight-wasm ではゲストとして実行するコードは hyperlight-wasm 上で実行可能な形式 (wasm32-wasip2) であれば良いため、C、Go、Rust, Python、JavaScript、C# など様々な言語で実装できるとのことです。

hyperlight-wasm ではアプリケーション開発者が hyperlight ゲストライブラリを使ったアプリケーションのコードの書き方や Rust の使い方がわからなくても、アプリケーションを単に Wasm モジュールとしてビルドし、hyperlight-wasm 上で実行するだけで済むようになるということになります。アプリケーションが hyperlight 上で動作していることを意識する必要がないまま、VM 高速作成やセキュアな実行といった hyperlight の利点をそのまま享受できる点が大きなメリットとなっています。
ブログにおける該当箇所は以下。

Introducing the Hyperlight Wasm guest
How compatible? Well, building Hyperlight with a WebAssembly runtime—wasmtime—enables any programming language to execute in a protected Hyperlight micro-VM without any prior knowledge of Hyperlight at all. As far as program authors are concerned, they’re just compiling for the wasm32-wasip2 target. This means they can run their programs locally using runtimes like wasmtime or Jco. Or run them on a server using for Nginx Unit, Spin, WasmCloud—or now also Hyperlight Wasm. If done right, developers don’t need to think about what runtime their code will run on as they’re developing it. That is a degree of developer flexibility that is only possible through standards.

Executing workloads in the Hyperlight Wasm guest isn’t just possible for compiled languages like C, Go, and Rust, but also for interpreted languages like Python, JavaScript, and C#. The trick here, much like with containers, is to also include a language runtime as part of the image. For example, for JavaScript, the StarlingMonkey JS Runtime was designed to natively run in WebAssembly.

Programming languages, runtimes, application platforms, and cloud providers are all starting to offer rich experiences for WebAssembly out of the box. If we do things right, you will never need to think about whether your application is running inside of a Hyperlight Micro-VM in Azure. You may never know your workload is executing in a Hyperlight Micro VM. And that’s a good thing.

サンプルコードを動かす

hyperlight-wasm の repo にサンプルアプリケーションがあるのでこちらも試してみます。実行環境の要件や必要なパッケージなどは hyperlight と同じなので、前回の環境をそのまま使用します。
サンプルアプリケーションの動作に必要な wasm ライブラリなどをビルド

rustup install 1.82.0
rustup default 1.82.0
just build
just build-wasm-examples
just build-rust-wasm-examples

cargo run --example helloworld を実行するとサンプルの helloworld が実行されます。

$ cargo run --example helloworld
   Compiling hyperlight-wasm v0.1.0 (/home/ubuntu/hyperlight-wasm/src/hyperlight_wasm)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.09s
     Running `target/debug/examples/helloworld`
Message from Rust Example to Wasm Function
Result from calling HelloWorld Function in Wasm Module test case 0) is: 0
Message from Rust Example to Wasm Function
Result from calling HelloWorld Function in Wasm Module test case 1) is: 0
Result from calling Echo Function in Wasm Module test case 2) is: Message from Rust Example to Wasm Function
Result from calling Echo Function in Wasm Module test case 3) is: Message from Rust Example to Wasm Function

こちらも実行されるコードの中身を見てみます。

ホスト側コード

まずホスト側のコードは src/hyperlight_wasm/examples/helloworld/main.rs になっていて、この中で wasm モジュールと call するゲスト関数のロード、sandbox の作成、ゲスト関数の実行などを行っています。この中で以下 4 つの wasm モジュールからゲスト関数をロードにして for loop で順次実行しています。

wasm モジュール名 ゲスト関数名
HelloWorld.wasm HelloWorld
HelloWorld.aot HelloWorld
RunWasm.wasm Echo
RustWasm.aot Echo
    let tests = vec![
        (
            "HelloWorld.wasm",
            "HelloWorld",
            Some(vec![ParameterValue::String(
                "Message from Rust Example to Wasm Function".to_string(),
            )]),
        ),
        (
            "HelloWorld.aot",
            "HelloWorld",
            Some(vec![ParameterValue::String(
                "Message from Rust Example to Wasm Function".to_string(),
            )]),
        ),
        (
            "RunWasm.wasm",
            "Echo",
            Some(vec![ParameterValue::String(
                "Message from Rust Example to Wasm Function".to_string(),
            )]),
        ),
        (
            "RunWasm.aot",
            "Echo",
            Some(vec![ParameterValue::String(
                "Message from Rust Example to Wasm Function".to_string(),
            )]),
        ),
    ];

SandboxBuilder で wasm 実行環境の sandbox を作成

    #[cfg(all(debug_assertions, feature = "inprocess"))]
        let mut sandbox = SandboxBuilder::new()
            .with_sandbox_running_in_process()
            .build()?;

        #[cfg(not(all(debug_assertions, feature = "inprocess")))]
        let mut sandbox = SandboxBuilder::new().build()?;

以下で HelloWorld.wasm などの wasm モジュールの path をホスト上で検索して load しています。

for (idx, case) in tests.iter().enumerate() {
        let (mod_path, fn_name, params_opt) = case;

        ...

       let mod_path = get_wasm_module_path(mod_path)?;

        // Load a Wasm module into the sandbox
        let mut loaded_wasm_sandbox = wasm_sandbox.load_module(mod_path)?;

set_wasm_module_path の実装はここ。デフォルトでは x64/debug が検索パスになります。対象の wasm モジュールは just build-wasm-examples などの実行時に作成されます。

$ ls -l x64/debug
total 45416
-rwxr-xr-x 1 root   root      26359 Apr 11 17:46 HelloWorld-wasi-libc.wasm
-rw-rw-r-- 1 ubuntu ubuntu   132952 Apr 11 17:46 HelloWorld.aot
-rw-rw-r-- 1 ubuntu ubuntu   132952 Apr 11 17:46 HelloWorld.wasm
-rwxr-xr-x 1 root   root       8662 Apr 11 17:45 HostFunction-wasi-libc.wasm
-rw-rw-r-- 1 ubuntu ubuntu    51616 Apr 11 17:46 HostFunction.aot
-rw-rw-r-- 1 ubuntu ubuntu    51616 Apr 11 17:46 HostFunction.wasm
-rwxr-xr-x 1 root   root      26977 Apr 11 17:46 RunWasm-wasi-libc.wasm
-rw-rw-r-- 1 ubuntu ubuntu   134040 Apr 11 17:46 RunWasm.aot
-rw-rw-r-- 1 ubuntu ubuntu   134040 Apr 11 17:46 RunWasm.wasm
-rw-rw-r-- 1 ubuntu ubuntu     1183 Apr 11 17:45 dockerbuild.log
-rw-rw-r-- 1 ubuntu ubuntu   198056 Apr 11 17:46 rust_wasm_samples.aot
-rw-rw-r-- 1 ubuntu ubuntu   198056 Apr 11 17:46 rust_wasm_samples.wasm
-rwxrwxr-x 1 ubuntu ubuntu 45448240 Apr 11 17:45 wasm_runtime

以下の箇所で関数名に処理を分岐し、wasm モジュール内に定義されているゲスト関数を call しています。

 if *fn_name == "Echo" {
            // Call a function in the Wasm module
            let ReturnValue::String(result) = loaded_wasm_sandbox.call_guest_function(
                fn_name,
                params_opt.clone(),
                ReturnType::String,
            )?
            else {
                panic!("Failed to get result from call_guest_function to Echo Function")
            };
            println!(
                "Result from calling Echo Function in Wasm Module \
                test case {idx}) is: {}",
                result
            );
        } else if *fn_name == "HelloWorld" {
            // Call a function in the Wasm module
            let ReturnValue::Int(result) = loaded_wasm_sandbox.call_guest_function(
                fn_name,
                params_opt.clone(),
                ReturnType::Int,
            )?
            else {
                panic!("Failed to get result from call_guest_function to HelloWorld Function")
            };

            println!(
                "Result from calling HelloWorld Function in Wasm Module \
            test case {idx}) is: {}",
                result
            );
        }
    }

ゲスト側コード

HelloWorld.wasm などのソースは src/wasmsamples 配下に c 言語で記述されています。Hello.wasm 内の HelloWorld 関数は引数で指定された文字列を printf で出力するだけのシンプルなものになっています。

https://github.com/hyperlight-dev/hyperlight-wasm/blob/main/src/wasmsamples/HelloWorld.c

RunWasm.wasm の Echo 関数は以下で定義。

src/wasmsamples/RunWasm.c
__attribute__((export_name("Echo")))
char* Echo(char* msg)
{
    return msg;
}

これをもとにサンプルコード実行時に出力される結果を見ると、Result from calling ... はホスト側コードの条件分岐の println! で出力されていて、Message from Rust ... は helloworld 関数実行時に Sandbox 内で printf により表示されていることがわかります。

Message from Rust Example to Wasm Function
Result from calling HelloWorld Function in Wasm Module test case 0) is: 0
Message from Rust Example to Wasm Function
Result from calling HelloWorld Function in Wasm Module test case 1) is: 0
Result from calling Echo Function in Wasm Module test case 2) is: Message from Rust Example to Wasm Function
Result from calling Echo Function in Wasm Module test case 3) is: Message from Rust Example to Wasm Function

ホスト側のコード src/hyperlight_wasm/examples/helloworld/main.rs は相変わらず hyperlight や hyperlight-wasm ライブラリを使って rust で記述する必要がありますが、ホスト側コードから呼び出すゲストコードは wasm モジュールとしてビルドするだけで良くなっていることが hyperlight との違いになってます。

モジュールの種類について

ビルドによって x64/debug 以下に生成される wasm モジュールを見ると、1 つのソースコードにつき wasi-libc.wasm, aot, wasm の 3 つが作られています。

HelloWorld-wasi-libc.wasm
HelloWorld.aot
HelloWorld.wasm

just build-wasm-examples でビルドを実行すると src/hyperlight_wasm/script/build-wasm-examples.sh が実行され、この中で c ソースから wasm モジュールの作成 → AOT コンパイル → .aot から .wasm への rename が処理されています。

build-wasm-examples.sh
  # not running in a container so use the docker image to build the wasm files
    echo Building docker image that has Wasm sdk. Should be quick if preivoulsy built and no changes to dockerfile.
    echo This will take a while if it is the first time you are building the docker image.
    echo Log in ${OUTPUT_DIR}/dockerbuild.log

    docker pull ghcr.io/deislabs/wasm-clang-builder:latest

    docker build --build-arg GCC_VERSION=12 --build-arg WASI_SDK_VERSION_FULL=20.0 --cache-from ghcr.io/deislabs/wasm-clang-builder:latest -t wasm-clang-builder:latest . 2> ${OUTPUT_DIR}/dockerbuild.log

    for FILENAME in $(find . -name '*.c')
    do
        echo Building ${FILENAME}
        # Build the wasm file with wasi-libc for wasmtime
        docker run --rm -i -v "${PWD}:/tmp/host" -v "${OUTPUT_DIR}:/tmp/output" wasm-clang-builder:latest /opt/wasi-sdk/bin/clang -flto -ffunction-sections -mexec-model=reactor -O3 -z stack-size=4096 -Wl,--initial-memory=65536 -Wl,--export=__data_end -Wl,--export=__heap_base,--export=malloc,--export=free,--export=__wasm_call_ctors -Wl,--strip-all,--no-entry -Wl,--allow-undefined -Wl,--gc-sections  -o /tmp/output/${FILENAME%.*}-wasi-libc.wasm /tmp/host/${FILENAME}

        # Build AOT for Wasmtime; note that Wasmtime does not support
        # interpreting, so its wasm binary is secretly an AOT binary.
        cargo run -p hyperlight-wasm-aot compile ${OUTPUT_DIR}/${FILENAME%.*}-wasi-libc.wasm ${OUTPUT_DIR}/${FILENAME%.*}.aot
        cp ${OUTPUT_DIR}/${FILENAME%.*}.aot ${OUTPUT_DIR}/${FILENAME%.*}.wasm
  • wasi-libc.wasm: Wasm モジュール
  • .aot: AOT コンパイルされたネイティブバイナリ
  • .wasm: .aot を rename したもの。内容は同じ。

*wasi-libc.wasm は文字通りの wasm モジュールなので wasmtime などで直接実行することができます。

$ file HelloWorld-wasi-libc.wasm
HelloWorld-wasi-libc.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)

$ wasmtime --invoke Hello HelloWorld-wasi-libc.wasm
Hello from Wasm in Hyperlight
warning: using `--invoke` with a function that returns values is experimental and may break in the future
0

wasm モジュールを追加する

自分で作った wasm モジュールを追加してホスト側コードから呼び出すこともできます。
ここでは 2 つの int を足すだけの簡単な Add 関数を定義した Calc.c を作ります。

Calc.c
#include <stdio.h>

__attribute__((export_name("Add")))
int Add(int a, int b) {
    printf("Add %d + %d\n", a, b);
    return a + b;
}

次に上記を wasm モジュールに変換します。ビルド時のオプションは src/hyperlight_wasm/script/build-wasm-examples.sh を参考に指定。

docker run --rm -it -v "${PWD}:/tmp/host" -v "./output:/tmp/output" wasm-clang-builder:latest \
    /opt/wasi-sdk/bin/clang \
    -flto -ffunction-sections \
    -mexec-model=reactor \
    -O3 \
    -z \
    stack-size=4096 \
    -Wl,--initial-memory=65536 \
    -Wl,--export=__data_end \
    -Wl,--export=__heap_base,--export=malloc,--export=free,--export=__wasm_call_ctors \
    -Wl,--strip-all,--no-entry \
    -Wl,--allow-undefined \
    -Wl,--gc-sections \
    -o /tmp/output/Calc-wasi-libc.wasm /tmp/host/Calc.c

できた wasm モジュールを AOT コンパイル。

cargo run -p hyperlight-wasm-aot compile output/Calc-wasi-libc.wasm output/Calc.aot

作成した Calc.aotx64/debug/ に配置。

mv output/Calc.aot x64/debug/

次に Add 関数を呼び出すように先程使用したホスト側コードの src/hyperlight_wasm/examples/helloworld/main.rs に追加します。

src/hyperlight_wasm/examples/helloworld/main.rs
        (
            "RunWasm.aot",
            "Echo",
            Some(vec![ParameterValue::String(
                "Message from Rust Example to Wasm Function".to_string(),
            )]),
        ),
+        (
+            "Calc.aot",
+            "Add",
+            Some(vec![
+                ParameterValue::Int(1),
+                ParameterValue::Int(2),
+            ])
+        ),
     ];

...

        } else if *fn_name == "HelloWorld" {
            // Call a function in the Wasm module
            let ReturnValue::Int(result) = loaded_wasm_sandbox.call_guest_function(
                fn_name,
                params_opt.clone(),
                ReturnType::Int,
            )?
            else {
                panic!("Failed to get result from call_guest_function to HelloWorld Function")
            };

            println!(
                "Result from calling HelloWorld Function in Wasm Module \
            test case {idx}) is: {}",
                result
            );
+        } else if *fn_name == "Add" {
+        // Call a function in the Wasm module
+        let ReturnValue::Int(result) = loaded_wasm_sandbox.call_guest_function(
+            fn_name,
+            params_opt.clone(),
+            ReturnType::Int,
+        )?
+        else {
+            panic!("Failed to get result from call_guest_function to HelloWorld Function")
+        };
+
+        println!(
+            "Result from calling Add Function in Wasm Module \
+        test case {idx}) is: {}",
+            result
+        );
+    }
+
+

cargo run --example helloworld で実行すると想定通り Add 関数が call されていることがわかります。

$ cargo run --example helloworld
   Compiling hyperlight-wasm v0.1.0 (/home/ubuntu/hyperlight-wasm/src/hyperlight_wasm)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.27s
     Running `target/debug/examples/helloworld`
Message from Rust Example to Wasm Function
Result from calling HelloWorld Function in Wasm Module test case 0) is: 0
Message from Rust Example to Wasm Function
Result from calling HelloWorld Function in Wasm Module test case 1) is: 0
Result from calling Echo Function in Wasm Module test case 2) is: Message from Rust Example to Wasm Function
Result from calling Echo Function in Wasm Module test case 3) is: Message from Rust Example to Wasm Function
+ Add 1 + 2
+ Result from calling Add Function in Wasm Module test case 4) is: 3

このように何らかの方法で hyperlight-wasm で実行可能な wasm モジュールを作ってしまえばホスト側コードに組み込んで実行することができます。hyperlight の場合に rust でゲストライブラリを使って組み込む方法と比較するとだいぶ楽に hyperlight 基盤で wasm アプリケーションを実行できるようになっています。

その他

Sandbox 環境の割当メモリを増やす

ロードする wasm モジュールのサイズが大きいとコード実行時に Sandbox のメモリ不足のエラーが出ることがありますが、この場合は hyperlightsrc/hyperlight_wasm/src/sandbox/wasm_sandbox.rs に記載例があるように SandboxBuilderguest_heap_size などで割当メモリ等を増やすことができます。

wasm_sandbox.rs
   let builder = SandboxBuilder::new()
            .with_guest_error_buffer_size(0x1000)
            .with_guest_input_buffer_size(0x8000)
            .with_guest_output_buffer_size(0x8000)
            .with_host_function_buffer_size(0x1000)
            .with_host_exception_buffer_size(0x1000)
            .with_guest_stack_size(0x2000)
            .with_guest_heap_size(0x100000)
            .with_guest_panic_context_buffer_size(0x800)
            .with_guest_function_call_max_execution_time_millis(max_exec_time)
            .with_guest_function_call_max_cancel_wait_millis(max_cancel_wait);

SandboxBuilder で設定可能なオプションは以下に一覧があります。

https://github.com/hyperlight-dev/hyperlight-wasm/blob/main/src/hyperlight_wasm/src/sandbox/sandbox_builder.rs

これからの話

hyperlight-wasm では任意のプログラミング言語で書いて hyperlight-wasm 上で実行可能な形式にビルドされた wasm モジュールであれば実行可能であるため、アプリケーション開発者は wasm モジュールを用意すればあとは hyperlight-wasm 上で高速かつセキュアにワークロードを実行できるようになります。hyperlight-wasm 上で実行されていることを意識しなくて良いため、開発者は hyperlight-wasm に関する知見やホスト側コードの書き方を学習しなくても利用できるという点がメリットです。

ただ現時点では sandbox 環境の作成などのホスト側コードは自分で作成する必要があるので、完全に hyperlight-wasm に関する知見がなくても利用できるというわけではなく、利用するにはある程度の学習コストがかかります。このあたりはブログ でも述べられていて、今後 hyperlight-wasm のデフォルトバインディングを増やし、簡単な HTTP サーバ等はすぐに利用できるように拡張していく予定とのことです。

You may also have noticed that, while we can support WASI APIs in the guest, the VMM host doesn’t provide its own default implementation of WASI interfaces, so you have to implement them yourselves. While many applications will appreciate that flexibility, including cloud vendors like Microsoft who are using Hyperlight to create products, it does mean that getting started and trying things out can take some time. For that reason, we’re planning to extend Hyperlight-Wasm with default bindings for some WASI interfaces soon. That way, if you just want to sandbox an HTTP server or a service that listens on a socket, you don’t need to do much else to get started.

また、クライドプロパイダーが hyperlight-wasm のホスト側を管理し、ユーザーはワークロードのアプリケーションのみを用意して hyperlight 上で動かすという現在のサーバレスや FaaS に近い形式のサービス等にも利用できるかもしれません。ブログでは Azure の Front DoorEdge Actions で近いうちに内部的に hyperlight を導入するとも述べられています。

That’s the logic behind our upcoming Azure Front DoorEdge Actions service, powered by Hyperlight and soon to be in private preview.

現時点では github repo 上の情報もそこまで多くないため既存の環境に組み込んで活用するのはなかなか難しいですが、アプリケーション開発者側にとってはwasm ワークロードをセキュアな hyperlight-wasm 環境上で実行でき、ホスト管理側は CPU、メモリをより効率的に利用できたり、hyperlight の高速起動による効率的なスケーリングを実現できるといったように双方にメリットがあります。このあたり今後の拡張が期待されます。

参考ブログ

Discussion