6 분 소요

  • 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의 실행 과정을 살펴보자.

https://user-images.githubusercontent.com/41617388/130311879-10e0ee70-6014-4d84-a70c-0ecd942b58de.png

  • Parsing(파싱)
    • 소스코드를 interpreter가 실행할 수 있도록 변경
  • Compiling + Optimizing (컴파일 + 최적화)
  • Re-optimizing (재최적화)
  • Execution (실행)
  • Garbage Collection (가비지 콜렉팅)

TradeOff는 모니터링과 컴파일링하는 과정이다. > 개선할 수 있음

웹 어셈블리는 그럼 어떤데?

1. Fetching

  • 서버에서 파일을 fetching하는 과정
  • 아무리 JS파일이 압축을 한다고 해도 WASM은 파일이 JS보다 더 작기 때문에(binary) 더 빠르게 fetching 한다.
  • 따라서 클라이언트, 서버간의 통신이 더 빠르다는 뜻이다. 느린 네트워크 환경에서도 효율적으로 작동할 수 있다.

2. Parse

https://user-images.githubusercontent.com/41617388/130312543-2219dad6-2801-45aa-872d-85d196a7ac9e.png

  • JS코드는 AST로 변환되고, 이로부터 JS엔진이 이해할 수 있는 중간 표현식 (Intermediate representation)인 bytecode를 생성한다.
  • WASM은 이미 중간 표현식으로 되어있으므로 이 과정이 불필요하다.
  • 단지, 디코딩과 유효성 검사를 통해 오류 여부만 확인한다.

3. Compile + Optimizing

https://user-images.githubusercontent.com/41617388/130312538-4ec1ecc8-95c9-47b2-9ad4-d68f67304856.png

  • JS는 실행 중에 컴파일링 된다. 어떤 코드들은 다시 컴파일 할 필요가 없는데도 컴파일 된다.
  • 브라우저에 따라서 WASM 컴파일은 다르게 수행될 수 있지만, WASM 자체는 기계어에 가까우며, 명시적 타입이 사용되었다.
  • 명시적 타입을 사용하면 이점이 뭔데?
    • 컴파일을 시작하기 전에 어떤 타입인지 observe 하는 데 시간을 사용하지 않아도 된다.
    • 컴파일러는 다른 타입을 기반으로 동일한 코드를 다른 버전으로 컴파일하지 않아도 된다.
    • 최적화는 이미 LLVM에서 수행이 되어 컴파일 시간에 덜 해도 된다.

4. Reoptimizing

https://user-images.githubusercontent.com/41617388/130312614-d6ce0158-c6d6-4b2d-93ed-f72f223ec188.png

  • JIT의 예측이 실패할 경우 재최적화가 실행된다.
  • prototype chain에 새 함수가 추가되거나 루프문에서 변수가 이전 반복에서 변경될때 수행
  • 이미 최적화한 코드를 버리고 다시 최적화를 수행해야 한다.
  • 다시 최적화한 코드를 재컴파일 해야한다.
  • WASM에서는 JIT이 assumption을 할 필요가 없다. 왜냐하면 명시적 타입이기 때문에..

5. Executing

https://user-images.githubusercontent.com/41617388/131213688-1d2d269e-b087-4240-99a6-694ee67bbd8c.png

  • 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 모듈 로딩 과정

https://user-images.githubusercontent.com/41617388/131221733-3334fb98-35a1-4144-81d2-5489a741a8b7.png

  • 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가지가 있다.

  1. Emscripten으로 c/c++ 애플리케이션 포팅하기.
  2. 어셈블리 수준에서 바로 WebAssembly를 작성하거나 생성하기.
  3. Rust 응용 프로그램을 작성하고 WebAssembly를 출력으로 지정합니다.
  4. TypeScript와 비슷한 AssemblyScript를 사용하여 WebAssembly바이너리 컴파일.

Emscripten

https://user-images.githubusercontent.com/41617388/131340859-605f0ce0-fcbd-4576-b248-114a18b2062e.png

  • C/C++ 소스코드를 clang+LLVM (오픈소스 C/C++ 컴파일러 툴체인)에 던져준다.
  • 컴파일 결과를 받아다가 .wasm 바이너리로 변환시킨다.
  • HTML과 자바스크립트 글루 코드를 생성한다.
    1. 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

  1. C파일 작성
int main() {
  return printf("Hello World!\\n");
}

  1. 컴파일
emcc main.c -Os
  -s WASM=1 -s
  -o $(출력 파일 명)

  • Os 컴파일 최적화 레벨. 모듈 인스턴스화를 허용한다
  • s WASM=1 컴파일러가 코드를 WASM으로 컴파일하게 만든다
  • SIDE_MODULE=1 웹 어셈블리 모듈만을 출력한다
  • o 출력 파일 경로
    1. 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

카테고리:

업데이트: