홈
WebAssembly 컴포넌트 모델은 상호 운용 가능한 WebAssembly 라이브러리, 애플리케이션, 환경을 구축하기 위한 광범위한 아키텍처입니다.
이 문서는 컴포넌트 모델의 사용자, 즉 라이브러리와 애플리케이션 개발자를 대상으로 합니다.
Note
컴파일러 및 Wasm 런타임 개발자는 컴포넌트 모델 명세를 참고하여 자신의 프로젝트에 컴포넌트 모델 지원을 추가하는 방법을 확인할 수 있습니다.
목차
| 컴포넌트 이해하기 | 컴포넌트 빌드하기 | 컴포넌트 사용하기 |
|---|---|---|
| 왜 컴포넌트인가? | C/C++ | 합성하기 |
| 컴포넌트 | C# | 실행하기 |
| 인터페이스 | Go | 배포하기 |
| 월드 | JavaScript | |
| Python | ||
| Rust | ||
| MoonBit |
WebAssembly 컴포넌트
모든 프로그래밍과 마찬가지로, 컴포넌트를 작성하는 목표는 기존 기능을 바탕으로 새로운 기능을 구축하여 사용할 수 있게 하는 것입니다.
WebAssembly 컴포넌트는 플랫폼에서 실행되며, 이 플랫폼은 웹 브라우저, 독립 실행형 런타임, 또는 (WebAssembly를 실행 파일로 컴파일하는 경우) 운영 체제일 수도 있습니다. 플랫폼은 컴포넌트를 실행함으로써 해당 컴포넌트가 구현하는 기능을 얻게 됩니다. 마찬가지로, 플랫폼은 컴포넌트 내의 코드가 외부 세계와 상호작용하는 데 사용할 수 있는 기능을 제공합니다.
예를 들면 다음과 같습니다.
- 컴포넌트 모델 사용자는 시스템 시간을 다른 시간대로 변환하는 컴포넌트를 빌드할 수 있습니다.
- 컴포넌트가 의도한 대로 작동하려면, 기반 플랫폼이 현재 시스템 시간과 시스템 시간대에 접근할 수 있는 수단을 컴포넌트에 제공해야 합니다.
WebAssembly 컴포넌트 빌드를 위한 API
일반적으로 컴포넌트를 실행하는 플랫폼은 컴포넌트에 필요한 기능에 접근하기 위한 잘 정의된 API를 제공해야 합니다. 예를 들어 표준 입력에서 읽기, 환경 변수에 접근하기, 네트워크 소켓 조작하기 등이 있습니다.
WebAssembly 컴포넌트가 의존할 수 있는 표준화된 공용 API 집합이 있으면 유용합니다. WASI(WebAssembly 시스템 인터페이스)는 이러한 API를 정의하는 표준화 과정의 명세입니다. 시스템이나 플랫폼은 WASI API의 일부 또는 전부를 컴포넌트에 노출할 수 있습니다.
상태
WASI의 현재 안정 릴리스는 2024년 1월 25일에 릴리스된 WASI 0.2.0입니다. WASI 0.2.0은 컴포넌트가 대상으로 삼을 수 있는 안정적인 WIT 정의 집합입니다. WASI 제안은 계속 발전하고 새로운 제안도 도입될 것입니다. 그러나 이제 컴포넌트 모델 사용자는 >= v0.2.0인 모든 안정 릴리스에 고정할 수 있습니다. WASI.dev 로드맵에서 예정된 릴리스를 추적합니다.
기여하기
오류, 누락, 모호한 부분 또는 기타 문제를 발견하시면 GitHub 이슈를 통해 알려 주십시오.
이 가이드에 콘텐츠를 기여하고 싶으시다면 기여 방법에 관한 정보는 기여 가이드를 참고하십시오.
컴포넌트 모델이 필요한 이유는 무엇입니까?
개략적으로 말하면, 컴포넌트 모델은 WebAssembly 코어 모듈을 기반으로 언어와 라이브러리 간의 상호 운용성을 향상시킵니다. 이를 위해 모듈 간 상호 작용의 안전성을 검사하는 데 사용되는 타입 시스템을 풍부하게 하고, 별도로 컴파일된 모듈 간의 저수준 호출 규약을 명확하게 정의하고 강제합니다. 코어 모듈의 한계가 무엇인지 이해하기 위해, 먼저 코어 모듈을 정의하는 것부터 시작합니다.
WebAssembly 코어 모듈
모듈은 WebAssembly 코어 명세에 정의되어 있습니다.
WebAssembly 프로그램은 직접 손으로 작성할 수도 있지만, Rust, C, Go, JavaScript, Python과 같은 상위 수준 프로그래밍 언어를 사용하여 WebAssembly 프로그램을 빌드할 가능성이 더 높습니다. 현재 많은 기존 툴체인은 단일 바이너리 .wasm 파일인 WebAssembly 코어 모듈을 생성합니다.
코어 모듈은 일반적으로 단일 바이너리 .wasm 파일에 대응합니다. 다음은 샘플 .wasm 파일에 대해 file 명령이 출력하는 내용입니다:
$ file adder.wasm
adder.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
이 파일은 wasm-tools CLI를 사용하여 검사할 수도 있습니다:
$ wasm-tools print adder.wasm | head -1
(module
코어 모듈은 정의들의 집합입니다. 정의의 종류는 다음과 같습니다:
- 함수는 실행 가능한 코드 단위(인수의 이름, 인수와 반환 값의 타입에 대한 선언과 함께 구성된 명령어 시퀀스)를 정의합니다.
- 선형 메모리는 명령어로 읽고 쓸 수 있는, 해석되지 않은 바이트의 버퍼를 정의합니다.
- 임포트는 모듈 내 함수를 실행하는 데 사용 가능해야 하는 다른 모듈의 이름을, 임포트된 모듈에서 필요한 함수의 타입 시그니처와 함께 정의합니다.
- 익스포트는 모듈 내 함수 중 외부에서 접근 가능해야 하는 함수의 이름을 정의합니다.
- 그 밖에도 여러 가지가 있습니다. 전체 목록은 코어 명세를 참조하십시오.
컴파일된 코어 모듈은 때때로 “WebAssembly 바이너리“라고 불리며, 일반적으로 단일 .wasm 파일에 대응합니다. 이러한 모듈은 브라우저에서 실행하거나, Wasmtime 또는 WAMR과 같은 별도의 런타임을 통해 실행할 수 있습니다.
코어 모듈의 한계
코어 모듈은 수행할 수 있는 계산과 그 기능을 외부 세계에 노출하는 방식이 제한되어 있습니다. WebAssembly 코어 모듈에서 함수는 본질적으로 정수(i32 또는 i64) 또는 부동소수점(f32 또는 f64) 타입만 사용하도록 제한됩니다. 이러한 타입만 함수의 인수로 전달할 수 있으며, 이러한 타입만 함수의 결과로 반환될 수 있습니다. 고수준 프로그래밍 언어에서 흔히 사용되는 문자열, 리스트, 배열, 열거형(enumeration), 구조체(레코드)와 같은 복합 타입은 정수와 부동소수점 수로 표현해야 합니다.
예를 들어, 함수가 문자열을 받으려면 문자열 인수를 두 개의 별도 인수, 즉 메모리에 대한 정수 오프셋과 문자열의 길이를 나타내는 정수로 표현할 수 있습니다. (선형) 메모리는 모듈 내에서 선언된, 초기화되지 않은 바이트 영역이라는 점을 상기하십시오.
의사 코드로 표현하면, 문자열을 조작하는 함수의 타입 시그니처는 다음과 같을 수 있습니다:
remove-duplicates: func(offset: i32, length: i32) -> [i32, i32]
remove-duplicates가 인수에 포함된 고유한 문자로 구성된 새 문자열을 생성하는 함수라고 가정합니다. 반환 타입은 두 개의 32비트 정수로 이루어진 리스트입니다. 첫 번째 정수는 모듈이 선언한 선형 메모리 중 하나에 대한 오프셋(새로 할당된 문자열이 시작되는 위치)이며, 두 번째 정수는 문자열의 길이입니다. 함수를 호출한 후, 호출자는 반환된 오프셋과 길이를 사용하여 해당 선형 메모리에 접근해 출력 문자열을 읽어야 합니다.
이것이 동작하려면 remove-duplicates 함수를 정의하는 모듈에 인수 문자열과 결과 문자열에 사용할 메모리를 익스포트하는 익스포트 선언도 포함되어야 합니다. 의사 코드:
export "string_mem" (mem 1)
그리고 remove-duplicates 함수를 사용하는 모듈은 이 메모리를 임포트해야 합니다. 의사 코드:
import "strings" "string_mem"
(가져오는 쪽에서도 가져오는 메모리의 크기를 선언해야 하므로, 이 의사 코드는 여전히 단순화된 것입니다.)
반환된 길이와 반환된 오프셋은 둘 다 정수이므로, 타입 시스템에는 둘이 혼동되는 것을 막는 장치가 전혀 없다는 점에 유의하십시오. 또한 입력 문자열과 출력 문자열에 사용되는 메모리의 이름은 관례로 정해야 하며, (오프셋과 길이의 합이 범위 안에 있는 한) 클라이언트 코드가 다른 메모리를 인덱싱하는 것을 막는 장치도 타입 시스템에는 전혀 없습니다.
우리는 다음과 같은 의사 코드 타입 시그니처를 작성하는 편을 선호합니다:
remove-duplicates: func(s: string) -> string
메모리 익스포트와 임포트를 완전히 없앨 수 있습니다.
복잡함은 여기서 끝나지 않습니다! 데이터 표현은 프로그래밍 언어마다 고유한 경우가 많습니다. 예를 들어 C의 문자열은 Rust나 JavaScript의 문자열과 완전히 다르게 표현됩니다. 게다가 이 방식이 동작하려면 모듈이 메모리를 임포트하고 익스포트해야 하는데, 언어마다 메모리 레이아웃에 대해 서로 다른 가정을 하기 때문에 오류가 발생하기 쉽습니다.
서로 다른 언어로 작성된 WebAssembly 모듈이 원활하게 상호 운용되려면, 이러한 더 풍부한 타입을 모듈 경계를 넘어 노출하는 합의된 방법이 필요합니다.
컴포넌트
컴포넌트는 지금까지 살펴본 두 가지 문제, 즉 코어 모듈 함수의 제한적인 타입 시스템과 언어 간 상호 운용성 문제를 해결합니다. 개념적으로 컴포넌트는 모듈의 가져온 함수와 내보낸 함수를 통해서만 상호 작용하도록 제한된 WebAssembly 바이너리(모듈을 포함할 수도 있고 포함하지 않을 수도 있음)입니다. 컴포넌트는 다른 바이너리 형식을 사용합니다:
$ file add.component.wasm
add.component.wasm: WebAssembly (wasm) binary module version 0x1000d
wasm-tools CLI로 파일을 검사하면 파일에 컴포넌트가 포함되어 있음을 더 분명하게 확인할 수 있습니다:
$ wasm-tools print add.component.wasm | head -1
(component
코어 모듈과 비교하여, 컴포넌트는 함수의 타입을 표현하기 위해 기본적으로 더 풍부한 메커니즘인 인터페이스도 사용합니다.
인터페이스
인터페이스는 WebAssembly 인터페이스 타입(WIT)이라는 별도의 언어로 표현됩니다. 인터페이스에는 타입의 정의와 함수의 타입 시그니처가 포함됩니다. 타입의 비트 수준 표현은 정규 ABI(Application Binary Interface, 애플리케이션 바이너리 인터페이스)에 의해 명시됩니다. 인터페이스와 정규 ABI는 함께 모듈 간의 저수준 호출 계약을 명확하게 정의하고 강제한다는 목표를 달성합니다.
상호 운용성
WebAssembly 코어 모듈은 이미 다양한 아키텍처와 운영 체제 간에 이식 가능합니다. 컴포넌트는 이러한 이점을 유지하면서, 컴포넌트 모델 ABI를 사용하여 다양한 프로그래밍 언어 간의 이식성을 더합니다. Go로 구현된 컴포넌트는 컴포넌트 모델 ABI의 공유 관례에 의존하여 C 또는 Rust 컴포넌트와 직접적이고 안전하게 통신할 수 있습니다. 컴포넌트를 작성할 때 의존하는 컴포넌트가 어떤 언어로 구현되었는지 알 필요조차 없으며, WIT로 표현된 컴포넌트 인터페이스만 알면 됩니다. 또한 컴포넌트는 한 컴포넌트의 익스포트가 다른 컴포넌트의 임포트를 충족하는 방식으로 더 큰 그래프로 합성될 수 있습니다.
컴포넌트 모델의 이점
모든 요소를 종합하면, 컴포넌트 모델은 WebAssembly 모듈을 캡슐화하는 바이너리 WebAssembly 형식을 도입합니다. 이 형식을 사용하면 WIT로 타입이 표현된 함수의 익스포트와 임포트를 통해서만 서로 상호 작용하는 WebAssembly 모듈을 구성할 수 있습니다.
컴포넌트 모델은 Wasm의 강력한 샌드박싱을 기반으로 더 많은 이점을 제공합니다. 풍부한 타입 덕분에 컴포넌트나 인터페이스가 무엇을 하는지 한눈에 파악하기 쉬우며, 어떤 나쁜 일이 일어날 수 없는지도 보장받을 수 있습니다. 더 풍부한 타입 시그니처는 정수와 부동소수점만으로 이루어진 타입 시그니처보다 더 풍부한 의미적 속성을 표현합니다. 컴포넌트 그래프 내의 관계는 정적으로 분석할 수 있습니다. 예를 들어, 비즈니스 로직을 포함한 컴포넌트가 개인 식별 정보를 포함한 컴포넌트에 접근할 수 없음을 검증할 수 있습니다.
또한 컴포넌트는 오직 자신의 임포트를 호출하고 자신의 익스포트가 호출되는 방식으로만 런타임이나 다른 컴포넌트와 상호작용합니다. 구체적으로, 코어 모듈과 달리 컴포넌트는 메모리를 익스포트할 수 없으므로, 자신의 메모리에 쓰고 다른 쪽이 그 메모리를 읽게 하는 방식으로 다른 쪽과 간접적으로 통신할 수 없습니다. 이는 샌드박싱을 강화할 뿐만 아니라 메모리에 대해 서로 다른 가정을 하는 언어 간의 상호 운용을 가능하게 합니다. 예를 들어, 가비지 컬렉션 메모리에 의존하는 컴포넌트가 기존의 선형 메모리를 사용하는 컴포넌트와 상호 운용할 수 있습니다.
컴포넌트 사용하기
이제 컴포넌트 모델이 어떻게 도움이 되는지 더 잘 이해하셨으니, 선호하는 언어로 컴포넌트를 빌드하는 방법을 살펴보십시오!
더 읽을거리
컴포넌트 모델 개념
WebAssembly 컴포넌트 모델은 여러 측면에서 코어 WebAssembly를 확장합니다. 컴포넌트 모델은 다음과 같은 역할을 합니다.
- 상위 수준 타입의 일관된 표현을 추가합니다
- 인터페이스 주도 개발을 가능하게 합니다
- 코어 WebAssembly를 합성 가능하게 만듭니다. 기능을 제공하는 컴포넌트와 그 기능을 사용하는 컴포넌트를 함께 합성하여 하나의 결과 컴포넌트로 만들 수 있습니다.
이 섹션에서는 컴포넌트 모델의 핵심 개념을 소개합니다. 컴포넌트 모델의 설계 근거는 이전 섹션을 참조하십시오.
컴포넌트
WebAssembly 컴포넌트는 정규 ABI를 준수하는 바이너리이며, 대개 컴포넌트 모델의 기능(상위 수준 타입, 인터페이스)으로 확장된 WebAssembly 코어 모듈입니다. WebAssembly 컴포넌트는 중첩 가능합니다. 즉, 0개 이상의 코어 모듈 및/또는 하위 컴포넌트를 함께 합성하여 포함할 수 있습니다. 예를 들어, 간단한 계산기를 구현하는 컴포넌트는 문자열을 부동소수점 수로 파싱하는 컴포넌트와 주요 산술 연산을 수행하는 컴포넌트를 합성하여 작성할 수 있습니다.
WebAssembly 인터페이스 타입(WIT)
WebAssembly 인터페이스 타입(WIT)은 WebAssembly 컴포넌트의 기능을 형식적으로 정의하는 데 사용되는 인터페이스 정의 언어(IDL)입니다. WIT는 WebAssembly 컴포넌트가 언어에 구애받지 않는 방식으로 타입 시그니처를 표현할 수 있게 해 주므로, 모든 컴포넌트 바이너리를 검사하고 합성하고 실행할 수 있습니다.
인터페이스
인터페이스는 타입 정의와 함수 선언(타입 시그니처가 함께 주어진 함수 이름)의 모음입니다. 일반적으로 하나의 인터페이스는 특정하고 한정된 기능 일부를 기술합니다.
예를 들어 wasi-cli에서는 stdin, stdout, stderr(일반적으로 명령줄과 유사한 환경에서 사용할 수 있는 스트림)를 구현하기 위해 세 개의 별도 인터페이스를 사용합니다
월드
월드는 컴포넌트가 제공하는 기능과 컴포넌트가 의존하는 기능을 표현하는 인터페이스와 타입의 모음입니다.
예를 들어 wasi-cli에는 command 월드가 포함되어 있으며, 이 월드는 무엇보다도 stdin, stdout, stderr 스트림을 나타내는 인터페이스에 의존합니다. command 월드를 구현하는 컴포넌트는 해당 인터페이스를 구현하는 환경에서 호출되어야 합니다.
패키지
패키지는 서로 관련된 인터페이스와 월드를 포함하는 WIT 파일의 집합입니다.
예를 들어 wasi-http 패키지에는 HTTP 프록시가 의존하는 인터페이스를 캡슐화하는 imports 월드와 imports에 의존하는 proxy 월드가 포함되어 있습니다.
비동기, 스트림, 퓨처
async func, stream<T>, future<T>를 사용할 수 있게 하는 새로운 컴포넌트 모델 기본 요소가 WASI 0.3과 함께 도입되었습니다. 이들을 함께 사용하면 인터페이스가 컴포넌트 경계를 넘어 합성되는 비동기 작업을 표현할 수 있습니다.
플랫폼
WebAssembly의 맥락에서 호스트는 WebAssembly 바이너리를 실행할 수 있는 WebAssembly 런타임을 의미합니다. 런타임은 브라우저 내부에 있을 수도 있고 독립적으로 존재할 수도 있습니다. 게스트는 호스트가 실행하는 WebAssembly 바이너리를 의미합니다. (이 용어들은 가상화에서 대응되는 개념을 빌려온 것으로, 가상화에서 게스트는 물리 하드웨어, 즉 “호스트” 위에서 실행되는 소프트웨어 기반 가상 머신입니다)
컴포넌트 모델은 코어 WebAssembly에 플랫폼이라는 개념을 도입하여, WebAssembly 게스트가 호스트 기능을 구조화되고 표준화된 방식으로 사용할 수 있게 합니다. 컴포넌트는 자신이 실행되는 플랫폼이 제공하는 기능을 임포트할 수 있습니다.
WASI
WebAssembly 시스템 인터페이스(WASI)는 일반적인 시스템 수준 함수를 위한 인터페이스 모음을 WIT로 정의합니다. WASI는 개발자에게 익숙한 기존 프로그램(예: wasi-cli 또는 wasi-http)을 모방하는 플랫폼을 컴포넌트 작성자를 위해 정의하여, 컴포넌트가 의존하는 기능을 표준화합니다.
Note
컴포넌트 모델은 Bytecode Alliance가 관리하며 공개적으로 설계됩니다.
목표, 사용 사례, 상위 수준 설계 선택에 대해서는
WebAssembly/component-model저장소를 참조하십시오.
컴포넌트
개념적으로 컴포넌트는 공유 메모리 대신 오직 인터페이스를 통해서만 상호 작용하는 자기 기술적(self-describing) WebAssembly 바이너리입니다. 각 용어가 무엇을 의미하는지 하나씩 살펴보겠습니다.
- 자기 기술적: WebAssembly 코어 모듈과 마찬가지로, 컴포넌트는 임포트 및 익스포트되는 함수의 이름과 타입을 모두 선언하는 임포트 및 익스포트 선언을 포함합니다. 코어 모듈과 비교하여 컴포넌트는 이러한 타입을 기술하기 위해 더 풍부한 타입 시스템을 사용하므로, 모듈이 어떤 기능을 제공하고 어떤 기능에 의존하는지 더 쉽게 이해할 수 있습니다.
- 상호 작용: 컴포넌트가 다른 컴포넌트와 상호 작용한다는 것은 해당 컴포넌트가 다른 컴포넌트에 정의된 함수를 호출하거나, 다른 컴포넌트가 해당 컴포넌트에 정의된 함수를 호출한다는 의미입니다. 인터페이스는 어떤 종류의 함수 호출이 유효한지를 명시합니다.
- 공유 메모리: “왜 컴포넌트 모델인가?” 섹션에서 WebAssembly 코어 모듈이 공유 메모리를 통해서만 복합 데이터를 교환할 수 있음을 살펴보았습니다. 컴포넌트는 코어 모듈과 동일한 방식으로 메모리를 사용하지만, 컴포넌트에서는 메모리가 결코 익스포트되거나 임포트되지 않는다는 점이 다릅니다. 즉, 메모리는 공유되지 않습니다.
논리적으로 컴포넌트는 코어 모듈 및/또는 다른 컴포넌트를 포함할 수 있는 구조입니다. 컴포넌트는 포함된 이러한 모듈과 하위 컴포넌트의 인터페이스를 WebAssembly 인터페이스 타입(WIT)을 사용하여 인코딩합니다.
컴포넌트에 대한 보다 형식적인 정의는 컴포넌트 모델 명세를 참고하십시오.
디스크상에서 컴포넌트는 특수한 형식의 WebAssembly 바이너리로 표현됩니다. 내부적으로 이 파일은 임포트와 익스포트를 통해 함께 합성된, 하나 이상의 전통적인(“코어”) WebAssembly 모듈과 하위 컴포넌트의 표현을 포함할 수 있습니다. 한쪽의 임포트가 다른 쪽의 익스포트로 충족된다면 두 모듈 또는 컴포넌트를 합성할 수 있습니다. 합성은 임의로 반복할 수 있으며, 서로 맞물리는 여러 모듈과 컴포넌트로 단일 컴포넌트를 합성할 수 있습니다. 인터페이스를 통해 특정 합성이 타당한지 검사할 수 있습니다.
각 컴포넌트는 월드로 기술되며, 월드는 컴포넌트의 모든 임포트와 익스포트를 기술하기 위해 여러 인터페이스를 함께 모을 수 있습니다. 월드는 임포트 및 익스포트되는 함수의 타입만 기술하며, 월드를 구현하는 코드는 컴포넌트가 내부적으로 정의합니다.
합성
한쪽의 임포트가 다른 쪽의 익스포트로 충족된다면 두 모듈 또는 컴포넌트를 합성할 수 있습니다. 합성은 임의로 반복할 수 있으며, 서로 맞물리는 여러 모듈과 컴포넌트로 단일 컴포넌트를 합성할 수 있습니다. 인터페이스를 통해 특정 합성이 타당한지 검사할 수 있습니다.
인터페이스
인터페이스는 계약에 의한 설계(design by contract)라는 개념에 기반합니다. 소프트웨어 설계에서 계약이란 코드 단위가 어떻게 동작해야 하는지를 정의한 명세입니다.
구체적으로 말하면, 인터페이스는 타입 정의와 함수 선언의 모음입니다. 개념적으로 인터페이스는 컴포넌트가 서로 상호 작용하거나 호스트와 상호 작용할 수 있게 하는, 단일한 목적을 가지며 합성 가능한 계약을 기술합니다.
- 단일 목적: 관례적으로 인터페이스는 서로 관련되어 있으며 함께 비교적 작은 기능 단위를 제공하는 타입과 함수를 기술합니다. 예를 들어 명령줄 환경에서 표준 입력 스트림으로부터 읽는 기능이 이에 해당합니다.
- 합성 가능: 인터페이스는 임포트하고 익스포트할 수 있습니다. 한 컴포넌트의 인터페이스는 다른 컴포넌트에 정의된 인터페이스를 기반으로 구축할 수 있습니다. 인터페이스는 타입 검사를 가능하게 하므로, 합성이 타당한 경우에만 인터페이스를 합성할 수 있습니다.
인터페이스의 타입과 함수는 컴포넌트와 호스트 간의 상호 작용을 가능하게 하는 데 사용됩니다. 예를 들면 다음과 같습니다.
- “HTTP 요청 수신” 인터페이스는 들어오는 요청, 나가는 응답, HTTP 메서드와 헤더 및 기타 데이터 구조를 나타내는 타입의 정의와 함께 “요청 처리” 함수 하나를 선언할 수 있습니다. 이는 wasi-http의
incoming-handler인터페이스와 비슷한 모습일 수 있습니다 - “벽시계(wall clock)” 인터페이스는 두 개의 함수를 선언할 수 있습니다. 하나는 현재 시각을 가져오는 함수이고, 다른 하나는 타이머의 정밀도(시간을 초, 밀리초, 나노초 또는 다른 단위 중 무엇으로 측정하는지)를 가져오는 함수입니다. 또한 특정 시점을 나타내는 타입도 정의할 것입니다. 이는 wasi-clocks의
wall-clock인터페이스와 비슷한 모습일 수 있습니다.
인터페이스를 서로 합성하는 예로, 두 개의 함수를 선언하는 “타이머” 인터페이스를 정의한다고 가정해 보십시오. 하나는 타이머를 시작하는 함수이고, 다른 하나는 제한 시간이 초과되었는지 조회하는 함수입니다. 이 인터페이스는 “벽시계” 인터페이스를 임포트하여 정의할 수 있습니다. 그 결과는 타이머 기능을 익스포트하고, “벽시계” 인터페이스가 임포트하는 모든 것을 임포트하는 인터페이스입니다.
인터페이스는 WIT 언어를 사용하여 정의합니다.
인터페이스에 대한 보다 형식적인 정의는 WIT 명세를 참고하십시오.
WIT 월드
WIT 월드(또는 간단히 “월드”)는 단일 인터페이스보다 더 넓은 범위를 갖는 계약입니다. 월드는 컴포넌트가 제공하는 기능과 컴포넌트가 동작하기 위해 필요로 하는 기능을 기술합니다.
월드는 어떤 임포트와 익스포트를 지정하는지에 따라 컴포넌트를 기술하는 데에도, 다른 컴포넌트를 위한 호스팅 환경을 기술하는 데에도 사용할 수 있습니다. 컴포넌트는 합성될 수 있으므로 월드는 컴포넌트와 호스트 환경 중 어느 쪽이든 나타낼 수 있습니다. 즉, 호스트 환경과 마찬가지로 컴포넌트도 다른 컴포넌트가 필요로 하는 기능을 제공할 수 있습니다.
컴포넌트로 월드를 충족하는 경우와 호스트로 월드를 충족하는 경우
한편으로 월드는 컴포넌트가 다른 컴포넌트와 어떻게 관계를 맺는지를 기술합니다. 즉, 컴포넌트가 노출하는 기능을 기술하고, 컴포넌트가 실행되기 위해 의존하는 기능을 선언합니다. 기능은 익스포트할 인터페이스를 정의하여 노출하고, 의존성은 인터페이스를 임포트하여 선언합니다. 월드는 컴포넌트의 겉면만 정의할 뿐, 내부 동작은 정의하지 않습니다.
다른 한편으로 월드는 컴포넌트를 위한 호스팅 환경, 즉 컴포넌트를 인스턴스화하고 그 기능을 호출할 수 있는 환경을 정의합니다.
- WebAssembly에서 인스턴스화란 모듈의 정적인 기술을 메모리 안의 동적인 구조로 바꾸는 것을 의미합니다. 이는 실행 파일을 로드하는 것과 유사합니다.
호스팅 환경은 모든 임포트에 대한 구현을 제공하고, 선택적으로 하나 이상의 익스포트를 호출함으로써 월드를 지원합니다. 컴포넌트를 만드는 애플리케이션 또는 라이브러리 개발자라면 컴포넌트가 대상으로 하는 월드를 지정하게 됩니다. 컴포넌트는 직접 만든, 사용 사례에 맞춘 고유한 임포트와 익스포트 집합을 가진 사용자 정의 월드 정의를 대상으로 할 수도 있고, 다른 사람이 이미 지정해 둔 기존 월드 정의를 대상으로 할 수도 있습니다. 어느 경우든 월드는 컴포넌트가 필요로 하는 모든 외부 기능을 지정합니다. 월드를 대상으로 하는 것은 특정 버전의 표준 라이브러리에 의존하는 것과 유사하지만, 컴포넌트를 사용하면 코드가 정확히 어떤 함수에 의존하는지를 정밀하게 지정할 수 있다는 점이 다릅니다.
예시: wasi:cli 월드
예를 들어, WASI(WebAssembly 시스템 인터페이스)는 명령줄 프로그램이 일반적으로 사용할 수 있으리라 기대하는 인터페이스(예: 파일 입출력, 난수 생성, 시계)를 임포트하는 “명령줄” 월드를 정의합니다. 이 월드에는 명령줄 도구를 실행하기 위한 익스포트가 하나 있습니다. 이 월드를 대상으로 하는 컴포넌트는 이 하나의 익스포트에 대한 구현을 반드시 제공해야 하며, 선택적으로 어떤 임포트든 호출할 수 있습니다. 예를 들어 특정 디렉터리에 있는 파일 크기의 요약을 출력하는 컴포넌트(Unix의 du 명령처럼)는 “명령줄” 월드를 대상으로 할 수 있으며, 월드가 임포트하는 파일 입출력 인터페이스에 의존하게 됩니다. 이 월드를 지원하는 호스팅 환경은 모든 임포트에 대한 구현을 반드시 제공해야 하며, 하나뿐인 익스포트를 호출할 수 있습니다. 예시로 든 디스크 사용량 컴포넌트를 실행한다는 것은 “명령줄” 월드를 지원하는 호스팅 환경에서 이 컴포넌트를 호출한다는 의미입니다.
월드와 인터페이스
월드는 인터페이스의 모음이며, 각 인터페이스는 방향성을 갖습니다. 각 인터페이스는 익스포트 또는 임포트 중 하나로 명시적으로 표시됩니다. 익스포트된 인터페이스는 외부 코드가 호출할 수 있는 반면, 임포트된 인터페이스는 외부 코드가 충족해야 합니다. 이러한 인터페이스는 컴포넌트의 엄격한 경계를 정의합니다. 컴포넌트가 자신 외부의 무언가와 상호 작용하는 방법은 자신의 익스포트가 호출되거나 자신의 임포트를 호출하는 것뿐입니다. 이 경계는 매우 강력한 샌드박싱을 제공합니다. 예를 들어 컴포넌트에 비밀 저장소에 대한 임포트가 없다면, 해당 저장소가 같은 프로세스에서 실행 중이더라도 컴포넌트는 그 비밀 저장소에 접근할 수 없습니다.
컴포넌트가 실행되려면 호스트 또는 다른 컴포넌트가 해당 컴포넌트의 임포트를 충족해야 합니다. 컴포넌트의 임포트 중 일부 또는 전부를 다른 컴포넌트의 일치하는 익스포트에 연결하는 것을 합성이라고 합니다.
월드는 WIT 파일에 정의되며, 하나의 WIT 파일에 여러 월드를 포함할 수 있습니다.
월드 예시
- (간단한) “HTTP 프록시” 월드는 “HTTP 요청 처리” 인터페이스를 익스포트하고 “HTTP 요청 전송” 인터페이스를 임포트합니다. 호스트 또는 다른 컴포넌트는 익스포트된 “처리” 인터페이스를 호출하여 HTTP 요청을 전달하고, 컴포넌트는 임포트된 “전송” 인터페이스를 통해 해당 요청을 전달합니다. 유용한 프록시가 되려면 컴포넌트는 I/O 및 시계 시간과 같은 인터페이스도 임포트해야 할 수 있습니다. 이러한 임포트가 없으면 컴포넌트는 디스크 캐싱이나 기타 필요한 기능을 수행할 수 없습니다.
- “정규식 파서” 월드는 “정규식 파싱” 함수를 익스포트하며, 아무것도 임포트하지 않습니다. 이는 이 월드를 구현하는 컴포넌트가 정규식을 파싱할 수 있다는 것뿐만 아니라 다른 API를 전혀 호출하지 않는다는 것도 선언합니다. 이러한 파서의 사용자는 구현을 살펴보지 않고도 파서가 파일 시스템에 접근하지 않으며 사용자의 정규식을 네트워크 서비스로 전송하지도 않는다는 것을 알 수 있습니다.
WIT 월드가 무엇인지에 대한 보다 공식적인 정의는 WIT 월드 명세를 참조하십시오.
WIT 패키지
WIT 패키지는 하나 이상의 WebAssembly 인터페이스 타입(WIT) 파일의 집합으로, 이 파일들을 모두 합치면 서로 관련된 인터페이스와 월드의 집합을 담고 있습니다. WIT는 컴포넌트 모델을 위한 인터페이스 정의 언어(IDL)입니다. 패키지는 월드와 인터페이스가 서로를 참조할 수 있는 방법을 제공하며, 이를 통해 컴포넌트 생태계가 공통 정의를 공유할 수 있게 합니다.
WIT 패키지는 쉽게 찾고 참조할 수 있도록 서로 관련된 인터페이스와 월드를 함께 묶습니다. 패키지는 월드가 아닙니다. 패키지는 하나 이상의 파일에 대응하며 월드를 포함합니다. 월드는 가져오는(import) 타입 및 인터페이스와 내보내는(export) 타입 및 인터페이스의 묶음입니다.
- WebAssembly System Interface(WASI, WebAssembly 시스템 인터페이스)는
wasi:clocks라는 이름의 패키지를 포함하여 여러 패키지를 정의합니다. HTTP 프록시 월드는 사용자 정의 시계 인터페이스를 정의할 필요 없이wasi:clocks/wall-clock인터페이스(“wasi:clocks패키지의wall-clock인터페이스“로 읽습니다)를 임포트할 수 있습니다.
WIT 패키지가 무엇인지에 대한 보다 형식적인 정의는 WIT 명세를 참조하십시오.
WASI 0.3의 네이티브 비동기
WASI 0.3은 비동기 기능을 가능하게 하는 새로운 정규 ABI 프리미티브를 컴포넌트 모델에 추가합니다. WASI 0.3을 대상으로 하는 컴포넌트는 WIT 파일에서 다음과 같은 새로운 기능을 사용할 수 있습니다.
async funcstream<T>future<T>
이러한 새로운 타입을 통해 인터페이스는 컴포넌트 경계를 넘어 합성되는 비동기 작업을 표현할 수 있습니다.
마이그레이션 메커니즘(예: WASI 0.2 컴포넌트가 이러한 프리미티브에 어떻게 매핑되는지)에 대해서는 WASI 0.2에서 WASI 0.3으로 마이그레이션하기를 참조하십시오.
인터페이스별 전체 diff를 포함하여 WASI 0.3 릴리스를 더 자세히 살펴보려면 WASI.dev의 WASI 0.3을 참조하십시오.
이 페이지는 컴포넌트 모델 개념 자체에 초점을 맞춥니다.
Note
WASI 0.3은 WASI 0.2를 대체하는 것이 아니라 그 위에 구축됩니다. 런타임은 두 버전을 나란히 호스팅할 수 있으며, 0.3 호스트는 경계에서 0.2 임포트를 폴리필할 수 있으므로, 애플리케이션은 툴체인과 의존성이 0.3 지원을 갖추어 감에 따라 점진적으로 마이그레이션할 수 있습니다.
WASI 0.3이 해결하는 비동기 문제
컴포넌트 모델의 정규 ABI는 타입이 지정된 값이 컴포넌트 경계를 넘는 방식을 정의합니다. WASI 0.3 이전까지 이 어휘 체계에는 일시 중단이나 비동기 완료라는 개념이 없었습니다. 모든 인터페이스 호출은 동기적으로 반환되었으며, 비동기 I/O는 해당 리소스를 획득한 컴포넌트에 범위가 한정된 리소스(준비 상태를 위한 pollable, 바이트 채널을 위한 input-stream 및 output-stream)로 모델링되었습니다.
이러한 방식은 두 당사자 간의 상호 작용에서는 잘 동작하지만, 컴포넌트가 체인으로 합성되면 제대로 동작하지 않습니다. 어떤 컴포넌트가 다른 컴포넌트가 다시 위임하는 작업을 기다리는 경우, 준비 상태 신호는 체인을 거슬러 올라가야 합니다. 준비 상태가 단일 컴포넌트에 범위가 한정된 리소스로 표현되면, 중간 컴포넌트는 오로지 호출자에게 웨이크업을 전달하기 위해 이벤트 루프를 계속 실행할 수밖에 없습니다. 리소스가 런타임이 경계를 넘어 접근할 수 있는 위치에 있지 않으므로 런타임은 도움을 줄 수 없습니다. 이를 샌드위치 문제라고 부르기도 합니다. 이는 단일 홉은 문제없이 기술하지만 그 이상으로는 준비 상태를 전파할 수 없는 비동기 어휘 체계를 가리킵니다.
네이티브 비동기 프리미티브는 이러한 표현력의 격차를 좁히는 데 도움이 됩니다. WIT 수준에서 async func, stream<T>, future<T>를 사용할 수 있게 하는 업데이트된 컴포넌트 ABI 메커니즘을 통해, 스케줄링과 웨이크업 전파는 개별 컴포넌트가 아닌 런타임의 역할이 됩니다.
컴포넌트는 WASI 0.2에서 필요했던 것처럼 준비 상태를 중계하기 위해 자체 이벤트 루프를 계속 실행하지 않고도 퓨처와 스트림을 전달할 수 있습니다.
비동기 함수, 스트림, 퓨처
비동기 함수(async func)
async로 선언된 WIT 함수는 해당 호출이 결과를 생성하기 전에 일시 중단될 수 있음을 런타임에 알립니다. 정규 ABI가 일시 중단과 재개를 처리합니다. 게스트는 pollable을 보지 않으며, 호스트는 폴링 루프를 보지 않습니다.
- handle: func(request: request) -> result<response, error-code>;
+ handle: async func(request: request) -> result<response, error-code>;
WIT에서 생성된 코드는 각 언어의 자연스러운 비동기 관용구를 따릅니다. Rust에서는 async fn, JavaScript에서는 Promise를 반환하는 함수, Python에서는 코루틴이 됩니다.
스트림(stream<T>)
T 값의 시퀀스를 위한, 타입이 지정된 비동기 채널입니다. 중요한 점은 stream<T>가 (WASI 0.2와 달리) 리소스가 아닌 정규 ABI 값이라는 것입니다 – 호출에서 반환되거나, 매개변수로 받아들여지거나, 기반 버퍼의 소유권을 포기하지 않고 한 컴포넌트에서 다른 컴포넌트로 넘겨질 수 있습니다.
또한 동일한 값을 하나 이상의 중간 컴포넌트를 곧바로 통과시켜 전달할 수도 있으며, 이때 해당 컴포넌트들은 웨이크업을 중계할 필요가 없습니다.
read-via-stream: func() -> tuple<stream<u8>, future<result<_, error-code>>>;
퓨처(future<T>)
나중에 사용할 수 있게 될 단일 값에 대한 타입이 지정된 핸들입니다. stream<T>와 마찬가지로 future<T>는 리소스가 아니라 값이므로, 기본 타입과 같은 방식으로 컴포넌트 경계를 넘나듭니다.
future<T>를 반환하는 동기 함수는 블로킹할 수 없다는 점에 유의하십시오. 호출자는 결과가 필요할 때 이를 await할 수 있습니다.
write-via-stream: func(data: stream<u8>) -> future<result<_, error-code>>;
WASI 0.3의 비동기 패턴 살펴보기
스트림과 종료 퓨처
읽기 작업은 데이터 채널과 완료 핸들을 튜플로 묶어 함께 반환합니다(wasi-filesystem의 read-via-stream):
read-via-stream: func() -> tuple<stream<u8>, future<result<_, error-code>>>;
두 부분은 서로 독립적입니다. 호출자는 스트림을 즉시 소비하거나, 일부만 샘플링하거나, 도중에 드롭할 수 있습니다. 어느 경우든 작업이 종료되면 퓨처가 성공 또는 실패 결과를 담아 완료됩니다. 같은 형태가 stdin, 파일 시스템 읽기, TCP 수신, 디렉터리 목록 조회에도 나타납니다.
스트림 매개변수, 퓨처 반환
쓰기 작업은 대칭적인 형태를 사용합니다. 게스트는 데이터를 stream<u8> 매개변수로 제공하고, 호스트는 스트림을 모두 소비하면 완료되는 future를 반환합니다. stdout, stderr, 파일 시스템 쓰기, TCP 송신은 모두 이 형태를 따릅니다(wasi-filesystem의 write-via-stream):
write-via-stream: func(data: stream<u8>) -> future<result<_, error-code>>;
다음 단계
이러한 기본 요소를 실제로 사용하는 엔드 투 엔드 Rust 예제는 Rust로 실행 가능한 컴포넌트 만들기를 참조하십시오. 런타임 지원 및 CLI 플래그에 대해서는 Wasmtime을 참조하십시오. WIT 구문에 대한 자세한 내용은 WIT 레퍼런스를 참조하십시오.
예제로 배우는 WIT
이 섹션에서는 WIT를 소개하기 위해 세 가지 예제를 다룹니다. 간단한 “clocks”(시계) 예제, 좀 더 정교한 “filesystems”(파일 시스템) 예제, 그리고 비동기 함수, 스트림, 퓨처를 소개하는 WASI 0.3 “CLI” 예제입니다.
전체 WIT 레퍼런스는 다음 섹션을 참조하십시오.
Clocks
다음은 wasi:clocks 패키지에 정의된 월드를 단순화한 버전입니다.
시계 기능을 제공하는 컴포넌트를 작성하려 한다고 가정해 보겠습니다. 이 컴포넌트는 재설정할 수 있는 “벽시계(wall clock)“를 나타냅니다(이 시계는 단조(monotonic) 시계가 아닙니다). (실제 wasi:clocks 패키지는 벽시계용 인터페이스와 단조 시계용 인터페이스, 두 개의 인터페이스를 제공합니다.)
월드 선언하기
하나의 인터페이스를 임포트하는 월드를 선언합니다:
package wasi-example:clocks;
/// The following is a simplified copy of a world from wasi:clocks.
/// For the full version, see https://github.com/WebAssembly/wasi-clocks/tree/main/wit
world imports {
import wall-clock;
}
설명을 위해 버전 번호는 제거했습니다.
이 파일에는 패키지 선언이 포함되어 있으며, 이 선언은 이 월드가 wasi-example 네임스페이스의 clocks 패키지에 속한다는 것을 선언합니다.
월드는 world 키워드 뒤에 imports라는 이름을 붙여 선언합니다. 월드 선언은 반드시 world로 시작해야 하지만, imports라는 이름은 임의로 선택한 것입니다. 그 뒤에는 중괄호로 둘러싸인 import 선언 목록이 오며, 각 선언은 import 키워드와 그 뒤에 오는 인터페이스 이름으로 구성됩니다. 각 선언 뒤에는 세미콜론이 붙습니다.
인터페이스 선언하기: wall-clock
package wasi-example:clocks;
/// The following is a simplified copy of an interface from wasi:clocks.
/// For the full version, see https://github.com/WebAssembly/wasi-clocks/tree/main/wit
interface wall-clock {
record datetime {
seconds: u64,
nanoseconds: u32,
}
now: func() -> datetime;
}
월드와 마찬가지로 인터페이스는 키워드(이 경우 interface)로 선언하며, 그 뒤에 이름이 오고, 이어서 중괄호로 둘러싸인 세미콜론으로 구분된 선언 목록이 옵니다. 이 경우 선언은 타입 선언 또는 함수 선언입니다.
타입 선언
레코드 타입은 WIT에서 선언할 수 있는 타입 중 하나입니다.
record datetime {
seconds: u64,
nanoseconds: u32,
}
record 키워드 뒤에는 이름이 오고, 이어서 쉼표로 구분된 필드 선언 목록이 옵니다. 각 필드 선언은 필드 이름(문자열), 콜론, 타입 이름 순으로 구성됩니다.
레코드는 이름이 있는 필드를 한데 묶는다는 점에서 C나 Rust의 struct와 유사합니다. 또한 메서드나 프로토타입이 없다는 점을 제외하면 JavaScript 객체와도 유사합니다.
요약하면, datetime 타입은 부호 없는 64비트 정수인 seconds와 부호 없는 32비트 정수인 nanoseconds라는 두 필드를 가진 레코드입니다.
함수 선언
다음은 now라는 이름의 함수를 선언합니다:
now: func() -> datetime;
빈 괄호 ()는 함수에 인수가 없음을 나타냅니다. 반환 타입은 마지막 화살표(->) 뒤에 오는 타입이며, 여기서는 datetime입니다. 종합하면, now()는 datetime을 반환하는 인수 없는(nullary) 함수입니다.
정리
imports 월드에는 벽시계용 인터페이스가 포함되어 있습니다. (실제 월드에는 보통 여러 인터페이스가 포함됩니다.) 벽시계 월드는 시간 값을 초와 나노초 단위로 나타내는 레코드 타입과 현재 시간을 가져오는 함수를 정의합니다.
예제로 배우는 WIT: 파일 시스템
앞의 예제는 준비 운동에 불과했습니다. 이제 WIT의 기본 제공 타입과 사용자 정의 타입을 더 많이 사용하는 예제를 살펴보겠습니다.
다음은 wasi-filesystem 패키지(파일 시스템 패키지)에 정의된 주 인터페이스를 매우 단순화한 버전입니다. 기능의 상당 부분이 제거되었습니다. 여기서 파일 디스크립터는 단 두 가지 작업만 지원합니다.
open-at(): 파일을 엽니다.read(): 특정 오프셋부터 시작하여 파일을 읽습니다.
package wasi-example:filesystem;
/// The following is a simplified copy of an interface from wasi:filesystems.
/// For the full version, see https://github.com/WebAssembly/wasi-filesystem/tree/main/wit
interface types {
enum error-code {
access,
bad-descriptor,
}
resource descriptor {
read: func(
length: filesize,
offset: filesize,
) -> result<tuple<list<u8>, bool>, error-code>;
open-at: func(
path: string,
) -> result<descriptor, error-code>;
}
}
이 인터페이스에서 사용되는 몇 가지 WIT 기능을 살펴보겠습니다.
열거형
enum error-code {
access,
bad-descriptor,
}
이 선언은 access와 bad-descriptor라는 두 가지 대안을 갖는 error-code라는 이름의 열거형 타입을 정의합니다. 중괄호 안의 내용은 쉼표로 구분된 이름의 목록일 뿐입니다. 열거형 타입은 C의 열거형과 비슷하며, 알려진 소수의 값 집합을 갖는 타입을 표현하는 데 유용합니다. 이 선언은 파일 시스템 작업이 반환할 수 있는 오류 코드를 표현합니다. 실제로는 발생할 수 있는 오류가 훨씬 더 많으며, 이는 열거형에 대안을 더 추가하여 표현합니다.
리소스
리소스는 객체에 대한 인터페이스를 기술합니다. 이는 WIT 인터페이스와 같은 종류의 “인터페이스“가 아닙니다. WIT 인터페이스는 서로 다른 여러 resource 선언을 포함할 수 있습니다. descriptor 리소스의 선언은 descriptor가 read와 open-at이라는 두 메서드를 구현하는 객체임을 나타냅니다. 메서드 선언을 하나씩 살펴보겠습니다.
파일에서 읽기
read: func(
length: filesize,
offset: filesize,
) -> result<tuple<list<u8>, bool>, error-code>;
메서드 선언은 앞서 clocks 예제에서 이미 본 것과 같은 일반 함수 선언과 동일한 구문을 사용합니다. 이 선언은 read() 메서드가 length와 offset이라는 두 인수를 가지며, 둘 다 filesize 타입임을 나타냅니다. read의 반환 타입은 result입니다.
result는 option과 마찬가지로 매개변수화된 타입입니다. 전체 타입을 살펴보기 전에 매개변수부터 살펴보겠습니다.
list역시 매개변수화된 타입입니다. 여기서는u8(부호 없는 8비트 정수)에 적용되었으므로list<u8>는 “바이트 목록“으로 읽을 수 있습니다.tuple은 크기가 알려진 목록과 비슷하며, 그 요소들은 서로 다른 타입을 가질 수 있습니다.tuple<list<u8>, bool>는 바이트 목록과 불리언으로 이루어진 2-튜플(쌍)을 나타냅니다.error-code는enum타입으로 정의되었습니다.
a와 b가 모두 타입이라면, result<a, b>는 a 또는 b 중 하나일 수 있는 타입을 나타냅니다. 항상 그런 것은 아니지만 대개는 이 경우처럼 b가 오류를 나타내는 타입입니다. 따라서 result<tuple<list<u8>, bool>, error-code> 타입은 “바이트 목록과 bool로 이루어진 튜플, 또는 오류 코드“를 의미합니다.
이는 read() 함수에 적합합니다. 이 함수는 읽을 바이트 수와 읽기를 시작할 파일 내 오프셋을 받으며, 결과는 오류이거나, 파일에서 읽은 데이터를 담은 바이트 목록과 파일 끝에 도달했는지를 나타내는 불리언의 쌍이기 때문입니다.
파일 열기
open-at() 메서드는 생성자입니다. 실패하지 않으면 descriptor를 반환하므로 이를 알 수 있습니다(이 메서드들은 리소스 타입 descriptor에 연결되어 있다는 점을 기억하십시오):
open-at: func(
path: string,
) -> result<descriptor, error-code>;
open-at()은 경로 문자열과 플래그를 받아 새 descriptor를 반환합니다.
WASI 0.3 CLI
위의 두 예제는 WASI 0.2부터 언어에 포함되어 온 WIT 기능을 사용합니다. WASI 0.3은 컴포넌트 모델의 정규 ABI에 async func, stream<T>, future<T>라는 세 가지 새로운 프리미티브를 추가했습니다. 이 예제에서는 세 가지를 모두 사용하는 wasi:cli 패키지의 단순화된 버전을 살펴봅니다.
비동기 함수
async로 선언된 함수는 결과를 반환하기 전에 일시 중단될 수 있습니다. 스케줄링은 런타임이 담당합니다. 게스트에게는 일반적인 호출로 보이며, 호스트에서는 바쁜 대기 루프가 보이지 않습니다:
package wasi-example:cli;
interface run {
run: async func() -> result;
}
매개변수가 없는 result 반환 타입은 “성공 또는 실패 중 하나이며, 어느 쪽에도 값이 붙지 않음“을 의미합니다. 바인딩 생성기는 각 쪽을 호스트 언어의 자연스러운 비동기 관용구로 생성합니다. 예를 들어 Rust에서는 async fn으로, JavaScript에서는 Promise를 반환하는 함수로 생성합니다.
스트림과 종료 퓨처
표준 입력에서 읽을 때는 stream<T>와 future를 짝지어 사용합니다:
interface stdin {
use types.{error-code};
read-via-stream: func() -> tuple<stream<u8>, future<result<_, error-code>>>;
}
stream<u8>은 바이트를 점진적으로 전달합니다. future는 작업이 종료되면 완료되며, 성공(밑줄은 “붙은 값 없음“을 의미합니다) 또는 error-code(types 인터페이스에서 enum으로 정의됨) 중 하나를 전달합니다. 두 부분은 서로 독립적입니다. 호출자는 스트림을 즉시 소비하거나, 일부만 샘플링하거나, 도중에 폐기할 수 있으며, 어느 경우든 퓨처가 결과를 알려 줍니다.
리소스와 달리 stream<T>와 future<T>는 값입니다. 이들은 원시 타입과 같은 방식으로 함수에서 반환될 수 있고, 매개변수로 전달받을 수 있으며, 컴포넌트 경계를 넘어 전달될 수 있습니다.
반전된 쓰기
표준 출력에 쓸 때는 방향이 반대가 됩니다. 호스트가 게스트에게 쓸 대상 리소스를 넘겨주는 대신, 게스트가 자신의 데이터를 stream<u8> 매개변수로 제공하고 호스트는 바이트가 소비되면 완료되는 future를 반환합니다:
interface stdout {
use types.{error-code};
write-via-stream: func(data: stream<u8>) -> future<result<_, error-code>>;
}
스트림 매개변수와 퓨처 반환으로 이루어진 이 형태는 WASI 0.3에서 게스트가 데이터를 쓰는 모든 곳에 나타납니다. stdout, stderr, 파일 시스템 쓰기, TCP 전송이 모두 이 형태를 따릅니다.
월드로 집계하기
월드는 임포트와 익스포트를 하나로 묶습니다. 아래의 command 월드는 I/O 인터페이스를 임포트하고 run을 익스포트합니다:
world command {
import stdin;
import stdout;
export run;
}
이 월드를 구현하는 컴포넌트는 run의 구현을 제공하며, 그 구현 내부에서 stdin에 대해 read-via-stream을, stdout에 대해 write-via-stream을 호출할 수 있습니다.
이 세 가지 프리미티브를 추가하게 된 계기인 합성 관련 배경을 포함하여 더 자세히 알아보려면 비동기, 스트림, 퓨처를 참조하십시오.
더 읽을거리
지금까지 WIT가 풍부한 타입을 사용하여 함수들이 서로 어떻게 관련되는지에 대한 다양한 개념을 표현할 수 있음을 살펴보았습니다. 이러한 개념은 코어 WebAssembly의 타입 시스템에서는 사용할 수 없습니다.
더 많은 WIT 예제는 튜토리얼 섹션을 참조하십시오. 다음 섹션인 WIT 레퍼런스에서는 WIT 구문을 더 자세히 다룹니다.
WIT 개요
WIT(Wasm Interface Type) 언어는 컴포넌트 모델의 인터페이스와 월드를 정의하는 데 사용됩니다. WIT는 범용 프로그래밍 언어가 아니며 동작을 정의하지 않습니다. WIT는 컴포넌트 간의 계약만을 정의합니다.
새 컴포넌트를 정의하려면 Wasm Interface Type(WIT) 언어로 코드를 작성하여 월드와 인터페이스를 정의해야 합니다. WIT는 사용하려는 기존 컴포넌트에 대한 문서 역할도 합니다.
이 주제에서는 WIT 언어의 핵심 요소에 대한 개요를 제공합니다. 공식 WIT 명세와 그 변경 이력은 WebAssembly/component-model 저장소에서 확인할 수 있습니다.
WIT 파일의 구조
WIT 파일에는 하나 이상의 인터페이스 또는 월드가 포함됩니다. 인터페이스나 월드는 타입, 함수 또는 둘 모두를 정의할 수 있습니다.
타입과 함수는 인터페이스나 월드 외부에서 정의할 수 없습니다.
파일은 선택적으로 패키지 선언으로 시작할 수 있습니다.
주석
WIT 주석 구문은 C++ 계열 언어에서 사용하는 구문과 유사합니다.
//부터 줄 끝까지의 모든 내용은 주석입니다./*…*/로 둘러싸인 모든 텍스트는 주석입니다.- C++ 계열과 달리 블록 주석은 중첩될 수 있습니다. 예:
/* blah /* rabbit */ rhubarb */.
- C++ 계열과 달리 블록 주석은 중첩될 수 있습니다. 예:
문서화
WIT는 문서화를 위한 특수 주석 형식을 정의합니다.
///부터 줄 끝까지의 모든 내용은 뒤따르는 항목에 대한 문서입니다./**…*/로 둘러싸인 모든 텍스트는 뒤따르는 항목에 대한 문서입니다.
예를 들면 다음과 같습니다.
/// Prints "hello".
print-hello: func();
/**
Prints "hello".
*/
print-hello: func();
식별자
식별자는 변수, 함수, 타입, 인터페이스, 월드의 이름입니다. WIT 식별자는 C, Rust, Java 같은 언어에서 익숙하게 접했을 규칙과는 약간 다른 규칙을 따릅니다. 이 규칙은 패키지를 제외한 모든 이름에 적용됩니다. 패키지 식별자는 조금 더 복잡하며 패키지 섹션에서 다룹니다.
- 식별자는 ASCII
kebab-case로 제한됩니다. 즉, 단일 하이픈으로 구분된 단어의 나열이어야 합니다.- 이중 하이픈(
--)은 허용되지 않습니다. - 하이픈은 나열의 시작이나 끝에는 올 수 없으며, 단어 사이에만 올 수 있습니다.
- 이중 하이픈(
- 식별자 앞에는
%기호 하나를 붙일 수 있습니다.- 식별자가 그대로 두면 WIT 키워드가 되는 경우에는 이것이 필수입니다. 예를 들어
interface는 올바른 식별자가 아니지만,%interface는 올바른 식별자입니다.
- 식별자가 그대로 두면 WIT 키워드가 되는 경우에는 이것이 필수입니다. 예를 들어
- 각 단어에는 ASCII 문자와 숫자만 포함할 수 있습니다.
- 첫 번째 단어는 숫자로 시작할 수 없습니다. 나머지 단어는 숫자 또는 ASCII 문자로 시작할 수 있습니다.
- 단어에는 ASCII가 아닌 유니코드 문자를 포함할 수 없습니다.
- 단어에는 문장 부호, 밑줄 등을 포함할 수 없습니다.
- 각 단어는 모두
lowercase이거나 모두UPPERCASE여야 합니다.- 식별자 안의 각 단어는 대소문자가 서로 달라도 됩니다. 예를 들어
WIT-demo는 허용됩니다.
- 식별자 안의 각 단어는 대소문자가 서로 달라도 됩니다. 예를 들어
- 식별자는
interface와 같은 WIT 키워드일 수 없습니다(단, 앞에%기호가 붙은 경우는 예외입니다).
내장 타입
이 절의 타입은 WIT 언어 자체에서 정의합니다.
원시 타입
WIT는 다음과 같은 원시 타입을 정의합니다:
| 식별자 | 설명 |
|---|---|
bool | true 또는 false인 불리언 값입니다. |
s8, s16, s32, s64 | 해당 너비의 부호 있는 정수입니다. 예를 들어 s32는 부호 있는 32비트 정수입니다. |
u8, u16, u32, u64 | 해당 너비의 부호 없는 정수입니다. 예를 들어 u32는 부호 없는 32비트 정수입니다. |
f32, f64 | 해당 너비의 부동소수점 수입니다. 예를 들어 f64는 64비트(배정밀도) 부동소수점 수입니다. 아래의 NaN에 관한 참고 사항을 확인하십시오. |
char | 유니코드 문자입니다. (정확히는 유니코드 스칼라 값입니다.) |
string | 유니코드 문자열, 즉 문자의 유한한 시퀀스입니다. |
f32및f64타입은 일반적인 IEEE 754 단정밀도 및 배정밀도 값 집합을 지원하지만, 논리적으로는 단 하나의nan값만 가진다는 점이 다릅니다. 값이 WIT 인터페이스를 통과하며 단일한 WITnan값으로 전달될 때 IEEE 754NaN의 정확한 비트 수준 표현이 보존된다는 보장은 없습니다.
리스트
임의의 타입 T에 대해 list<T>는 T 타입 값의 순서 있는 시퀀스를 나타냅니다. T는 내장 타입이든 사용자 정의 타입이든 어떤 타입이라도 될 수 있습니다:
list<u8> // byte buffer
list<customer> // a list of customers
이는 Rust의 Vec이나 Java의 List와 유사합니다.
옵션
임의의 타입 T에 대한 option<T>는 T 타입의 값을 포함하거나 아무 값도 포함하지 않을 수 있습니다. T는 내장 타입이든 사용자 정의 타입이든 어떤 타입이든 될 수 있습니다. 예를 들어, 조회 함수는 조회 키를 찾지 못했을 가능성을 허용하기 위해 option을 반환할 수 있습니다:
option<customer>
이는 Rust의 Option, C++의 std::optional, Haskell의 Maybe와 유사합니다.
이는 variant 타입의 특수한 경우입니다. WIT는 이를 표현하는 공통된 방법을 제공하여 모든 값 타입마다 variant 타입을 만들 필요가 없도록 하고, option 타입이 있는 언어에 관용적으로 매핑될 수 있도록 이 타입을 정의합니다.
Result
임의의 타입 T와 E에 대한 result<T, E>는 T 타입의 값 또는 E 타입의 값을 포함할 수 있습니다(둘 다 포함할 수는 없습니다). 예를 들어, HTTP 요청 함수는 result를 반환할 수 있으며, 이때 성공 케이스(T 타입)는 HTTP 응답을 나타내고 오류 케이스(E 타입)는 발생할 수 있는 다양한 종류의 오류를 나타냅니다:
result<http-response, http-error>
이는 Rust의 Result 또는 Haskell의 Either와 유사합니다.
이는 variant 타입의 특수한 경우입니다. WIT는 이러한 동작을 표현하는 공통된 방법을 제공하여 개발자가 값 타입과 오류 타입의 모든 조합마다 variant 타입을 만들 필요가 없도록 하고, result 또는 “either” 타입이 있는 언어에 관용적으로 매핑될 수 있도록
result타입을 정의합니다.
때로는 한쪽 또는 양쪽 케이스에 연관된 데이터가 없는 경우가 있습니다. 예를 들어, print 함수는 실패하면 오류 코드를 반환할 수 있지만, 성공하면 반환할 것이 없습니다. 이 경우 다음과 같이 해당 타입을 생략할 수 있습니다:
result<u32> // no data associated with the error case
result<_, u32> // no data associated with the success case
result // no data associated with either case
밑줄 _는 “데이터 없음“을 나타내며, 일반적으로 대상 언어에서 unit 타입으로 표현됩니다(예: Rust의 (), JavaScript의 null).
튜플
tuple 타입은 지정된 타입의 값들로 이루어진 순서가 있는 고정 길이 시퀀스입니다. 이는 record와 유사하지만, 필드가 이름 대신 인덱스로 식별된다는 점이 다릅니다.
tuple<u64, string> // An integer and a string
tuple<u64, string, u64> // An integer, then a string, then an integer
이는 Rust나 OCaml의 튜플과 유사합니다.
스트림
임의의 타입 T에 대한 stream<T>는 T 타입 값들의 비동기적이고 순서가 있는 시퀀스를 나타냅니다. 모든 값을 한 번에 포함하는 list<T>와 달리, stream<T>는 값을 점진적으로 전달합니다. 생산자는 요소가 사용 가능해지는 대로 밀어 넣고, 소비자는 요소가 도착하는 대로 받습니다.
stream<u8> // a stream of bytes
stream<log-entry> // a stream of records
stream<T>는 리소스가 아니라 정규 ABI 값입니다. 따라서 원시 타입과 같은 방식으로 함수에서 반환되거나, 매개변수로 전달되거나, 컴포넌트 경계를 넘어 전달될 수 있습니다. 중간 컴포넌트는 스트림을 소비하지 않고 그대로 넘겨줄 수도 있습니다.
스트림은 WASI 0.3에서 추가되었으며, 스트림이 종료되었음을 알리는 future와 함께 쓰이는 경우가 가장 많습니다. 스트림과 퓨처로 이루어진 튜플 형태는 wasi:filesystem, wasi:cli, wasi:sockets 전반에 걸쳐 나타납니다.
근본적인 동기는 비동기, 스트림, 퓨처를 참조하십시오.
퓨처
임의의 타입 T에 대한 future<T>는 나중에 사용 가능해질 T 타입의 단일 값에 대한 타입이 지정된 핸들입니다. future<T>를 반환하는 함수는 값이 생성될 때까지 블록되지 않으며, 호출자는 값이 필요할 때 퓨처를 await합니다.
future<u32> // a future that will resolve to a u32
future<result<response, error-code>> // a future that will resolve to either a response or an error
stream<T>와 마찬가지로 future<T>는 리소스가 아니라 정규 ABI 값이므로, 기본 타입과 같은 방식으로 컴포넌트 경계를 넘어 이동합니다.
퓨처는 WASI 0.3에서 추가되었습니다. 이는 JavaScript의 Promise 또는 Rust의 std::future::Future와 유사합니다.
근본적인 동기는 비동기, 스트림, 퓨처를 참조하십시오.
사용자 정의 타입
새로운 도메인별 타입은 interface 또는 world 안에서 정의할 수 있습니다.
레코드
record 타입은 쉼표로 구분된 이름 있는 필드들의 집합을 선언하며, 각 필드는 name: type 형식입니다. 레코드 인스턴스는 모든 필드에 대한 값을 포함합니다. 필드 타입은 기본 제공 타입이거나 사용자 정의 타입일 수 있습니다. 구문은 다음과 같습니다.
record customer {
id: u64,
name: string,
picture: option<list<u8>>,
account-manager: employee,
}
레코드는 C 또는 Rust의 struct와 유사합니다.
사용자 정의 레코드는 제네릭일 수 없습니다(즉, 타입으로 매개변수화될 수 없습니다). 내장 타입만 제네릭일 수 있습니다.
variant
variant 타입은 구조가 달라지는 데이터를 나타냅니다. 선언은 케이스 목록을 정의하며, 각 케이스는 이름을 가지고, 선택적으로 해당 케이스와 연관된 데이터 타입을 가집니다. variant 타입의 인스턴스는 정확히 하나의 케이스에 해당합니다. 케이스는 쉼표로 구분됩니다. 구문은 다음과 같습니다.
variant allowed-destinations {
none,
any,
restricted(list<address>),
}
이는 “허용된 목적지는 없음(none), 모두(any), 또는 특정 주소 목록으로 제한됨 중 하나“라고 읽을 수 있습니다.
variant는 Rust의 enum이나 OCaml의 판별 공용체(discriminated union)와 비슷합니다. C에서 가장 가까운 개념은 태그된 공용체(tagged union)입니다. 다만 WIT의 variant는 “태그”(케이스)를 처리할 뿐 아니라 각 태그에 맞는 올바른 데이터 형태도 강제합니다.
사용자 정의 variant는 제네릭일 수 없습니다(즉, 타입으로 매개변수화할 수 없습니다). 내장 타입만 제네릭일 수 있습니다.
열거형
enum 타입은 어떤 케이스에도 연관 데이터가 없는 variant 타입입니다.
enum color {
hot-pink,
lime-green,
navy-blue,
}
이는 판별 공용체가 없는 언어에서 더 단순한 표현을 제공할 수 있습니다. 예를 들어, WIT enum은 C/C++ enum으로 직접 변환될 수 있습니다.
리소스
리소스는 컴포넌트 외부에 존재하는 어떤 엔티티에 대한 핸들입니다. 리소스는 복사할 수 없거나 복사해서는 안 되는 엔티티, 즉 값이 아닌 참조로 전달되어야 하는 엔티티를 나타냅니다. 컴포넌트는 핸들을 통해 서로 리소스를 전달할 수 있습니다. 컴포넌트는 리소스의 소유권을 전달하거나, 리소스에 대한 소유권 없는 참조를 전달할 수 있습니다.
참조의 빌림(borrowing)과 소유권(ownership) 개념에 익숙하지 않다면 Rust 문서를 참조하십시오.
단순한 일반 데이터인 다른 WIT 타입과 달리, 리소스는 메서드를 통해서만 동작을 노출합니다. 리소스는 인터페이스를 구현하는 객체로 생각할 수 있습니다. (여기서 “인터페이스“는 WIT 인터페이스가 아니라 객체 지향 프로그래밍에서의 의미로 사용됩니다.)
예를 들어, blob(바이너리 대형 객체)을 리소스로 모델링할 수 있습니다. 다음 WIT는 생성자 하나, 메서드 두 개, 정적 함수 하나를 포함하는 blob 리소스 타입을 정의합니다:
resource blob {
constructor(init: list<u8>);
write: func(bytes: list<u8>);
read: func(n: u32) -> list<u8>;
merge: static func(lhs: blob, rhs: blob) -> blob;
}
blob 예제에서 보듯이, 리소스는 다음을 포함할 수 있습니다:
- 메서드: 핸들인
self(this라고도 함) 매개변수를 암묵적으로 받는 함수입니다. (일부 프로그래밍 언어는self대신this키워드를 사용합니다.)read와write는 메서드입니다. - 정적 함수: 암묵적인
self매개변수가 없지만 C++나 Java의 정적 함수와 유사하게 리소스 타입의 스코프 안에 중첩되도록 의도된 함수입니다.merge는 정적 함수입니다. - 최대 하나의 생성자: 자신을 포함하는 리소스 타입의 핸들을 반환하는 함수에 대한 문법적 설탕(syntactic sugar)인 함수입니다. 생성자는
constructor로 선언합니다.
메서드는 빌린(borrowed) self 매개변수를 받는 함수로 다시 작성할 수 있고, 생성자는 호출자가 소유하는 값을 반환하는 함수로 다시 작성할 수 있습니다. 예를 들어, 위의 blob 리소스는 다음과 같이 근사할 수 있습니다:
resource blob;
blob-constructor: func(bytes: list<u8>) -> blob;
blob-write: func(self: borrow<blob>, bytes: list<u8>);
blob-read: func(self: borrow<blob>, n: u32) -> list<u8>;
blob-merge: static func(lhs: blob, rhs: blob) -> blob;
resource 타입 이름을 borrow<...>로 감싸면 “빌린(borrowed)” 리소스를 나타냅니다. 빌린 리소스는 호출이 진행되는 동안 호출자가 피호출자에게 리소스를 일시적으로 빌려주는 것을 나타냅니다. 반면, 소유된(owned) 리소스의 소유자가 해당 리소스를 드롭(drop)하면 리소스는 파기됩니다. (리소스를 드롭한다는 것은 기반 프로그래밍 언어가 지원하는 경우 명시적으로 드롭하거나, 소유권을 다른 함수로 이전하지 않고 반환하는 것을 의미합니다.)
더 정확히 말하면, 이들은 리소스의 빌린
handles또는 소유된handles입니다.handles에 대한 자세한 내용은 업스트림 컴포넌트 모델 명세를 참조하십시오.
플래그
flags 타입은 이름이 붙은 불리언의 집합입니다.
flags allowed-methods {
get,
post,
put,
delete,
}
flags타입은 논리적으로 각 필드가bool타입인 레코드 타입과 동등하지만, 바이너리 수준에서는 더 효율적으로(비트필드로) 표현됩니다.
타입 별칭
type ... = ...을 사용하여 새로운 타입 별칭을 정의할 수 있습니다. 타입 별칭은 타입에 더 짧거나 더 의미 있는 이름을 부여할 때 유용합니다:
type buffer = list<u8>;
type http-result = result<http-response, http-error>;
함수
함수는 이름과 함수 타입으로 정의됩니다. 레코드 필드와 마찬가지로, 이름과 타입은 콜론으로 구분됩니다:
do-nothing: func();
함수 타입은 func 키워드 뒤에 괄호로 묶이고 쉼표로 구분된 매개변수(이름과 타입) 목록이 오는 형태입니다. 함수가 값을 반환하는 경우, 화살표 기호(->) 뒤에 반환 타입을 붙여 표현합니다:
// This function does not return a value
print: func(message: string);
// These functions return values
add: func(a: u64, b: u64) -> u64;
lookup: func(store: kv-store, key: string) -> option<string>;
여러 값을 반환하는 함수를 표현하려면 임의의 복합 타입(튜플이나 레코드 등)을 사용할 수 있습니다.
get-customers-paged: func(cont: continuation-token) -> tuple<list<customer>, continuation-token>;
함수는 인터페이스 안에 선언하거나, 월드에서 임포트 또는 익스포트로 선언할 수 있습니다.
비동기 함수
함수를 async로 선언할 수 있으며, 이는 호출이 결과를 생성하기 전에 일시 중단될 수 있음을 나타냅니다:
// An async function returning a result
handle: async func(request: request) -> result<response, error-code>;
스케줄링은 런타임이 담당하며, WIT 관점에서 이 호출은 일반 함수처럼 보입니다. 바인딩 생성기는 각 측을 호스트 언어의 자연스러운 비동기 관용구로 생성합니다. Rust에서는 async fn, JavaScript에서는 Promise를 반환하는 함수 등입니다.
비동기 함수는 stream<T> 및 future<T>와 함께 WASI 0.3에 추가되었습니다. 근본적인 동기에 대해서는 비동기, 스트림, 퓨처를 참조하십시오.
인터페이스
인터페이스는 이름이 붙은 타입과 함수의 집합으로, 중괄호로 묶이며 interface 키워드로 시작합니다:
interface canvas {
type canvas-id = u64;
record point {
x: u32,
y: u32,
}
draw-line: func(canvas: canvas-id, from: point, to: point);
}
인터페이스 안의 타입과 함수는 쉼표로 구분되지 않는다는 점에 유의하십시오.
다른 곳의 정의 사용하기
인터페이스는 use 지시문을 통해 다른 인터페이스에 선언된 타입을 재사용할 수 있습니다. use 지시문에는 타입이 선언된 인터페이스, 점, 그리고 재사용할 타입들을 중괄호로 묶은 목록을 차례로 지정해야 합니다. 그러면 해당 인터페이스는 use에 명시된 타입을 참조할 수 있습니다.
interface types {
type dimension = u32;
record point {
x: dimension,
y: dimension,
}
}
interface canvas {
use types.{dimension, point};
type canvas-id = u64;
draw-line: func(canvas: canvas-id, from: point, to: point, thickness: dimension);
}
canvas 인터페이스는 types 인터페이스에 선언된 dimension 및 point 타입을 사용합니다.
타입을 하나만 사용하더라도 반드시 중괄호로 묶어야 합니다. 예를 들어,
use types.{dimension}은 올바르지만use types.dimension은 올바르지 않습니다.
파일들이 같은 패키지(사실상 같은 디렉터리)에 있는 한 이는 여러 파일에 걸쳐서도 동작합니다. 다른 패키지의 정의를 사용하는 방법에 대해서는 명세를 참조하십시오.
월드
대략적으로 말해, 월드는 컴포넌트의 계약을 기술합니다. 월드는 임포트와 익스포트의 집합을 기술하며, 중괄호로 묶이고 world 키워드로 시작합니다. 임포트와 익스포트는 인터페이스일 수도 있고 특정 함수일 수도 있습니다. 익스포트는 컴포넌트가 제공하는 인터페이스나 함수를 기술합니다. 임포트는 컴포넌트가 의존하는 인터페이스나 함수를 기술합니다.
interface printer {
print: func(text: string);
}
interface error-reporter {
report-error: func(error-message: string);
}
world multi-function-device {
// The component implements the `printer` interface
export printer;
// The component implements the `scan` function
export scan: func() -> list<u8>;
// The component needs to be supplied with an `error-reporter`
import error-reporter;
}
이 코드는 multi-function device라는 월드를 정의하며, 이 월드에는 printer 인터페이스와 scan 함수라는 두 개의 익스포트가 있습니다. 익스포트된 printer 인터페이스는 같은 파일에 정의되어 있습니다. 임포트된 error-reporter 인터페이스 또한 같은 파일에 정의되어 있습니다. error-reporter 인터페이스를 보면, 월드가 인터페이스를 임포트할 때 인터페이스의 이름만이 아니라 타입과 함수 선언을 포함한 전체 인터페이스를 제공해야 한다는 것을 알 수 있습니다.
다른 패키지의 인터페이스
다른 패키지에 정의된 인터페이스를 임포트하고 익스포트하려면 package/name 구문을 사용할 수 있습니다:
world http-proxy {
export wasi:http/incoming-handler;
import wasi:http/outgoing-handler;
}
이 예제에서 보듯이, 임포트와 익스포트는 패키지 수준이 아니라 인터페이스 수준에서 적용됩니다. 한 패키지에 정의된 인터페이스 하나를 임포트하면서, 같은 패키지에 정의된 다른 인터페이스를 익스포트할 수 있습니다. 패키지는 정의들을 한데 묶을 뿐이며, 일관된 동작의 집합을 기술하지는 않습니다.
WIT는 패키지를 해석하는 방법을 정의하지 않으므로, 도구마다 서로 다른 방식으로 패키지를 해석할 수 있습니다.
인라인 인터페이스
인터페이스는 월드 안에 인라인으로 선언할 수 있습니다:
world toy {
export example: interface {
do-nothing: func();
}
}
다른 월드 포함하기
다른 월드를 include할 수 있습니다. 이렇게 하면 여러분의 월드는 해당 월드의 모든 익스포트를 익스포트하고, 해당 월드의 모든 임포트를 임포트하게 됩니다.
world glow-in-the-dark-multi-function-device {
// The component provides all the same exports, and depends on
// all the same imports, as a `multi-function-device`...
include multi-function-device;
// ...but also exports a function to make it glow in the dark
export glow: func(brightness: u8);
}
use 지시문과 마찬가지로, 다른 패키지의 월드를 include할 수 있습니다.
패키지
패키지는 인터페이스와 월드의 집합이며, 같은 디렉터리에 있는 여러 파일에 걸쳐 정의될 수 있습니다. 각 WIT 파일은 하나의 패키지와 연결되며, 이 패키지는 해당 파일에서 직접 선언되거나 같은 디렉터리에 있는 다른 파일에서 선언됩니다.
패키지를 선언하려면 package 지시문을 사용하여 패키지 ID를 지정하십시오. 패키지 ID는 콜론으로 구분된 네임스페이스와 이름을 반드시 포함해야 하며, 선택적으로 semver를 준수하는 버전 번호를 포함할 수 있습니다:
package documentation:example;
package documentation:example@1.0.1;
모든 파일은 .wit 확장자를 가져야 하며 같은 디렉터리에 있어야 합니다. 패키지가 여러 파일에 걸쳐 있는 경우 하나의 파일에만 패키지 선언이 있으면 되지만, 여러 파일에 패키지 선언이 있다면 패키지 ID가 모두 서로 일치해야 합니다. 예를 들어, 다음 documentation:http 패키지는 네 개의 파일에 걸쳐 있습니다:
// types.wit
interface types {
record request { /* ... */ }
record response { /* ... */ }
}
// incoming.wit
interface incoming-handler {
use types.{request, response};
// ...
}
// outgoing.wit
interface outgoing-handler {
use types.{request, response};
// ...
}
// http.wit
package documentation:http@1.0.0;
world proxy {
export incoming-handler;
import outgoing-handler;
}
이 패키지는 types.wit에서 요청 및 응답 타입을 정의하고, incoming.wit에서 수신 핸들러 인터페이스를 정의하고, outgoing.wit에서 발신 핸들러 인터페이스를 정의하며, http.wit에서 패키지를 선언하고 이러한 인터페이스를 사용하는 월드를 정의합니다.
WIT 언어에 대한 보다 공식적인 정의는 WIT 명세를 참고하십시오.
WASI 0.2에서 WASI 0.3으로 마이그레이션하기
WASI 0.3은 네이티브 비동기 프리미티브인 async func, stream<T>, future<T>를 중심으로 WASI의 인터페이스를 재구성합니다. wasi:cli, wasi:http, wasi:filesystem, wasi:sockets의 변경 사항 대부분은 이러한 프리미티브로 전환한 데 따른 결과입니다.
이 페이지에서는 WASI 0.2와 WASI 0.3의 개념 간 대응 관계를 다룹니다. 모든 WASI 0.3 인터페이스를 WIT 수준에서 비교한 내용은 WASI.dev의 WASI 0.3을 참조하십시오.
마이그레이션해야 합니까?
당장은 아닙니다 – WASI 0.2는 이전과 마찬가지로 호스트에서 사용할 수 있으며, WASI 0.3은 순전히 기능을 추가하는 변경입니다.
이와 별개로, WASI 0.3 런타임은 호스트 경계에서 0.2 임포트를 네이티브 0.3 프리미티브에 매핑하여 0.2를 폴리필할 수 있으며, Wasmtime의 wasmtime serve는 이미 동일한 바이너리에서 0.3 컴포넌트와 0.2 컴포넌트를 모두 실행하면서 컴포넌트별로 디스패치합니다. 다음이 필요한 경우에는 마이그레이션하는 것이 적절합니다.
- 컴포넌트 경계를 넘어 합성할 수 있는 비동기(샌드위치 문제가 사라집니다).
- 더 새로운 인터페이스 형태 — 특히
wasi:http에서 아홉 개의 리소스가 두 개로 축소된 점입니다. - 계속 출시되고 있는 0.3 대상 툴체인의 일급 지원.
개념 대응
WASI 0.3은 모든 wasi:io 리소스를 정규 ABI 프리미티브로 대체합니다. 대응 관계는 대부분 일대일입니다.
WASI 0.2 (wasi:io) | WASI 0.3 (컴포넌트 모델) |
|---|---|
resource pollable | future<T> |
resource input-stream | stream<u8> |
resource output-stream | stream<u8> (호출 안으로 전달됨) |
poll(list<pollable>) | 퓨처에 대한 await |
리소스에 대한 subscribe() | 호출에서 future 반환 |
start-foo / finish-foo | 단일 func 또는 async func |
WIT에서 변경된 사항
읽기를 위한 스트림과 퓨처의 조합
WASI 0.2의 읽기 호출은 단일 input-stream 리소스를 반환했으며, 종료 오류는 이를 소비하는 과정에서만 드러났습니다. WASI 0.3은 이러한 관심사를 분리합니다. 호출은 데이터를 위한 stream<u8>와 결과를 위한 future<result<_, error-code>>를 튜플로 묶어 반환합니다.
// WASI 0.2 (filesystem read)
read-via-stream: func(offset: filesize) -> result<input-stream, error-code>;
// WASI 0.3 (filesystem read)
read-via-stream: func(offset: filesize) -> tuple<stream<u8>, future<result<_, error-code>>>;
WASI 0.3에서는 호출자가 읽기가 정상적으로 완료되었는지 알기 위해 스트림을 끝까지 소비할 필요가 없습니다. 퓨처는 어느 경우든 확정됩니다.
쓰기 방향의 전환
WASI 0.2의 쓰기 경로는 게스트에게 호스트가 소유한 리소스(output-stream)를 넘겨주고 게스트가 그 안으로 바이트를 밀어 넣도록 했습니다. WASI 0.3은 이를 뒤집습니다. 게스트가 데이터를 stream<u8> 값으로 제공하고, 호스트는 스트림 소비를 마치면 확정되는 future를 반환합니다.
// WASI 0.2: receive an output-stream resource, write into it
get-stdout: func() -> output-stream;
// WASI 0.3: pass a stream value in, receive a completion future
write-via-stream: func(data: stream<u8>) -> future<result<_, error-code>>;
2단계 호출의 통합
WASI 0.2는 일시 중단될 수 있는 연산을 start-foo / finish-foo 쌍으로 모델링했으며, 그 사이에 준비 상태를 확인하기 위한 pollable을 두었습니다. WASI 0.3은 각 쌍을 단일 호출로 통합합니다:
// WASI 0.2
start-connect: func(network: borrow<network>, remote-address: ip-socket-address) -> result<_, error-code>;
finish-connect: func() -> result<tuple<input-stream, output-stream>, error-code>;
// WASI 0.3
connect: async func(remote-address: ip-socket-address) -> result<_, error-code>;
연산이 호스트에서 일시 중단되어야 하는 경우(예: connect) 통합된 호출은 async func입니다. 기존에 논블로킹 디스패치만을 위해 2단계 형태를 사용했던 연산은 대신 일반 func로 통합될 수 있습니다(bind, listen).
인터페이스 주요 변경 사항
인터페이스별 전체 변경 사항은 WASI.dev의 WASI 0.3에서 확인할 수 있습니다. 마이그레이션 작업을 유발할 가능성이 가장 큰 세 가지 변경 사항은 다음과 같습니다:
wasi:io가 제거되었습니다. 이 패키지에는 0.3.0 릴리스가 없습니다. 이 패키지가 노출하던 모든 리소스(pollable,input-stream,output-stream)는 위의 개념 매핑에 따라 컴포넌트 모델 기본 요소로 대체됩니다.wasi:http의 리소스가 9개에서 2개로 줄어듭니다. incoming/outgoing × request/response/body 조합과future-trailers,future-incoming-response,response-outparam은 모두request와response가 되며, 본문은stream<u8>로, 트레일러는future로 표현됩니다. 이제 핸들러는async func입니다:
// WASI 0.2
handle: func(request: incoming-request, response-out: response-outparam);
// WASI 0.3
handle: async func(request: request) -> result<response, error-code>;
proxy 월드는 service로 대체되며, 새로운 middleware 월드는 핸들러를 임포트하는 동시에 익스포트합니다.
wasi:sockets에서network리소스가 제거됩니다. 네트워크 접근 권한은 모든bind,connect, DNS 조회에 일일이 전달되는 대신 월드 수준에서 부여됩니다. WASI 0.2의 일곱 가지 소켓 인터페이스는 하나의types인터페이스와ip-name-lookup으로 통합되며, TCPlisten은 별도의accept루프를 요구하는 대신stream<tcp-socket>을 직접 반환합니다.
인터페이스별 소규모 변경 사항, 즉 async func로 바뀌는 파일 시스템 메서드, wasi:clocks의 이름 변경(wall-clock → system-clock, datetime → instant), wasi:random의 max-len 이름 변경, 새로운 공유 wasi:cli/types 인터페이스는 위에 링크된 WASI.dev 페이지에 문서화되어 있습니다.
도구 요구 사항
| 도구 | 최소 버전 | 비고 |
|---|---|---|
| Wasmtime | 46+ | WASI 0.3과 component-model-async가 기본적으로 활성화되어 있습니다. |
wit-bindgen | 0.46+ | 0.3 바인딩 생성에는 async 기능을 사용하십시오. |
| jco | 최신 | 0.3 호스트 바인딩은 preview3-shim 패키지로 제공됩니다. |
wkg | 0.15+ | wasi:cli@0.3.0 및 관련 패키지를 가져오는 데 필요합니다. |
| Rust | nightly | 현재 stable 버전에 번들된 wasm-component-ld는 wit-bindgen 0.58의 0.3 출력을 처리하기에는 너무 오래되었습니다. |
더 읽을거리
- 비동기, 스트림, 퓨처 — 개념적 기반
- Rust로 실행 가능한 컴포넌트 만들기 — 0.3
async fn run()패턴을 사용한 Rust 실전 예제 - WASI.dev의 WASI 0.3 — 인터페이스별 WIT 수준 전체 변경 사항
컴포넌트 만들기
많은 인기 프로그래밍 언어를 WebAssembly로 컴파일할 수 있지만, 지원 수준은 언어마다 다릅니다. 이 문서에서는 WASI를 사용하는 WebAssembly를 대상 플랫폼으로 지원하는 컴파일러와 런타임을 갖춘 언어를 자세히 설명합니다.
이 문서는 계속 갱신되는 문서이므로, 툴체인의 발전 사항을 알고 계시다면 주저하지 말고 문서에 기여하십시오. 특정 언어 지원 개발에 대한 자세한 내용은 게스트 언어 Special Interest Group 제안서 문서에서 확인하십시오.
컴포넌트의 이점 중 하나는 호스트 런타임 간 이식성입니다. 런타임은 컴포넌트를 임포트하거나 실행하기 위해 해당 컴포넌트가 어떤 월드를 대상으로 하는지만 알면 됩니다. 이 언어 가이드는 examples/tutorial/wit/adder/world.wit에 정의된 공통 adder 월드를 사용하여 이를 보여 주고자 합니다. 또한 컴포넌트를 실행할 수 있도록 example 월드를 이해하는 예제 호스트가 examples/example-host에 제공되어 있습니다. 각 툴체인 섹션에서는 이 월드의 컴포넌트를 만드는 과정을 단계별로 설명하며, 이렇게 만든 컴포넌트는 예제 호스트에서 실행하거나 해당 툴체인의 애플리케이션에서 실행할 수 있습니다. 이를 통해 툴체인 내부와 툴체인 간에 컴포넌트를 사용하는 전체 과정을 제공하는 것을 목표로 합니다.
각 섹션에서는 특정 툴체인에서 컴포넌트를 빌드하고 실행하는 방법을 다룹니다. 마지막 섹션인 WebAssembly 텍스트 형식(WAT)에서는 고수준 언어 프런트엔드를 사용하지 않고 WebAssembly 컴포넌트를 직접 작성하는 방법을 자세히 설명합니다.
간단한 컴포넌트 빌드하기
이 섹션에서는 다음 WIT 인터페이스로 덧셈기를 구현하는 간단한 WebAssembly 컴포넌트를 빌드하는 방법을 안내합니다:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
언어
이 가이드는 다양한 언어로 구현되어 있습니다:
C/C++ 도구
WebAssembly 컴포넌트는 LLVM용 C 언어 계열 프런트엔드인 clang을 사용하여 C 및 C++로 빌드할 수 있습니다.
wit-bindgen은 주어진 .wit 파일로부터 게스트 언어 바인딩을 생성하는 도구입니다. C 또는 C++ 코드를 WebAssembly 컴포넌트로 컴파일할 때, C 또는 C++를 “게스트” 언어라고 하고 WebAssembly를 “호스트” 언어라고 합니다. 이 경우 “바인딩“은 C 또는 C++ 선언, 즉 WIT 함수에 대응하는 타입 시그니처와 WIT 타입에 대응하는 타입 정의입니다. 바인딩 생성기는 선언만 생성하므로, 자체 .wit 파일을 개발하는 경우 이러한 선언을 실제로 구현하는 코드는 직접 작성해야 합니다. WASI에 내장된 WIT 인터페이스의 경우, 해당 코드는 사용하게 될 WebAssembly 런타임의 일부입니다.
C/C++에는 현재 통합 툴체인이 없습니다. 그러나 wit-bindgen은 Rust, C, Java(TeaVM), TinyGo용 소스 수준 바인딩을 생성할 수 있으며, 향후 더 많은 언어 생성기를 추가할 수 있습니다.
wit-bindgen을 사용하면 wasm32-wasi 대상과 함께 clang을 사용하여 WebAssembly 모듈로 직접 컴파일할 수 있는 C 애플리케이션을 빌드할 수 있습니다.
1. 의존성 다운로드
먼저 다음 의존성을 설치하십시오.
wit-bindgenCLIwasm-toolswasm-tools는 컴파일된 WebAssembly 모듈과 컴포넌트를 검사하는 데 사용할 수 있으며, 선택적인 수동 워크플로에서 preview1 모듈과 preview2 컴포넌트 간 변환에도 사용할 수 있습니다.
WASI SDK- WASI SDK는 WASI를 지원하는 C/C++ 툴체인으로, C/C++를 WebAssembly로 컴파일하는 데 필요한 여러 아티팩트와 함께 WASI 인터페이스로 구현된 C 표준 라이브러리(
libc)의 한 버전을 포함합니다. - Linux 시스템에서는 “Install” 섹션으로 건너뛸 수 있습니다. 소스에서 빌드하려면 README의 처음부터 시작하십시오.
- WASI SDK는 WASI를 지원하는 C/C++ 툴체인으로, C/C++를 WebAssembly로 컴파일하는 데 필요한 여러 아티팩트와 함께 WASI 인터페이스로 구현된 C 표준 라이브러리(
WASI SDK를 설치하면 WASI sysroot로 구성된 로컬 버전의 clang이 포함됩니다. (sysroot는 특정 대상 플랫폼용 헤더 파일과 라이브러리를 담고 있는 디렉터리입니다.) WASI SDK를 사용할 수 있도록 구성하려면 이 지침을 따르십시오.
2. WIT에서 프로그램 스켈레톤 생성
먼저 샘플 adder/world.wit 파일의 내용을 로컬 파일에 붙여 넣으십시오. 그런 다음 이 파일을 사용하여 wit-bindgen으로 C 스켈레톤을 생성하십시오.
$ wit-bindgen c path/to/adder/world.wit
Generating "adder.c"
Generating "adder.h"
Generating "adder_component_type.o"
이 명령은 여러 파일을 생성합니다.
adder.h(adder월드 기반). 이 헤더 파일에는 일부 보일러플레이트와 함께add함수의 프로토타입이 포함되어 있으며, 다음과 같은 형태여야 합니다. (함수 이름 앞에 “exports” 접두사가 붙어 있습니다.)
uint32_t exports_docs_adder_add_add(uint32_t x, uint32_t y);
adder.c는 컴포넌트 모델 ABI와 인터페이스하여 함수를 호출합니다. 이 파일에는 다음과 같은 형태의extern선언이 포함되어 있습니다.
extern void __component_type_object_force_link_adder(void);
adder_component_type.o에는__component_type_object_force_link_adder함수의 정의를 포함한 오브젝트 코드가 들어 있으며, 이 파일은clang을 통해 링크해야 합니다.
3. 프로그램 코드 작성
다음으로, adder 월드를 구현하는 코드, 즉 adder.h에 선언된 인터페이스 함수의 정의를 충족하는 코드를 담은 component.c 파일을 생성하십시오.
#include "adder.h"
uint32_t exports_docs_adder_add_add(uint32_t x, uint32_t y)
{
return x + y;
}
4. wasi-sdk의 wasm32-wasip2-clang으로 WebAssembly Preview 2 컴포넌트 컴파일
“P1“은 WASI API의 초기 버전인 WASI Preview 1을 가리킵니다. “P2“는 컴포넌트 모델을 도입한 WASI Preview 2를 가리킵니다.
과거에는 P2 컴포넌트를 빌드하려면 P1 컴포넌트에서 변환해야 했지만, 이제는 WASI SDK가 설치한 wasm32-wasip2-clang 바이너리를 사용하여 P2 컴포넌트를 직접 빌드할 수 있습니다.
필요한 경우 /opt/wasi-sdk를 WASI SDK를 설치한 경로로 변경하십시오.
/opt/wasi-sdk/bin/wasm32-wasip2-clang \
-o adder.wasm \
-mexec-model=reactor \
component.c \
adder.c \
adder_component_type.o
이 명령의 각 부분을 살펴보면 다음과 같습니다.
-o adder.wasm은 바이너리 WebAssembly 코드를 담을 출력 파일을 설정합니다.-mexec-model=reactor는 생성되는 코드에 원하는 실행 모델을 제어합니다. 인수는reactor또는command중 하나일 수 있습니다. 이 경우에는 reactor 컴포넌트를 빌드하기 위해-mexec-model=reactor를 전달합니다. 리액터 컴포넌트는 라이브러리에 더 가깝고, command 컴포넌트는 실행 파일에 더 가깝습니다.component.c에는adder월드를 구현하기 위해 작성한 코드가 들어 있습니다.adder.c와adder_component_type.o는wit-bindgen이 생성한 것으로,component.c를 WebAssembly 바이너리로 빌드하는 데 필요한 스캐폴딩(예: 함수 익스포트)을 포함합니다.
이 명령이 완료되면 소스 폴더에 adder.wasm이라는 새 파일이 생성됩니다.
다음 명령으로 adder.wasm이 유효한 WebAssembly 컴포넌트인지 확인할 수 있습니다.
> wasm-tools print adder.wasm | head -1
(component
P2를 지원하지 않는 플랫폼용으로 빌드하는 경우처럼 P1 모듈을 빌드하거나 기존 P1 모듈을 P2 모듈로 변환하거나 둘 다 해야 하는 사용 사례라면, wasi-sdk의 clang과 wasm-tools를 사용하는 보다 수동적인 방법에 대한 자세한 내용을 아래에서 확인할 수 있습니다.
수동 P1 및 P2 빌드
clang을 통해 컴포넌트 코드를 WebAssembly P1 모듈로 컴파일하십시오.
WASI SDK README의 “Use” 섹션에 따라 WASI_SDK_PATH를 정의했다고 가정하고 다음을 실행하십시오.
$WASI_SDK_PATH/bin/clang \
-o adder.wasm \
-mexec-model=reactor \
component.c \
adder.c \
adder_component_type.o
다음 명령으로 adder.wasm이 유효한 WebAssembly P1 컴포넌트(즉, WebAssembly 코어 모듈)인지 확인할 수 있습니다.
> wasm-tools print adder.wasm | head -1
(module $adder.wasm
또는 게시된
ghcr.io/webassembly/wasi-sdk컨테이너 이미지를 사용하여 빌드를 수행할 수도 있습니다.예를 들어,
wasi-sdk가 설치된 컨테이너에 진입하려면 다음과 같이 하십시오.docker run --rm -it \ --mount type=bind,src=path/to/app/src,dst=/app \ ghcr.io/webassembly/wasi-sdk:wasi-sdk-27
path/to/app/src를 샘플 앱의 코드가 들어 있는 디렉터리의 절대 경로로 바꾸십시오.컨테이너 내부에서 소스 코드는
/app에서 사용할 수 있습니다. 해당 디렉터리로 이동한 후 다음을 실행할 수 있습니다./opt/wasi-sdk/bin/clang \ -o adder.wasm \ -mexec-model=reactor \ component.c \ adder.c \ adder_component_type.oDockerfile을 사용하면 시스템에 WASI SDK를 설치할 필요가 없습니다.
참고 항목:
wasi-sdk의Dockerfile
wasm-tools로 P1 컴포넌트를 P2 컴포넌트로 변환
다음으로, P1 컴포넌트를 P2 컴포넌트로 변환해야 합니다. 이를 위해 wasm-tools component new를 사용할 수 있습니다:
wasm-tools component new adder.wasm -o adder.component.wasm
Note
.component.확장자에는 특별한 의미가 없습니다..wasm파일은 모듈일 수도 있고 컴포넌트일 수도 있습니다.
(선택 사항) wasm-tools로 WASI를 지원하는 WebAssembly (P2) 컴포넌트 빌드하기
코드가 임포트해야 하는 WASI API를 참조하는 경우(예: stdio.h 같은 표준 라이브러리 임포트를 통해) wasm-tools component new가 실패할 수 있다는 점에 유의하십시오.
WASI SDK는 여전히 컴포넌트와 직접 호환되지 않는 WASI Preview 1 API(이름에 wasi_snapshot_preview1이 포함된 API)를 참조하므로, WASI 인터페이스를 사용하려면 추가 단계가 필요합니다.
예를 들어, 위 코드를 printf()를 참조하도록 수정하면 P1 컴포넌트로 컴파일됩니다:
#include "adder.h"
#include <stdio.h>
uint32_t exports_docs_adder_add_add(uint32_t x, uint32_t y)
{
uint32_t result = x + y;
// On traditional platforms, printf() prints to stdout, but on Wasm platforms,
// stdout and the idea of printing to an output stream is
// introduced and managed by WASI.
//
// When building this code with wasi-libc (as a part of wasi-sdk), the printf call
// below is implemented with code that uses `wasi:cli/stdout` and `wasi:io/streams`.
printf("%d", result);
return result;
}
그러나 이 모듈은 P2 컴포넌트로 변환하는 데 실패합니다:
> wasm-tools component new adder.wasm -o adder.component.wasm
error: failed to encode a component from module
Caused by:
0: failed to decode world from module
1: module was not valid
2: failed to resolve import `wasi_snapshot_preview1::fd_close`
3: module requires an import interface named `wasi_snapshot_preview1`
P1 컴포넌트로부터 WASI 인터페이스를 사용하는 P2 컴포넌트를 빌드하려면 어댑터 모듈을 활용해야 합니다. 어댑터 모듈은 WASI Preview 1 API 함수의 정의를 WASI Preview 2 API 함수를 사용하여 제공합니다.
여기에 설명된 대로 적절한 리액터 어댑터 모듈을 다운로드하여 지금까지 작업한 .c 및 .wasm 파일이 있는 디렉터리에 저장하십시오.
wasi_snapshot_preview1.reactor.wasm의 링크된 릴리스를 받아 wasi_snapshot_preview1.wasm으로 이름을 바꾸거나, 여기의 안내에 따라 wasmtime에서 소스로부터 직접 빌드할 수 있습니다(먼저 반드시 git submodule update --init을 실행하십시오).
이제 preview1을 preview2로 어댑트하여 컴포넌트를 빌드할 수 있습니다:
wasm-tools component new \
adder.wasm \
--adapt wasi_snapshot_preview1.wasm \
-o adder.component.wasm
5. 빌드된 컴포넌트 검사하기
마지막으로, 컴포넌트의 임포트와 익스포트(WASI 임포트를 사용한 경우 이를 포함)의 WIT 표현을 검사할 수 있습니다:
$ wasm-tools component wit adder.component.wasm
package root:component;
world root {
import wasi:io/error@0.2.2;
import wasi:io/streams@0.2.2;
import wasi:cli/stdin@0.2.2;
import wasi:cli/stdout@0.2.2;
import wasi:cli/stderr@0.2.2;
import wasi:cli/terminal-input@0.2.2;
import wasi:cli/terminal-output@0.2.2;
import wasi:cli/terminal-stdin@0.2.2;
import wasi:cli/terminal-stdout@0.2.2;
import wasi:cli/terminal-stderr@0.2.2;
import wasi:clocks/wall-clock@0.2.2;
import wasi:filesystem/types@0.2.2;
import wasi:filesystem/preopens@0.2.2;
export add: func(x: s32, y: s32) -> s32;
}
...
6. wasmtime --invoke로 컴포넌트 실행하기
Wasmtime을 임베드하는 호스트 애플리케이션을 작성하지 않고 add 익스포트를 빠르게 실행하려면, Wasmtime CLI로 직접 호출할 수 있습니다.
wasmtime run --invoke 'add(2, 2)' adder.wasm
Wasmtime 버전에 따라 축약형도 동작할 수 있습니다:
wasmtime --invoke 'add(2,2)' adder.wasm
7. 예제 C 호스트에서 컴포넌트 실행하기
이 저장소에는 add 인터페이스를 구현하는 컴포넌트를 실행할 수 있는 C 애플리케이션이 포함되어 있습니다. 이 애플리케이션은 Wasmtime C API를 사용하여 Wasmtime을 임베드합니다: component-model/examples/example-c-host/host.c.
이 애플리케이션은 세 개의 인수를 받습니다: 더할 두 숫자와 덧셈을 실행한 Wasm 컴포넌트입니다. 예를 들면 다음과 같습니다.
./adder-host <x> <y> <path-to-component.wasm>
Dockerfile을 사용하여 C 애플리케이션으로 add 컴포넌트를 실행하거나, 애플리케이션을 직접 실행할 수 있습니다.
옵션 A: 호스트를 직접 컴파일하고 실행하기
시스템에 Wasmtime C API 헤더와 라이브러리가 설치되어 있다면, 호스트를 직접 컴파일하고 실행할 수 있습니다:
Linux에서는 다음 명령이 Dockerfile과 같은 방식으로 C API 아티팩트를 /usr/local에 설치합니다:
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
gcc libc6-dev curl xz-utils ca-certificates
WASMTIME_VERSION=42.0.1
case "$(uname -m)" in
x86_64) WASMTIME_ARCH=x86_64 ;;
aarch64|arm64) WASMTIME_ARCH=aarch64 ;;
*) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac
curl -sL "https://github.com/bytecodealliance/wasmtime/releases/download/v${WASMTIME_VERSION}/wasmtime-v${WASMTIME_VERSION}-${WASMTIME_ARCH}-linux-c-api.tar.xz" \
| sudo tar xJ --strip-components=1 -C /usr/local
sudo ldconfig
cd component-model/examples/example-c-host
gcc -o adder-host host.c -lwasmtime
./adder-host 1 2 /absolute/path/to/adder.wasm
Linux에서 libwasmtime.so가 기본 라이브러리 경로에 없다면, 실행하기 전에 LD_LIBRARY_PATH를 설정하십시오:
LD_LIBRARY_PATH=/path/to/wasmtime/lib ./adder-host 1 2 /absolute/path/to/adder.wasm
예상 출력:
1 + 2 = 3
옵션 B: Docker로 실행하기
Wasmtime C API를 설치하는 대신, C 애플리케이션을 빌드하도록 제공된 Dockerfile을 사용할 수 있습니다.
component-model/examples/example-c-host에서 다음을 실행하십시오:
cd component-model/examples/example-c-host
docker build -t example-c-host:latest .
그런 다음 컴포넌트를 볼륨으로 전달하여 컨테이너를 실행하십시오:
docker run --rm \
-v "$(pwd)/../example-host/add.wasm":/component/add.wasm:ro \
example-c-host:latest
예상 출력:
1 + 2 = 3
기본 명령은 adder-host 1 2 /component/add.wasm을 실행하므로, 다음과 같이 인수를 재정의할 수도 있습니다:
docker run --rm \
-v "$(pwd)/../example-host/add.wasm":/component/add.wasm:ro \
example-c-host:latest 40 2 /component/add.wasm
C# 도구
C#로 작성하는 WebAssembly 컴포넌트는 componentize-dotnet으로 빌드할 수 있습니다. componentize-dotnet은 완전히 AOT(ahead-of-time) 컴파일된 컴포넌트를 만드는 데 사용할 수 있는 NuGet 패키지로, .NET 개발자에게 Rust 및 TinyGo에 견줄 만한 컴포넌트 개발 경험을 제공합니다.
componentize-dotnet으로 컴포넌트 빌드하기
componentize-dotnet은 여러 도구를 하나로 감싼 원스톱 도구 역할을 합니다:
- NativeAOT-LLVM (컴파일)
- wit-bindgen (WIT 임포트 및 익스포트)
- wasm-tools (컴포넌트 변환)
- WASI SDK (NativeAOT-LLVM이 사용하는 SDK)
- Wac (컴포넌트 합성에 사용)
먼저 .NET SDK를 설치하십시오. 이 안내에서는 .NET 10 SDK 프리뷰를 사용합니다. 생성한 바이너리를 실행할 수 있도록 wasmtime도 설치되어 있어야 합니다. 컴포넌트를 합성하려면 wac도 설치해야 합니다.
1. 새 프로젝트 만들기
.NET SDK를 설치했으면 새 프로젝트를 만드십시오:
dotnet new install BytecodeAlliance.Componentize.DotNet.Templates
dotnet new componentize.wasi.cli -o adder
cd adder
2. WIT 월드 만들기 또는 다운로드하기
다음으로, 대상으로 삼을 WIT 월드를 만들거나 다운로드하십시오.
이 예제에서는 두 개의 월드를 포함하는 WIT 파일을 사용합니다(처음에는 example 월드만 사용합니다). 다음 내용을 복사하여 “wit/component.wit“라는 새 파일에 붙여 넣으십시오.
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world example {
export add;
}
world hostapp {
import add;
}
adder.csproj 프로젝트 파일에서 기존 <ItemGroup>과 같은 수준에 새 <ItemGroup>을 추가하십시오:
<ItemGroup>
<Wit Update="wit/component.wit" World="example" />
</ItemGroup>
이 컴포넌트는 기능을 익스포트하기만 하므로 dotnet은 이를 라이브러리 프로젝트로 간주합니다. adder.csproj에서 <OutputType>을 라이브러리로 변경하십시오:
- <OutputType>Exe</OutputType>
+ <OutputType>Library</OutputType>
그리고 자동으로 생성된 Program.cs 파일을 삭제하십시오:
rm Program.cs
3. adder 월드의 구현 작성하기
dotnet build로 프로젝트를 빌드하려고 하면 다음과 같은 오류가 발생합니다:
➜ dotnet build
Restore complete (8.6s)
You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
adder failed with 1 error(s) (25.6s)
/path/to/adder/obj/Debug/net10.0/wasi-wasm/wit_bindgen/AdderWorld.wit.exports.docs.adder.v0_1_0.AddInterop.cs(15,19): error CS0103: The name 'AddImpl' does not exist in the current context
Build failed with 1 error(s) in 34.6s
이는 구현을 제공하겠다고 약속했지만 example 월드에 대한 구현을 아직 작성하지 않았기 때문입니다.
이 문제를 해결하려면 Component.cs라는 파일에 다음 코드를 추가하십시오:
namespace ExampleWorld.wit.exports.docs.adder.v0_1_0;
public class AddImpl : IAdd
{
public static uint Add(uint x, uint y)
{
return x + y;
}
}
그런 다음 컴포넌트를 빌드할 수 있습니다:
dotnet build
컴포넌트는 bin/Debug/net10.0/wasi-wasm/native/adder.wasm에 생성됩니다.
4. (선택 사항) 예제 호스트에서 컴포넌트 실행하기
다음 섹션을 진행하려면 Rust 툴체인이 설치되어 있어야 합니다.
이 저장소에는 adder 월드를 구현하는 컴포넌트를 실행할 수 있는, Rust로 작성된 WebAssembly 호스트 예제가 포함되어 있습니다.
git clone https://github.com/bytecodealliance/component-docs.gitcd component-docs/component-model/examples/example-hostcargo run --release -- 1 2 <PATH>/adder.wasm
- 이중 대시는
cargo에 전달되는 플래그와 사용자 코드에 전달되는 플래그를 구분합니다. - 인수 1과 2는 adder에 전달되는 인수입니다.
<PATH>자리에는 생성된adder.wasm파일이 들어 있는 디렉터리를 대신 입력하십시오.
참고: 호스트가 WASI 인터페이스를 사용하는 컴포넌트를 실행할 때는, 빌드된 컴포넌트를 실행하기 위해 명시적으로 링커에 WASI를 추가해야 합니다.
실행에 성공하면 다음과 같은 출력이 표시되어야 합니다(물론 예제 호스트와 adder 컴포넌트의 경로는 환경에 따라 다릅니다):
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/debug/example-host 1 2 /path/to/adder.wasm`
1 + 2 = 3
올바르게 구성되지 않은 경우 다음과 같은 오류가 표시될 수 있습니다:
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/release/example-host 1 2 /path/to/adder.component.wasm`
Error: Failed to instantiate the example world
Caused by:
0: component imports instance `wasi:io/error@0.2.2`, but a matching implementation was not found in the linker
1: instance export `error` has the wrong type
2: resource implementation is missing
이러한 종류의 오류는 일반적으로 해당 호스트가 WASI 임포트를 충족하지 못한다는 것을 나타냅니다.
인터페이스를 임포트하는 컴포넌트 빌드하기
지금까지는 라이브러리 컴포넌트를 다루었습니다. 이제 hostapp 월드를 구현하는 command 컴포넌트를 만들겠습니다. 이 컴포넌트는 adder 컴포넌트에서 익스포트한 add 인터페이스를 임포트하고 add 함수를 호출합니다. 이후 이 command 컴포넌트를 방금 빌드한 adder 라이브러리 컴포넌트와 합성할 것입니다.
이제 adder 컴포넌트를 가져와 다른 WebAssembly 컴포넌트에서 실행하겠습니다.
dotnet new componentize.wasi.cli는 실행 파일을 만드는 새 프로젝트를 생성합니다.
현재 프로젝트의 상위 디렉터리로 이동한 후 새 프로젝트를 생성하십시오:
cd ..
dotnet new componentize.wasi.cli -o host-app
cd host-app
다음 WIT 파일을 프로젝트의 wit/add.wit 파일에 복사하십시오:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world example {
export add;
}
world hostapp {
import add;
}
이를 host-app.csproj 프로젝트 파일의 최상위 수준에 새 ItemGroup으로 추가하십시오:
<ItemGroup>
<Wit Update="wit/add.wit" World="hostapp" />
</ItemGroup>
World가 example에서 hostapp으로 변경된 점에 주목하십시오. 이전 예제에서는 이 WIT 파일의 클래스 라이브러리, 즉 export 함수를 구현하는 데 중점을 두었습니다. 이제는 애플리케이션의 실행 파일 측면, 즉 hostapp 월드에 중점을 두겠습니다.
Program.cs를 다음과 같이 수정하십시오:
// Pull in all imports of the `hostapp` world, namely the `add` interface.
// example.component refers to the package name defined in the WIT file.
using HostappWorld.wit.imports.docs.adder.v0_1_0;
uint left = 1;
uint right = 2;
var result = AddInterop.Add(left, right);
Console.WriteLine($"{left} + {right} = {result}");
다시 한번 dotnet build로 컴포넌트를 컴파일하십시오:
$ dotnet build
Restore complete (0.4s)
You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
host-app succeeded (1.1s) → bin/Debug/net10.0/wasi-wasm/host-app.dll
Build succeeded in 2.5s
이 시점에서 두 개의 WebAssembly 컴포넌트가 생깁니다:
example월드를 구현하는 컴포넌트.hostapp월드를 구현하는 컴포넌트.
host-app 컴포넌트는 example 월드에 정의된 add 함수에 의존하므로 첫 번째 컴포넌트와 합성해야 합니다. wac plug를 실행하여 host-app 컴포넌트를 adder 컴포넌트와 합성할 수 있습니다:
wac plug \
bin/Debug/net10.0/wasi-wasm/native/host-app.wasm \
--plug ../adder/bin/Debug/net10.0/wasi-wasm/native/adder.wasm \
-o main.wasm
다음과 같은 오류 메시지가 표시되면:
error: the socket component had no matching imports for the plugs that were provided
두 .wit 파일(adder 컴포넌트용 파일과 host-app 컴포넌트용 파일)의 패키지 이름이 동일한지 확인하십시오.
host-app.csproj에 다음 내용을 추가하여 이 과정을 자동화할 수도 있습니다:
<Target Name="ComposeWasmComponent" AfterTargets="Publish">
<PropertyGroup>
<EntrypointComponent>bin/$(Configuration)/$(TargetFramework)/wasi-wasm/native/host-app.wasm</EntrypointComponent>
<DependencyComponent>../adder/bin/$(Configuration)/$(TargetFramework)/wasi-wasm/native/adder.wasm</DependencyComponent>
</PropertyGroup>
<MakeDir Directories="dist" />
<Exec Command="$(WacExe) plug $(EntrypointComponent) --plug $(DependencyComponent) -o dist/main.wasm" />
</Target>
이를 위해서는 원래의 adder.wasm 컴포넌트가 host-app 컴포넌트가 있는 디렉터리를 기준으로 ../adder에 있어야 합니다.
dotnet build를 다시 실행하면 ./dist/main.wasm에 합성된 컴포넌트가 생성됩니다.
그런 다음 합성된 컴포넌트를 실행할 수 있습니다:
wasmtime run ./dist/main.wasm
1 + 2 = 3
더 많은 구성 옵션은 componentize-dotnet 문서를 참조하십시오.
Go 도구
이 가이드는 adder/world.wit 패키지에 정의된 adder 월드를 구현하는 Go 컴포넌트를 빌드하는 과정을 안내합니다. 이 컴포넌트는 add 함수가 있는 add 인터페이스를 포함하는 adder 월드를 구현합니다.
이 내용은 기본적인 소개라는 점에 유의하십시오. 더 많은 예제는 componentize-go 저장소의 예제를 참조하십시오.
그래도 궁금한 점이 있으면 Zulip에서 componentize-go 메인테이너에게 언제든지 문의하거나 저장소에 이슈를 등록하십시오.
1. 도구 설치
- Go 1.25.5+
- componentize-go 최신 버전
2. Go 프로젝트 생성
component-docs 저장소를 로컬에 클론한 상태에서 다음을 실행하십시오:
mkdir go-adder && cd go-adder
2. 컴포넌트가 구현할 월드 결정
adder 월드를 구현할 것이므로 WIT를 프로젝트에 복사할 수 있습니다. wit라는 하위 디렉터리를 만들고 wit/adder.wit라는 파일에 다음 코드를 붙여 넣으십시오:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
3. Wasm 컴포넌트용 바인딩 생성
다음 명령을 실행하여 컴포넌트용 바인딩을 생성하십시오:
componentize-go --world adder bindings
go mod tidy
프로젝트 디렉터리는 다음과 같은 구조가 됩니다:
$ tree
.
├── go.mod
├── go.sum
├── wit
│ └── adder.wit
└── wit_exports.go
각 파일의 역할은 다음과 같습니다:
go.mod에는 공유 WIT 타입의 필수 라이브러리가 포함되어 있습니다(go.bytecodealliance.org/pkg 참조)wit_exports.go는 Go WebAssembly 컴파일러를 위한//go:wasmexport메서드를 정의합니다.
이 상태에서 컴파일하려고 하면 wit_exports.go 파일에서 오류가 발생합니다:
could not import wit_component/export_docs_adder_add (no required module provides package "wit_component/export_docs_adder_add")
이 모듈은 다음 섹션에서 만듭니다.
4. add 함수 구현
add 디렉터리에 export_docs_adder_add/exports.go라는 파일을 만들고 다음 코드를 붙여 넣으십시오:
package export_docs_adder_add
func Add(x uint32, y uint32) uint32 {
return x + y
}
5. 컴포넌트 빌드하기
Go 애플리케이션을 WebAssembly로 컴파일하려면 다음 명령을 실행하십시오:
componentize-go build
이 명령은 다음 섹션에서 실행할 main.wasm 파일을 출력합니다.
5. add 컴포넌트 테스트하기
컴포넌트가 제대로 작동하는지 확인하기 위해, adder 월드를 대상으로 하는 컴포넌트를 실행할 수 있는 Rust 애플리케이션에서 이 컴포넌트를 실행해 보겠습니다. Rust 툴체인이 설치되어 있는지 반드시 확인하십시오.
이 애플리케이션은 wasmtime을 사용하여 Rust “호스트”/“임베더” 바인딩을 생성하고, WASI 월드를 가져오며, 컴포넌트를 실행합니다.
go-adder 디렉터리 안에서 다음을 실행하십시오.
$ cd ../component-model/examples/example-host
$ cargo run --release -- 1 2 ../../../go-adder/main.wasm
1 + 2 = 3
이로써 Go로 기본적인 WebAssembly 컴포넌트를 성공적으로 빌드하고 실행했습니다 🎉
JavaScript 도구
WebAssembly는 원래 JavaScript가 아닌 워크로드를 브라우저에서 네이티브에 가까운 속도로 실행하기 위한 기술로 개발되었습니다.
JavaScript의 WebAssembly 컴포넌트 모델 지원은 다음 도구들의 조합으로 제공됩니다:
- StarlingMonkey: WebAssembly 컴포넌트를 인식하는 JavaScript 엔진
componentize-js: JavaScript 파일로 WebAssembly 컴포넌트를 빌드하는 도구jco: 컴포넌트화, 타입 생성, Node.js 및 브라우저 환경에서의 컴포넌트 실행을 위한 다목적 도구
관련 도구(tsc)로 먼저 JS로 트랜스파일한다면 TypeScript도 역시 사용할 수 있다는 점에 유의하십시오. jco는 기본적으로 타입 선언 파일(.d.ts)을 생성하며, TypeScript 코드베이스에서 사용할 수 있는 타입 정의를 생성하는 jco types 하위 명령도 제공합니다.
Warning
emscripten과 같은 인기 있는 프로젝트도 WebAssembly 모듈을 빌드하지만, 이러한 모듈은 컴포넌트 모델을 인식하지 않습니다.코어 WebAssembly 모듈에는 컴포넌트 모델이 제공하는 고급 기능(풍부한 타입, 구조화된 언어 간 상호 운용, 합성)이 포함되어 있지 않습니다.
jco 설치하기
componentize-js를 사용하는 jco는 Node 패키지 관리자(Node Package Manager, npm)를 통해 설치할 수 있습니다:
npm install -g @bytecodealliance/jco
Note
jco와componentize-js는npm install -D를 사용하여 프로젝트 로컬 방식으로 설치할 수 있습니다.
JavaScript로 컴포넌트를 빌드하는 과정 개요
JavaScript로 WebAssembly 컴포넌트를 빌드하는 과정은 일반적으로 다음으로 구성됩니다:
- 컴포넌트가 대상으로 할 인터페이스 결정하기(즉, 주어진 WebAssembly 인터페이스 타입(“WIT”) 월드)
- 인터페이스를 충족하는 JavaScript를 작성하여 컴포넌트 생성
- 인터페이스를 준수하는 JavaScript를 WebAssembly로 컴파일
jco로 리액터 컴포넌트 빌드하기
리액터 컴포넌트는 오랫동안 실행되며 시간이 지남에 따라 반복적으로 호출되도록 설계된 WebAssembly 컴포넌트입니다. 실행 파일에 해당하는 “command” 컴포넌트와 달리, 리액터 컴포넌트는 기능 라이브러리에 해당합니다.
컴포넌트는 WebAssembly 인터페이스 타입을 통해 인터페이스를 노출하며, 이는 컴포넌트가 고수준 타입을 상호 교환 가능하게 사용할 수 있도록 하는 컴포넌트 모델과 함께 작동합니다.
WIT란 무엇입니까?
WebAssembly 인터페이스 타입(“WIT”)은 기능을 정의하기 위한 다양한 기능을 갖춘 인터페이스 정의 언어(“IDL”)이지만, 대부분의 경우 WIT를 처음부터 작성할 필요는 없습니다. 컴포넌트가 수행해야 할 작업을 정의하는 기존 인터페이스를 다운로드하는 것으로 충분한 경우가 많습니다.
adder 월드에는 두 숫자를 더하는 단일 add 함수를 가진 인터페이스가 포함되어 있습니다. adder라는 새 디렉터리를 만들고 다음 WIT 코드를 world.wit라는 파일에 붙여 넣으십시오.
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
adder 월드 안의 export add; 선언은 생성된 WebAssembly 컴포넌트와 상호 작용하는 환경이 add 함수를 호출할 수 있음을 의미합니다. 이 맥락에서 add 인터페이스의 정규화된 전체 이름은 docs:adder/add.add@0.1.0입니다. 이 이름의 구성 요소는 다음과 같습니다:
docs:adder는 네임스페이스와 패키지이며,docs가 네임스페이스이고adder가 패키지입니다.add는add함수를 포함하는 인터페이스의 이름입니다.add는 공교롭게도 함수 자체의 이름이기도 합니다.@0.1.0은 패키지에 선언된 버전 번호와 일치해야 하는 버전 번호입니다.
WIT 구문에 대해 더 알아보려면 WIT 설명서를 참고하십시오.
JS WebAssembly 컴포넌트 구현하기
adder 월드를 구현하기 위해 JavaScript ES 모듈을 작성할 수 있습니다. 다음 코드를 adder 디렉터리의 adder.js라는 파일에 붙여 넣으십시오.
export const add = {
add(x, y) {
return x + y;
}
};
Warning
이 파일을 사용하여 JavaScript 프로젝트를 만드는 경우,
jco는 JavaScript 모듈에서만 동작하므로package.json에"type":"module"옵션을 반드시 설정하십시오.
위 코드에서:
- JavaScript 모듈(파일) 자체는
adder월드에 해당합니다 - 내보낸
add객체는 WIT에서export된add인터페이스에 대응합니다 add객체 안에 정의된add함수는add인터페이스 안의add함수에 대응합니다
WIT와 JavaScript가 준비되면 jco를 사용하여 jco componentize로 JS 모듈에서 WebAssembly 컴포넌트를 만들 수 있습니다.
Note
componentize-js를 직접 호출할 수도 있으며, 이는 API와 명령줄 모두를 통해 사용할 수 있습니다.
우리 컴포넌트는 매우 단순하여(숫자 값만 다루는 코어 WebAssembly를 연상시킵니다) 실제로 WebAssembly 시스템 인터페이스 기능(파일 접근, 네트워킹 및 기타 시스템 기능)을 전혀 사용하지 않습니다. 따라서 jco componentize를 호출할 때 불필요한 모든 WASI 기능을 --disable할 수 있습니다.
adder 디렉터리 안에서 다음을 실행하십시오.
jco componentize \
--wit world.wit \
--world-name adder \
--out adder.wasm \
--disable=all \
adder.js
Note
jco를 프로젝트 로컬 의존성으로 사용하는 경우npx jco를 실행할 수 있습니다.
다음과 같은 출력이 표시되어야 합니다.
OK Successfully written adder.wasm.
이제 adder 디렉터리에 adder.wasm 파일이 생성되어 있어야 합니다. 다음 명령으로 이 파일에 컴포넌트가 포함되어 있는지 확인할 수 있습니다.
$ wasm-tools print adder.wasm | head -1
(component
Warning
--disable=all을 사용하면 컴포넌트는 디버깅이나 로깅에 유용할 수 있는 어떤 WASI 인터페이스에도 접근할 수 없습니다.예를 들어
stdio없이는console.log(...)나console.error(...)를 사용할 수 없고,random없이는Math.random()을 사용할 수 없으며,clocks없이는Date.now()나new Date()를 사용할 수 없습니다.
Math.random()이나Date.now()를 호출하면 겉보기에는 유효한 출력이 반환되지만, 실제 무작위성이나 타임스탬프의 정확성은 없다는 점에 유의하십시오.
example-host에서 컴포넌트 실행하기
Note
example-hostRust 프로젝트는 Rust 툴체인, 특히cargo를 사용하므로, 이 섹션의 코드를 실행하려면 추가 의존성(예: Rust 툴체인)을 설치해야 할 수 있습니다.
빌드한 컴포넌트를 실행하려면 example-host 프로젝트를 사용할 수 있습니다.
이 저장소에는 adder 월드를 구현하는 컴포넌트를 실행할 수 있는, Rust로 작성된 WebAssembly 호스트 예제가 포함되어 있습니다.
git clone https://github.com/bytecodealliance/component-docs.gitcd component-docs/component-model/examples/example-hostcargo run --release -- 1 2 <PATH>/adder.wasm
- 이중 대시는
cargo에 전달되는 플래그와 사용자 코드에 전달되는 플래그를 구분합니다. - 인수 1과 2는 adder에 전달되는 인수입니다.
<PATH>자리에는 생성된adder.wasm파일이 들어 있는 디렉터리를 대신 입력하십시오.
참고: 호스트가 WASI 인터페이스를 사용하는 컴포넌트를 실행할 때는, 빌드된 컴포넌트를 실행하기 위해 명시적으로 링커에 WASI를 추가해야 합니다.
출력은 다음과 같습니다.
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/debug/example-host 1 2 /path/to/adder.wasm`
1 + 2 = 3
올바르게 구성되지 않은 경우 다음과 같은 오류가 표시될 수 있습니다:
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/release/example-host 1 2 /path/to/adder.component.wasm`
Error: Failed to instantiate the example world
Caused by:
0: component imports instance `wasi:io/error@0.2.2`, but a matching implementation was not found in the linker
1: instance export `error` has the wrong type
2: resource implementation is missing
이러한 종류의 오류는 일반적으로 해당 호스트가 WASI 임포트를 충족하지 못한다는 것을 나타냅니다.
출력 자체는 흥미롭지 않지만, example-host에 포함된 코드는 이를 위해 많은 작업을 수행합니다.
- 제공된 경로(위 명령에서는
/path/to/adder.wasm)의 WebAssembly 바이너리를 로드합니다 add인터페이스 안의export된add함수를 인수와 함께 호출합니다- 결과를 출력합니다
중요한 Rust 코드는 대략 다음과 같습니다.
#![allow(unused)]
fn main() {
let component = Component::from_file(&engine, path).context("Component file not found")?;
let (instance, _) = Example::instantiate_async(&mut store, &component, &linker)
.await
.context("Failed to instantiate the example world")?;
instance
.call_add(&mut store, x, y)
.await
.context("Failed to call add function")
}
WebAssembly가 제공하는 강력한 기능과 새로운 역량을 간단히 되짚어 보겠습니다. 우리는 외부 함수 인터페이스, 하위 프로세스, 네트워크 호출 없이도 엄격한 인터페이스를 통해 Rust에서 JavaScript를 작성하고, 로드하고, 인스턴스화하고, 실행했습니다.
JavaScript 애플리케이션(브라우저 포함)에서 컴포넌트 실행하기
브라우저에서 사용할 수 있는 JavaScript 런타임은 WebAssembly 코어 모듈을 실행할 수 있지만 아직 WebAssembly 컴포넌트는 실행할 수 없으므로, WebAssembly 컴포넌트(JavaScript로 작성되었든 아니든)는 JavaScript 래퍼와 브라우저에서 실행할 수 있는 하나 이상의 WebAssembly 코어 모듈로 “트랜스파일“되어야 합니다.
기존 WebAssembly 컴포넌트(예: adder 월드를 구현하는 adder.wasm)가 있다면 jco transpile을 사용하여 해당 WebAssembly 컴포넌트를 실행 가능한 JavaScript로 트랜스파일할 수 있습니다. adder 디렉터리에서 다음을 실행하십시오.
jco transpile adder.wasm -o dist/transpiled
다음과 비슷한 출력이 표시됩니다.
Transpiled JS Component Files:
- dist/transpiled/adder.core.wasm 10.6 MiB
- dist/transpiled/adder.d.ts 0.11 KiB
- dist/transpiled/adder.js 21.1 KiB
- dist/transpiled/interfaces/docs-adder-add.d.ts 0
Note
이미 만든 것과 비슷한 JS 및 WIT 파일이 포함된 완전한 프로젝트는
jco예제adder컴포넌트를 참조하십시오.이 프로젝트를 로컬로 가져온 경우
npm run transpile도 실행할 수 있으며, 그 결과는dist/transpiled에 출력됩니다.
jco 트랜스파일 덕분에, 생성된 dist/transpiled/adder.js 파일을 임포트하여 JavaScript용으로 구현된 코어 WebAssembly 명세를 지원하는 런타임을 사용하는 모든 JavaScript 애플리케이션에서 실행할 수 있습니다.
Node.js에서 이 컴포넌트를 사용하려면 다음과 같은 코드를 작성할 수 있습니다.
import { add } from "./dist/transpiled/adder.js";
console.log("1 + 2 = " + add.add(1, 2));
이 코드를 adder 디렉터리의 run.js라는 파일에 붙여 넣으면 node로 JavaScript 모듈을 직접 실행할 수 있습니다. 먼저 같은 디렉터리에 package.json 파일을 만들어야 합니다.
{
"name": "adder-wasm",
"description": "Simple codebase for compiling an add interface to WebAssembly with jco",
"type": "module"
}
Note
package.json파일을 만들지 않거나"type": "module"속성을 생략하면 다음과 같은 오류 메시지가 표시됩니다.
SyntaxError: Cannot use import statement outside a module.
그런 다음 다음 명령으로 모듈을 실행할 수 있습니다.
node run.js
다음과 같은 출력이 표시됩니다.
1 + 2 = 3
이는 이전 섹션에서 언급한 Rust 호스트 코드와 직접 비교할 수 있습니다. 여기서는 jco의 컴포넌트 트랜스파일 기능 덕분에 Node.js를 WebAssembly 실행을 위한 호스트로 사용할 수 있습니다.
jco transpile을 사용하면 (어떤 언어로 컴파일되었든) 모든 WebAssembly 바이너리를 JavaScript에서 네이티브로 실행할 수 있습니다.
Python 도구
componentize-py로 컴포넌트 빌드하기
componentize-py는 Python 애플리케이션을 WebAssembly 컴포넌트로 변환하는 도구입니다.
먼저 Python 3.10 이상과 pip이 아직 설치되어 있지 않다면 설치하십시오. 그런 다음 componentize-py를 설치하십시오:
pip install componentize-py
다음으로, 대상으로 삼을 WIT 월드를 생성하거나 다운로드하십시오. 이 예제에서는 add 함수가 있는 adder 월드를 사용합니다.
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
프로젝트용 새 디렉터리를 만들고 그 안에 wit라는 하위 디렉터리를 만드십시오. 이 코드를 복사하여 wit/component.wit라는 파일에 붙여 넣으십시오.
(IDE나 타입 검사기를 위해) WIT 월드에 대한 바인딩을 생성하려면 bindings 하위 명령을 사용하여 생성할 수 있습니다. 대상으로 하는 월드가 포함된 WIT 인터페이스의 경로를 지정하십시오:
componentize-py --wit-path wit --world adder bindings .
Note
다음 단계에서
componentize를 실행하기 위해 바인딩을 미리 생성할 필요는 없습니다.componentize는 바인딩을 즉석에서 생성하여 결과로 만들어지는 컴포넌트에 함께 번들링합니다.바인딩 생성을 두 번 실행하려고 하면,
bindings폴더가 이미 존재하는 경우 실패합니다.
바인딩은 기본적으로 wit_world라는 폴더에 생성됩니다:
<project folder>
├── wit
│ └── component.wit
└── wit_world
├── exports
│ ├── add.py
│ └── __init__.py
├── __init__.py
└── types.py
wit_world/exports 폴더에는 우리가 구현할 수 있는 add 메서드를 가진 Add 프로토콜이 들어 있으며, 이는 adder 월드 WIT에 정의된 익스포트를 나타냅니다.
adder 월드(특히 익스포트되는 add 인터페이스)를 구현하려면 다음 코드를 app.py라는 파일에 넣으십시오:
from wit_world import exports
class Add(exports.Add):
def add(self, x: int, y: int) -> int:
return x + y
이제 componentize 하위 명령을 사용하여 애플리케이션을 WebAssembly 컴포넌트로 컴파일할 수 있습니다:
componentize-py \
--wit-path wit/component.wit \
--world adder \
componentize \
app \
-o add.wasm
Component built successfully
예제 호스트에서 컴포넌트 실행하기
다음 섹션을 진행하려면 Rust 툴체인이 설치되어 있어야 합니다.
이 저장소에는 adder 월드를 구현하는 컴포넌트를 실행할 수 있는, Rust로 작성된 WebAssembly 호스트 예제가 포함되어 있습니다.
git clone https://github.com/bytecodealliance/component-docs.gitcd component-docs/component-model/examples/example-hostcargo run --release -- 1 2 <PATH>/adder.wasm
- 이중 대시는
cargo에 전달되는 플래그와 사용자 코드에 전달되는 플래그를 구분합니다. - 인수 1과 2는 adder에 전달되는 인수입니다.
<PATH>자리에는 생성된adder.wasm파일이 들어 있는 디렉터리를 대신 입력하십시오.
참고: 호스트가 WASI 인터페이스를 사용하는 컴포넌트를 실행할 때는, 빌드된 컴포넌트를 실행하기 위해 명시적으로 링커에 WASI를 추가해야 합니다.
실행에 성공하면 다음과 같은 출력이 표시되어야 합니다(물론 예제 호스트와 adder 컴포넌트의 경로는 환경에 따라 다릅니다):
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/debug/example-host 1 2 /path/to/adder.wasm`
1 + 2 = 3
올바르게 구성되지 않은 경우 다음과 같은 오류가 표시될 수 있습니다:
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/release/example-host 1 2 /path/to/adder.component.wasm`
Error: Failed to instantiate the example world
Caused by:
0: component imports instance `wasi:io/error@0.2.2`, but a matching implementation was not found in the linker
1: instance export `error` has the wrong type
2: resource implementation is missing
이러한 종류의 오류는 일반적으로 해당 호스트가 WASI 임포트를 충족하지 못한다는 것을 나타냅니다.
Python 애플리케이션으로 HTTP, CLI, TCP 컴포넌트를 빌드해 보려면 componentize-py의 예제를 참조하십시오.
Python 애플리케이션에서 컴포넌트 실행하기
WebAssembly 컴포넌트는 Python 애플리케이션에서도 호출할 수 있습니다. 이 섹션에서는 Python 네이티브 도구를 사용하여 예제의 미리 빌드된 add.wasm 컴포넌트를 호출하는 과정을 안내합니다.
먼저 Python 3.11 이상과 pip이 아직 설치되어 있지 않다면 설치하십시오. 그런 다음 wasmtime-py를 설치하십시오:
$ pip install wasmtime
먼저 Python 호스트 애플리케이션에서 컴포넌트를 호출할 수 있도록 바인딩을 생성하십시오. 필요한 경우 이전 섹션에서 생성한 add.wasm 파일을 제거하십시오.
# Get an add component that does not import the WASI CLI world
$ wget https://github.com/bytecodealliance/component-docs/raw/main/component-model/examples/example-host/add.wasm
$ python3 -m wasmtime.bindgen add.wasm --out-dir add
생성된 add 패키지에는 컴포넌트에 필요한 모든 익스포트/임포트가 포함되어 있으며, 타입 검사와 자체 문서화를 최대한 돕도록 타입 주석이 달려 있습니다. 패키지 안에는 Root 클래스가 있으며, 이 클래스에는 컴포넌트가 익스포트한 add 함수를 호출하는 add 함수가 있습니다. 이제 add를 호출하는 Python 프로그램을 작성할 수 있습니다. 다음 코드를 복사하여 host.py라는 파일에 붙여 넣으십시오:
from add import Root
from wasmtime import Store
def main():
store = Store()
component = Root(store)
print("1 + 2 =", component.add(store, 1, 2))
if __name__ == '__main__':
main()
Python 호스트 프로그램을 실행하십시오:
$ python3 host.py
1 + 2 = 3
wasmtime-py를 통해 Python 호스트를 사용하는 또 다른 예제는 componentize-py 저장소에서 찾을 수 있습니다.
간단한 컴포넌트 빌드하기 (Rust)
Rust는 툴체인에서 사용할 수 있는 대상을 통해 WebAssembly 코어와 WebAssembly 컴포넌트를 일급으로 지원합니다.
wasm32-unknown-unknown(WebAssembly 코어)wasm32-wasip1(WASI P1)wasm32-wasip2(WASI P2)
Note
위 대상을 사용하려면 Rust 툴체인(예:
rustup)을 통해 해당 대상이 활성화되어 있는지 확인하십시오.예를 들어,
wasm32-wasip2대상을 추가하려면 다음과 같이 합니다(사용 가능한 모든 툴체인을 표시하려면rustup toolchain list를 사용할 수 있습니다).rustup target add wasm32-wasip2
기본 제공 지원 덕분에 Rust 코드(및 표준 라이브러리)는 네이티브 도구를 사용하여 WebAssembly로 컴파일할 수 있습니다:
cargo build --target wasm32-wasip2
Warning
과거에는
cargo-component사용이 권장되었지만, 이제는 네이티브 도구를 직접 사용할 수 있으므로 이 프로젝트는 지원 중단 절차를 밟고 있습니다.
1. 설정
WebAssembly 바이너리를 검사하고 조작할 수 있도록 wasm-tools를 설치하십시오:
cargo install --locked wasm-tools
WebAssembly 바이너리를 위한 빠르고 안전한 런타임인 wasmtime을 설치하십시오:
curl https://wasmtime.dev/install.sh -sSf | bash
2. Rust로 WebAssembly 프로젝트 만들기
cargo new로 Rust 프로젝트를 새로 만드십시오:
$ cargo new --lib adder
Creating library `adder` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Rust WebAssembly 프로젝트를 빌드할 때는 WebAssembly로 컴파일할 수 있는 바이너리 크레이트와 라이브러리 크레이트 중 하나를 만들 수 있으며(각각 “command” 컴포넌트와 “reactor” 컴포넌트를 생성합니다), 이 경우에는 리액터 컴포넌트를 선택하십시오.
Note
command 컴포넌트와 리액터 컴포넌트의 차이는 아직 중요하지 않지만, 바이너리와 공유 라이브러리의 차이와 비슷한 것으로 생각할 수 있습니다.
Rust 프로젝트에 반드시 추가해야 하는 한 가지는 Cargo.toml에서 crate-type을 설정하는 것입니다:
[lib]
crate-type = ["cdylib"]
리액터 컴포넌트(함수 라이브러리의 “등가물”)를 빌드하므로, Rust가 제공하는 cdylib(“c dynamic library“의 약자) 크레이트 타입을 사용해야 합니다.
3. add 인터페이스 추가하기
docs:adder WebAssembly 인터페이스 타입(WIT) 패키지의 adder 월드 월드가 익스포트하는 add 인터페이스를 구현하는 컴포넌트를 Rust로 만들겠습니다.
wit/world.wit라는 파일을 만들고 다음 내용으로 채우십시오:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
이 파일의 (WIT) 타입은 우리 컴포넌트가 충족해야 하는 인터페이스(adder world)를 나타냅니다. 이를 두고 우리 컴포넌트가 add 인터페이스(그 자체로 단일 함수 add를 포함함)를 “익스포트“한다고 말합니다.
이러한 타입을 다루는 방식은 다른 인터페이스 정의 언어(IDL) 툴체인(예: protobuf)과 비슷한데, 인터페이스를 쉽게 구현할 수 있도록 해 주는 언어 수준의 바인딩이 필요하다는 점에서 그렇습니다.
4. adder 인터페이스용 바인딩 생성하기
Rust 툴체인은 WebAssembly 바이너리를 네이티브로 컴파일할 수 있지만, 의도한 (WIT) 인터페이스 타입(wit/world.wit)과 일치하는 바인딩을 자동으로 생성하지는 (아직) 못합니다.
wit-bindgen을 사용하여 바인딩을 생성할 수 있습니다:
cargo add wit-bindgen
Note
올바른 프로젝트의 의존성에
wit-bindgen이 추가되도록 위 명령은cargo new로 생성한adder디렉터리 안에서 실행해야 합니다. 또는Cargo.toml의 의존성 섹션에wit-bindgen을 직접 추가할 수도 있습니다.
wit-bindgen-cli크레이트를 통해wit-bindgen을 바이너리로 사용할 수도 있지만, 여기서는 코드 우선(code-first) 방식의 바인딩 빌드에 초점을 맞춥니다.
wit-bindgen이 Rust 프로젝트에 포함되면(즉, Cargo.toml에 추가되면) 이를 사용하여 WIT 인터페이스에 대한 Rust 코드 바인딩을 생성할 수 있습니다. src/lib.rs 파일을 다음과 같이 수정하십시오:
#![allow(unused)]
fn main() {
mod bindings {
//! This module contains generated code for implementing
//! the `adder` world in `wit/world.wit`.
//!
//! The `path` option is actually not required,
//! as by default `wit_bindgen::generate` will look
//! for a top-level `wit` directory and use the files
//! (and interfaces/worlds) there-in.
wit_bindgen::generate!({
path: "wit/world.wit",
});
}
}
여기서는 wit_bindgen::generate 매크로가 출력한 코드를 담는 bindings라는 모듈을 생성합니다. wit_bindgen은 다양한 struct, interface, enum 등을 생성할 수 있으므로, 이러한 새 타입을 나중에 참조할 수 있는 모듈로 분리해 두는 것이 바람직한 경우가 많습니다.
현재 이 코드는 별다른 일을 하지 않는데, 이는 아직 구현을 추가하지 않았기 때문입니다.
5. 생성된 Guest 트레이트를 통해 adder 월드 구현하기
src/lib.rs에서 bindings::Guest 트레이트를 구현하여 컴포넌트의 기능을 채울 수 있습니다. 코드는 다음과 비슷한 모습이어야 합니다:
mod bindings {
//! This module contains generated code for implementing
//! the `adder` world in `wit/world.wit`.
//!
//! The `path` option is actually not required,
//! as by default `wit_bindgen::generate` will look
//! for a top-level `wit` directory and use the files
//! (and interfaces/worlds) there-in.
// The line below will be expanded as Rust code containing
wit_bindgen::generate!({
path: "wit/adder/world.wit",
});
// In the lines below we use the generated `export!()` macro reuse and
use super::AdderComponent;
export!(AdderComponent);
}
/// Struct off of which the implementation will hang
///
/// The name of this struct is not significant.
struct AdderComponent;
impl bindings::exports::docs::adder::add::Guest for AdderComponent {
fn add(x: u32, y: u32) -> u32 {
x + y
}
}
위 코드 목록에서 주목할 점이 몇 가지 있습니다:
AdderComponent구조체가 도입되지만, 이는Guest트레이트의 구현체로서만 유용합니다.bindings::exports::docs::adder::add::Guest트레이트는 내보내지는docs:adder/add인터페이스를 그대로 반영합니다.- (1)과 (2)에 따라,
AdderComponent는 생성된 바인딩을 통해 (WIT 관점에서)adder월드를 구현합니다. export!()매크로는wit_bindgen::generate!매크로에 의해 생성되며, 중요한 설정을 수행합니다.export!는bindings모듈 내부에서 사용하는 것이 가장 쉽지만, 그 경우super::AdderComponent구조체를 참조해야 합니다
Note
wit_bindgen::generate!매크로가 생성한 코드를 자세히 살펴보려면cargo-expand크레이트를 사용할 수 있습니다
6. 컴포넌트 빌드하기
이제 네이티브 Rust 툴체인으로 WASI P2 컴포넌트를 빌드하여 컴포넌트를 빌드해 보겠습니다.
cargo build --target=wasm32-wasip2
이 명령은 디버그 빌드를 수행하며, target/wasm32-wasip2/debug/adder.wasm에 WebAssembly 컴포넌트를 생성합니다:
du -hs target/wasm32-wasip2/debug/adder.wasm
3.3M target/wasm32-wasip2/debug/adder.wasm
Rust와 같은 컴파일 언어의 WebAssembly 컴포넌트로서 3메가바이트는 큰 크기입니다. 더 많은 최적화를 수행하도록 릴리스 모드로 컴파일해 보겠습니다:
cargo build --target=wasm32-wasip2 --release
릴리스 모드로 컴파일하면 훨씬 작은 바이너리를 얻을 수 있습니다:
$ du -hs target/wasm32-wasip2/release/adder.wasm
16K target/wasm32-wasip2/release/adder.wasm
Rust 툴체인에서 일반적으로 사용할 수 있는 여러 최적화 옵션을 사용하여 바이너리 출력을 제어할 수 있다는 점에 유의하십시오.
Warning
--release로 빌드하면 결과.wasm파일에서 모든 디버그 관련 정보가 제거됩니다.로컬에서 프로토타이핑하거나 테스트할 때는 오류 발생 시 유용한 백트레이스를 얻기 위해(예:
wasmtime::WasmBacktraceDetails::Enable사용 시)--release를 사용하지 않는 것이 좋을 수 있습니다. 참고: 이 경우 결과.wasm파일은 상당히 커집니다(4MB 이상일 가능성이 높습니다).
7. 빌드된 컴포넌트 검사하기
이제 WIT 바이너리가 있으므로 WebAssembly 컴포넌트 도구를 사용하여 이를 살펴볼 수 있습니다.
예를 들어, WebAssembly 컴포넌트는 자기 문서화(self-documenting)되어 있어 이 정보를 포함하고 있으므로, wasm-tools를 사용하여 컴포넌트의 WIT 패키지를 출력할 수 있습니다:
wasm-tools component wit target/wasm32-wasip2/release/adder.wasm
위 명령은 아래와 같은 출력을 생성해야 합니다:
package root:component;
world root {
export docs:adder/add@0.1.0;
}
package docs:adder@0.1.0 {
interface add {
add: func(x: u32, y: u32) -> u32;
}
}
8. adder 컴포넌트 실행하기
컴포넌트가 작동하는지 확인하기 위해, adder 월드를 대상으로 하는 컴포넌트를 실행할 수 있는 Rust 애플리케이션에서 이를 실행해 보겠습니다.
이 애플리케이션은 wasmtime을 사용하여 Rust “호스트”/“임베더” 바인딩을 생성하고, WASI 월드를 가져오며, 컴포넌트를 실행합니다.
component-docs 저장소를 로컬에 클론한 상태에서 다음을 실행하십시오:
$ cd examples/example-host
$ cargo run --release -- 1 2 ../add/target/wasm32-wasip2/release/adder.wasm
1 + 2 = 3
이로써 Rust로 기본적인 WebAssembly 컴포넌트를 성공적으로 빌드하고 실행했습니다 🎉
Warning
TinyGo용 컴포넌트 모델 도구는 현재 유지 관리되지 않습니다. 대신 Go 도구를 사용하는 것을 권장합니다.
TinyGo 도구
TinyGo 컴파일러 v0.34.0 이상은 WebAssembly 컴포넌트 모델과 WASI 0.2.0을 네이티브로 지원합니다.
이 가이드에서는 adder/world.wit 패키지에 정의된 adder 월드를 구현하는 컴포넌트를 빌드하는 과정을 단계별로 안내합니다. 이 컴포넌트는 add 함수를 가진 add 인터페이스를 포함하는 adder 월드를 구현합니다.
1. 도구 설치
TinyGo 설치 안내에 따라 TinyGo 컴파일러를 설치하십시오.
또한 wasm-tools 저장소에서 wasm-tools CLI 도구를 설치하십시오.
Warning
wit-bindgen-go에는 자체적으로 벤더링된wasm-tools버전이 포함되어 있지만, tinygo를 사용하려면 여전히 이를 설치해야 합니다. 가능성은 낮지만, 이전 버전의wasm-tools를 사용하는 경우 버전 불일치가 발생할 수 있습니다. 문제가 발생하는 경우 로컬wasm-tools를 최신 상태로 유지하고 있는지 반드시 확인하십시오.Rust 툴체인을 사용하여
wasm-tools를 설치하는 경우 다음과 같이 설치할 수 있습니다:cargo install --locked wasm-tools@1.235.0 --force또는 cargo binstall을 통해 설치할 수 있습니다:cargo binstall wasm-tools@1.235.0
설치를 확인하려면 다음 명령을 실행하십시오:
$ tinygo version
tinygo version 0.34.0 ...
$ wasm-tools -V
wasm-tools 1.255.0 ...
선택 사항: WIT 파일의 임포트를 해석하려면 wkg CLI 도구를 설치하십시오. wkg CLI는 Wasm 컴포넌트 패키지 관리자의 일부입니다. 수동으로 또는 cargo를 사용하여 설치하려면 wasm-pkg-tools 설치 안내를 참조하십시오.
2. Go 프로젝트 생성
이제 Go 프로젝트를 생성하십시오:
mkdir add && cd add
go mod init example.com
다음 tool을 설치하십시오:
go get -tool go.bytecodealliance.org/cmd/wit-bindgen-go
Note
go tool은 Golang 1.24에서 도입되었으며 Go 프로젝트의 도구를 관리하는 데 사용할 수 있습니다.
위 도구를 추가한 후 go mod tidy도 실행하는 것을 고려하십시오.
2. 컴포넌트가 구현할 월드 결정
adder 월드를 구현할 것이므로 WIT를 프로젝트에 복사할 수 있습니다. wit라는 하위 디렉터리를 생성하고 다음 코드를 wit/component.wit라는 파일에 붙여 넣으십시오:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
include wasi:cli/imports@0.2.0;
export add;
}
TinyGo의 wasip2 대상을 사용하고 있으므로 include wasi:cli/imports@0.2.0 줄이 필요합니다. TinyGo는 컴포넌트가 wasi:cli/command@0.2.0 월드(wasi:cli의 일부)를 대상으로 한다고 가정하므로 wasi:cli/imports@0.2.0의 임포트가 필요합니다.
wkg를 사용하여 임포트를 자동으로 해석하고 다운로드하기
wkg 같은 도구는 임포트를 해석하여 완전한 WIT 패키지를 빌드하는 데 편리합니다.
wkg wit build 명령을 실행하면 WIT가 컴포넌트 모델 바이너리 형식으로 인코딩됩니다. 부수 효과로, 이 명령은 임포트를 해석하고 관련된 모든 임포트된 네임스페이스와 패키지로 wit 폴더를 채웁니다.
$ wkg wit build
WIT package written to docs:adder@0.1.0.wasm
3. Wasm 컴포넌트용 바인딩 생성하기
이제 WIT 정의를 하나의 .wasm 파일로 묶었으므로, 빌드 지시문을 추가하여 WebAssembly 컴포넌트용 바인딩을 생성할 수 있습니다:
go tool wit-bindgen-go generate --world adder --out internal ./docs:adder@0.1.0.wasm
Note
go tool지시문(Golang 1.24에서 추가됨)은 Bytecode Alliance의 Golang 도구 모음에 포함된wit-bindgen-go를 설치하고 사용할 수 있게 합니다.
internal 디렉터리에는 해당 WIT 패키지에 대해 생성된 Go 코드가 들어 있습니다.
$ tree internal
internal
├── docs
│ └── adder
│ ├── add
│ │ ├── add.exports.go
│ │ ├── add.wasm.go
│ │ ├── add.wit.go
│ │ └── empty.s
│ └── adder
│ └── adder.wit.go
└── wasi
├── cli
│ ├── environment
│ │ ├── empty.s
│ │ ├── environment.wasm.go
│ │ └── environment.wit.go
│ ├── exit
│ │ ├── empty.s
│ │ ├── exit.wasm.go
│ │ └── exit.wit.go
│ ├── stderr
│ │ ├── empty.s
│ │ ├── stderr.wasm.go
│ │ └── stderr.wit.go
│ ├── stdin
│ │ ├── empty.s
│ │ ├── stdin.wasm.go
│ │ └── stdin.wit.go
│ ├── stdout
│ │ ├── empty.s
│ │ ├── stdout.wasm.go
│ │ └── stdout.wit.go
│ ├── terminal-input
│ │ ├── empty.s
│ │ ├── terminal-input.wasm.go
│ │ └── terminal-input.wit.go
│ ├── terminal-output
│ │ ├── empty.s
│ │ ├── terminal-output.wasm.go
│ │ └── terminal-output.wit.go
│ ├── terminal-stderr
│ │ ├── empty.s
│ │ ├── terminal-stderr.wasm.go
│ │ └── terminal-stderr.wit.go
│ ├── terminal-stdin
│ │ ├── empty.s
│ │ ├── terminal-stdin.wasm.go
│ │ └── terminal-stdin.wit.go
│ └── terminal-stdout
│ ├── empty.s
│ ├── terminal-stdout.wasm.go
│ └── terminal-stdout.wit.go
├── clocks
│ ├── monotonic-clock
│ │ ├── empty.s
│ │ ├── monotonic-clock.wasm.go
│ │ └── monotonic-clock.wit.go
│ └── wall-clock
│ ├── empty.s
│ ├── wall-clock.wasm.go
│ └── wall-clock.wit.go
├── filesystem
│ ├── preopens
│ │ ├── empty.s
│ │ ├── preopens.wasm.go
│ │ └── preopens.wit.go
│ └── types
│ ├── abi.go
│ ├── empty.s
│ ├── types.wasm.go
│ └── types.wit.go
├── io
│ ├── error
│ │ ├── empty.s
│ │ ├── error.wasm.go
│ │ └── error.wit.go
│ ├── poll
│ │ ├── empty.s
│ │ ├── poll.wasm.go
│ │ └── poll.wit.go
│ └── streams
│ ├── empty.s
│ ├── streams.wasm.go
│ └── streams.wit.go
├── random
│ ├── insecure
│ │ ├── empty.s
│ │ ├── insecure.wasm.go
│ │ └── insecure.wit.go
│ ├── insecure-seed
│ │ ├── empty.s
│ │ ├── insecure-seed.wasm.go
│ │ └── insecure-seed.wit.go
│ └── random
│ ├── empty.s
│ ├── random.wasm.go
│ └── random.wit.go
└── sockets
├── instance-network
│ ├── empty.s
│ ├── instance-network.wasm.go
│ └── instance-network.wit.go
├── ip-name-lookup
│ ├── abi.go
│ ├── empty.s
│ ├── ip-name-lookup.wasm.go
│ └── ip-name-lookup.wit.go
├── network
│ ├── abi.go
│ ├── empty.s
│ ├── network.wasm.go
│ └── network.wit.go
├── tcp
│ ├── abi.go
│ ├── empty.s
│ ├── tcp.wasm.go
│ └── tcp.wit.go
├── tcp-create-socket
│ ├── empty.s
│ ├── tcp-create-socket.wasm.go
│ └── tcp-create-socket.wit.go
├── udp
│ ├── abi.go
│ ├── empty.s
│ ├── udp.wasm.go
│ └── udp.wit.go
└── udp-create-socket
├── empty.s
├── udp-create-socket.wasm.go
└── udp-create-socket.wit.go
39 directories, 91 files
add.exports.go 파일에는 Exports 구조체가 들어 있으며, 이 구조체에는 Go 코드에서 구현해야 하는 익스포트된 함수의 선언이 담겨 있습니다.
4. add 함수 구현하기
add 디렉터리에 main.go라는 파일을 만들고 다음 코드를 붙여 넣으십시오:
//go:generate go tool wit-bindgen-go generate --world adder --out internal ./docs:adder@0.1.0.wasm
package main
import (
"example.com/internal/docs/adder/add"
)
func init() {
add.Exports.Add = func(x uint32, y uint32) uint32 {
return x + y
}
}
// main is required for the `wasi` target, even if it isn't used.
func main() {}
Go의 init 함수는 다른 어떤 작업보다 먼저 수행해야 하는 초기화 작업에 사용됩니다. 여기서는 이 함수를 사용하여 Add 함수를 익스포트합니다.
5. 컴포넌트 빌드하기
TinyGo를 사용하여 컴포넌트를 빌드할 수 있습니다. 내부적으로 TinyGo는 wasm-tools를 호출하여 WIT 파일을 모듈에 임베드하고 이를 컴포넌트화합니다.
tinygo build -target=wasip2 \
-o adder.wasm \
--wit-package docs:adder@0.1.0.wasm \
--wit-world adder main.go
-target=wasip2플래그는 Preview 2 메서드를 사용하여 코드를 WebAssembly로 컴파일하도록 지정합니다.-o adder.wasm플래그는 출력을 현재 디렉터리의add.wasm에 저장하도록 지정합니다.--wit-package플래그는 사용 중인 WIT 코드의 패키지 이름을 지정합니다.--wit-world플래그는 컴포넌트의 임포트와 익스포트를 정의하는 WIT 월드가adder임을 지정합니다.
이제 adder 월드를 충족하고 add 함수를 익스포트하는 adder 컴포넌트가 준비되었습니다.
Warning
기본적으로 tinygo는 디버그 관련 정보를 모두 .wasm 파일에 포함합니다. 이는 프로토타이핑이나 로컬 테스트 시 오류가 발생했을 때 유용한 백트레이스를 얻는 데 바람직합니다(예:
wasmtime::WasmBacktraceDetails::Enable사용 시). 디버그 데이터를 제거하고 바이너리 파일을 최적화하려면-no-debug로 빌드하십시오. 그 결과 생성되는 .wasm 파일은 상당히 작아집니다(최대 75%까지 크기 감소).
wasm-tools component wit 명령으로 확인할 수 있습니다:
$ wasm-tools component wit adder.wasm
package root:component;
world root {
import wasi:io/error@0.2.0;
import wasi:io/streams@0.2.0;
import wasi:cli/stdout@0.2.0;
import wasi:random/random@0.2.0;
export add: func(x: s32, y: s32) -> s32;
}
...
5. add 컴포넌트 테스트하기
다음 섹션을 진행하려면 Rust 툴체인이 설치되어 있어야 합니다.
add 컴포넌트를 실행하려면 example 월드를 이해하는 WASI 런타임을 갖춘 호스트 프로그램을 사용해야 합니다.
이 저장소에는 adder 월드를 구현하는 컴포넌트를 실행할 수 있는, Rust로 작성된 WebAssembly 호스트 예제가 포함되어 있습니다.
git clone https://github.com/bytecodealliance/component-docs.gitcd component-docs/component-model/examples/example-hostcargo run --release -- 1 2 <PATH>/adder.wasm
- 이중 대시는
cargo에 전달되는 플래그와 사용자 코드에 전달되는 플래그를 구분합니다. - 인수 1과 2는 adder에 전달되는 인수입니다.
<PATH>자리에는 생성된adder.wasm파일이 들어 있는 디렉터리를 대신 입력하십시오.
참고: 호스트가 WASI 인터페이스를 사용하는 컴포넌트를 실행할 때는, 빌드된 컴포넌트를 실행하기 위해 명시적으로 링커에 WASI를 추가해야 합니다.
실행에 성공하면 다음과 같은 출력이 표시됩니다(물론 예제 호스트와 adder 컴포넌트의 경로는 환경에 따라 다릅니다).
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/debug/example-host 1 2 /path/to/adder.wasm`
1 + 2 = 3
올바르게 구성되지 않은 경우 다음과 같은 오류가 표시될 수 있습니다:
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/release/example-host 1 2 /path/to/adder.component.wasm`
Error: Failed to instantiate the example world
Caused by:
0: component imports instance `wasi:io/error@0.2.2`, but a matching implementation was not found in the linker
1: instance export `error` has the wrong type
2: resource implementation is missing
이러한 종류의 오류는 일반적으로 해당 호스트가 WASI 임포트를 충족하지 못한다는 것을 나타냅니다.
MoonBit
MoonBit은 WebAssembly 컴포넌트를 포함한 최신 WebAssembly를 일급으로 지원하는 프로그래밍 언어입니다.
이 가이드에서는 MoonBit을 사용하여 WebAssembly 컴포넌트를 빌드하는 방법을 설명합니다. 인터페이스 정의에는 WIT(WebAssembly 인터페이스 타입)를, 코드 생성에는 wit-bindgen 툴체인을 활용합니다.
이 튜토리얼에서는 docs:adder 패키지에 정의된 adder 월드를 구현하는 컴포넌트를 빌드하는 과정을 단계별로 안내합니다. 이 컴포넌트는 두 수를 더하는 add 함수가 포함된 add 인터페이스를 익스포트합니다.
1. 도구 설치
MoonBit 설치하기
먼저 MoonBit 컴파일러와 툴체인을 설치하십시오. MoonBit 다운로드 페이지의 설치 안내를 따르십시오.
MoonBit 설치를 확인하십시오(아래는 작성 시점의 버전입니다):
$ moon version --all
moon 0.1.20250826 (8ab6c9e 2025-08-26) ~/.moon/bin/moon
moonc v0.6.25+d6913262c (2025-08-27) ~/.moon/bin/moonc
moonrun 0.1.20250826 (8ab6c9e 2025-08-26) ~/.moon/bin/moonrun
moon-pilot 0.0.1-95f12db ~/.moon/bin/moon-pilot
Wasm 툴체인 설치하기
다음 도구가 필요합니다:
- WIT에서 MoonBit 바인딩을 생성하기 위한 wit-bindgen
- 컴포넌트 변환을 위한 wasm-tools
- (선택 사항) Wasm 런타임으로 사용할 wasmtime
각 GitHub 릴리스에서 실행 파일을 다운로드하거나, Rust 툴체인이 설치되어 있다면 다음과 같이 설치할 수 있습니다:
cargo install wit-bindgen-cli
cargo install wasm-tools
cargo install wasmtime-cli
설치를 확인하십시오(아래는 작성 시점의 버전입니다):
$ wit-bindgen --version
wit-bindgen-cli 0.45.0
$ wasm-tools --version
wasm-tools 1.238.0
$ wasmtime --version
wasmtime 36.0.2
2. 인터페이스 정의하기(WIT)
MoonBit 프로젝트를 생성하기 전에 WIT를 사용하여 컴포넌트 인터페이스를 정의해야 합니다.
프로젝트용 디렉터리를 만들고 WIT 파일을 정의하십시오:
mkdir moonbit-adder && cd moonbit-adder
mkdir wit
다음 내용으로 wit/world.wit를 만드십시오:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
이 WIT 정의는 다음을 수행합니다:
- 버전이
0.1.0인docs:adder패키지를 선언합니다 u32매개변수 두 개를 받아u32를 반환하는 단일 함수가 있는add인터페이스를 정의합니다add인터페이스를 익스포트하는adder월드를 생성합니다
3. MoonBit 프로젝트 구조 생성하기
wit-bindgen을 사용하여 MoonBit 프로젝트 구조와 바인딩을 생성하십시오:
wit-bindgen moonbit wit/world.wit --out-dir . \
--derive-eq \
--derive-show \
--derive-error
이 명령은 다음과 같은 디렉터리 구조를 생성합니다:
.
├── ffi
│ ├── moon.pkg.json
│ └── top.mbt
├── gen
│ ├── ffi.mbt
│ ├── gen_interface_docs_adder_add_export.mbt
│ ├── interface
│ │ └── docs
│ │ └── adder
│ │ └── add
│ │ ├── moon.pkg.json
│ │ ├── stub.mbt
│ │ └── top.mbt
│ ├── moon.pkg.json
│ ├── world
│ │ └── adder
│ │ ├── moon.pkg.json
│ │ └── stub.mbt
│ └── world_adder_export.mbt
├── moon.mod.json
├── wit
│ └── world.wit
└── world
└── adder
├── ffi_import.mbt
├── import.mbt
├── moon.pkg.json
└── top.mbt
생성된 파일은 다음과 같습니다:
moon.mod.json: MoonBit 모듈 구성gen/: 생성된 익스포트 바인딩interface/: 생성된 익스포트 인터페이스 바인딩world/: 생성된 익스포트 월드 바인딩stub.mbt: 주요 구현 파일
interface/: 생성된 임포트 인터페이스 바인딩world/: 생성된 임포트 월드 바인딩
wit-bindgen 도구는 WebAssembly 컴포넌트 인터페이스를 처리하는 MoonBit 바인딩을 생성합니다.
moon check를 실행하면 gen/interface/docs/adder/add/stub.mbt 파일에 미완성 코드가 있다는 경고가 표시되며, 이 코드가 우리가 완성해야 할 부분입니다.
4. 컴포넌트 로직 구현하기
gen/interface/docs/adder/add/stub.mbt에 add 함수를 구현하십시오:
#![allow(unused)]
fn main() {
// Generated by `wit-bindgen` 0.45.0.
///|
pub fn add(x : UInt, y : UInt) -> UInt {
x + y
}
}
5. 빌드 구성하기
gen/moon.pkg.json이 WebAssembly 대상에 맞게 올바르게 구성되어 있는지 확인하십시오:
{
// link configuration for Wasm backend
"link": {
"wasm": {
"exports": [
// Export for cabi_realloc
"cabi_realloc:cabi_realloc",
// Export per the interface definition
"wasmExportAdd:docs:adder/add@0.1.0#add"
],
"export-memory-name": "memory",
"heap-start-address": 16
}
},
"import": [
{
"path": "docs/adder/ffi",
"alias": "ffi"
},
{
"path": "docs/adder/gen/interface/docs/adder/add",
"alias": "add"
}
]
}
5. WebAssembly 컴포넌트 빌드하기
MoonBit 코드를 WebAssembly 코어 모듈로 빌드하십시오:
moon build
생성한 모듈로부터 올바른 WebAssembly 컴포넌트를 만들려면 wasm-tools를 사용하십시오:
wasm-tools component embed wit target/wasm/release/build/gen/gen.wasm \
--encoding utf16 \
--output adder.wasm
wasm-tools component new adder.wasm --output adder.component.wasm
wasm-tools를 사용하여 컴포넌트의 인터페이스를 확인할 수 있습니다:
wasm-tools component wit adder.component.wasm
출력되는 WIT는 다음과 완전히 같지는 않더라도 비슷해야 합니다:
package root:component;
world root {
export docs:adder/add@0.1.0;
}
package docs:adder@0.1.0 {
interface add {
add: func(x: u32, y: u32) -> u32;
}
}
6. 컴포넌트 테스트하기
예제 호스트 사용하기
컴포넌트를 테스트하려면 이 저장소에서 제공하는 example-host를 사용하십시오:
git clone https://github.com/bytecodealliance/component-docs.git
cd component-docs/component-model/examples/example-host
cp /path/to/adder.component.wasm .
cargo run --release -- 5 3 adder.component.wasm
예상 출력:
5 + 3 = 8
Wasmtime 사용하기
wasmtime으로 컴포넌트를 직접 테스트할 수도 있습니다:
$ wasmtime run --invoke 'add(10, 20)' adder.component.wasm
30
고급
–derive-eq –derive-show
이 두 옵션은 생성된 모든 타입에 derive(Eq) 및/또는 derive(Show)를 추가합니다.
–derive-error
이 옵션은 이름에 ’Error’가 포함된 variant / 열거형을 하위 오류(suberror)로 생성합니다. 이를 통해 MoonBit의 오류 처리와 더 쉽게 통합할 수 있습니다.
예를 들어, 다음 인터페이스의 경우:
package docs:adder@0.1.0;
interface add {
variant computation-error {
overflow
}
add: func(x: u32, y: u32) -> result<u32, computation-error>;
}
world adder {
import add;
}
다음 타입과 함수가 생성됩니다
#![allow(unused)]
fn main() {
// Generated by `wit-bindgen` 0.45.0. DO NOT EDIT!
///|
pub(all) suberror ComputationError {
Overflow
} derive(Show, Eq)
///|
pub fn add(x : UInt, y : UInt) -> Result[UInt, ComputationError] {
let return_area = @ffi.malloc(8)
wasmImportAdd(x.reinterpret_as_int(), y.reinterpret_as_int(), return_area)
let lifted4 = match @ffi.load8_u(return_area + 0) {
0 => Result::Ok(@ffi.load32(return_area + 4).reinterpret_as_uint())
1 => {
let lifted = match @ffi.load8_u(return_area + 4) {
0 => ComputationError::Overflow
_ => panic()
}
Result::Err(lifted)
}
_ => panic()
}
@ffi.free(return_area)
return lifted4
}
}
이는 다음과 같이 사용할 수 있습니다:
#![allow(unused)]
fn main() {
// Generated by `wit-bindgen` 0.45.0.
///|
fn init {
let _ = @add.add(1, 2).unwrap_or_error() catch { Overflow => ... }
}
}
–ignore-stub
이는 인터페이스가 업데이트되어 프로젝트를 다시 생성하려고 하지만 stub 파일은 변경되지 않기를 원하는 경우에 해당합니다. 이러한 수정을 방지하려면 --ignore-stub 옵션을 사용할 수 있습니다.
–project-name
기본적으로 프로젝트 이름은 MoonBit 파일에 정의된 이름에 따라 생성됩니다. 이 옵션을 사용하여 프로젝트 이름을 지정할 수 있습니다. 더 큰 프로젝트의 일부로 프로젝트를 생성하는 경우에도 이 옵션을 사용할 수 있습니다.
–gen-dir
기본적으로 내보내기 부분은 gen 아래에 생성됩니다. 이 옵션을 사용하여 다른 디렉터리를 지정할 수 있습니다.
참고 자료 및 추가 읽을거리
언어 독립적 도구
wasm-tools는 WebAssembly 모듈과 컴포넌트를 다루기 위한 일련의 하위 명령을 제공합니다.
WAT (WebAssembly 텍스트 형식)
WAT(WebAssembly 텍스트 형식)는 wasm-tools 및 기타 도구를 사용하여 WebAssembly 바이너리 형식으로 컴파일할 수 있는 텍스트 기반 언어입니다. 테스트와 실험을 위한 작은 예제를 작성할 때 유용합니다.
다음은 WAT로 표현한 모듈의 예입니다:
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "docs:adder/add@0.1.0#add" (func $add))
)
이 모듈에는 함수와 익스포트라는 두 개의 최상위 선언이 있습니다.
이 함수 선언은 $lhs와 $rhs라는 두 개의 인수를 받는 $add라는 이름의 함수를 선언합니다. (WAT에서 변수 이름은 항상 $로 시작합니다.) 인수 타입과 결과 타입은 명시적으로 지정해야 합니다. 이 경우 두 인수와 결과의 타입은 모두 i32(32비트 정수)입니다. 함수 본문은 WebAssembly 명령어의 목록입니다. 두 개의 local.get 명령어는 $lhs와 $rhs의 값을 스택에 푸시합니다. i32.add 명령어는 스택 맨 위의 두 값을 팝하여 더한 다음, 그 결과를 스택에 남깁니다.
export 선언은 방금 선언한 함수를 외부에서 호출할 때 사용할 이름에 연결합니다. 이 WAT 코드를 사용하여 WIT 파일에 지정된 인터페이스를 구현하려 하므로, 외부 이름은 특정 규칙을 따라야 합니다. "docs:adder/add@0.1.0#add"라는 이름은 다음과 같이 나누어 볼 수 있습니다:
docs는 패키지 이름입니다.adder는docs패키지 안에 있는 월드의 이름입니다.add는 해당 월드에 정의된 인터페이스의 이름입니다.- 0.1.0은 버전 번호입니다.
- 이와 별개로,
add는add인터페이스에 정의된 함수의 이름입니다. 이 모든 요소는 사용 중인 특정.wit파일에서 비롯됩니다(아래 참조).
WAT로 할 수 있는 일은 이보다 훨씬 많습니다. 자세한 내용은 Mozilla Developer Network의 WAT 상세 가이드를 참조하십시오.
wat2wasm 도구는 WAT를 바이너리 .wasm 형식으로 변환하지만, 컴포넌트를 생성하지는 않습니다.
wasm-tools를 사용하여 WAT로 컴포넌트 빌드하기
wasm-tools를 사용하면 WAT로 컴포넌트를 생성할 수 있습니다. 다음은 adder 월드를 구현하고 단순히 두 수를 더하는 컴포넌트를 WAT로 생성하는 방법입니다.
-
Wasm 모듈과 컴포넌트를 저수준에서 조작하기 위한 도구인
wasm-tools를 설치하십시오. -
add함수는 다음 월드 안에 정의되어 있습니다. 다음과 같은 내용으로adder.wit라는 파일을 생성하십시오:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
- 두 매개변수를 더하는
add함수를 익스포트하는add코어 모듈을 WAT로 정의하십시오. 다음과 같은 내용으로add.wat라는 파일을 생성하십시오(WAT 섹션의 예제와 동일합니다):
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "docs:adder/add@0.1.0#add" (func $add))
)
-
wasm-tools를 사용하여 컴포넌트 메타데이터가 내장된 바이너리 코어 모듈을 생성하십시오:wasm-tools component embed adder.wit add.wat -o add.wasm -
방금 만든 바이너리 코어 모듈로부터 새 컴포넌트
.wasm파일을 생성하려면wasm-tools를 사용하십시오:wasm-tools component new add.wasm -o add.component.wasm.component.wasm접미사는 관례일 뿐입니다. 출력 파일 이름을add_component.wasm으로 지정하거나.wasm접미사가 붙은 다른 이름으로 지정할 수도 있습니다.
Wasmtime으로 컴포넌트 실행하기
컴포넌트의 익스포트 중 하나를 호출하여 컴포넌트를 “실행“할 수 있습니다. 호스트와 런타임은 특정 익스포트가 있는 컴포넌트의 실행만 지원하는 경우가 많습니다.
wasmtime CLI를 사용하면 방금 빌드한 컴포넌트의 add 함수에 인수를 전달하여 실행할 수 있습니다:
wasmtime run --invoke 'add(1, 2)' add.component.wasm
출력은 3입니다. 괄호 안의 인수를 변경하여 add()에 다른 인수를 전달해 볼 수 있습니다.
이 예제는 wasmtime 34.0.1에서 테스트되었습니다. 이전 버전의 wasmtime은 --invoke 옵션을 지원하지 않을 수 있습니다. 컴포넌트를 지원하고 표준을 준수하는 다른 WebAssembly 런타임에서도 이 컴포넌트를 실행할 수 있습니다.
컴포넌트 모델을 지원하는 기타 언어
이 가이드는 아직 작성 중이며, WebAssembly 컴포넌트를 지원하는 모든 언어 툴체인의 예제를 다루지는 않습니다.
더 많은 언어가 컴포넌트 모델 지원을 계속 추가함에 따라, 해당 언어에서 컴포넌트를 사용하기 시작하는 방법에 대한 문서 기여를 환영합니다.
이 섹션에 언어 예제 추가하기
사용 중인 언어 툴체인에 대해 이 섹션을 문서화하는 데 관심이 있으십니까? 이 섹션의 다른 가이드와 유사한 가이드를 추가하는 PR을 이 저장소에 생성하십시오.
컴포넌트 가져오기 및 재사용
이 섹션에서는 기존 WebAssembly 컴포넌트를 재사용하는 방법에 대한 언어별 가이드를 제공하며, 특히 adder 컴포넌트를 사용하여 calculator 컴포넌트를 완성하는 방법을 다룹니다.
adder 컴포넌트에는 다음과 같은 WIT 인터페이스가 있습니다.
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
calculator 컴포넌트에는 다음과 같은 인터페이스가 있습니다.
package docs:calculator@0.1.0;
interface calculate {
enum op {
add,
}
eval-expression: func(op: op, x: u32, y: u32) -> u32;
}
world calculator {
export calculate;
import docs:adder/add@0.1.0;
}
world app {
import calculate;
}
언어
이 가이드는 다양한 언어로 구현되어 있습니다.
| 언어 |
|---|
| Rust |
| Javascript |
| 기타 언어 |
컴포넌트 임포트 및 재사용하기 (Rust)
인터페이스 임포트하기
앞서 생성한 월드 파일(wit/world.wit)에는 임포트가 지정되어 있지 않습니다. 컴포넌트가 다른 컴포넌트를 사용하는 경우, world.wit 파일을 편집하여 해당 컴포넌트의 인터페이스를 임포트할 수 있습니다.
예를 들어, 이전 튜토리얼에서 설명한 대로 adder 컴포넌트를 생성하고 빌드했으며, 이 컴포넌트를 calculator 컴포넌트에서 사용하려 한다고 가정하겠습니다. 다음은 add 인터페이스를 임포트하는 calculator 월드 예제의 일부입니다:
// in the 'calculator' project
// wit/world.wit
package docs:calculator;
interface calculate {
eval-expression: func(expr: string) -> u32;
}
world calculator {
import docs:adder/add@0.1.0;
export calculate;
}
임포트할 패키지 참조하기
docs:adder 패키지는 다른 프로젝트에 있으므로, 먼저 wkg에 이 패키지를 찾는 방법을 알려 주어야 합니다. 이를 위해 프로젝트에 사용자 지정 wkg.toml을 추가합니다:
[overrides]
"docs:adder" = { path = "../adder/wit" } # directory containing the WIT package
이 구성 파일을 추가한 후 wkg wit fetch를 실행하면, wkg는 패키지 docs:adder를 지정된 경로에서 찾을 수 있다고 가정하고 그 내용을 로컬 프로젝트의 wit/deps 아래로 가져옵니다.
Rust에서 임포트 호출하기
이제 adder의 WIT 파일에 있는 add 선언을 calculator 프로젝트에서 볼 수 있습니다.
calculate 구현에서 임포트한 add 인터페이스를 호출하려면 다음과 같이 하십시오:
#![allow(unused)]
fn main() {
// src/lib.rs
// Generated code that includes both the import stubs for adder functionality
// and the stubs for exports is generated into the `bindings` module below
//
// Note that while wit_bindgen::generate only creates stubs for imports,
// not implementation -- this component will be built with *unsatisfied*
// (but usable) imports (e.g. the add).
mod bindings {
use super::Component;
wit_bindgen::generate!({
generate_all
});
export!(Component);
}
/// The struct from which all implementation will hang
struct Component;
// Implementation of the `docs:calculator/calculate` export
impl bindings::exports::docs::calculator::calculate::Guest for Component {
fn eval_expression(expr: String) -> u32 {
// TODO: Cleverly parse `expr` into values and operations, and evaluate them meticulously.
bindings::docs::calculator::add::add(123, 456)
}
}
}
eval_expression의 구현을 채우고 실제 표현식을 파싱하는 작업은 독자의 연습 과제로 남겨 둡니다.
임포트 충족하기
cargo build를 사용하여 빌드하면 add 인터페이스는 충족되지 않은 상태(즉, 임포트된 상태)로 남습니다.
calculator는 add 인터페이스에 대한 의존성을 갖게 되었지만, 해당 인터페이스의 adder 구현을 링크하지는 않았습니다. 이는 adder 크레이트를 참조하는 것과는 다릅니다(실제로 calculator는 WIT 파일을 구현하는 Rust 프로젝트가 없더라도 add 인터페이스를 임포트할 수 있습니다).
wasm-tools component wit를 실행하여 calculator의 월드를 확인하면 이를 알 수 있습니다:
# Do a release build to prune unused imports (e.g. WASI)
$ cargo build --target=wasm32-wasip2 --release
$ wasm-tools component wit ./target/wasm32-wasip1/release/calculator.wasm
package root:component;
world root {
import docs:adder/add@0.1.0;
export docs:calculator/calculate@0.1.0;
}
임포트가 충족되지 않았으므로, calculator.wasm 컴포넌트는 현재 형태로는 단독으로 실행될 수 없습니다. 다음 단계는 add 임포트를 충족하여 calculate만 익스포트되도록 하고, 컴포넌트를 실행할 수 있게 만드는 것입니다.
다른 컴포넌트의 익스포트를 통해 임포트를 충족하는 과정을 “합성“이라고 합니다. calculator.wasm을 adder.wasm과 합성하여 하나의 독립된 컴포넌트로 만드는 방법에 대한 자세한 내용은 컴포넌트 합성 가이드를 참조하십시오.
컴포넌트 가져오기 및 재사용 (Javascript)
Javascript 컴포넌트에서 사용하기 위한 기존 코드 합성
유용한 WebAssembly 컴포넌트를 빌드하는 데 기능을 export하는 것이 핵심인 것처럼, 기능을 import하고 재사용하는 것은 WebAssembly의 강점을 활용하는 데 핵심입니다.
다시 말해, WIT와 컴포넌트 모델은 WebAssembly가 합성할 수 있게 해 줍니다. 이는 이미 존재하는 기능을 기반으로 빌드하고, 기존 기능에 의존하는 새로운 기능을 export할 수 있다는 의미입니다.
(이전 예제에서) 문자열을 뒤집는 것에 더해, 받은 텍스트를 또한 대문자로 변환하는 공유 기능을 빌드하려 한다고 가정해 보겠습니다.
뒤집기 기능을 재사용하는 동시에 뒤집기와 대문자 변환을 수행할 수 있는 새 인터페이스를 익스포트할 수 있습니다.
문자열을 뒤집는 기능과 대문자로 변환하는 기능을 노출하는 string-reverse-upper라는 jco 예제 프로젝트를 살펴보겠습니다.
이 기능을 가능하게 하기 위해 작성할 수 있는 WIT는 다음과 같습니다:
package example:string-reverse-upper@0.1.0;
@since(version = 0.1.0)
interface reversed-upper {
reverse-and-uppercase: func(s: string) -> string;
}
world revup {
//
// NOTE, the import below translates to:
// <namespace>:<package>/<interface>@<package version>
//
import example:string-reverse/reverse@0.1.0;
export reversed-upper;
}
이번에 빌드하는 revup이라는 이름의 world는 네임스페이스 example의 패키지 string-reverse에 있는 인터페이스 reverse에 의존합니다.
revup 월드를 구현하는 컴포넌트와 그 기능을 합성하기만 하면, 해당 인터페이스와 일치하는 어떤 WebAssembly 컴포넌트든 사용할 수 있습니다.
revup 월드는 reverse-and-uppercase 함수(JavaScript에서는 reverseAndUppercase)를 포함하는 reversed-upper 인터페이스를 export(제공)하기 위해 reverse를 import(하여 사용)합니다.
Note
기능은
world가 아니라interface를 통해 가져옵니다.world도 포함하거나 사용할 수 있지만, 이를 위한 구문은 약간 다릅니다.
이를 동작하게 하는 JavaScript(jco/examples의 string-reverse-upper.js)는 다음과 같습니다:
/**
* This module is the JS implementation of the `revup` WIT world
*/
/**
* The import here is *virtual*. It refers to the `import`ed `reverse` interface in component.wit.
*
* These types *do not resolve* when the first `string-reverse-upper` component is built,
* but the types are relevant for the resulting *composed* component.
*/
import { reverseString } from 'example:string-reverse/reverse@0.1.0';
/**
* The JavaScript export below represents the export of the `reversed-upper` interface,
* which which contains `revup` as it's primary exported function.
*/
export const reversedUpper = {
/**
* Represents the implementation of the `reverse-and-uppercase` function in the `reversed-upper` interface
*
* This function makes use of `reverse-string` which is *imported* from another WebAssembly binary.
*/
reverseAndUppercase(s) {
return reverseString(s).toLocaleUpperCase();
},
};
위의 WIT 파일을 wit 하위 디렉터리에 두는 경우, wit/deps 하위 디렉터리도 생성하고 ../string-reverse/wit/component.wit를 wit/deps에 복사해야 합니다.
jco componentize로 컴포넌트를 빌드할 수 있습니다:
npx jco componentize \
string-reverse-upper.mjs \
--wit wit/ \
--world-name revup \
--out string-reverse-upper.incomplete.wasm \
--disable=all
오류 메시지가 표시되면,
wit/component.wit파일이package example:string-reverse-upper@0.1.0;으로 시작하는지, 그리고wit/deps/디렉터리에package example:string-reverse@0.1.0;으로 시작하는 파일이 있는지 확인하십시오. 일반적으로 주 패키지는wit디렉터리의 최상위에 있어야 하며, 모든 의존성은 해당 디렉터리의 하위 디렉터리(일반적으로deps)에 있어야 합니다.
WebAssembly 컴포넌트를 성공적으로 빌드했지만, 다른 예제들과 달리 이 컴포넌트는 아직 완성되지 않았습니다.
jco wit를 실행하여 생성된 컴포넌트의 WIT를 출력해 보면 이를 확인할 수 있습니다:
npx jco wit string-reverse-upper.incomplete.wasm
다음과 같은 출력이 표시되어야 합니다:
package root:component;
world root {
import example:string-reverse/reverse@0.1.0;
export example:string-reverse-upper/reversed-upper@0.1.0;
}
이는 컴포넌트에 아직 충족되지 않은 import가 있음을 알려 줍니다. reverse에 있는 reverseString 함수가 존재하는 것처럼 사용하고 있지만, 이 함수는 아직 WebAssembly 컴포넌트의 실제 일부가 아닙니다(그래서 이름을 .incomplete.wasm으로 지었습니다).
앞서 빌드한 두 컴포넌트(string-reverse-upper/string-reverse-upper.incomplete.wasm와 string-reverse/string-reverse.wasm)를 합성하려면 WebAssembly 합성 도구(wac)가 필요합니다. wac plug를 사용할 수 있습니다:
wac plug \
-o string-reverse-upper.wasm \
--plug ../string-reverse/string-reverse.wasm \
string-reverse-upper.incomplete.wasm
Note
jco저장소의 전체 프로젝트를 사용하는 경우,npm run compose로도 이 단계를 실행할 수 있습니다.
이제 “완전한” 컴포넌트인 새 컴포넌트 string-reverse-upper.wasm이 생성되어 있어야 합니다. npx jco wit의 출력을 확인하여 모든 임포트가 충족되었는지 확인할 수 있습니다:
npx jco wit string-reverse-upper.wasm
다음과 같은 출력이 표시되어야 합니다:
package root:component;
world root {
export example:string-reverse-upper/reversed-upper@0.1.0;
}
마치 아무 기능도 가져오지 않은 것과 같습니다. string-reverse.wasm에 있던 기능이 string-reverse-upper.wasm에 병합되었으며, 이제 이 컴포넌트는 고급 기능을 export하기만 합니다.
트랜스파일된 결과를 사용하면 WebAssembly를 지원하는 모든 네이티브 JavaScript 환경에서 이 완성된 컴포넌트를 실행할 수 있습니다:
npx jco transpile string-reverse-upper.wasm -o dist/transpiled
Note
예제 프로젝트에서는 대신
npm run transpile을 실행할 수 있으며, 이 명령은dist/transpiled/string-reverse-upper.js의 확장자도.mjs로 변경합니다.
다음과 같은 출력이 표시되어야 합니다:
Transpiled JS Component Files:
- dist/transpiled/interfaces/example-string-reverse-upper-reversed-upper.d.ts 0.12 KiB
- dist/transpiled/string-reverse-upper.core.wasm 10.1 MiB
- dist/transpiled/string-reverse-upper.core2.wasm 10.1 MiB
- dist/transpiled/string-reverse-upper.d.ts 0.19 KiB
- dist/transpiled/string-reverse-upper.js 6.13 KiB
Tip
두 개의 코어 WebAssembly 파일이 있다는 점에 주목하십시오. 이는 우리에게 필요한 최종 기능을 만드는 데 두 개의 코어 WebAssembly 모듈이 관여했기 때문입니다.
트랜스파일된 컴포넌트를 실행하려면 다음과 같은 코드를 작성할 수 있습니다:
/**
* If this import listed below is missing, please run
*
* ```
* npm run build && npm run compose && npm run transpile`
* ```
*/
import { reversedUpper } from "./dist/transpiled/string-reverse-upper.js";
const result = reversedUpper.reverseAndUppercase("!dlroW olleH");
console.log(`reverseAndUppercase('!dlroW olleH') = ${result}`);
Note
jco예제 프로젝트에서는npm run transpiled-js를 실행할 수 있습니다.
다음과 같은 출력이 표시되어야 합니다:
reverseAndUppercase('!dlroW olleH') = HELLO WORLD!
jco transpile을 사용하여 Javsacript에서 컴포넌트 실행하기
재사용 가능한 기능을 WebAssembly 컴포넌트로 패키징하더라도 그 기능을 노출할 방법이 없다면 쓸모가 없습니다. 이 섹션에서는 WebAssembly 컴포넌트에서 WIT를 사용하는 방법을 조금 더 깊이 살펴봅니다.
다른 컴포넌트(또는 WebAssembly 호스트)가 사용할 수 있도록 WIT 인터페이스를 export하는 것은 WebAssembly 프로그램 개발의 기본입니다.
문자열을 뒤집는 기능을 노출하는 string-reverse라는 jco 예제 프로젝트를 살펴보겠습니다.
string-reverse와 같은 프로젝트를 처음부터 빌드하려면 먼저 다음과 같은 WIT로 시작합니다. string-reverse라는 새 디렉터리에서 이 코드를 wit/component.wit라는 파일에 붙여 넣으십시오:
package example:string-reverse@0.1.0;
@since(version = 0.1.0)
interface reverse {
reverse-string: func(s: string) -> string;
}
world string-reverse {
export reverse;
}
WIT에 대한 조금 더 깊이 있는 속성 강좌로서, 위 코드가 설명하는 내용은 다음과 같습니다:
example이라는 네임스페이스를 정의했습니다.example네임스페이스 안에string-reverse라는 패키지를 정의했습니다.- 이 WIT 파일은
example:string-reverse패키지의0.1.0버전에 해당합니다. reverse-string이라는 함수 하나를 포함하는reverse라는 인터페이스를 정의했습니다.reverse인터페이스가0.1.0버전부터 존재했음을 명시합니다.reverse-string함수(정규화된 전체 이름은example:string-reverse/reverse.reverse-string)는 문자열을 받아 문자열을 반환합니다.reverse인터페이스가 제공하는 기능을 익스포트하는string-reverse라는world를 정의했습니다.
Warning
reverse가 실제로 문자열을 뒤집는다는 것을 어떻게 알 수 있을까요?안타깝게도 이 문제는 이 수준에서는 사실상 해결할 수 없습니다. 이는 여러분과 해당 WIT 인터페이스를 구현하는 컴포넌트 작성자 사이의 문제입니다.
물론 WebAssembly에서는 원한다면 어떤 바이너리든 실행하기 전에 정적 검사를 강제할 수 있습니다.
이제 component 월드를 구현하는 JS 코드가 어떤 모습인지 살펴보겠습니다. 다음 코드를 string-reverse.mjs라는 파일에 붙여 넣으십시오:
/**
* This module is the JS implementation of the `string-reverse` WIT world
*/
/**
* The JavaScript export below represents the export of the `reverse` interface,
* which which contains `reverse-string` as its primary exported function.
*/
export const reverse = {
/**
* This JavaScript will be interpreted by `jco` and turned into a
* WebAssembly binary with a single export (this `reverse` function).
*/
reverseString(s) {
return s.split("")
.reverse()
.join("");
}
};
JavaScript에는 내장된 문자열 뒤집기 메서드가 없으므로, 이 코드는
split()을 사용해 문자열을 문자 배열로 변환하고, 배열을 뒤집은 다음,join()을 사용해 배열을 다시 문자열로 변환합니다.
Note
설명과 함께 전체 코드 목록을 보려면
examples/tutorials/jco/string-reverse폴더를 참조하십시오.
jco를 사용하여 이 컴포넌트를 컴파일하려면 string-reverse 디렉터리 안에서 다음을 실행할 수 있습니다:
npx jco componentize \
--wit wit/component.wit \
--world-name string-reverse \
--out string-reverse.wasm \
--disable=all \
string-reverse.mjs
다음과 같은 출력이 표시되어야 합니다:
OK Successfully written string-reverse.wasm.
Note
이전 예제와 마찬가지로 고급 WebAssembly 시스템 인터페이스 기능을 전혀 사용하지 않으므로 모두
--disable합니다.
string-reverse폴더의 코드를 사용하는 경우,jco componentize명령을 직접 입력하는 대신npm run build로 빌드 명령을 실행할 수도 있습니다.
이제 WebAssembly 바이너리가 있으므로, (어디에서 왔든 상관없는!) WebAssembly 바이너리를 JavaScript 모듈로 트랜스파일하여 jco로 네이티브 JavaScript 컨텍스트에서 실행할 수도 있습니다.
npx jco transpile string-reverse.wasm -o dist/transpiled
다음과 같은 출력이 표시되어야 합니다:
Transpiled JS Component Files:
- dist/transpiled/interfaces/example-string-reverse-reverse.d.ts 0.1 KiB
- dist/transpiled/string-reverse.core.wasm 10.1 MiB
- dist/transpiled/string-reverse.d.ts 0.15 KiB
- dist/transpiled/string-reverse.js 2.55 KiB
Tip
참고로, 트랜스파일은 TypeScript 프로젝트에서 사용할 수 있는 TypeScript 선언 파일을 실제로 생성합니다.
이제 트랜스파일된 모듈이 있으므로, 코어 WebAssembly를 지원하는 모든 JavaScript 환경(Node.js든 브라우저든)에서 이를 실행할 수 있습니다.
Node.js의 경우 다음과 같은 코드를 사용할 수 있습니다. 다음 코드를 string-reverse 디렉터리의 run.js라는 파일에 붙여 넣으십시오:
// If this import listed below is missing, please run `npm run transpile`
import { reverse } from "./dist/transpiled/string-reverse.js";
const reversed = reverse.reverseString("!dlroW olleH");
console.log(`reverseString('!dlroW olleH') = ${reversed}`);
Note
jco예제 프로젝트에서는npm run transpiled-js를 실행하여 기존 코드를 빌드할 수 있습니다.
이전과 마찬가지로 package.json 파일도 필요합니다:
{
"name": "string-reverse",
"description": "Simple codebase for compiling a string reversing interface to WebAssembly with jco",
"type": "module"
}
그런 다음 다음을 실행하십시오:
node run.js
(이전 단계에서 jco transpile을 실행하여) dist/transpiled 폴더가 채워져 있다고 가정하면, 다음과 같은 출력이 표시되어야 합니다:
reverseString('!dlrow olleh') = hello world!
이 맥락에서는 다소 중복되지만, NodeJS에서 수행한 작업은 WebAssembly와 jco 툴체인의 유용성을 보여 줍니다. jco의 도움으로 다음을 수행했습니다:
- WIT로 정의된 인터페이스를 준수하여 JavaScript를 WebAssembly 모듈로 컴파일했습니다(
jco compile) - 컴파일된 WebAssembly 모듈(어떤 언어로 작성된 것이든 가능)을 호환되는 모든 JS 런타임에서 사용할 수 있는 모듈로 변환했습니다(
jco transpile) - 트랜스파일된 WebAssembly 컴포넌트를 JavaScript 네이티브 런타임(NodeJS)에서 실행했습니다
컴포넌트 가져오기 및 재사용(기타 언어)
이 가이드는 작성 중이며, 컴포넌트를 지원하는 모든 언어 툴체인의 예제를 포함하고 있지는 않습니다.
이 가이드에 나와 있지 않은 언어의 경우에도, 해당되는 범위에서 다른 가이드의 주요 원칙을 따르고 로컬 WebAssembly 툴체인의 도움을 받으면 WebAssembly 컴포넌트를 임포트하여 재사용할 수 있는 경우가 많습니다.
일반적으로 해당 언어의 WebAssembly 툴체인은 다음을 수행하는 방법을 포함해야 합니다.
- 다른 컴포넌트를 임포트하는 컴포넌트 만들기
- 컴포넌트를 로드하고 실행할 수 있는 호스트/플랫폼 생성
- 컴포넌트 합성(빌드 시점에 수행할 수도 있음)
wac와 같은 범용 도구를 사용하면 언어와 관계없이 컴포넌트를 합성할 수 있다는 점에 유의하십시오.
가이드에 새 언어 추가하기
사용하시는 언어 툴체인에 대해 이 섹션을 문서화하는 데 관심이 있으십니까? 이 저장소에 (이 섹션의 다른 가이드와 유사한) 가이드를 추가하는 PR을 생성하십시오.
실행 가능한 컴포넌트 만들기
이 섹션에는 실행 가능한 컴포넌트를 만드는 방법에 대한 언어별 가이드가 포함되어 있습니다.
리액터(라이브러리와 유사한) 컴포넌트에서 임의의 함수 실행하기
실제로 WebAssembly 컴포넌트에서 내보낸 모든 인터페이스는 다음 방법 중 하나로 실행할 수 있습니다:
- 기능을 임포트하고 재사용하는 사용자 정의 호스트/컴포넌트 만들기
wasmtime run --invoke와 같은 고수준의 범용 도구 사용하기
예를 들어, 다음 WIT를 충족하는 WebAssembly 컴포넌트가 있다고 가정합니다:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
add 인터페이스 내부에서 익스포트된 add 함수를 사용하려면 해당 기능을 임포트하고 사용하도록 빌드된 호스트나 다른 컴포넌트를 작성해야 합니다. 그 예시는 이 저장소에 있는 example-host에서 확인할 수 있습니다.
또는 wasmtime run --invoke처럼 컴포넌트에 대해 범용적으로 동작하는 도구를 사용할 수 있습니다:
wasmtime run --invoke 'add(1, 2)' add.component.wasm
Wasmtime에는 익스포트를 범용적으로 해석하고, 인수를 WebAssembly 인수로 변환하며, 기존 컴포넌트를 동적으로 실행할 수 있는 코드가 포함되어 있습니다.
바이너리처럼 동작하는 컴포넌트 만들기
임의의 함수를 실행하려면 사용자 정의 호스트/플랫폼이나 wasmtime run --invoke와 같은 동적 도구가 필요하지만, 바이너리처럼 취급할 수 있는 컴포넌트(즉, CLI 애플리케이션)도 빌드할 수 있습니다.
개괄적으로 보면, 라이브러리보다는 바이너리에 가까운(즉, wasmtime run과 같은 도구로 쉽게 실행할 수 있는) 컴포넌트를 만드는 방법은 최소 두 가지가 있습니다:
- “command” 컴포넌트 만들기
wasi:cli/run인터페이스 내보내기
command 컴포넌트는 단순히 _start 익스포트를 가지는 반면, wasi:cli/run run 인터페이스를 내보내는 컴포넌트는 WASI를 통해 stdout, stderr와 같은 CLI 환경을 모방하는, 더 견고하고 표준화된 인터페이스를 활용할 수 있습니다.
이 섹션에서는 관련 언어에서 (바이너리처럼) 간단하게 실행할 수 있는 컴포넌트를 만드는 방법을 살펴봅니다.
언어
이 가이드는 다양한 언어에 대해 구현되어 있습니다:
| 언어 |
|---|
| Rust |
| Javascript |
| 기타 언어 |
실행 가능한 컴포넌트 만들기 (Rust)
이 페이지에는 WASI 0.2와 WASI 0.3에 대한 내용이 모두 포함되어 있습니다. 버전 간에 차이가 있는 부분에서는 아래 탭을 사용하여 버전을 전환하십시오.
WASI 0.3 툴체인 참고 사항. Rust의
wasm32-wasip3대상은 현재 Tier 3이며 미리 빌드된 아티팩트가 없습니다. 이 대상으로 빌드하려면 표준 라이브러리를 소스에서 직접 빌드해야 합니다. 따라서 이 페이지의 0.3 예제는wasm32-wasip2를 타깃으로 하는 라이브러리/리액터 패턴을 사용하며, 여기서는wit-bindgen의async기능이 0.3 바인딩 생성을 처리합니다.fn main()커맨드 컴포넌트 패턴에 대해서는 아직 Rust다운 0.3 방식이 없습니다.
command 컴포넌트 만들기
커맨드는 wasmtime(또는 다른 wasi:cli 호스트)에서 직접 실행할 수 있도록 하는 특정 익스포트를 가진 컴포넌트입니다. Rust 관점에서 보면, 이는 라이브러리 크레이트(lib) 패키지가 아니라 main 함수가 있는 애플리케이션(bin) 패키지에 해당합니다.
command 컴포넌트는 컴포넌트에 자연스러운 main 형태의 시작점이 있음을 나타내는 WebAssembly 코어 익스포트 _start를 포함하는 방식으로 동작합니다.
1. 새 Rust 바이너리 프로젝트 만들기
cargo로 커맨드를 만들려면 다음을 실행하십시오:
cargo new runnable-example
라이브러리 컴포넌트와 달리, 여기에는 --lib 플래그가 없습니다(cargo new의 기본값은 --bin입니다).
생성된 Rust 소스 파일의 이름은 lib.rs가 아니라 main.rs이며, main 함수를 포함합니다.
이 프로젝트에서는 직접 만든 크레이트나 서드파티 크레이트를 임포트하는 것을 포함하여 평소와 똑같이 Rust 코드를 작성할 수 있습니다.
프로젝트를 구성하는 모든 크레이트는 빌드 시점에 함께 링크되어 하나의 Wasm 컴포넌트로 컴파일됩니다. 이 경우 모든 링크는 Rust 수준에서 이루어지며, WIT나 컴포넌트 합성은 관여하지 않습니다. Wasm 인터페이스를 임포트하는 경우에만 WIT와 합성이 관여하게 됩니다.
2. 관련 Rust 코드 작성하기
다음 코드를 runnable-example/src/main.rs에 삽입할 수 있습니다:
pub fn main() {
eprintln!("Hello World!");
}
3. 컴포넌트 빌드하기
컴포넌트를 빌드하려면 cargo를 사용하십시오:
cargo build --target=wasm32-wasip2
컴포넌트를 릴리스 모드로 빌드할 수도 있습니다:
cargo build --target=wasm32-wasip2 --release
4. wasmtime으로 컴포넌트 실행하기
command 컴포넌트를 실행하려면 다음과 같이 하십시오.
wasmtime run ./target/wasm32-wasip2/debug/runnable-example.wasm
0.3
wasi:cli/run인터페이스를 익스포트하는 실행 가능한 컴포넌트에 대해서는 아래의 라이브러리/리액터 패턴을 참조하십시오.
라이브러리 컴포넌트를 wasi:cli/run 인터페이스를 통해 실행할 수 있도록 하기
때로는 (익스포트된 인터페이스를 통해) 라이브러리로 사용할 수 있으면서도 command 컴포넌트처럼 단독으로 실행할 수도 있는 컴포넌트를 만드는 것이 유용합니다.
리액터(라이브러리형) 컴포넌트는 직접 사용하도록 의도된 인터페이스를 익스포트하지만, WASI CLI의 wasi:cli/run 인터페이스를 함께 익스포트하여 이 라이브러리가 명령줄 인터페이스를 통해 실행되는 바이너리와 유사하게 실행될 수도 있음을 소비자에게 알릴 수 있습니다.
command 컴포넌트와 달리 라이브러리 컴포넌트에는 _start가 없지만, wasi:cli/run 인터페이스를 익스포트하면 이러한 익스포트를 인식하는 도구가 주어진 WebAssembly 바이너리를 쉽게 실행할 수 있습니다(예: wasmtime run).
1. 새 Rust 라이브러리 프로젝트 생성하기
wasi:cli/run을 익스포트하는 간단한 컴포넌트를 빌드하려면 먼저 새 Rust 프로젝트를 생성하십시오:
cargo new --lib runnable-example
새 프로젝트를 생성한 후 runnable-example/Cargo.toml을 수정하여 다음 줄을 추가함으로써 cdylib 크레이트가 되도록 하십시오:
[lib]
crate-type = ['cdylib']
WIT 인터페이스로부터 Rust 바인딩도 생성할 것이므로 wit-bindgen을 추가하십시오:
cargo add wit-bindgen
2. 적절한 WIT 인터페이스 추가하기
그런 다음 적절한 WIT 인터페이스를 추가하십시오. 예를 들어 “Hello World“를 출력하는 간단한 컴포넌트의 경우, runnable-example/wit/component.wit에 다음 내용을 추가하십시오:
package example:runnable;
interface greet {
greet: func(name: string) -> string;
}
world greeter {
export greet;
export wasi:cli/run@0.2.7;
}
이러한 방식으로 라이브러리 컴포넌트를 빌드하면 다음 두 가지가 이루어집니다:
- 다른 컴포넌트/호스트가
greet인터페이스를 사용할 수 있게 합니다 - 이 컴포넌트가 CLI처럼 실행될 수 있음을 나타내는 인터페이스(
wasi:cli/run)를 노출합니다- 컴포넌트가 실행될 때 무엇을 하는지에 대해서는 아무것도 보장되지 않는다는 점에 유의하십시오
greet는 우리가 만든 것이지만, wasi:cli는 잘 알려진 인터페이스입니다. wkg를 사용하여 이 인터페이스를 로컬 WIT로 해석할 수 있습니다:
wkg wit fetch
WASI 0.3의 경우
wkg0.15 이상을 사용하십시오. 이전 버전은wasi:cli@0.3.0-rc-2026-03-15패키지를 디코딩하지 못합니다.
이 시점에서 deps 하위 폴더와 원래의 component.wit이 포함된 wit 폴더가 있어야 합니다.
다른 컴포넌트나 플랫폼이 greet 인터페이스 익스포트를 사용할 수 있으므로, 위 WIT를 충족하도록 만들 컴포넌트는 라이브러리로 사용될 수 있습니다. 더 중요한 점은, 이 컴포넌트가 wasi:cli/run 덕분에 범용적으로 실행 가능한 컴포넌트로도 인식될 수 있으므로, wasi:cli 인터페이스를 지원하거나 인식하는 모든 도구(예: wasmtime run)와 함께 동작할 수 있다는 것입니다.
3. 컴포넌트 코드 작성하기
다음 코드를 runnable-example/src/lib.rs에 삽입할 수 있습니다:
#![allow(unused)]
fn main() {
mod bindings {
use super::Component;
wit_bindgen::generate!();
export!(Component);
}
/// Component off of which implementation will hang (this can be named anything)
struct Component;
/// Implementation for the `greet` interface export
impl bindings::exports::example::runnable::greet::Guest for Component {
fn greet(name: String) -> String {
format!("Hello {name}!")
}
}
/// Implementation for `wasi:cli/run` interface export
impl bindings::exports::wasi::cli::run::Guest for Component {
fn run() -> Result<(), ()> {
eprintln!("Hello World!");
Ok(())
}
}
}
4. 컴포넌트 빌드하기
컴포넌트를 빌드하려면 cargo를 사용하십시오:
cargo build --target=wasm32-wasip2
컴포넌트는 릴리스 모드로도 빌드할 수 있습니다:
cargo build --target=wasm32-wasip2 --release
5. wasmtime으로 컴포넌트 실행하기
wasmtime으로 컴포넌트를 실행할 수 있으며, 일반적인 리액터 컴포넌트와 달리 실행할 인터페이스와 함수를 지정할 필요가 없습니다(wasi:cli/run이 자동으로 감지되어 사용됩니다):
$ wasmtime run target/wasm32-wasip2/debug/runnable_example.wasm
Hello World!
Cargo는 라이브러리 크레이트 이름의 하이픈을 밑줄로 변환하므로, 패키지를
runnable-example로 생성했더라도 아티팩트는runnable_example.wasm이 됩니다.
버전 고정. WASI 0.3 도구(
wit-bindgen, Wasmtime, jco 등)는 모두 동일한 WIT 버전을 대상으로 해야 합니다. 2026-06-11 WASI 0.3.0 릴리스 시점 기준으로wit-bindgen0.58과 Wasmtime 45는 여전히 WIT의0.3.0-rc-2026-03-15스냅샷을 포함하고 있으므로, 해당 도구들이 갱신되기 전까지 게시된0.3.0으로 고정하면 실행 시점에no exported instance named wasi:cli/run@0.2.6(또는 이와 유사한) 오류가 발생합니다. 그때까지는 RC 버전으로 고정하십시오.
실행 가능한 컴포넌트 만들기 (Javascript)
wasi:cli/run을 익스포트하여 만들기
jco로 만든 컴포넌트는 다른 언어로 작성된 WebAssembly 컴포넌트와 마찬가지로 wasi:cli/run 인터페이스를 익스포트할 수 있습니다.
wasi:cli/run 인터페이스를 익스포트하면 생태계 도구가 여러분이 빌드한 컴포넌트와 상호 운용(및 실행)할 수 있습니다(예: wasmtime run). wasi:cli/run을 준수하는 컴포넌트는 매우 간결하게 작성할 수 있습니다.
예를 들면 다음과 같습니다.
export const run = {
run() {
console.log("Hello World!");
}
}
다음 WIT를 사용하면 wasi:cli를 인식하는 도구가 위 컴포넌트를 “실행 가능한” 컴포넌트로 인식하도록 만들 수 있습니다:
package runnable:js-component;
world component {
export wasi:cli/run@0.2.4;
}
실행 가능한 컴포넌트 만들기 (기타 언어)
이 가이드는 아직 작성 중이며, 컴포넌트를 지원하는 모든 언어 툴체인에 대한 예제를 포함하고 있지는 않습니다.
이 가이드에 나와 있지 않은 언어의 경우, 사용 가능한 WebAssembly 툴체인의 도움을 받아 다른 가이드에서 다루는 핵심 개념을 적용하면 실행 가능한 컴포넌트를 만들 수 있는 경우가 많습니다.
일반적으로 해당 언어의 WebAssembly 툴체인에는 다음을 수행하는 방법이 포함되어 있을 수 있습니다.
_start익스포트를 가진 WebAssembly 컴포넌트(“command” 컴포넌트)를 생성합니다wasi:cli/run인터페이스를 익스포트하는 컴포넌트를 생성합니다
이 섹션에 언어 예제 추가하기
사용하시는 언어 툴체인에 대해 이 섹션의 문서를 작성하는 데 관심이 있으십니까? 이 섹션의 다른 가이드와 유사하게 가이드를 추가하는 PR을 이 저장소에 생성하십시오.
WIT 리소스 사용하기
이 섹션에서는 WIT 리소스를 사용하는 방법에 대한 언어별 가이드를 제공합니다.
리소스는 컴포넌트 경계의 한쪽에서만 구현되는 기능을 나타냅니다. 예를 들어 다른 컴포넌트나 기반 플랫폼/호스트에서 구현되는 기능이 이에 해당합니다.
리소스의 예는 다음과 같습니다.
package docs:calc-resource@0.1.0;
interface types {
enum operation {
add,
sub,
mul,
div,
}
variant execute-error {
divide-by-zero,
unexpected(string),
}
resource stack-calculator {
constructor();
push-operand: func(operand: u32);
push-operation: func(operation: operation);
execute: func() -> result<u32, error>;
}
}
world calculator {
export types;
}
위의 스택 기반 계산기를 구현하는 호스트나 컴포넌트는 해당 기능을 import하는 컴포넌트와 리소스(stack-calculator 엔티티)를 공유하지만, 실제 구현은 공유하지 않습니다. stack-calculator 리소스에 대한 모든 호출은 해당 기능을 export한 컴포넌트 내부에서 처리됩니다.
언어
이 가이드는 다음과 같은 여러 언어로 구현되어 있습니다.
| 언어 |
|---|
| Rust |
WIT 리소스 사용하기(Rust)
리소스는 컴포넌트 외부(즉, 호스트나 다른 컴포넌트)에 존재하는 엔터티에 대한 핸들입니다.
스택 기반 역폴란드 표기법(RPN) 계산기 예제
이 섹션에서 예제로 사용할 리소스는 역폴란드 표기법(RPN) 계산기입니다. (어느 정도 연배가 있는 엔지니어라면 1970년대 휴대용 계산기에서 이를 기억할 것입니다.)
RPN 계산기는 상태를 가진 엔터티입니다. 사용자는 계산기 내부에서 유지되는 스택에 피연산자와 연산을 푸시한 다음, 스택을 평가하여 값을 산출합니다.
WIT에서 이 리소스는 다음과 같습니다:
package docs:rpn@0.1.0;
interface types {
enum operation {
add,
sub,
mul,
div
}
resource engine {
constructor();
push-operand: func(operand: u32);
push-operation: func(operation: operation);
execute: func() -> u32;
}
}
world calculator {
export types;
}
컴포넌트에서 리소스 구현 및 내보내기
Rust로 계산기를 구현하려면 다음과 같이 하십시오:
-
라이브러리 컴포넌트를 생성하고, 위의 WIT를
wit/world.wit로 저장한 다음,wit-bindgen을 추가하십시오:cargo add wit-bindgenCargo.toml에cdylib크레이트 타입도 반드시 포함하십시오:[lib] crate-type = ["cdylib"] -
계산기 상태를 나타내는 Rust
struct를 정의하십시오:#![allow(unused)] fn main() { use std::cell::RefCell; struct CalcEngine { stack: RefCell<Vec<u32>>, } }스택을
RefCell로 감싸는 이유는 무엇입니까? 뒤에서 보겠지만, 계산기 엔진에 대해 생성된 Rust 트레이트는self에 대한 불변 참조를 가집니다. 하지만 이 트레이트를 구현할 때는 스택을 변경해야 합니다. 따라서RefCell<T>나Arc<RwLock<T>>와 같이 내부 가변성을 허용하는 타입이 필요합니다. -
내보낸 리소스에 대해 생성된 바인딩(
bindings.rs)에는GuestX라는 이름의 트레이트가 포함되며, 여기서X는 리소스 이름입니다. 계산기engine리소스의 경우 이 트레이트는GuestEngine입니다. 2단계에서 정의한struct에 이 트레이트를 구현하십시오:#![allow(unused)] fn main() { mod bindings { wit_bindgen::generate!(); } use bindings::exports::docs::rpn::types::{Guest, GuestEngine, Operation}; impl GuestEngine for CalcEngine { fn new() -> Self { CalcEngine { stack: RefCell::new(vec![]) } } fn push_operand(&self, operand: u32) { self.stack.borrow_mut().push(operand); } fn push_operation(&self, operation: Operation) { let mut stack = self.stack.borrow_mut(); let right = stack.pop().unwrap(); // TODO: error handling! let left = stack.pop().unwrap(); let result = match operation { Operation::Add => left + right, Operation::Sub => left - right, Operation::Mul => left * right, Operation::Div => left / right, }; stack.push(result); } fn execute(&self) -> u32 { self.stack.borrow_mut().pop().unwrap() // TODO: error handling! } } } -
이제
engine계약을 구현하는 동작하는 계산기 타입이 있지만, 이 타입을engine리소스 타입에 연결하는 작업이 아직 남아 있습니다. 이는 생성된Guest트레이트를 구현하여 수행합니다. 이 WIT의 경우Guest트레이트에는 연관 타입 외에는 아무것도 포함되어 있지 않습니다. 빈struct를 사용하여Guest트레이트를 구현할 수 있습니다. 리소스에 대한 연관 타입(이 예제에서는Engine)을 리소스 트레이트를 구현하는 타입(이 예제에서는GuestEngine을 구현하는CalcEnginestruct)으로 설정하십시오. 그런 다음export!매크로를 사용하여 매핑을 익스포트합니다:#![allow(unused)] fn main() { struct Component; impl Guest for Component { type Engine = CalcEngine; } bindings::export!(Component); }
이것으로 계산기 engine 리소스의 구현이 완료됩니다. cargo build --target=wasm32-wasip2를 실행하여 컴포넌트 .wasm 파일을 생성하십시오.
컴포넌트에서 리소스 가져오기 및 사용하기
다른 컴포넌트에서 계산기 엔진을 사용하려면 해당 컴포넌트가 리소스를 임포트해야 합니다.
-
이전 섹션에서 설명한 대로 실행 가능한 컴포넌트를 생성하십시오.
-
프로젝트에
wit/component.wit를 추가하고 RPN 계산기 타입을 임포트하는 WIT 월드를 작성하십시오:package docs:rpn-cmd; world app { import docs:rpn/types@0.1.0; } -
docs:rpn(engine리소스를 포함함)에 대한 관련 WIT 파일을 가져올 수 있도록wkg.toml파일을 생성하십시오:[overrides] "docs:rpn" = { path = "../path/to/docs-rpn/wit" }이 작업을 마친 후
wkg wit fetch를 실행하여 모든 WIT를 로컬에서 사용할 수 있도록 할 수 있습니다. -
이제 리소스는 생성된 바인딩에서 적절한 연관 함수를 갖춘
struct로 나타납니다. 이를 사용하여 테스트 앱을 구성하십시오.mod bindings { wit_bindgen::generate!({ generate_all, }); } use bindings::docs::rpn::types::{Engine, Operation}; fn main() { let calc = Engine::new(); calc.push_operand(1); calc.push_operand(2); calc.push_operation(Operation::Add); let sum = calc.execute(); println!("{sum}"); }
컴포넌트를 있는 그대로 빌드하면 충족되지 않은 임포트, 즉 docs:rpn/types 임포트가 있는 WebAssembly 컴포넌트가 생성됩니다.
컴포넌트를 빌드한 후에는 리소스를 구현하는 .wasm 컴포넌트와 합성해야 합니다.. 합성을 통해 충족되지 않은 임포트가 없는 컴포넌트가 생성되면, 합성된 command 컴포넌트를 wasmtime run으로 실행할 수 있습니다.
또는 docs:rpn/types 임포트(및 관련 리소스)를 제공할 수 있는 호스트를 사용하여 컴포넌트를 “불완전한” 상태로 실행할 수도 있습니다(호스트가 예상되는 임포트를 제공하여 컴포넌트를 “완성“하기 때문입니다).
호스트에서 리소스 구현을 구현하고 익스포트하기
Wasm 런타임을 호스팅하는 경우, 게스트가 사용할 수 있도록 호스트에서 리소스를 익스포트할 수 있습니다.
런타임 호스팅은 이 책의 범위를 벗어나므로 여기서는 대략적인 개요만 제공합니다. 이는 Wasmtime 런타임에 한정된 내용이며, 다른 런타임에서는 다르게 표현될 수 있습니다.
-
wasmtime::component::bindgen!을 사용하여 호스트로서 제공할 WIT를 지정하십시오.#![allow(unused)] fn main() { wasmtime::component::bindgen!({ path: "../wit" }); } -
with필드를 통해 호스트에서 리소스를 어떻게 표현할지bindgen!에 알려 주십시오. 이는 어떤 Rust 타입이든 될 수 있습니다. 예를 들어 RPN 엔진은CalcEngine구조체로 표현할 수 있습니다.#![allow(unused)] fn main() { wasmtime::component::bindgen!({ path: "../wit", with: { "docs:rpn/types/engine": CalcEngine, } }); }리소스의 호스트 표현을 지정하지 않으면 기본값으로 빈 열거형이 사용됩니다. 리소스는 대개 상태를 가지므로 이는 거의 유용하지 않습니다.
-
표현 타입이 내장 타입이 아니라면 이를 정의하십시오.
#![allow(unused)] fn main() { struct CalcEngine { /* ... */ } } -
호스트로서 이미
Host트레이트를 구현하고 있을 것입니다. 이제Host트레이트와 동일한 타입에HostX트레이트(여기서X는 리소스 이름)를 구현해야 합니다.#![allow(unused)] fn main() { impl docs::rpn::types::HostEngine for MyHost { fn new( &mut self, ) -> wasmtime::Result<wasmtime::component::Resource<docs::rpn::types::Engine>> { /* ... */ } fn push_operand( &mut self, self_: wasmtime::component::Resource<docs::rpn::types::Engine>, operand: u32, ) -> wasmtime::Result<()> { /* ... */ } // etc. } }중요: 이 트레이트는 리소스 표현이 아니라 ‘전체’ 호스트 타입에 구현합니다! 따라서 이 함수들의
self참조는 ‘전체’ 호스트 타입을 가리킵니다. 리소스의 인스턴스 메서드의 경우, 인스턴스는wasmtime::component::Resource타입의 두 번째 매개변수(self_)로 식별됩니다. -
호스트에
wasmtime::component::ResourceTable을 추가하십시오.#![allow(unused)] fn main() { struct MyHost { calcs: wasmtime::component::ResourceTable, } } -
리소스 메서드 구현에서 이 테이블을 사용하여 리소스 표현의 인스턴스를 저장하고 접근하십시오.
#![allow(unused)] fn main() { impl docs::rpn::types::HostEngine for MyHost { fn new( &mut self, ) -> wasmtime::Result<wasmtime::component::Resource<docs::rpn::types::Engine>> { self.calcs.push(CalcEngine::new()) } fn push_operand( &mut self, self_: wasmtime::component::Resource<docs::rpn::types::Engine>, operand: u32, ) -> wasmtime::Result<()> { let calc_engine = self.calcs.get(&self_)?; // calc_engine is a CalcEngine - call its functions calc_engine.push_operand(operand); Ok(()) } // etc. } }
컴포넌트에서 HTTP 사용하기
이 섹션에는 WebAssembly 컴포넌트로 간단한 HTTP 서비스를 제공하는 방법에 대한 언어별 가이드가 포함되어 있습니다.
언어
이 가이드는 다양한 언어로 구현되어 있습니다:
| 언어 |
|---|
| Rust |
Rust
1. 설정
Rust 툴체인에 wasm32-wasip2 대상을 추가하십시오.
#![allow(unused)]
fn main() {
rustup target add wasm32-wasip2
}
wasmtime을 설치하십시오. Wasmtime CLI에는 WebAssembly HTTP 컴포넌트 제공을 지원하는 HTTP 서버가 내장되어 있습니다.
curl https://wasmtime.dev/install.sh -sSf | bash
2. Rust WebAssembly 프로젝트 생성하기
cargo new로 새 Rust 프로젝트를 생성하십시오:
cargo new wasm-http-hello-world
cd wasm-http-hello-world
Wasm 컴포넌트용 Rust 비동기 표준 라이브러리인 wstd를 cargo add로 의존성에 추가하십시오:
cargo add wstd
wstd는 Rust WebAssembly 컴포넌트의 사용 편의성을 높이기 위해 WASI 표준 인터페이스 (wasi:http)에 대한 관용적인 Rust 바인딩을 제공합니다. wstd를 사용하므로 WIT 파일을 추가하거나 wit-bindgen에 직접 의존할 필요가 없습니다.
Note
wstd없이도 Rust로 HTTP 컴포넌트를 빌드할 수 있습니다.wstd없이 HTTP 컴포넌트를 빌드하려면 컴포넌트의wasi:http임포트/익스포트를 WIT로 정의하고,wkg로 WIT 의존성을 가져오고,wit-bindgen으로 Rust 바인딩을 생성해야 합니다.두 방식 모두 유효하지만
wstd가 더 뛰어난 개발자 경험을 제공하므로 여기서는 이를 사용합니다.
wstd와wit-bindgen은 상호 배타적이지 않으며 같은 프로젝트에서 공존할 수 있습니다.
3. HTTP 핸들러 작성하기
src/main.rs에 HTTP 핸들러를 구현합니다. 파일은 다음과 같아야 합니다:
use wstd::http::{Body, Request, Response, Result, StatusCode};
// WASI HTTP server components don't use a traditional `main` function.
// They export a function named `handle` which takes a `Request`
// argument, and which may be called multiple times on the same
// instance. To let users write a familiar `fn main` in a file
// named src/main.rs, wstd provides this `wstd::http_server` macro, which
// transforms the user's `fn main` into the appropriate `handle` function.
#[wstd::http_server]
async fn main(req: Request<Body>) -> Result<Response<Body>> {
match req.uri().path() {
"/" => home(req).await,
_ => not_found(req).await,
}
}
async fn home(_req: Request<Body>) -> Result<Response<Body>> {
Ok(Response::new("Hello, world!\n".into()))
}
async fn not_found(_req: Request<Body>) -> Result<Response<Body>> {
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(().into())
.expect("builder succeeds"))
}
4. 컴포넌트 컴파일 및 실행하기
컴포넌트 빌드하기:
cargo build --release --target wasm32-wasip2
컴포넌트의 .wasm 바이너리는 target/wasm32-wasip2/release/wasm-http-hello-world.wasm에서 찾을 수 있습니다.
컴포넌트를 실행하려면 컴포넌트 모델을 지원하는 참조 구현 호스트인 wasmtime을 사용할 수 있습니다.
특히 wasmtime serve 하위 명령을 사용할 수 있으며, 이 명령은 http://localhost:8080에서 HTTP 서버를 실행하고 우리 컴포넌트를 사용하여 웹 요청을 처리합니다. wasmtime은 요청을 처리할 때마다 컴포넌트의 새로운 인스턴스를 생성합니다.
wasmtime serve -Scli -Shttp target/wasm32-wasip2/release/wasm-http-hello-world.wasm
curl -i localhost:8080으로 테스트할 수 있습니다
HTTP/1.1 200 OK
transfer-encoding: chunked
date: Mon, 13 Apr 2026 23:22:20 GMT
Hello, world!
이로써 Rust로 기본적인 WebAssembly HTTP 컴포넌트를 성공적으로 빌드하고 실행했습니다 🎉
5. 더 나아가기
wstd를 사용하는 프로젝트의 더 많은 예제를 살펴보십시오:
컴포넌트 실행하기
런타임이 지원하는 표준 WIT 월드는 두 가지입니다. 이 두 월드는 wasi:cli/command 월드와 wasi:http/proxy 월드입니다. 그 밖의 모든 WIT 월드와 인터페이스는 사용자 정의로 간주됩니다. 다음 섹션에서는 두 월드 중 하나를 구현하는 컴포넌트를 실행하는 방법과 사용자 정의 익스포트를 호출하는 방법을 살펴봅니다.
Wasmtime
이 페이지에는 WASI 0.2와 WASI 0.3에 대한 내용이 모두 포함되어 있습니다. 버전별로 내용이 다른 부분에서는 아래 탭을 사용하여 버전을 전환하십시오.
Wasmtime은 컴포넌트 모델의 참조 구현입니다. Wasmtime은 wasi:cli/command 월드를 구현하는 컴포넌트의 실행과 wasi:http/proxy 월드를 구현하는 컴포넌트의 서빙을 지원합니다. Wasmtime은 컴포넌트에서 내보낸 함수를 호출할 수도 있습니다.
Wasmtime으로 command 컴포넌트 실행하기
Wasmtime으로 command 컴포넌트를 실행하려면 다음을 실행하십시오:
wasmtime run <path-to-wasm-file>
이전 버전의
wasmtime을 사용하는 경우, 코어 모듈이 아닌 컴포넌트를 실행한다는 것을 지정하기 위해--wasm component-model플래그를 추가해야 할 수 있습니다.
기본적으로 Wasmtime은 컴포넌트가 모든 시스템 리소스에 접근하는 것을 거부합니다. 예를 들어, 컴포넌트는 파일 시스템이나 환경 변수에 접근할 수 없습니다. 접근 권한 부여 방법과 기타 Wasmtime 기능에 대한 정보는 Wasmtime 가이드를 참조하십시오.
Wasmtime으로 HTTP 컴포넌트 실행하기
wasmtime serve 하위 명령을 사용하여 HTTP 프록시 월드를 구현하는 컴포넌트를 실행할 수 있습니다. Wasmtime CLI는 v18.0.0부터 이러한 컴포넌트의 서빙을 지원합니다.
Wasmtime으로 HTTP 컴포넌트를 실행하려면 다음을 실행하십시오:
wasmtime serve <path-to-wasm-file>
다음 튜토리얼 중 하나를 통해 HTTP 컴포넌트를 빌드하고 실행해 보십시오
-
Hello WASI HTTP 튜토리얼 - 간단한 Rust 기반 HTTP 컴포넌트를 빌드하고 서빙합니다
-
HTTP 인증 미들웨어 튜토리얼 - HTTP 인증 미들웨어 컴포넌트를 비즈니스 로직 컴포넌트와 합성합니다
사용자 정의 익스포트가 있는 컴포넌트 실행하기
Wasmtime 버전 33.0.0부터 사용자 정의 익스포트가 있는 컴포넌트 호출을 지원합니다.
예를 들어, 컴포넌트가 두 개의 숫자 인수를 받는 함수 add를 익스포트하는 경우, 다음 명령으로 이 기능을 사용할 수 있습니다.
wasmtime run --invoke 'add(1, 2)' <path-to-wasm-file>
함수가 인수를 받지 않더라도 호출을 작은따옴표로 감싸고 괄호를 반드시 포함하십시오. 내보낸 함수에 인수를 전달할 때 다양한 WIT 타입을 표현하는 방법의 전체 목록은 WAVE 저장소를 방문하여 확인하십시오.
jco
jco는 JavaScript에서 컴포넌트를 다루기 위한 완전한 네이티브 JavaScript 도구입니다. wasi:cli/command 월드를 지원합니다. jco는 또한 WebAssembly 컴포넌트를 ECMAScript 모듈(ES 모듈)로 트랜스파일하는 기능과 JavaScript 및 WIT로부터 WebAssembly 컴포넌트를 빌드하는 기능도 제공합니다.
jco로 컴포넌트를 실행하려면 다음을 실행하십시오.
jco run <path-to-wasm-file> <command-args...>
jco의 WASI 구현은 컴포넌트에 기반 시스템 리소스에 대한 전체 접근 권한을 부여합니다. 예를 들어, 컴포넌트는 jco 프로세스의 모든 환경 변수를 읽거나 파일 시스템의 어느 위치에서든 파일을 읽고 쓸 수 있습니다.
컴포넌트 합성 및 배포
컴포넌트 모델은 컴포넌트가 서로 간에, 그리고 호스트와 어떻게 인터페이스하는지를 정의합니다. 이 섹션에서는 사용자 정의 코드로 컴포넌트를 작성하거나 기존 컴포넌트를 합성하는 것부터 애플리케이션에서 컴포넌트를 사용하고 레지스트리를 통해 배포하는 것까지, 컴포넌트를 다루는 방법을 설명합니다.
컴포넌트 합성하기
WebAssembly 컴포넌트 모델을 사용하면 애플리케이션과 컴포넌트가 원래 어떤 언어로 작성되었는지와 관계없이 함께 동작할 수 있습니다. 컴포넌트 모델은 코드를 이식 가능한 바이너리 형식으로 패키징하고, 표준화된 애플리케이션 바이너리 인터페이스(ABI)와 함께 WIT로 작성된 기계 판독 가능한 인터페이스를 제공하여 이를 실현합니다. 예를 들어 Rust 패키지(크레이트)를 다른 Rust 코드와 함께 컴파일하여 더 상위 수준의 라이브러리나 애플리케이션을 만들 수 있는 것과 마찬가지로, WebAssembly 컴포넌트도 다른 컴포넌트와 합성할 수 있습니다.
컴포넌트 모델의 상호 운용은 언어별 외부 함수 인터페이스(FFI)보다 더 편리하고 표현력이 뛰어납니다. 일반적인 C FFI는 언어별 타입을 사용하므로, 최소한 어느 정도의 C 언어 래핑이나 변환 없이는 임의의 언어 간에 링크할 수 없습니다. 반면 컴포넌트 모델은 인터페이스를 표현하는 공통된 방식과 해당 인터페이스의 표준 바이너리 표현을 제공합니다. 따라서 임포트와 익스포트의 형태가 같다면 둘은 곧바로 맞물립니다.
합성이란 무엇입니까?
컴포넌트를 합성할 때는 하나의 주 컴포넌트의 임포트를 하나 이상의 다른 의존성 컴포넌트의 익스포트에 연결하여 새 컴포넌트를 만듭니다. 새 컴포넌트는 원래 컴포넌트와 마찬가지로 .wasm 파일이며, 그 인터페이스는 다음과 같이 정의됩니다.
- 새 컴포넌트는 주 컴포넌트와 동일한 익스포트를 익스포트합니다.
- 새 컴포넌트는 의존성 컴포넌트의 익스포트를 익스포트하지 않습니다.
- 새 컴포넌트는 의존성 컴포넌트의 모든 임포트를 임포트합니다.
- 새 컴포넌트는 주 컴포넌트의 임포트 중 의존성이 충족하지 못한 모든 임포트를 임포트합니다.
- 여러 컴포넌트가 같은 인터페이스를 임포트하는 경우, 새 컴포넌트는 해당 인터페이스를 임포트하며, 그 임포트가 여러 다른 곳에서 선언되었다는 사실을 “기억“하지 않습니다.
예를 들어, 다음과 같은 월드를 가진 두 컴포넌트를 생각해 보십시오.
// component `validator`
package docs:validator@0.1.0;
interface validator {
validate-text: func(text: string) -> string;
}
world validator {
export validator;
import docs:regex/match@0.1.0;
}
// component 'regex'
package docs:regex@0.1.0;
interface match {
first-match: func(regex: string, text: string) -> string;
}
world regex {
export match;
}
이 예제에서 validator는 주 컴포넌트이고 regex는 의존성 컴포넌트입니다. validator를 regex와 합성하면, validator의 docs:regex/match@0.1.0 임포트가 regex의 match 익스포트에 연결됩니다. 최종적으로 합성된 컴포넌트는 docs:validator/validator@0.1.0를 익스포트하며 임포트는 없습니다. 합성된 컴포넌트는 docs:regex/match@0.1.0를 익스포트하지 않습니다. 이는 합성된 컴포넌트의 내부 구현 세부 사항이 되었습니다.
컴포넌트 합성 도구는 현재 초기 단계에 있습니다. 다음은 오류를 방지하거나 진단하기 위한 몇 가지 팁입니다.
- 합성은 인터페이스 수준에서 이루어집니다. 초기 컴포넌트가 함수를 직접 임포트하면 합성이 실패합니다. 합성 과정에서 “컴포넌트
path/to/component에<name>이라는 이름의 비인스턴스 임포트가 있습니다“와 같은 오류가 보고되면, 모든 임포트와 익스포트가 인터페이스로 정의되어 있는지 확인하십시오. - 합성은 비대칭적입니다. 합성은 단순히 “컴포넌트를 서로 붙이는 것“이 아니라, 임포트가 있는 주 컴포넌트를 가져와 의존성 컴포넌트를 사용하여 그 임포트를 충족하는 것입니다. 예를 들어
validator에는regex가 충족할 수 있는 의존성이 있으므로validator구현을regex구현과 합성하는 것은 타당하지만, 반대로 하는 것은 동작하지 않습니다.regex에는validator가 충족할 수 있는 의존성은커녕 의존성 자체가 전혀 없기 때문입니다. - 합성은 인터페이스 버전을 따지며, 현재 도구들은 버전을 추론하거나 주입하는 시점이 일관되지 않습니다. 다른 Rust 컴포넌트가
test:mypackage에서 인터페이스를 임포트하는 경우, 이는test:mypackage@0.1.0와 일치하지 않습니다.wasm-tools component wit를 사용하면.wasm파일에 포함된 임포트와 익스포트를 확인하고 서로 일치하는지 점검할 수 있습니다.
WAC로 컴포넌트 합성하기
WebAssembly Compositions (WAC) CLI를 사용하여 명령줄에서 컴포넌트를 합성할 수 있습니다.
빠르고 간단한 합성을 수행하려면 wac plug 명령을 사용하십시오. wac plug는 “플러그” 컴포넌트의 익스포트를 “소켓” 컴포넌트에 꽂아 소켓 컴포넌트의 임포트를 충족합니다. 소켓 컴포넌트는 주 컴포넌트이고, 플러그 컴포넌트는 의존성 컴포넌트입니다. 예를 들어, 위의 validator 월드를 구현하는 컴포넌트는 자신의 match 임포트를 충족해야 합니다. 이 컴포넌트가 소켓입니다. 반면 regex 월드를 구현하는 컴포넌트는 match 인터페이스를 익스포트하며, 플러그로 사용할 수 있습니다. wac plug는 regex 컴포넌트의 익스포트를 validator 컴포넌트의 임포트에 꽂아 다음과 같은 합성 결과물을 만들 수 있습니다.
wac plug validator-component.wasm --plug regex-component.wasm -o composed.wasm
컴포넌트는 자신이 의존하는 둘 이상의 컴포넌트와 합성할 수도 있습니다.
wac plug path/to/component.wasm --plug path/to/dep1.wasm --plug path/to/dep2.wasm -o composed.wasm
여기서 component.wasm은 인터페이스를 익스포트하는 dep1.wasm과 dep2.wasm에서 인터페이스를 임포트하는 컴포넌트입니다. 이러한 의존성이 충족되어 내부에 담긴 합성된 컴포넌트는 composed.wasm에 저장됩니다.
plug 구문은 전이 의존성을 다루지 않습니다. 예를 들어, dep1.wasm에 dep3.wasm으로 충족하려는 미충족 임포트가 있다면, 합성 순서를 신중하게 정해야 합니다. 먼저 dep1.wasm을 dep3.wasm과 합성한 다음, dep1.wasm 대신 그 합성된 컴포넌트를 참조할 수 있습니다. 하지만 이 방식은 전이 의존성이 많아지면 확장되지 않으며, 이것이 WAC 언어가 만들어진 이유입니다.
WAC 언어를 사용한 고급 합성
wac plug는 위와 같은 컴포넌트 합성에서 흔히 쓰이는 패턴을 편리하게 수행하기 위한 도구입니다. 하지만 합성은 얼마든지 복잡해질 수 있습니다. wac plug로 충분하지 않은 경우, WAC 언어를 사용하면 얼마든지 복잡한 합성을 만들 수 있습니다.
WAC 파일에서는 WAC 언어를 사용하여 합성을 기술합니다. 예를 들어, 다음은 앞에서 다룬 validator 컴포넌트를 만드는 데 사용할 수 있는 WAC 파일입니다.
//composition.wac
// Provide a package name for the resulting composition
package docs:composition;
// Instantiate the regex-impl component that implements the `regex` world.
// Bind this instance's exports to the local name `regex`.
let regex = new docs:regex-impl { };
// Instantiate the validator-impl component which implements the `validator` world
// and imports the match interface from the regex component.
let validator = new docs:validator-impl { match: regex.match, ... };
// Export all remaining exports of the validator instance
export validator...;
그런 다음 wac compose를 사용하여 컴포넌트를 합성할 수 있으며, 이때 --dep 플래그로 컴포넌트 이름과 .wasm 파일 간의 관계를 지정합니다.
wac compose --dep docs:regex-impl=regex-component.wasm \
--dep docs:validator-impl=validator-component.wasm \
-o composed.wasm \
composition.wac
또는 컴포넌트를 예상되는 구조에 맞춰 deps 디렉터리에 배치할 수 있으며, 가까운 향후에는 레지스트리에서 컴포넌트를 가져올 수 있게 될 예정입니다. 자세한 내용은 wac 문서를 참조하십시오.
컴포넌트와 WIT 배포 및 가져오기
현대 애플리케이션은 서드파티 패키지에 광범위하게 의존합니다. 그 정도가 매우 커서 패키지 배포는 거의 그 자체로 하나의 산업이 되었습니다. 전통적으로 패키지 배포 서비스는 단일 프로그래밍 언어에 특화되어 있었습니다. 예를 들어, JavaScript 개발자는 NPM의 패키지를 사용하는 데 익숙하며, Rust 개발자는 crates.io를 사용합니다. 일부 런타임은 바이너리 배포와 링킹을 지원하여 제한적인 언어 간 상호 운용성을 제공합니다. 예를 들어, Maven 패키지는 Java 런타임을 대상으로 하는 어떤 언어로든 작성할 수 있습니다. 이러한 서비스는 “패키지 관리자” 또는 “레지스트리” 등 다양한 이름으로 불립니다.
게시와 배포는 핵심 컴포넌트 모델에서 정의되지 않지만, 컴포넌트 생태계의 중요한 부분을 이룹니다. 예를 들어, JavaScript로 작성하면서 C로 작성되어 Wasm으로 컴파일된 고도로 최적화된 머신 러닝 알고리즘을 가져오고 싶다면 레지스트리에서 가져올 수 있으며, 이상적으로는 NPM 레지스트리에서 NPM 패키지를 추가하는 것만큼 쉽게 가져올 수 있습니다.
Bytecode Alliance Packaging Special Interest Group (SIG)에 참여하여 WebAssembly 컴포넌트의 패키징과 호스팅 개선에 참여할 수 있습니다.
wkg 레지스트리 도구
wasm-pkg-tools 프로젝트를 사용하면 Open Container Initiative(OCI) 레지스트리에서 WebAssembly 컴포넌트를 가져오고 해당 레지스트리에 게시할 수 있습니다.
wasm-pkg-tools에는 컴포넌트와 WIT 패키지의 배포 및 가져오기를 쉽게 해 주는 wkg CLI 도구가 포함되어 있습니다. wkg를 사용하는 일반적인 방법은 패키지를 이름으로 지정하는 것입니다. 예를 들면 example:adder@1.0.0와 같습니다. 이 방식으로 wkg를 사용하면 wkg 구성이 패키지의 물리적 위치를 대신 처리하므로, 그 위치를 알 필요가 없습니다. 하지만 필요한 경우 wkg oci 하위 명령을 사용할 때 OCI 참조로 지정하여 wkg로 OCI 아티팩트를 직접 다룰 수도 있습니다.
wkg에는 여러 하위 명령이 있습니다.
wkg oci: 임의의 OCI 레지스트리에 WebAssembly 아티팩트를 푸시하거나 해당 레지스트리에서 풀합니다wkg publish: 패키지 이름으로 컴포넌트 또는 WIT 패키지를 게시합니다wkg get: 패키지 이름으로 컴포넌트 또는 WIT 패키지를 풀합니다wkg wit: WIT 파일 및 의존성을 다룹니다wkg config:wkg구성을 다룹니다
다음 섹션에서는 wkg로 수행할 수 있는 작업 중 일부를 자세히 설명합니다.
wkg 구성 파일
대부분의 wkg 명령(wkg oci는 예외)을 사용할 때는 물리적 위치를 다루지 않고 패키지 이름만 다룹니다. wkg 구성 파일은 패키지 이름을 물리적 위치에 매핑하는 데 사용됩니다. 이 파일로 다음 항목을 구성할 수 있습니다.
- 특정 네임스페이스에 속한 패키지의 기본 레지스트리입니다. 예를 들어,
wasi:clocks나wasi:http와 같은wasi패키지의 위치입니다. - 네임스페이스의 나머지 패키지와 같은 위치에 저장되지 않은 특정 패키지에 대한 레지스트리 재정의입니다. 예를 들어,
wasi:key-value가 다른wasi패키지와 다른 레지스트리에 저장되어 있다면 재정의를 사용합니다. - 앞의 항목 중 어디에도 나열되지 않은 모든 패키지의 기본 레지스트리입니다.
구성 파일에는 비공개 레지스트리용 자격 증명이나 권한이 있는 레지스트리에 푸시하기 위한 자격 증명, 그리고 기타 구성 옵션도 포함됩니다. 더 많은 구성 옵션은 wkg 문서를 참조하십시오.
예를 들어 wasi:clocks, wasi:http와 같은 WASI 패키지를 가져오려면 namespace_registries 섹션 아래에 wasi 네임스페이스에 대한 줄을 추가할 수 있습니다. 구체적으로, 아래 예시는 wkg가 WebAssembly OCI GitHub 컨테이너 레지스트리에서 WASI 패키지를 가져오도록 구성하며, 이 레지스트리에는 WASI 릴리스 시 최신 인터페이스가 게시됩니다. wkg 구성 파일을 편집하려면 wkg config --edit를 실행하십시오.
모든 패키지 이름은 네임스페이스 필드와 그 뒤에 오는 패키지 필드로 구성된다는 점을 기억하십시오. 예를 들어 패키지 이름
wasi:clocks의 네임스페이스 필드는wasi이고 패키지 필드는clocks입니다. 이러한 방식으로, 다음 구성을 사용하면wkg는 모든wasi:<package field>의 가져오기와 게시를 구성된 위치로 라우팅해야 한다는 것을 알게 됩니다.
# $XDG_CONFIG_HOME/wasm-pkg/config.toml
default_registry = "ghcr.io"
[namespace_registries]
# Tell wkg that packages with the `wasi` namespace are in an OCI registry
# under ghcr.io/webassembly
wasi = { registry = "wasi", metadata = { preferredProtocol = "oci", "oci" = {registry = "ghcr.io", namespacePrefix = "webassembly/" } } }
좀 더 일반적인 예로, 다음 구성은 docs 네임스페이스를 가진 모든 패키지에 대해 ttl.sh OCI 레지스트리를 사용하도록 wkg에 지시합니다.
# $XDG_CONFIG_HOME/wasm-pkg/config.toml
default_registry = "ghcr.io"
[namespace_registries]
# Instruct wkg to use the OCI protocol to fetch packages with the `docs` namespace from ttl.sh/wasm-components
docs = { registry = "docs", metadata = { preferredProtocol = "oci", "oci" = {registry = "ttl.sh", namespacePrefix = "wasm-components/" } } }
참고:
wkg구성의package_registry_overrides섹션에서 레지스트리 이름을 참조하여 네임스페이스의 특정 패키지에 대한 재정의를 제공할 수 있습니다.
wkg publish로 패키지 이름을 통해 WIT와 컴포넌트 배포하기
패키지를 게시할 위치를 지정하도록 wkg를 구성했다면, wkg publish 명령을 사용하여 다른 사람이 사용할 수 있도록 컴포넌트나 인터페이스를 게시할 수 있습니다.
WIT에서 다음과 같은 adder 월드를 정의했다고 가정해 보십시오.
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
이 WIT를 Wasm 컴포넌트로 감싸서 wkg를 사용해 게시할 수 있습니다. 네, 제대로 들으신 것이 맞습니다! WIT를 Wasm으로 패키징하는 것입니다. 이 월드 파일을 tutorial/wit/adder라는 디렉터리에 저장했다면 다음을 실행할 수 있습니다.
# Package the contents of add WIT directory as Wasm
wkg wit build --wit-dir tutorial/wit/adder
# Publish the produced component
wkg publish docs:adder@0.1.0.wasm
wkg 구성 섹션에서 설명한 대로 wkg를 구성했다면, 이 명령은 컴포넌트를 ttl.sh/wasm-components/docs/adder:0.1.0에 게시합니다. 그런 다음 형식을 wit로 지정하여 wkg get으로 이 WIT를 가져올 수 있습니다.
wkg get --format wit docs:adder@0.1.0 --output adder.wit
WIT 인터페이스를 게시하는 대신, 다음을 실행하여 빌드된 컴포넌트를 게시할 수 있습니다.
wkg publish adder.wasm --package docs:adder@0.1.0
그런 다음 다음을 실행하여 컴포넌트를 가져올 수 있습니다.
wkg get docs:adder@0.1.0 --output adder.wasm
wkg oci를 사용한 좀 더 일반적인 작업
wkg oci 하위 명령을 사용하면 모든 OCI 레지스트리에 Wasm 아티팩트를 푸시하거나 해당 레지스트리에서 풀할 수 있습니다. wkg publish 및 wkg get과 달리 WIT 패키지를 제공할 필요가 없습니다.
컴포넌트를 OCI 레지스트리에 푸시하려면 wkg oci pull을 사용하십시오. 아래 예시는 컴포넌트를 GitHub 컨테이너 레지스트리에 푸시합니다.
wkg oci push ghcr.io/user/component:0.1.0 component.wasm
컴포넌트를 풀하려면 다음을 실행하십시오.
wkg oci pull ghcr.io/user/component:0.1.0 -o component.wasm
wkg를 사용하여 WIT 패키지 의존성 가져오기
패키지가 다른 패키지에 종속되어 있어 단일 패키지를 가져오는 것만으로는 충분하지 않은 경우가 있습니다. 예를 들어 다음 월드는 wasi:http/proxy가 필요한 간단한 Wasm 서비스를 기술합니다.
package foo:wasi-http-service;
world target-world {
include wasi:http/proxy@0.2.3;
}
wkg get --format wit wasi:http@0.2.3 -o wit/deps/http/로 wasi:http 패키지를 간단히 가져오고 싶을 수도 있습니다. 그러나 wasi:http는 wasi:clocks, wasi:io와 같은 다른 WASI 패키지에 종속됩니다. 패키지와 그 모든 의존성을 확실히 가져오려면 wkg wit fetch를 사용하십시오. 이 명령은 지정된 WIT 디렉터리(기본값은 wit)에 정의한 월드가 포함된 패키지를 읽습니다. 그런 다음 의존성을 가져와 잠금 파일과 함께 deps 하위 디렉터리에 기록합니다. (잠금 파일은 프로젝트에서 사용할 각 의존성의 정확한 버전을 지정합니다.)
위 파일을 ./wit/world.wit로 저장한 후, 다음 명령을 실행하여 의존성을 가져오십시오:
wkg wit fetch
./wit 디렉터리는 다음과 같이 채워집니다:
wit
├── deps
│ ├── wasi-cli-0.2.3
│ │ └── package.wit
│ ├── wasi-clocks-0.2.3
│ │ └── package.wit
│ ├── wasi-http-0.2.3
│ │ └── package.wit
│ ├── wasi-io-0.2.3
│ │ └── package.wit
│ └── wasi-random-0.2.3
│ └── package.wit
└── world.wit
이제 원하는 언어 툴체인을 사용하여 바인딩을 생성하고 컴포넌트를 만들 수 있습니다.
튜토리얼
직접 해 보면서 배우는 것을 좋아하신다면, 이 튜토리얼에서는 계산기 예제를 통해 컴포넌트를 빌드하고, 합성하고, 실행하는 방법을 차근차근 살펴봅니다. 계산기는 덧셈, 뺄셈, 곱셈 등 많은 연산을 수행할 수 있습니다.
이 예제에서는 각 연산이 하나의 컴포넌트가 되며, 이 컴포넌트는 예상되는 연산자를 사용하여 식을 평가하는 eval-expression 컴포넌트와 합성됩니다. 컴포넌트마다 연산을 하나씩 두므로 이 계산기는 과장될 정도로 세분화되어 있으며, 이는 애플리케이션의 독립적인 로직을 하나의 컴포넌트에 담을 수 있는 방식을 보여 주기 위한 것입니다.
프로덕션 환경에서는 컴포넌트가 단순한 수학 연산보다 더 넓은 범위를 가질 가능성이 높습니다.
최종 솔루션은 다음 세 가지 컴포넌트로 구성됩니다:
- 계산기 엔진,
- 덧셈 연산
- 명령줄 인터페이스.
이들을 별도의 Wasm 컴포넌트로 빌드한 후에는 하나의 실행 가능한 컴포넌트로 합성하고, wasmtime CLI를 사용하여 테스트합니다.
계산기 인터페이스
튜토리얼 목적상 “계산기 엔진” 인터페이스와 “덧셈 연산” 인터페이스를 각각 하나의 WIT 파일을 포함하는 두 개의 별도 WIT 패키지에 넣습니다.
이 구성이 과도해 보일 수 있지만, 이는 컴포넌트가 서로 다른 작성자와 패키지에서 제공되는 실제 사용 사례를 보여 줍니다.
이 파일들은 컴포넌트 북 저장소의 examples/tutorial/wit 디렉터리에서 wit/adder/world.wit 및 wit/calculator/world.wit 경로로 찾을 수 있습니다:
// wit/adder/world.wit
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
// wit/calculator/world.wit
package docs:calculator@0.1.0;
interface calculate {
enum op {
add,
}
eval-expression: func(op: op, x: u32, y: u32) -> u32;
}
world calculator {
export calculate;
import docs:adder/add@0.1.0;
}
world app {
import calculate;
}
이 파일들은 다음을 정의합니다:
add인터페이스를 익스포트하는adder월드입니다. 다시 말하지만, 계산기와 같은 컴포넌트는 숫자를 더해야 할 때 이를 호출할 수 있습니다.- 계산기 컴포넌트를 기술하는 월드
calculator. 이 월드는 calculator 인터페이스를 익스포트합니다. 따라서 다른 컴포넌트가 이 인터페이스를 호출하여 계산을 수행할 수 있습니다. 이 월드는 (add와 같은) 연산 인터페이스를 임포트합니다. 따라서 해당 연산을 수행하려면 다른 컴포넌트가 필요합니다. - 평가 함수와, 계산에 포함될 수 있는 연산을 구분하는 열거형을 포함하는 인터페이스
calculate. 이 튜토리얼에서 유일한 연산은add입니다. calculate인터페이스를 임포트하는 “주” 앱 컴포넌트를 기술하는app월드입니다. 이 컴포넌트는 명령줄 인수를 받아 계산기 컴포넌트의eval-expression함수에 전달합니다.
add 컴포넌트 만들기
언어 가이드를 참조하여 adder/wit/world.wit의 adder 월드를 구현하는 컴포넌트를 만드십시오.
참고로, 완성된 예제를 확인하십시오.
calculator 컴포넌트 만들기
언어 가이드를 참조하여 wit/calculator/world.wit의 calculator 월드를 구현하는 컴포넌트를 만드십시오.
참고로, 완성된 예제를 확인하십시오.
완성된 컴포넌트는 adder 월드에서 add 함수를 임포트하고, op 열거형이 add와 일치하면 이 함수를 호출해야 합니다.
command 컴포넌트 만들기
command는 wasmtime(또는 다른 wasi:cli 호스트)에서 직접 실행할 수 있게 하는 특정 익스포트를 가진 컴포넌트입니다.
WebAssembly 호스트는 이 컴포넌트가 wasi:cli/run 인터페이스를 익스포트하기를 기대하며, 이 인터페이스는 WASI에서 main 함수에 해당합니다.
command 컴포넌트를 빌드하려면 wasi:cli 인터페이스를 준수하는 --lib 크레이트를 빌드하도록 cargo를 구성해야 합니다. 중요한 점은 컴포넌트가 반드시 wasi:cli/run을 익스포트해야 한다는 것입니다.
실행 가능한 새 Wasm 애플리케이션의 뼈대를 만들려면 다음과 같이 하십시오.
cargo new --lib example
cd example
이 컴포넌트는 calculate 인터페이스를 임포트하는 app 월드를 구현합니다.
Cargo.toml에서 이 크레이트를 cdylib로 구성하십시오.
[lib]
crate-type = "cdylib"
또한 계산 함수를 임포트하여 사용하는 WIT 인터페이스를 실행 가능하도록 수정해야 합니다.
world app {
import calculate;
+ export wasi:cli/run@0.2.7;
}
calculator 월드가 add 인터페이스를 임포트하므로, command 컴포넌트도 adder WIT를 의존성으로 가져와야 합니다.
이를 위해 wasm-pkg-tools의 wkg를 사용할 수 있습니다. 먼저 wkg가 docs:adder 저장소를 찾을 수 있도록 구성해야 합니다.
# $XDG_CONFIG_HOME/wasm-pkg/config.toml
default_registry = "ghcr.io"
[namespace_registries]
# Tell wkg that the component-book WITs can be found at ghcr.io/bytecodealliance/docs
docs = { registry = "docs", metadata = { preferredProtocol = "oci", "oci" = {registry = "ghcr.io", namespacePrefix = "bytecodealliance/" } } }
Note
docs:adderWIT 패키지는 GHCR에 미리 게시해 두었으므로, 위 구성을 통해 쉽게 찾고 접근할 수 있습니다.직접 만든 사용자 정의 WIT를 사용할 수 있게 하려면
wkg publish명령을 사용하십시오.
프로젝트의 뼈대가 준비되었으므로, 이제 다음과 같은 명령줄 애플리케이션을 실제로 구현해야 합니다.
- 세 개의 인수, 즉 두 개의 피연산자와 연산자 이름(“1 2 add”)을 받습니다.
- 연산자 이름을 파싱하고
op열거형에서 지원하는 연산자인지 확인합니다 - 인수를 전달하여
calculate인터페이스의eval_expression을 호출합니다.
참고로 완성된 코드 목록을 확인하십시오.
계산기 합성하기
이제 wac을 사용하여 컴포넌트들을 하나의 실행 가능한 계산기 컴포넌트로 결합할 준비가 되었습니다.
다음 작업을 수행합니다.
- calculator 컴포넌트의
adder임포트를 충족하기 위해 calculator 컴포넌트를 add 컴포넌트와 합성합니다. - command 컴포넌트의
calculate임포트를 충족하기 위해 이렇게 해결된 calculator 컴포넌트를 command 컴포넌트와 한 번 더 합성합니다.
그 결과 모든 임포트가 충족되고 단일 익스포트(wasi:cli/run 인터페이스)를 가진 완전한 command 컴포넌트가 만들어지며, 이 컴포넌트는 wasmtime으로 실행할 수 있습니다.
wac plug calculator.wasm --plug adder.wasm -o composed.wasm
wac plug command.wasm --plug composed.wasm -o final.wasm
계산기 실행하기
이제 모든 것이 맞아떨어집니다! wasmtime CLI의 이전 릴리스에는 컴포넌트 모델 지원이 포함되어 있지 않으므로, 최신 릴리스(v14.0.0 이상)를 사용하고 있는지 확인한 후 wasmtime CLI로 최종 컴포넌트를 실행하십시오.
wasmtime run final.wasm 1 2 add
1 + 2 = 3
무한한 공간 저 너머로!
이 실습을 확장하여 컴포넌트를 더 추가하려면 calculator.wit를 수정하여 연산자 월드를 하나 더 추가하고 op 열거형을 확장하십시오. 그런 다음 확장된 열거형을 지원하도록 command 컴포넌트와 calculator 컴포넌트를 수정하십시오.
이 튜토리얼을 확장하는 또 다른 방법으로, op 열거형을 제거하고 대신 eval-expression이 문자열을 입력받도록 수정할 수 있습니다. 이 문자열을 파싱하여 어떤 연산자 컴포넌트를 호출할지 결정하는 것입니다. 어쩌면 이 파서 자체도 하나의 컴포넌트가 될 수 있지 않을까요?!
정규 ABI
ABI는 애플리케이션 바이너리 인터페이스로, 바이너리 형식으로 데이터를 주고받는 방식에 대한 약속입니다. ABI는 특히 비트와 바이트 수준의 데이터 레이아웃을 다룹니다. 예를 들어 ABI는 정수를 표현하는 방식(빅 엔디언인가, 리틀 엔디언인가?), 문자열을 표현하는 방식(널 종료 문자 시퀀스에 대한 포인터인가, 길이 접두사 방식인가? UTF-8 인코딩인가, UTF-16 인코딩인가?), 그리고 복합 타입을 표현하는 방식(구조체 시작점으로부터 각 필드의 오프셋)을 정의할 수 있습니다.
컴포넌트 모델은 모든 컴포넌트가 따르는 ABI인 정규 ABI를 정의합니다. 이를 통해 컴포넌트가 서로 다른 언어로 빌드되었더라도 혼동 없이 서로 통신할 수 있음이 보장됩니다. 내부적으로 C 컴포넌트는 Rust 컴포넌트와 상당히 다른 방식으로 문자열을 표현할 수 있지만, 정규 ABI는 두 컴포넌트가 그 사이의 경계를 넘어 문자열을 전달할 수 있는 형식을 제공합니다.
정규 ABI가 무엇인지에 대한 보다 공식적인 정의는 정규 ABI 설명서를 참고하십시오.
네이티브 비동기 확장
WASI 0.3은 async func, stream<T>, future<T>를 정규 ABI 프리미티브로 추가했습니다. 기존 규칙은 모든 인터페이스 호출이 동기적으로 반환된다고 가정했기 때문에, 이를 지원하려면 ABI 자체를 확장해야 했습니다. 이 절에서는 이러한 확장이 무엇인지 개략적으로 설명합니다. 전체 명세는 업스트림 동시성 설명서와 정규 ABI 설명서를 참고하십시오.
비동기 함수 ABI
async로 선언된 WIT 함수는 기존의 (동기) 시그니처에 더해 논블로킹 코어 함수 시그니처를 갖습니다. 두 시그니처 모두 계속 사용할 수 있으므로, 어느 쪽도 상대방의 호출 규약을 채택할 필요 없이 동기 호출자가 비동기 피호출자를 호출하고 비동기 호출자가 동기 피호출자를 호출할 수 있습니다.
동기 로어링(WIT 수준 시그니처를 실제 코어 Wasm 함수 매개변수 및 결과로 매핑하는 것)에서 매개변수와 반환 값은 들어맞는 경우 평탄한(flat) 코어 Wasm 타입으로 전달되고, 그렇지 않은 경우 선형 메모리 포인터(매개변수에는 입력 포인터(in-pointer), 결과에는 출력 포인터(out-pointer))를 통해 전달됩니다. 비동기 로어링은 실제 함수 반환 값으로 i32 상태 코드를 추가합니다. 논리적 반환 값은 호출이 완료되면 호출자가 제공한 출력 포인터에 기록됩니다. 상태 코드의 하위 비트는 다음 세 가지 경우를 구분합니다.
- 호출이 아직 매개변수 읽기를 시작하지 않았습니다.
- 호출이 매개변수를 읽었지만 아직 결과를 쓰지 않았습니다.
- 호출이 반환되었으며, 매개변수 메모리와 결과 메모리가 모두 소비되었습니다.
런타임은 진행 중인 각 비동기 호출을 서브태스크(subtask)로 표현합니다. 호출자는 단일 서브태스크를 기다리거나 대기 가능 집합(waitable set)에 속한 서브태스크 중 어느 하나를 기다릴 수 있습니다. 해당 대기 내장 함수는 아래에 나열되어 있습니다. 런타임이 완료를 알리면 호출자는 실행을 재개하고 결과를 소비할 수 있습니다.
경계를 넘나드는 스트림과 퓨처
stream<T>와 future<T>는 리소스가 아니라 정규 ABI 값입니다. 전송 수준에서 각 끝은 컴포넌트별 핸들 테이블에 대한 정수 인덱스로 표현되며(리소스가 사용하는 것과 동일한 일반적인 메커니즘), 값과 리소스의 구분은 인코딩이 아니라 아래의 소유권 규칙에서 드러납니다. 각각에는 읽기 가능 끝과 쓰기 가능 끝이라는 두 개의 끝이 있습니다. 소유권 규칙은 간단합니다.
- 새로운
stream.new또는future.new내장 함수를 통해 스트림이나 퓨처를 생성한 컴포넌트는 두 끝을 모두 받습니다. - 다른 컴포넌트나 호스트로부터 스트림이나 퓨처를 받은 컴포넌트는 읽기 가능 끝의 고유한 소유권을 얻습니다.
- 스트림이나 퓨처를 경계 너머로 전달하는 컴포넌트는 읽기 가능한 끝의 소유권을 이전합니다.
쓰기 가능한 끝은 고정되어 있습니다. 즉, 스트림이나 퓨처를 생성한 컴포넌트에 남아 있으며 경계 너머로 이전할 수 없습니다.
코어 Wasm 코드는 선형 메모리 버퍼를 전달하여 stream.read 내장 함수로 스트림에서 읽고 stream.write로 스트림에 씁니다. 이러한 내장 함수는 완료 기반입니다. 즉, 호출은 값을 즉시 버퍼로 복사하거나 버퍼에서 복사해 오며, 그렇지 않으면 작업이 동시에 계속 진행됨을 나타내는 “blocked” 센티널 값을 반환합니다. 퓨처는 이와 유사한 future.read와 future.write를 사용합니다.
이는 OS 수준의 완료 기반 I/O(Linux의 io_uring, Windows의 Overlapped I/O)와 같은 형태이며, 이 때문에 바인딩 생성기는 별도의 연결 작업 없이 스트림과 퓨처를 호스트 언어의 기존 동시성 기본 요소에 매핑할 수 있습니다.
기타 내장 함수
비동기 지원은 다음과 같은 여러 정규 ABI 내장 함수를 추가로 제공합니다.
task.return은 비동기 함수의 익스포트 측에서 결과를 전달하는 데 사용됩니다.task.cancel은 진행 중인 서브태스크를 취소하는 데 사용됩니다.- Waitable-set 프리미티브는 진행 중인 여러 서브태스크를 기다리거나 그중 하나를 선택하는 데 사용됩니다(비동기 호출 결과를 소비하는 데 쓰이는 메커니즘입니다).
전체 목록은 정규 ABI 설명서에 있으며, 동시성 설명서에서는 설계 근거와 더 넓은 동시성 모델을 다룹니다.
자주 묻는 질문(FAQ)
이 페이지에는 코어 WebAssembly, WebAssembly 컴포넌트(즉, 컴포넌트 모델) 및 WebAssembly 생태계 전반과 관련하여 자주 묻는 질문과 함께 혼동하기 쉬운 개념에 대한 설명이 정리되어 있습니다.
Q: WebAssembly에서 모듈과 컴포넌트의 차이점은 무엇입니까?
WebAssembly 모듈(더 정확히는 “WebAssembly 코어 모듈“이라고 합니다)은 WebAssembly 코어 명세를 준수하는 바이너리입니다.
WebAssembly 컴포넌트는 다음과 같습니다:
- (WebAssembly 코어 바이너리 형식과 달리) 컴포넌트 모델 바이너리 형식을 준수합니다.
- 타입 정보를 인코딩하기 위해 WebAssembly 인터페이스 타입 명세를 사용합니다.
- 풍부한 타입과 코어 WebAssembly에 존재하는 타입 사이를 변환하기 위해 컴포넌트 모델 정규 ABI를 준수합니다.
WebAssembly 컴포넌트는 코어 모듈을 포함할 수 있으며 실제로 포함하는 경우가 많지만, 일반적으로 WebAssembly 코어 모듈은 컴포넌트를 포함할 수 없습니다. WebAssembly 컴포넌트와 WebAssembly 코어 모듈은 서로 다른 바이너리 형식을 가집니다.
WebAssembly 컴포넌트는 바이너리 형식과 텍스트 형식(“WAT”, WebAssembly 텍스트 형식) 모두로 표현할 수 있습니다.
Q: WebAssembly 바이너리가 컴포넌트인지 모듈인지 어떻게 구별할 수 있습니까?
WebAssembly 바이너리를 텍스트 형식으로 변환하고 나면(예: wasm-tools print 같은 도구 사용), WebAssembly 코어 모듈과 WebAssembly 컴포넌트를 쉽게 구별할 수 있습니다.
WebAssembly 코어 모듈은 일반적으로 최상위 (module) S-표현식으로 구성됩니다:
(module
;; ...
)
WebAssembly 컴포넌트는 일반적으로 (component) S-표현식으로 구성됩니다(중첩된 (core:module)/(component) S-표현식을 포함할 수도 있습니다):
(component
;; ...
)
Q: WebAssembly 컴포넌트와 WebAssembly 시스템 인터페이스(WASI)는 서로 어떤 관계입니까?
WebAssembly 코어 모듈은 사용 가능한 기본 요소를 사용하여 상위 수준 타입을 표현할 수 있지만, 모든 바이너리와 플랫폼이 각자 임시방편적인 방식으로 그렇게 할 수 있습니다. 컴포넌트 모델은 대부분의 고수준 언어에서 익숙한 풍부한 타입 집합에 대해 바이너리와 플랫폼 전반에서 일관된 표현을 제공합니다. WebAssembly 컴포넌트가 사용할 수 있는 풍부한 타입 집합을 WebAssembly 인터페이스 타입(WIT)이라고 합니다.
WebAssembly 시스템 인터페이스(WASI)는 WebAssembly 커뮤니티 그룹의 하위 그룹인 WASI 서브그룹이 궁극적인 표준화를 목표로 개발한 (WIT로 명세된) API 집합입니다. WASI는 시스템이나 플랫폼이 WebAssembly 컴포넌트에 노출할 수 있는 인터페이스, 함수, 타입을 정의합니다. 언뜻 보면 WASI의 많은 부분은 stdin, stdout, 파일 쓰기와 같이 프로그램에 대한 전통적인 기대에 부합한다는 점에서 UNIX와 유사합니다.
그러나 wasi:http처럼 일부 WASI 시스템 인터페이스는 명령줄보다 훨씬 높은 수준에서 동작합니다. wasi:http는 인터넷의 보편성과 웹을 플랫폼으로 삼는 WebAssembly 컴포넌트의 일반적인 사용 사례로 인해 표준화된 플랫폼으로 포함되었습니다.
WIT를 사용하면 플랫폼 구축자는 WebAssembly 컴포넌트가 접근하기를 기대하는 어떤 인터페이스든 정의할 수 있으며, WASI는 공유된 표준 추상화 집합 위에 인터페이스를 구축할 수 있게 해 줍니다.
Q: Preview 1, Preview 2, Preview 3이라는 용어를 자주 봅니다. 이 용어들은 무엇을 가리킵니까?
Preview 1은 WITX를 기반으로 했으며 현재는 더 이상 사용되지 않는 컴포넌트 모델의 첫 번째 버전을 가리킵니다. Preview 2는 WebAssembly 인터페이스 타입(WIT)을 사용하는 컴포넌트 모델의 더 새로운 버전을 가리킵니다. Preview 3(WASI 0.3, 2026년 6월 11일 릴리스)은 컴포넌트 모델에 네이티브 비동기를 추가합니다. 이제 async func, stream<T>, future<T>는 정규 ABI 프리미티브이며, wasi:io 패키지는 제거되었습니다. 개념적 기반은 비동기, 스트림, 퓨처를 참조하고, 기존 컴포넌트에서 무엇이 바뀌는지는 WASI 0.2에서 WASI 0.3으로 마이그레이션하기를 참조하십시오.
많은 프로그래밍 언어 툴체인이 Preview 1 컴포넌트만 네이티브로 지원할 수 있지만, Preview 1 컴포넌트는 Preview 2 컴포넌트로 자동으로 어댑트될 수 있으므로 실제로는 문제가 되지 않습니다.
다소 혼란스러울 수 있지만, WASI Preview 1 “컴포넌트“는 사실 WebAssembly 코어 모듈입니다. 더 정확히 말하면, Preview 1 “컴포넌트“는 명확하게 정의된 임포트 및 익스포트 집합을 갖춘 WebAssembly 코어 모듈입니다(레거시 명세).
Q: WASI 0.3이란 무엇입니까?
WASI 0.3은 2026년 6월 11일에 발표된 WASI의 최신 마일스톤 릴리스입니다. 이 릴리스는 컴포넌트 모델이 이번 릴리스를 위해 추가한 세 가지 새로운 정규 ABI 프리미티브인 async func, stream<T>, future<T> 위에 WASI의 인터페이스를 재구성합니다. wasi:io 패키지는 제거되었으며, 그 기능은 이제 컴포넌트 모델이 직접 제공합니다.
그 실질적인 효과로 WASI 인터페이스는 더 작아지고 합성하기가 더 쉬워졌습니다. wasi:http 핸들러는 응답을 반환하는 async func이고, stdin, stdout, stderr는 stream<u8>를 사용하며, 비동기 작업은 각 컴포넌트가 자체 이벤트 루프를 실행하지 않고도 컴포넌트 경계를 넘어 준비 상태를 전파합니다.
인터페이스별 변경 사항 개요는 WASI.dev의 WASI 0.3을 참조하십시오.
Q: WASI 0.2에서 WASI 0.3으로 마이그레이션해야 합니까?
당장은 아닙니다. WASI 0.3 런타임은 호스트 경계에서 0.2 임포트를 네이티브 0.3 프리미티브에 매핑하여 0.2를 폴리필할 수 있으며, Wasmtime의 wasmtime serve는 이미 동일한 바이너리에서 0.3 컴포넌트와 0.2 컴포넌트를 모두 실행하고 컴포넌트별로 디스패치합니다.
컴포넌트 경계를 넘는 합성 가능한 비동기 처리나 더 새로운 인터페이스 형태, 특히 wasi:http에서 아홉 개의 리소스를 두 개로 축소한 구조를 원한다면 마이그레이션하는 것이 적절합니다.
개념 매핑과 인터페이스별 주요 내용은 WASI 0.2에서 WASI 0.3으로 마이그레이션하기를 참조하십시오.
Q: wasi:io는 어떻게 되었습니까?
WASI 0.3은 wasi:io 패키지를 완전히 제거합니다. 이 패키지가 제공하던 리소스(pollable, input-stream, output-stream)는 각각 컴포넌트 모델의 네이티브 정규 ABI 프리미티브인 future<T>, stream<u8>, stream<u8>(함수 매개변수로 사용)로 대체됩니다.
그 동기는 이른바 샌드위치 문제였습니다. 비동기 준비 상태가 단일 컴포넌트 범위의 리소스로 표현되면, 그 준비 상태 신호가 컴포넌트 경계를 넘어 깔끔하게 전파될 수 없었습니다. 정규 ABI에 네이티브 비동기가 도입되면서 런타임이 스케줄링과 웨이크업 전파를 담당합니다.
기반 개념은 비동기, 스트림, 퓨처를 참조하십시오.
Q: 컴포넌트 임포트란 무엇입니까?
WebAssembly 컴포넌트는 자기 기술적(self-describing)입니다. 필요한 외부 기능(플랫폼이나 다른 컴포넌트가 제공해야 하는 기능)에 대한 정보가 바이너리에 포함되어 있습니다. 예를 들어, 외부 환경 변수가 필요할 수 있는 WebAssembly 컴포넌트는 wasi:cli/environment와 같은 WASI 인터페이스를 임포트할 수 있습니다.
Note
wasi:cli/environment인터페이스가 제공하는 값이 호스트 머신의 환경 변수라는 보장은 없습니다. 이는 플랫폼이 노출하는wasi:cli/environment구현에서 플랫폼에 맡겨진 선택입니다.예를 들어, 실제로 플랫폼은 민감한 환경 변수를 생략하거나 아예 아무것도 제공하지 않도록 선택할 수 있습니다.
임포트는 WIT로 가장 쉽게 설명할 수 있습니다.
package example-namespace:example-package;
world example-world {
import wasi:cli/environment@0.2.4;
}
wasi:cli의 environment 인터페이스는 환경 변수와 상호 작용하기 위한 다양한 타입과 함수를 제공합니다.
이 컴포넌트는 wasi:cli/environment 인터페이스를 “임포트“한다고 하며, 그 안에서 사용 가능한 함수와 타입을 사용합니다.
Q: 컴포넌트 익스포트란 무엇입니까?
WebAssembly 컴포넌트는 자기 기술적입니다. 임포트와 함께, WebAssembly 컴포넌트는 자신이 익스포트하는 기능도 기술할 수 있으며, 컴포넌트의 호출자(예: 다른 컴포넌트나 WebAssembly 호스트)는 이를 참조할 수 있습니다.
익스포트는 WIT로 가장 쉽게 설명할 수 있습니다.
package example-namespace:example-package;
interface example-interface {
say-hello: func(name: string) -> string;
}
world example-world {
export example-interface;
}
위에서 정의한 example-world를 구현하는 컴포넌트의 경우, 호출자는 WebAssembly 바이너리에 example-namespace:example-package/example-interface 인터페이스를 통해 호출할 수 있는 say-hello 함수가 있을 것으로 기대할 수 있습니다.
이 컴포넌트는 example-interface 인터페이스를 “익스포트“한다고 하며, 이를 통해 그 안의 함수와 타입을 사용할 수 있게 합니다.
아직 궁금한 점이 있으십니까?
여러분의 질문(또는 여기에서 다루어야 한다고 생각하는 질문)을 GitHub 이슈로 등록하여 컴포넌트 북(Component Book)에 기여해 주십시오.
유용한 링크
다음 참고 자료는 컴포넌트 모델과 관련 생태계/프로젝트를 이해하는 데 도움이 됩니다.
템플릿
Bytecode Alliance는 WASI HTTP 컴포넌트를 시작하는 데 도움이 되는 템플릿을 제공합니다:
- sample-wasi-http-js - WASI HTTP JavaScript 템플릿
- sample-wasi-http-rust - WASI HTTP Rust 템플릿