예제로 배우는 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 구문을 더 자세히 다룹니다.