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에서 네이티브로 실행할 수 있습니다.