VS Code for the Web에서 WebAssembly 실행
2023년 6월 5일, Dirk Bäumer
VS Code for the Web (https://vscode.dev)가 출시된 지 꽤 되었습니다. 저희의 목표는 브라우저에서 편집/컴파일/디버깅의 전체 사이클을 지원하는 것이었습니다. JavaScript와 TypeScript와 같은 언어의 경우 브라우저에 JavaScript 실행 엔진이 내장되어 있어 상대적으로 쉽습니다. 하지만 다른 언어의 경우 코드를 실행하고 디버깅할 수 있어야 하므로 더 어렵습니다. 예를 들어, 브라우저에서 Python 소스 코드를 실행하려면 Python 인터프리터를 실행할 수 있는 실행 엔진이 필요합니다. 이러한 언어 런타임은 보통 C/C++로 작성됩니다.
WebAssembly는 가상 머신을 위한 이진 명령어 형식입니다. WebAssembly 가상 머신은 현재 최신 브라우저에 내장되어 있으며 C/C++를 WebAssembly 코드로 컴파일하는 도구 체인이 존재합니다. 현재 WebAssembly로 무엇을 할 수 있는지 알아보기 위해 C/C++로 작성된 Python 인터프리터를 WebAssembly로 컴파일하여 VS Code for the Web에서 실행해 보기로 결정했습니다. 다행히 Python 팀은 이미 CPython을 WASM으로 컴파일하는 작업을 시작했으며, 저희는 그 노력에 편승할 수 있었습니다. 탐색 결과는 아래 짧은 동영상에서 확인할 수 있습니다.

VS Code 데스크톱에서 Python 코드를 실행하는 것과 별반 달라 보이지 않습니다. 그럼 왜 이것이 멋진 걸까요?
- Python 소스 코드(
app.py및hello.py)는 GitHub 리포지토리에 호스팅되어 있으며 GitHub에서 직접 읽어옵니다. Python 인터프리터는 작업 공간의 파일에 완전히 액세스할 수 있지만 다른 파일에는 액세스할 수 없습니다. - 샘플 코드는 여러 파일로 구성됩니다.
app.py는hello.py에 의존합니다. - 출력은 VS Code의 터미널에 깔끔하게 표시됩니다.
- Python REPL을 실행하고 완전히 상호 작용할 수 있습니다.
- 그리고 물론, 이것은 웹에서 **실행**됩니다.
추가적으로, WebAssembly(WASM) 코드로 컴파일된 Python 인터프리터는 VS Code for the Web에서 실행하기 위해 수정이 필요하지 않습니다. CPython 팀에서 생성한 비트와 동일한 비트를 사용합니다.
어떻게 작동하나요?
WebAssembly 가상 머신에는 SDK(예: Java 또는 .NET)가 포함되어 있지 않습니다. 따라서 즉시 사용할 수 있는 WebAssembly 코드는 콘솔에 출력하거나 파일 내용을 읽을 수 없습니다. WebAssembly 사양은 WebAssembly 코드가 가상 머신을 실행하는 호스트의 함수를 어떻게 호출할 수 있는지 정의합니다. VS Code for the Web의 경우 호스트는 브라우저입니다. 따라서 가상 머신은 브라우저에서 실행되는 JavaScript 함수를 호출할 수 있습니다.
Python 팀은 인터프리터의 WebAssembly 바이너리를 두 가지 방식으로 제공합니다. 하나는 emscripten으로 컴파일된 것이고, 다른 하나는 WASI SDK로 컴파일된 것입니다. 둘 다 WebAssembly 코드를 생성하지만, 호스트 구현으로 제공되는 JavaScript 함수에 대해 다른 특성을 가집니다.
- emscripten - 웹 플랫폼 및 Node.js에 특별한 초점을 맞춥니다. WASM 코드 생성 외에도 브라우저 또는 Node.js 환경에서 WASM 코드를 실행하는 호스트 역할을 하는 JavaScript 코드를 생성합니다. 예를 들어, JavaScript 코드는 C
printf문을 브라우저 콘솔에 출력하는 함수를 제공합니다. - WASI SDK - C/C++ 코드를 WASM으로 컴파일하고 WASI 사양을 준수하는 호스트 구현을 가정합니다. WASI는 WebAssembly 시스템 인터페이스의 약자입니다. 파일 및 파일 시스템, 소켓, 시계, 난수 등 여러 운영 체제와 유사한 기능을 정의합니다. WASI SDK로 C/C++ 코드를 컴파일하면 WebAssembly 코드만 생성되며 JavaScript 함수는 생성되지 않습니다. C
printf문 내용을 출력하는 데 필요한 JavaScript 함수는 호스트에서 제공해야 합니다. 예를 들어, Wasmtime은 WASI를 운영 체제 호출에 연결하는 WASI 호스트 구현을 제공하는 런타임입니다.
VS Code의 경우 WASI를 지원하기로 결정했습니다. 저희의 주요 초점은 브라우저에서 WASM 코드를 실행하는 것이지만, 실제로 순수한 브라우저 환경에서 실행하는 것은 아닙니다. VS Code 확장 기능을 호스트하는 표준 방식인 VS Code의 확장 호스트 워커에서 WebAssembly를 실행해야 합니다. 확장 호스트 워커는 브라우저의 워커 API 외에도 전체 VS Code 확장 API를 제공합니다. 따라서 C/C++ 프로그램의 printf 호출을 브라우저 콘솔에 연결하는 대신, 실제로는 VS Code의 터미널 API에 연결하려고 합니다. WASI에서 이를 수행하는 것이 emscripten보다 저희에게 더 쉬웠습니다.
현재 VS Code의 WASI 호스트 구현은 WASI 스냅샷 미리 보기 1을 기반으로 하며, 이 블로그 게시물에서 설명하는 모든 구현 세부 정보는 해당 버전에 적용됩니다.
내 WebAssembly 코드를 어떻게 실행할 수 있나요?
Python을 VS Code for the Web에서 실행한 후, 저희가 사용한 접근 방식이 WASI로 컴파일될 수 있는 모든 코드를 실행할 수 있게 해준다는 것을 빠르게 깨달았습니다. 따라서 이 섹션에서는 WASI SDK를 사용하여 작은 C 프로그램을 WASI로 컴파일하고 VS Code의 확장 호스트 내에서 실행하는 방법을 시연합니다. 이 예제는 독자가 VS Code의 확장 API에 익숙하고 VS Code for the Web용 확장을 작성하는 방법을 알고 있다고 가정합니다.
실행할 C 프로그램은 다음과 같은 간단한 "Hello World" 프로그램입니다.
#include <stdio.h>
int main(void)
{
printf("Hello, World\n");
return 0;
}
최신 WASI SDK가 설치되어 있고 PATH에 추가되어 있다고 가정하면, C 프로그램은 다음 명령을 사용하여 컴파일할 수 있습니다.
clang hello.c -o ./hello.wasm
이 명령은 hello.c 파일 옆에 hello.wasm 파일을 생성합니다.
VS Code에 새로운 기능은 확장을 통해 추가되며, WebAssembly를 VS Code에 통합할 때도 동일한 모델을 따릅니다. WASM 코드를 로드하고 실행하는 확장을 정의해야 합니다. 확장의 package.json 매니페스트의 중요한 부분은 다음과 같습니다.
{
"name": "...",
...,
"extensionDependencies": [
"ms-vscode.wasm-wasi-core"
],
"contributes": {
"commands": [
{
"command": "wasm-c-example.run",
"category": "WASM Example",
"title": "Run C Hello World"
}
]
},
"devDependencies": {
"@types/vscode": "1.77.0",
},
"dependencies": {
"@vscode/wasm-wasi": "0.11.0-next.0"
}
}
ms-vscode.wasm-wasi-core 확장은 WASI API를 VS Code API에 연결하는 WebAssembly 실행 엔진을 제공합니다. 노드 모듈 @vscode/wasm-wasi는 VS Code에서 WebAssembly 코드를 로드하고 실행하기 위한 프록시를 제공합니다.
아래는 WebAssembly 코드를 로드하고 실행하는 실제 TypeScript 코드입니다.
import { Wasm } from '@vscode/wasm-wasi';
import { commands, ExtensionContext, Uri, window, workspace } from 'vscode';
export async function activate(context: ExtensionContext) {
// Load the WASM API
const wasm: Wasm = await Wasm.load();
// Register a command that runs the C example
commands.registerCommand('wasm-wasi-c-example.run', async () => {
// Create a pseudoterminal to provide stdio to the WASM process.
const pty = wasm.createPseudoterminal();
const terminal = window.createTerminal({
name: 'Run C Example',
pty,
isTransient: true
});
terminal.show(true);
try {
// Load the WASM module. It is stored alongside the extension's JS code.
// So we can use VS Code's file system API to load it. Makes it
// independent of whether the code runs in the desktop or the web.
const bits = await workspace.fs.readFile(
Uri.joinPath(context.extensionUri, 'hello.wasm')
);
const module = await WebAssembly.compile(bits);
// Create a WASM process.
const process = await wasm.createProcess('hello', module, { stdio: pty.stdio });
// Run the process and wait for its result.
const result = await process.run();
if (result !== 0) {
await window.showErrorMessage(`Process hello ended with error: ${result}`);
}
} catch (error) {
// Show an error message if something goes wrong.
await window.showErrorMessage(error.message);
}
});
}
아래 동영상은 VS Code for the Web에서 실행되는 확장 기능을 보여줍니다.

WebAssembly의 소스로 C/C++ 코드를 사용했으며 WASI는 표준이므로 WASI를 지원하는 다른 도구 체인도 있습니다. 예를 들어: Rust, .NET 또는 Swift가 있습니다.
VS Code의 WASI 구현
WASI와 VS Code API는 파일 시스템 또는 stdio(예: 터미널)와 같은 개념을 공유합니다. 이를 통해 VS Code API 위에 WASI 사양을 구현할 수 있었습니다. 그러나 실행 동작의 차이가 과제였습니다. WebAssembly 코드 실행은 동기적입니다(예: WebAssembly 실행이 시작되면 JavaScript 워커는 실행이 완료될 때까지 차단됨). 반면에 VS Code와 브라우저의 대부분 API는 비동기적입니다. 예를 들어, WASI에서 파일을 읽는 것은 동기적이지만 해당 VS Code API는 비동기적입니다. 이러한 특성은 VS Code 확장 호스트 워커 내에서 WebAssembly 코드를 실행하는 데 두 가지 문제를 야기합니다.
- WebAssembly 코드를 실행하는 동안 확장 호스트가 차단되지 않도록 방지해야 합니다. 그렇지 않으면 다른 확장이 실행되지 못합니다.
- 비동기 VS Code 및 브라우저 API 위에 동기 WASI API를 구현할 메커니즘이 필요합니다.
첫 번째 경우는 해결하기 쉽습니다. WebAssembly 코드를 별도의 워커 스레드에서 실행합니다. 두 번째 경우는 동기 코드를 비동기 코드로 매핑해야 하므로 동기 실행 스레드를 일시 중단하고 비동기적으로 계산된 결과가 사용 가능해지면 다시 시작해야 하므로 더 어렵습니다. WebAssembly용 JavaScript-Promise 통합 제안은 WASM 계층에서 이 문제를 해결하며 V8에 제안의 실험적인 구현이 있습니다. 그러나 저희가 이 작업을 시작했을 때 V8 구현은 아직 사용할 수 없었습니다. 그래서 다른 구현을 선택했는데, SharedArrayBuffer와 Atomics를 사용하여 동기 WASI API를 VS Code의 비동기 API에 매핑합니다.
접근 방식은 다음과 같습니다.
- WASM 워커 스레드는 VS Code 측에서 호출되어야 하는 코드에 대한 필요한 정보를 포함하는
SharedArrayBuffer를 생성합니다. - 공유 메모리를 VS Code의 확장 호스트 워커로 보내고 Atomics.wait를 사용하여 확장 호스트 워커가 작업을 완료할 때까지 기다립니다.
- 확장 호스트 워커는 메시지를 받아 적절한 VS Code API를 호출하고, 결과를
SharedArrayBuffer에 다시 쓰고, Atomics.store 및 Atomics.notify를 사용하여 WASM 워커 스레드가 깨어나도록 알립니다. - WASM 워커는
SharedArrayBuffer에서 결과 데이터를 읽어 WASI 콜백으로 반환합니다.
이 접근 방식의 유일한 어려움은 SharedArrayBuffer 및 Atomics가 사이트가 교차 출처 격리(cross-origin isolated) 상태여야 한다는 것입니다. CORS의 전염성 때문에 이는 그 자체로 어려운 작업이 될 수 있습니다. 그렇기 때문에 현재는 Insiders 버전 insiders.vscode.dev에서만 기본적으로 활성화되며 vscode.dev에서는 쿼리 매개변수 ?vscode-coi=on을 사용하여 활성화해야 합니다.
아래는 위에서 WebAssembly로 컴파일한 C 프로그램에 대한 WASM 워커와 확장 호스트 워커 간의 상호 작용을 더 자세히 보여주는 다이어그램입니다. 주황색 상자의 코드는 WebAssembly 코드이며 녹색 상자의 모든 코드는 JavaScript에서 실행됩니다. 노란색 상자는 SharedArrayBuffer를 나타냅니다.

웹 쉘
이제 C/C++ 및 Rust 코드를 WebAssembly로 컴파일하여 VS Code에서 실행할 수 있게 되었으므로, VS Code for the Web에서도 쉘을 실행할 수 있는지 탐색했습니다.
Unix 쉘 중 하나를 WebAssembly로 컴파일하는 것을 조사했습니다. 그러나 일부 쉘은 WASI에서 현재 사용할 수 없는 운영 체제 기능(프로세스 생성 등)에 의존합니다. 이로 인해 약간 다른 접근 방식을 취하게 되었습니다. TypeScript로 기본 쉘을 구현하고 ls, cat, date와 같은 Unix 핵심 유틸리티만 WebAssembly로 컴파일하려고 시도했습니다. Rust는 WASM 및 WASI에 대한 지원이 매우 좋기 때문에 Rust로 작성된 GNU coreutils의 크로스 플랫폼 재구현인 uutils/coreutils를 시도해 보았습니다. 결과적으로 최소한의 웹 쉘을 만들 수 있었습니다.

사용자 지정 WebAssembly 또는 명령을 실행할 수 없다면 쉘은 매우 제한적입니다. 웹 쉘을 확장하기 위해 다른 확장은 파일 시스템에 추가 마운트 포인트를 제공하거나 웹 쉘에 입력될 때 호출되는 명령을 제공할 수 있습니다. 명령을 통한 간접 호출은 터미널에 입력되는 내용과 실제 WebAssembly 실행을 분리합니다. Python 확장에서 이 지원을 처음부터 사용하면 프롬프트에 python app.py를 입력하여 쉘 내에서 직접 Python 코드를 실행하거나 일반적으로 /usr/local/lib/python3.11 아래에 마운트되는 기본 Python 3.11 라이브러리를 나열할 수 있습니다.

다음 단계는?
WASM 실행 엔진 확장 및 웹 쉘 확장은 모두 미리 보기 상태의 실험적인 기능이므로 WebAssembly를 사용하여 프로덕션 준비가 된 확장을 구현하는 데 사용해서는 안 됩니다. 이 기술에 대한 조기 피드백을 얻기 위해 공개적으로 제공되었습니다. 질문이나 피드백이 있으면 해당 vscode-wasm GitHub 리포지토리에 이슈를 열어주세요. 이 리포지토리에는 Python 예제, WASM 실행 엔진 및 웹 쉘의 소스 코드도 포함되어 있습니다.
우리가 알고 있는 것은 다음과 같은 주제를 더 탐색할 것이라는 것입니다.
- WASI 팀은 사양의 preview2 및 preview3을 작업 중이며, 저희도 이를 지원할 계획입니다. 새 버전은 WASI 호스트 구현 방식을 변경할 것입니다. 그러나 WASM 실행 엔진 확장에서 노출되는 API를 대부분 안정적으로 유지할 수 있을 것이라고 확신합니다.
- 추가 운영 체제와 유사한 기능(프로세스 또는 futex 등)으로 WASI를 확장하는 WASIX 작업도 있습니다. 이 작업을 계속해서 지켜볼 것입니다.
- VS Code의 많은 언어 서버는 JavaScript 또는 TypeScript가 아닌 언어로 구현됩니다. 이러한 언어 서버를
wasm32-wasi로 컴파일하여 VS Code for the Web에서도 실행하는 가능성을 탐색할 계획입니다. - 웹에서 Python 디버깅 개선. 이에 대한 작업을 시작했습니다. 계속 지켜봐 주세요.
- 확장 B가 확장 A가 기여한 WebAssembly 코드를 실행할 수 있도록 지원을 추가합니다. 예를 들어 이를 통해 임의의 확장이 Python WebAssembly를 기여한 확장을 재사용하여 Python 코드를 실행할 수 있습니다.
wasm32-wasi용으로 컴파일된 다른 언어 런타임이 VS Code의 WebAssembly 실행 엔진 위에서 실행되도록 보장합니다. VMware Labs는 Ruby 및 PHPwasm32-wasi바이너리를 제공하며 둘 다 VS Code에서 실행됩니다.
감사합니다.
Dirk와 VS Code 팀
행복한 코딩 되세요!