웹 어셈블리 (WASM)
- C, C++, Rust 등으로 작성한 코드를 바이너리로 컴파일하고 그 결과를 웹 브라우저 환경에서 호출하고 실행할 수 있는 새로운 유형의 기술
- JS 엔진 내에서 나란히 JS와 함께 실행된다.
웹 어셈블리의 역사
모질라와 asm.js
- 2013년에 모질라가 발표한 asm.js는 C/C++ 코드를 JS로 변환하는 방법을 제공했다.
- Emscripten과 같은 툴을 사용하면 C/C++ -> asm.js로 변환해 쉽게 배포할 수 있다.
그럼 왜 asm.js를 안씀? WASM이랑 똑같은데?
- 결국은 JS 코드, 큰 파일 사이즈
- 숫자만 가능 (연산에만 치중)
- Web API 호출이 불가
- WASM은 바이너리 형식이어서 더 효율적으로 전송될 수 있다.
- WASM 모듈은 Promise 기반의 접근 방식을 사용 해 코드의 로드를 비동기적으로 처리한다.
WASM이 JS보다 뭐가 나은데?
https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/
WASM이 왜 빠른지 알기 위해서는 우선 JS의 실행 과정을 살펴보자.
- Parsing(파싱)
- 소스코드를 interpreter가 실행할 수 있도록 변경
- Compiling + Optimizing (컴파일 + 최적화)
- Re-optimizing (재최적화)
- Execution (실행)
- Garbage Collection (가비지 콜렉팅)
TradeOff는 모니터링과 컴파일링하는 과정이다. > 개선할 수 있음
웹 어셈블리는 그럼 어떤데?
1. Fetching
- 서버에서 파일을 fetching하는 과정
- 아무리 JS파일이 압축을 한다고 해도 WASM은 파일이 JS보다 더 작기 때문에(binary) 더 빠르게 fetching 한다.
- 따라서 클라이언트, 서버간의 통신이 더 빠르다는 뜻이다. 느린 네트워크 환경에서도 효율적으로 작동할 수 있다.
2. Parse
- JS코드는 AST로 변환되고, 이로부터 JS엔진이 이해할 수 있는 중간 표현식 (Intermediate representation)인 bytecode를 생성한다.
- WASM은 이미 중간 표현식으로 되어있으므로 이 과정이 불필요하다.
- 단지, 디코딩과 유효성 검사를 통해 오류 여부만 확인한다.
3. Compile + Optimizing
- JS는 실행 중에 컴파일링 된다. 어떤 코드들은 다시 컴파일 할 필요가 없는데도 컴파일 된다.
- 브라우저에 따라서 WASM 컴파일은 다르게 수행될 수 있지만, WASM 자체는 기계어에 가까우며, 명시적 타입이 사용되었다.
- 명시적 타입을 사용하면 이점이 뭔데?
- 컴파일을 시작하기 전에 어떤 타입인지 observe 하는 데 시간을 사용하지 않아도 된다.
- 컴파일러는 다른 타입을 기반으로 동일한 코드를 다른 버전으로 컴파일하지 않아도 된다.
- 최적화는 이미 LLVM에서 수행이 되어 컴파일 시간에 덜 해도 된다.
4. Reoptimizing
- JIT의 예측이 실패할 경우 재최적화가 실행된다.
- prototype chain에 새 함수가 추가되거나 루프문에서 변수가 이전 반복에서 변경될때 수행
- 이미 최적화한 코드를 버리고 다시 최적화를 수행해야 한다.
- 다시 최적화한 코드를 재컴파일 해야한다.
- WASM에서는 JIT이 assumption을 할 필요가 없다. 왜냐하면 명시적 타입이기 때문에..
5. Executing
- JIT을 최적화하는 방법은 있긴 있지만, 그러려면 JIT의 내부 구조를 잘 알아야 한다.
- 대부분의 개발자는 잘 모르고, 하려고 해도 어렵다.
- 또한 브라우저마다 최적화가 다르기 때문에 브라우저 내부 코드로 인해 성능이 떨어질 수 있다.
- JS가 필요로하는 최적화 방법들(타입 specialization)이 필요가 없다.
- wasm은 컴파일러를 타겟으로 디자인되어 있어 컴파일러 최적화된 결과물을 출력한다. 컴퓨터에 더 이상적인 명령어로 구성될 수 있음을 말한다.
6. Garbage Collecting
- JS 엔진이 알아서 콜렉팅을 해주지만, 어느 시점에 발생할지 알 수 없으므로 단점일 수가 있다.
- WASM은 GC를 제공하지 않는다. 메모리는 C, C++처럼 manually 하게 관리해야 한다. 더 어렵게 만들수도 있지만, performance를 더 consistent 하게 할 수도 있다.
JS와 Web API
- JS, Web과 상호작용할 수 있는 API 사양을 발표했다.
웹어셈블리 네임스페이스 메소드
브라우저 내의 전역 웹어셈블리 객체에서 사용할 수 있는 다양한 객체를 다룬다.
instantiate()
- 웹 어셈블리 코드를 컴파일하고 인스턴스화 하기 위한 기본 API
instantiateStreaming()
- instantiate()와 동일한 기능을 수행하지만, 스트리밍을 통해 모듈을 컴파일하고 인스턴스화한다.
compile()
- 컴파일만 하고 인스턴스화는 X
compileStreaming()
- 스트리밍으로 compile
validate()
- 바이너리 코드 유효성 검사
WASM 모듈 로딩 과정
- fetch로 wasm파일 가져오기
- response로 받아온 Bytes를 ArrayBuffer 생성
- buffer와 option으로 받은 import객체로 WebAssembly.instantiate() 호출
fetch('example.wasm')
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer, importObj))
.then(({module, instance}) => {
//모듈이나 인스턴스로 작업 수행
})
// 위와 동일하지만 간단하게 한번에 처리해줌
WebAssembly.instantiateStreaming(fetch('example.wasm'), importObj)
.then(({module, instance}) => {
//모듈이나 인스턴스로 작업 수행
})
- WebAssembly.Module(module), WebAssembly.Instance(instance)를 포함하는 객체를 반환한다
WebAssembly 객체
WebAssembly.Module
- 이미 컴파일 된 statelss 웹어셈블리 코드를 포함
- Web Worker와 효과적으로 공유됨
- IndexedDB에 캐싱됨
- 여러 번 인스턴스화 될 수 있음
WebAssembly.Instance
- WebAssembly.Module의 상태가 저장되고 실행 가능한 인스턴스
- 익스포트된 웹어셈블리 함수를 호출할 수 있다
- instance.exports.sayHello() : sayHello는 함수명
WebAssembly.Memory
- 값을 저장하고 로드하는 데 사용할 수 있는 선형 배열. 사이즈 조절이 가능한 ArrayBuffer이다.
- 웹어셈블리 Instance에 의해서 접근되는 메모리를 담고 있음
- JS, WASM 모두 해당 메모리에 접근하거나 변경 가능
- 새로운 Memory를 만들기 위해서는 인자로 initial값과 maximum? 값을 전달해줘야함
WebAssembly.Table
- 함수 참조를 저장하는 공간
- JS, WASM 모두 해당 메모리에 접근하거나 변경 가능
- element, initial, maximum?을 인자로 전달 가능
- element는 테이블에 저장될 값의 유형을 말함. 현재는 ‘anyfunc’만 가능
- get(), set()으로 요소들에 접근 가능
WebAssembly.CompileError
- 디코딩, 혹은 유효성 검사 시 발생하는 에러
WebAssembly.LinkError
- 인스턴스화 하는 동안 문제가 발생
WebAssembly.RuntimeError
튜토리얼
현재 WASM을 사용하는 방법은 총 4가지가 있다.
- Emscripten으로 c/c++ 애플리케이션 포팅하기.
- 어셈블리 수준에서 바로 WebAssembly를 작성하거나 생성하기.
- Rust 응용 프로그램을 작성하고 WebAssembly를 출력으로 지정합니다.
- TypeScript와 비슷한 AssemblyScript를 사용하여 WebAssembly바이너리 컴파일.
Emscripten
- C/C++ 소스코드를 clang+LLVM (오픈소스 C/C++ 컴파일러 툴체인)에 던져준다.
- 컴파일 결과를 받아다가 .wasm 바이너리로 변환시킨다.
- HTML과 자바스크립트 글루 코드를 생성한다.
- Emscripten SDK 설치
git clone <https://github.com/emscripten-core/emsdk.git>
cd emsdk
# Download and install the latest SDK tools.
./emsdk install latest
# 현재 사용자를 위한 최신 버전의 SDK를 활성화한다
./emsdk activate latest
# Activate PATH and other environment variables in the current terminal
source ./emsdk_env.sh
- C파일 작성
int main() {
return printf("Hello World!\\n");
}
- 컴파일
emcc main.c -Os
-s WASM=1 -s
-o $(출력 파일 명)
Os
컴파일 최적화 레벨. 모듈 인스턴스화를 허용한다s WASM=1
컴파일러가 코드를 WASM으로 컴파일하게 만든다SIDE_MODULE=1
웹 어셈블리 모듈만을 출력한다o
출력 파일 경로- WASM 파일 생성 완료!
Glue Code
컴파일된 wasm 파일을 JS로 로딩하기 위해서는 glue code가 필요하다.
HTML과 글루 코드 출력
emcc with-glue.c -Os -s WASM=1 -o with-glue.html
o
의 확장자를html
로 하면 html파일과 js파일이 자동으로 만들어진다.- C/C++의 main을 html이 로드 되자마자 실행 할 것이다.
사용자 정의 HTML 템플릿 사용
emcc custom-loading.c -Os -s WASM=1 \\
-o custom-loading.js
<script type="application/javascript" src="custom-loading.js"></script>
Module({
canvas: (() => document.getElementById('canvas'))(),
})
.then(() => {
console.log('Loaded!');
});
- 를 html에 추가
글루 코드 없이 C컴파일
emcc without-glue.c -Os -s WASM=1 -s SIDE_MODULE=1 \\
-s EXPORTED_FUNCTIONS='["_doubler"]' \\
-o without-glue.wasm
<script type="application/javascript">
// importObj
const env = {};
loadWasm('without-glue.wasm', { env })
.then(({ instance }) => {
console.log(instance);
const m = instance.exports;
const result = m.doubler(4);
console.log(result);
});
</script>
/**
* Emscripten의 WASM 모듈을 위한 WebAssembly.Instance 생성자에 전달할
* importObj.env 객체를 디폴트 값으로 셋팅 후 반환한다.
*/
const getDefaultEnv = () => ({
__memory_base: 0,
__table_base: 0,
memory: new WebAssembly.Memory({ initial: 256 }),
table: new WebAssembly.Table({ initial: 2, element: 'anyfunc' }),
abort: console.log,
});
/**
* 지정된 .wasm 파일로부터 컴파일된 WebAssembly.Instance 인스턴스를 반환한다.
*/
function loadWasm(fileName, importObj = { env: {} }) {
// 지정된 importObj.env 값으로 디폴트 env값을 재설정한다.
const allEnv = Object.assign({}, getDefaultEnv(), importObj.env);
// importObj값이 유효한지 확인한다.
const allImports = Object.assign({}, importObj, { env: allEnv });
// 모듈을 인스턴스화한 결과를 리턴한다. (인스턴스와 모듈)
return fetch(fileName)
.then(response => {
if (response.ok) {
return response.arrayBuffer();
}
throw new Error(`Unable to fetch WebAssembly file ${fileName}`);
})
.then(bytes => WebAssembly.instantiate(bytes, allImports));
}
Emscripten Module
- Emscripten의 자바스크립트 글루 코드를 가져와 로딩한 후에는 Module을 전역적인 범위(window.Module)에서 사용할 수 있다.
JS에서 컴파일된 C/C++ 함수 호출
Module.ccall()
ccall(ident, returnType, argTypes, args, opts)
const result = Module.ccall(
'c_add', // 함수 이름
'number', // 리턴 타입
['number', 'number'], // 인자 타입
[10, 20] // 인자
)
// 30
- JS에서 컴파일 된 C함수를 호출하여 결과를 반환함
Module.cwrap()
cwrap(ident, returnType, argTypes)
const c_javascript_add = Module.cwrap(
'c_add', // 함수 이름
'number', // 리턴 타입
['number', 'number'], // 인자 타입
)
console.log(c_javascript_add(10, 20)); // 30
- 결과가 아닌 재사용 가능한 JS 함수를 반환
WASM 인스턴스에서 호출
WebAssembly.instantiateStreaming(fetch('simple.wasm'), importObj)
.then(result => {
const addNumbers = result.instatnce.exports._addTwoNumbers(2, 4);
// 3
});
C/C++에서 JS 함수 호출
emscripten_run_script()로 코드 문자열 실행
- 가장 직접적이지만 다소 느린 방법
- 문자열을 코드로 인식해 eval()을 이용해 코드를 실행시킨다.
emscripten_run_script("alert('hi')");
EM_ASM()으로 인라인 JS 실행
#include <emscripten.h>
int main() {
EM_ASM({
console.log('js code' + [$0, $1]);
}, 100, 30);
return 0;
}
EM_JS()로 인라인 JS 재사용
EM_JS(return_type, function_name, arguments, code)
#include <emscripten.h>
EM_JS(void, take_args, (int x, int y), {
console.log(`received ${x}, ${y}`)
});
int main() {
take_args(2, 4);
return 0;
}
EMSCRIPTEN_KEEPALIVE
- 모듈 외부에서 함수를 사용하려면 선언해야함
- 아니면 컴파일러는 데드코드로 취급해 제거
글루 코드 없이 JS와 통합
- C/C++ 파일 안에서 JS를 작성하기보다는 웹어셈블리 인스턴스화 코드에 함수를 전달한다.
import 객체를 이용
const env = {
_exampleFunc: value => {
console.log(`echo ${value}`);
}
}
- importObj.env에 함수를 정의하여 추가
C/C++에서 import된 함수를 호출
loadWasm('main.wasm', {env})
.then(({instance}) => {
const result = instance.exports._exampleFunc(33);
console.log(result);
// 33이 두 번 출력 됨
})
고급 기능
fetch API
- Emscripten은 fetch API를 지원한다.
- XHR로 파일을 전송할 수 있도록 해주며,
- 브라우저의 로컬 IndexedDB 저장소에 다운로드한 파일을 유지시킨다.
- 네트워크 요청은 비동기/동기로 선택해서 요청 가능하다
File System API
- FS 라이브러리를 사용 해 파일 작업을 지원한다.
AssemblyScript
- 링크
- TypeScript 기반
- 컴파일이 statically하게 된다. JIT과는 다른 방식! (C/C++과 같이..)
- 문제는 아직 정식 release가 안 된것 같음
Use cases
https://madewithwebassembly.com/
- figma
- sketch up
- 구글 어스
- autocad in web
- 그 외 게임들..
단점?
- 높은 러닝커브
- C/C++ 기반 지식이 필요
- 보안 문제
- GC가 없음
- DOM에 대한 직접적인 접근이 없음
이후 로드맵
- Garbage Collection
- 스레딩
- 모듈화 (WASM이 직접 Web API 호출)
ref
- https://webassembly.org/
- https://developer.mozilla.org/ko/docs/WebAssembly
- https://www.assemblyscript.org/introduction.html
- https://d2.naver.com/helloworld/8257914
- 웹 어셈블리 (Wasm과 C/C++를 이용한 고성능 웹 어플리케이션 개발) - 마이크 루크