[번역] 웹어셈블리 모듈의 생성과 동작

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 네 번째 글인 Creating and working with WebAssembly modules의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작 (현재글)
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래

 

웹어셈블리는 자바스크립트가 아닌 프로그래밍 언어를 웹페이지에서 실행하기 위한 방법이다. 예전에는 웹페이지 내의 다른 부분들과 상호작용하기 위해 브라우저에서 코드를 실행시키기 위한 유일한 선택지가 자바스크립트였다.

그래서 사람들이 웹어셈블리가 빠르다고 이야기할 때, 유일한 비교대상은 자바스크립트다. 하지만 그것이 웹어셈블리와 자바스크립트 중에서 하나만 선택해서 사용해야 한다는 의미는 아니다.

사실, 우리는 개발자들이 같은 어플리케이션 내에서 웹어셈블리와 자바스크립트를 모두 사용하길 바란다. 심지어 웹어셈블리를 직접 작성하지 않더라도, 그 이점을 누릴 수 있다.

웹어셈블리 모듈은 자바스크립트에서 사용될 수 있는 함수들을 정의한다. 그러므로 오늘날 npm 을 이용해서 lodash와 같은 모듈을 다운로드 받아서 API 에 있는 함수를 호출하듯이, 나중에는 웸어셈블리 모듈을 다운로드 받을 수 있게 될 것이다.

그러면, 웹어셈블리 모듈을 어떻게 생성할 수 있고, 어떻게 자바스크립트에 이용할 수 있는지를 살펴보자.

웹어셈블리는 어디에 어울릴까?

어셈블리에 대해 설명한 글에서, 나는 컴파일러가 상위 레벨 프로그래밍 언어들을 어떻게 읽어들여서 기계 코드로 번역하는지에 대해서 이야기했다.

04-01-langs09-500x306.png

이 그림에서 웹어셈블리는 어떤 역할을 할까?

어쩌면 웹어셈블리가 또 하나의 대상 어셈블리 언어라고 생각할지도 모르겠다. 어느 정도는 사실이지만, 이들 각각의 언어들 (x86, ARM)은 특정한 기계의 아키텍쳐와 연관된다는 점에서 차이가 있다.

사용자의 기계에서 실행될 코드를 웹을 통해서 전달할 때, 당신은 코드가 실행될 대상 아키텍쳐가 무엇인지를 알 수가 없다.

그러므로 웹어셈블리는 다른 종류의 어셈블리와는 약간 다르다. 웹어셈블리는 실제의 물리적인 기계가 아닌 개념상의 기계를 위한 언어이다.

이러한 이유로, 웹어셈블리의 명령(instruction)들은 가끔 가상 명령(virtual instruction)이라고도 불린다. 이들은 자바스크립트 코드보다는 훨씬 더 기계 코드와 직접적으로 맵핑된다. 이들은 공통적으로 널리 쓰이는 하드웨어에서 효과적으로 사용될 수 있는 일종의 교집합을 나타낸다. 하지만 이들이 특정한 하드웨어를 위한 특정 기계 코드에 직접적으로 맵핑되는 것은 아니다.

04-02-langs08-500x326.png

브라우저는 웹어셈블리를 다운로드한다. 그러면 브라우저는 웹어셈블리로부터 대상 기계의 어셈블리 코드를 위한 짧은 홉(hop)을 만들어낼 수 있다.

.wasm 으로 컴파일하기

현재 웹어셈블리를 가장 잘 지원하는 컴파일러 도구 모음(toolchain)은 LLVM이라 불린다. 많은 수의 프론트엔드와 백엔드가 LLVM으로 변환이 가능하다.

Note: 대부분의 웹어셈블리 모듈 개발자들은 C나 Rust 등의 언어들로 개발한 후 웹어셈블리로 컴파일하지만, 웹어셈블리 모듈을 생성하기 위한 다른 방법도 있다. 예를 들면, 타입스크립트를 이용해 웹어셈블리 모듈을 생성할 수 있는 실험적인 도구도 존재하고, 직접 텍스트 형태로 코드를 작성할 수도 있다.

우리가 C를 이용해 웹어셈블리를 작성한다고 가정하자. 우리는 clang 프론트엔드를 이용해서 C를 LLVM 중간 표현 방식으로 변환할 수 있을 것이다. LLVM의 IR로 변환하고 나면, LLVM이 그것을 이해할 수 있으므로 LLVM이 몇가지 최적화를 할 수 있게 된다.

LLVM의 IR(intermidiate representation)에서 웹어셈블리로 변환하기 위해서 우리는 백엔드가 필요하다. 현재 LLVM 프로젝트에서 하나가 진행되고 있다. 이 백엔드는 대부분의 사항이 완료되었고, 곧 완성될 예정이다. 하지만, 지금 당장 사용하기에는 약간 어려울 수도 있다.

지금 당장 사용하기에 좀더 쉬운 Emscripten 이라는 또다른 도구도 있다. 이는 자신만의 백엔드를 갖고 있는데, 이 백엔드는 또다른 대상 (asm.js)으로 컴파일한 후에 그것을 웹어셈블리로 변환하는 방식으로 웹어셈블리를 생성할 수 있다. 하지만 내부적으로는 LLVM을 사용하고 있기 때문에, Emscripten를 이용하면 두개의 백엔드 사이를 전환할 수 있다.

04-03-toolchain07-500x411.png

Emscripten은 전체 C/C++ 코드베이스를 변환할 수 있게 해 주는 많은 추가적인 도구와 라이브러리를 포함하기 때문에, 컴파일러라고 하기 보다는 소프트웨어 개발자 킷 (SDK: Software Developer Kit)에 가깝다. 예를 들어 시스템 개발자들은 주로 쓰기와 읽기를 할 수 있는 파일시스템을 갖고 있는데, Emscripten은 IndexedDB를 이용해서 파일 시스템을 시뮬레이트할 수 있다.

당신이 어떤 도구 모음을 사용했는지와는 관계 없이, 최종 결과는 .wasm 으로 끝나는 파일이 된다. 아래에서 .wasm의 구조에 대해 좀 더 설명하도록 하겠다. 먼저 이것을 자바스크립트에서 어떻게 사용할 수 있는지부터 살펴보자.

자바스크립트에서 .wasm 모듈 로딩하기

.wasm 파일은 웹어셈블리 모듈이며, 자바스크립트에서 로드될 수 있다. 이 시점부터 로딩 과정은 약간 복잡해진다.

function fetchAndInstantiate(url, importObject) {
  return fetch(url).then(response =>
    response.arrayBuffer()
  ).then(bytes =>
    WebAssembly.instantiate(bytes, importObject)
  ).then(results =>
    results.instance
  );
}

여기에 대한 더 깊은 설명은 우리 문서에서 확인할 수 있다.

우리는 이 과정을 좀더 쉽게 만들기 위해 작업하고 있다. 우리는 도구 모음을 좀더 발전시키고, 현존하는 webpack과 같은 모듈 번들러나 SystemJS와 같은 로더들을 통합할 수 있게 되길 기대한다. 우리는 웹어셈블리 모듈을 로딩하는 것이 자바스크립트 모듈을 로딩하는 것만큼 쉬워질 수 있다고 믿는다.

하지만, 웹어셈블리 모듈과 JS 모듈 사이에는 중요한 차이점이 있다. 현재, 웹어셈블리의 함수들은 파라미터나 반환값으로 오직 숫자형(정수 혹은 부동소수)만을 사용할 수 있다.

04-04-memory04-500x93.png

문자열과 같은 더 복잡한 데이터 형을 위해서는, 웹어셈블리 모듈의 메모리를 사용해야만 한다.

만약 당신이 대부분의 작업을 자바스크립트로 해 왔다면, 메모리에 직접 접근하는 것이 그리 익숙하지는 않을 것이다. C나 C++ 혹은 Rust와 같은 더 고성능의 언어들은 주로 메모리를 직접 관리한다. 웹어셈블리 모듈의 메모리는 이러한 언어들에서 찾을 수 있는 힙(heap)을 시뮬레이트한다.

이를 위해서, 웹어셈블리 모듈은 자바스크립트에서 배열 버퍼라 불리는 것을 사용한다. 배열 버퍼는 바이트의 배열이다. 그러므로 배열의 인덱스들은 메모리의 주소의 용도로 사용된다.
당신이 자바스크립트와 웹어셈블리 사이에서 문자열을 전달하려고 한다면, 해당 문자들을 동등한 문자 코드로 변환해야만 한다. 그리고 그 코드를 메모리 배열에 저장한다. 인덱스들이 정수형이기 때문에 인덱스 하나를 웹어셈블리 함수로 전달할 수 있다. 그러므로 해당 문자열의 첫번째 문자에 해당하는 인덱스가 포인터로써 사용될 수 있다.

04-05-memory12-500x400.png

아마 웹개발자들에 의해 사용될 웹어셈블리 모듈을 개발하는 사람은 누구나 해당 모듈 주변에 래퍼를 생성해 줄 것이다. 그렇게 되면 모듈을 사용하는 입장에서는 메모리 관리에 대해 알 필요가 없게 된다.
만약 좀더 배우고 싶다면, 웹어셈블리의 메모리 다루기에 대한 우리의 문서를 확인해보기 바란다.

.wasm 파일의 구조

만약 당신이 상위 레벨 언어로 코드를 작성한 후에 웹어셈블리로 컴파일한다면, 웹어셈블리 모듈이 어떻게 구성되는지에 대해서는 알 필요가 없다. 하지만 기본을 이해하는 것은 도움이 될 것이다.

아직 읽어보지 않았다면, 어셈블리에 대한 글을 읽어보길 권한다. (이 시리즈의 3부)
다음은 웹어셈블리로 변환할 C 함수이다.

int add42(int num) {
  return num + 42;
}

WASM Explorer를 이용하면 이 함수를 컴파일 할 수 있다.

.wasm 파일을 열어보면 (만약 에디터가 지원한다면), 다음과 같은 것을 보게 될 것이다.

00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
00 41 2A 6A 0B

이것은 모듈의 “바이너리” 표현 방식이다. 따옴표로 강조한 이유는 보통은 16진법으로 표시되기 때문인데, 이는 쉽게 바이너리 형태나 인간이 읽을 수 있는 형태로 변환이 가능하다.

예를 들어 num + 42 는 다음과 같을 것이다.

04-06-hex_binary_asm01-500x254.png

코드의 작동 방식 : 스택 기계

궁금해할 것 같아서 설명하자면, 이들 명령들은 아래와 같이 동작할 것이다.

04-07-hex_binary_asm02-500x175.png

그림 번역 :
get_local 0 -> 첫번째 파라미터의 값을 읽어서 스택에 추가한다
i32.const 42 -> 상수값을 스택에 추가한다
i32.add -> 스택의 최상위에 있는 두 값을 더해서 결과값을 스택에 추가한다

add 연산이 어디에서 값을 가져와야 하는지에 대해서는 전혀 언급이 없는 것을 눈치챘을지도 모르겠다. 그 이유는 웹어셈블리가 스택 기계라고 불리는 것의 일종이기 때문이다. 이는 한 연산이 실행되기 전에 그 연산이 필요로 하는 모든 값들이 스택에 쌓여 있다는 의미이다.

add와 같은 연산들은 스스로 몇 개의 값이 필요한지를 알고 있다. add는 두 개를 필요로 하기 때문에, 스택의 최상단에서 두 개의 값을 가져올 것이다. 이는 add 명령이 출처나 대상 레지스터를 지정할 필요가 없으므로 short 형(단일 바이트)이 될 수 있다는 것을 의미한다. 이는 .wasm 파일의 사이즈를 줄여주는데, 이로 인해 다운로드에 걸리는 시간도 줄어들게 된다.

비록 웹어셈블리가 스택 기계의 특징을 갖고있긴 하지만, 물리적 기계에서도 똑같이 동작하는 것은 아니다. 브라우저가 웹어셈블리를 자신이 구동되고 있는 기계를 위한 기계 코드로 번역할 때는 레지스터를 사용할 것이다. 하지만 웹어셈블리 코드는 레지스터를 명시하지 않기 때문에, 브라우저로 하여금 해당 기계에 맞는 최선의 레지스터 할당을 할 수 있도록 유연성을 제공해 준다.

모듈의 섹션

.wasm 파일에는 add42 함수 자신 이외에도 다른 부분이 존재한다. 이들을 섹션이라고 한다. 어떤 섹션들은 어떤 모듈에 필수 요건이며, 어떤 섹션들은 선택 요건이다.

필수 요건 :

  1. 타입. 이 모듈에 정의된 함수들이나 임포트한 함수들을 위한 함수 서명(signatures)을 포함한다.
  2. 함수. 이 모듈에 정의된 각 함수에 대한 인덱스를 제공한다.
  3. 코드. 이 모듈의 각 함수에 대한 실제 함수 본문.

선택 요건 :

  1. 익스포트(export). 다른 어셈블리 모듈이나 자바스크립트에서 사용가능한 함수, 메모리, 테이블, 전역 등을 생성한다.
  2. 임포트(import). 다른 어셈블리 모듈이나 자바스크립트에서 임포트한 함수, 메모리, 테이블, 전역 등을 지정한다.
  3. 시작. 웹어셈블리 모듈이 로드되었을 때 자동으로 실행될 함수 (기본적으로 메인 함수와 유사함)
  4. 전역. 모듈을 위한 전역 변수들을 선언한다.
  5. 메모리. 이 모듈이 사용할 메모리를 정의한다.
  6. 테이블. 자바스크립트 객체와 같이 웹어셈블리 모듈 외부에 있는 값들을 맵핑할 수 있도록 해 준다. 이는 간접적으로 함수를 호출할 수 있도록 할 때 특히 유용하다.
  7. 데이터. 임포트된 메모리나 로컬 메모리를 초기화한다.
  8. 요소. 임포트된 테이블이나 로컬 테이블을 초기화한다.

섹션에 대한 추가적인 설명을 원한다면 이들 섹션들이 어떻게 동작하는지에 대한 설명을 참고하길 바란다.

다음 주제

이제 웹어셈블을 어떻게 사용하는지 알았으니, 왜 어셈블리가 빠른지에 대해 알아보자.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark