1.8. WASM Component Model - ffi.wit
WebAssembly Component Model (bytecodealliance.org, Preview 2 는 2024년 1월에 안정화되었습니다) 은 타입이 부여된 언어 간 인터페이스를 정의하기 위한 기반입니다. 전통적인 C ABI FFI (1.7) 가 의존하던 raw pointer 와 길이 쌍 모델을, 이 체계는 record / variant / resource 라는 풍부한 타입으로 대체합니다.
1.8.1. WIT - WebAssembly Interface Types
WIT 는 언어 중립적인 인터페이스 정의 언어입니다.
package example:fs;
interface fs {
record stat {
size: u64,
mtime: u64,
is-dir: bool,
}
variant error {
not-found,
permission-denied,
io-error(string),
}
resource file {
constructor(path: string);
read: func(buf-size: u32) -> result<list<u8>, error>;
close: func();
}
open: func(path: string) -> result<file, error>;
}
이렇게 정의된 인터페이스는 Rust, Go, Python, rok 등 어떤 언어에서도 import 또는 export 의 형태로 자유롭게 활용됩니다.
1.8.2. rok 측 binding 형태
import 측은 외부 component 를 씁니다. 여기서 wit/fs.wit 는 맨 위에 package example:fs; 를 둔 1.8.1 의 interface 입니다.
ffi::wit::import { "wit/fs.wit" } // 이 소스 파일 기준 상대 경로
use example::fs::fs::{File, Error};
let f = File::new("data.txt");
let bytes = try f.read(1024); // throws (Error) -> List<u8>
f.close();
export 측은 component 를 제공합니다. calculator.wit 는 world 안에 add: func(a: s32, b: s32) -> s32 를 선언합니다.
ffi::wit::export { "calculator.wit" }
#[wit::export]
fn add(a: i32, b: i32) -> i32 { a + b }
경로와 위치
- 경로는 소스 파일 기준 상대 경로입니다.
.wit파일이나 package 디렉토리를 가리키며, 디렉토리의deps/에 의존이 있습니다. - 이 항목은 프로그램의 패키지와 의존 패키지에서 쓸 수 있습니다. binding 은 그 항목이 있는 모듈 (의존 패키지면 그 의존 패키지의 모듈) 안
ns::pkg::iface(wasi::filesystem::types) 아래에 들어갑니다. - WIT 타입은 PascalCase rok 타입이 됩니다 (
descriptor-type→DescriptorType,file→File). 함수와 필드는 snake_case 가 됩니다.
World
이름 붙은 파일의 world 는 그 component 자체입니다. 그 밖이면 키워드가 정합니다: ffi::wit::import 는 interface 를 import, ffi::wit::export 는 export 합니다. include ... with { a as b } 는 포함한 항목의 이름을 바꿉니다.
타입 대응
| WIT | rok |
|---|---|
record | struct |
variant, enum | enum |
flags | Default 를 가진 bool 필드의 struct (flag 32 개까지) |
option<T> | T? (중첩 option 은 층을 유지: option<option<T>> → T??) |
result<T, E> | Result<T, E>. 이것을 돌려주는 함수는 throws (E) -> T |
tuple<..> | 튜플 |
list<T> | List<T> |
list<T, N> | [T; N] |
string | str (UTF-8) |
char, bool, f32, f64 | 같음 |
sN / uN | iN / uN |
future<T> | AsyncFuture<T> |
stream<T> | rok::core::channel::Receiver<T> (sender 를 닫으면 끝남) |
error-context | rok::bare::ffi::wit::ErrorContext |
import 한 resource | using / Drop 으로 해제되는 handle struct |
export 하는 resource | 사용자의 #[wit::export] pub struct |
Async
async func 는 import 와 export 모두에서 async fn 입니다. 동기 task (preview 2 의 main, 동기 export) 는 component model 을 기다릴 수 없으며 그러면 런타임 panic 이므로, async import 는 async export 안에서 await 합니다. preview 3 command 는 wasi:cli/run@0.3.x 를 #[wit::export] async fn run() -> Result<(), ()> 로 export 합니다. async export 호출은 동시에 실행되며, 각각이 component model task 입니다. 호출은 자기 차례에 결과를 돌려줍니다. async export 안에서 socket 과 timer 는 host 의 event 사이에 기다리며, component model event 도 대기 중이면 적어도 10 ms 마다 확인합니다 (preview 2 pollable 과 component model 의 event 는 한꺼번에 기다릴 수 없기 때문). 아무것도 전해 주지 않는 것을 기다리는 async export 는 계속 기다립니다. 다른 호출이 그것을 전해 줄 수도 있습니다.
생성된 binding 이 부르는 async canonical ABI 는 rok::std::wit 에 있습니다: executor 위에서 돌며, stream<T> 는 core 타입에 대응합니다. 메모리 helper, block, handle, ErrorContext 는 rok::bare::ffi::wit 에 남습니다.
Export
export 함수는 그 항목의 모듈에 있는 #[wit::export] fn 이며, snake_case 이름으로 WIT 함수와 짝지어집니다. export 하는 resource 는 #[wit::export] pub struct 이고, 그 impl 이 new (constructor), 메서드, static 함수를 줍니다. 실패할 수 있는 constructor (result<r, ..>) 는 throw 하는 new 이며, resource 의 destructor 는 그 Drop 을 실행합니다. 빠진 export 는 컴파일 에러입니다.
WIT 를 export 하고 진입 파일에 fn main 도 top-level 문도 없는 패키지는 reactor 입니다: wasi:cli/run 을 export 하지 않습니다 (4.16.1).
타깃
WIT 에는 wasm32-wasi (WASI preview 2 component) 가 필요하며, 다른 타깃에서는 컴파일 에러입니다. export 는 main 전에 불릴 수 있습니다: 런타임은 첫 export 호출에서 시작합니다.
미리 빌드한 의존 패키지는 그 ffi::wit 항목이 읽는 .wit 파일을 ROKHIR 에 담고 다닙니다 (refs-lang 9.4.7). 의존하는 쪽은 그것을 디스크에서 읽지 않습니다.
제약
import 한 interface 는 component 가 export 하는 interface 의 타입을 쓸 수 없으며, 쓰면 컴파일 에러입니다.
1.8.3. C FFI vs WIT 차이
두 방식의 성격은 여러 축에서 뚜렷하게 갈립니다.
| 측면 | C FFI (1.7) | WIT (이 장) |
|---|---|---|
| 타입 시스템 | C primitive + 사용자 struct | record / variant / resource |
| 안전성 | 낮음 (raw ptr 손수 관리) | 높음 (typed) |
| 메모리 관리 | malloc/free | resource (자동 destructor) |
| 에러 처리 | errno / 반환값 convention | result<T, E> 1급 |
| String | null-terminated | length-prefixed UTF-8 |
| List | (ptr, len) manual | list<T> 자동 |
| 호출 비용 | 거의 없음 (direct call) | trampoline (~ns) |
| 적용 범위 | 모든 native target | wasm32-wasi (preview 2 component) |
| unsafe 필요 | ✓ | ✗ |
1.8.4. 선택 기준
선택의 기준은 단순합니다 - 무엇을 호출할지, 어디에 배포할지에 따라 달라집니다.
| 시나리오 | 권장 |
|---|---|
| 전통 C 라이브러리 호출 | C FFI (1.7) |
| 시스템 콜 (libc, Win32) | C FFI |
| GPU / SIMD intrinsic | C FFI |
| 다른 언어와 안전 인터페이스 | WIT |
| WASM 환경 배포 | WIT |
| 플러그인 / 샌드박스 격리 | WIT |
| Modular / Mojo 등 다국어 | WIT |
이 장이 갖는 가장 큰 의의는, rok 이 전통 C FFI 와 WIT 두 갈래를 모두 지원한다는 점에 있습니다. 사용자는 안전성, 성능, target 환경의 제약에 맞추어 자유롭게 한쪽을 고르면 됩니다.
1.8.5. 참고
더 깊이 파고들고 싶다면 다음 자료들을 권합니다.