WebAssembly
WebAssembly
Zig는 WebAssembly를 대상으로 하는 빌드를 별다른 설정 없이 바로 지원해요. "그냥 되는" 수준으로 빌드 타깃 하나를 고르고 컴파일하면 그만이죠. 이번에는 실제로 Zig 코드를 WebAssembly로 컴파일해서 돌려 보는 과정을, 크게 **프리스탠딩(freestanding)**과 WASI 두 가지 방식으로 나눠서 살펴볼게요. 실행 환경이 무엇이냐에 따라 이 둘 중 하나를 택하는 셈이에요.
본문
Freestanding
웹 브라우저나 nodejs처럼 호스트 환경이 따로 있는 곳에서는 freestanding OS 타깃으로 executable을 빌드해요. 다음 예시는 Zig 코드를 WebAssembly로 컴파일한 뒤 nodejs에서 실행하는 경우예요.
extern fn print(i32) void;
export fn add(a: i32, b: i32) void {
print(a + b);
}
$ zig build-exe math.zig -target wasm32-freestanding -fno-entry --export=add
여기서 math.zig는 extern fn print(i32) void;로 환경(여기서는 JS)이 제공할 함수를 선언하고, export fn add(...)로 밖으로 노출될 함수를 정의하고 있어요. --export=add 덕분에 add 함수가 wasm 바이너리의 export로 남고, -fno-entry로 진입점 없이 컴파일합니다.
이제 이 wasm을 nodejs에서 로드해서 호출해 봅시다.
const fs = require('fs');
const source = fs.readFileSync("./math.wasm");
const typedArray = new Uint8Array(source);
WebAssembly.instantiate(typedArray, {
env: {
print: (result) => { console.log(`The result is ${result}`); }
}}).then(result => {
const add = result.instance.exports.add;
add(1, 2);
});
$ node test.js
The result is 3
WebAssembly.instantiate를 호출할 때 두 번째 인자로 env: { print: ... }를 넘기는데, 이게 바로 Zig 쪽에서 extern으로 선언했던 print 함수를 채워 주는 역할이에요. instance의 exports.add로 wasm 쪽 add 함수를 꺼내 add(1, 2)를 호출하면, Zig 코드 안에서 print(a + b)가 실행되면서 JS의 print가 호출되어 콘솔에 The result is 3이 찍히죠.
WASI
Zig 표준 라이브러리는 **WASI(WebAssembly System Interface)**를 일급(first-class)으로 지원해요. WASI는 wasm 안에서 파일 시스템·인자·표준 입출력 같은 시스템 기능을 쓰게 해 주는 인터페이스인데, 표준 라이브러리가 이를 자연스럽게 다뤄 줍니다.
먼저 명령줄 인자를 출력하는 예시를 볼게요.
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const args = try init.minimal.args.toSlice(init.arena.allocator());
for (0.., args) |i, arg| {
std.debug.print("{d}: {s}\n", .{ i, arg });
}
}
$ zig build-exe wasi_args.zig -target wasm32-wasi
main이 std.process.Init을 받는 모습이 특이하죠. WASI 런타임이 제공하는 시작 정보(여기서는 명령줄 인자)를 init을 통해 받아서, init.minimal.args로 인자 목록을 꺼내 순서대로 출력합니다. 이제 wasmtime으로 실행해 보면 다음과 같이 인자가 그대로 전달돼요.
$ wasmtime wasi_args.wasm 123 hello
0: wasi_args.wasm
1: 123
2: hello
좀 더 흥미로운 예시로, 런타임이 열어 준 preopen 목록을 꺼내는 경우도 있어요. 이건 표준 라이브러리의 std.fs.wasi.Preopens로 지원됩니다.
const std = @import("std");
pub fn main(init: std.process.Init) void {
for (init.preopens.map.keys(), 0..) |preopen, i| {
std.log.info("{d}: {s}", .{ i, preopen });
}
}
$ zig build-exe wasi_preopens.zig -target wasm32-wasi
preopen은 wasm이 접근할 수 있도록 런타임이 미리 열어 둔 디렉터리 같은 자원을 말해요. --dir=.을 주면 현재 디렉터리가 열려서, init.preopens.map에서 그 목록을 확인할 수 있습니다.
$ wasmtime --dir=. wasi_preopens.wasm
0: stdin
1: stdout
2: stderr
3: .
여기서 stdin·stdout·stderr은 기본으로 열려 있는 표준 스트림이고, 3: .이 --dir=.로 열어 준 현재 디렉터리예요. 이렇게 WASI를 쓰면 웹 브라우저가 아닌 일반적인 호스트에서도 wasm 프로그램이 파일이나 인자 등 시스템 기능에 접근할 수 있답니다.
더 알아보기
Freestanding— 호스트 환경(브라우저·nodejs)에서 wasm executable을 빌드하는 방식WASI— 파일 시스템·인자·표준 입출력 등 시스템 기능을 wasm에 제공하는 인터페이스Build Targets— 컴파일 타깃(Target) 지정과 맞춤 설정