avatar
Published on

Rust로 web assembly 만들어보기 (2) - Rust로 간단한 Web Assembly 만들기

Author
  • avatar
    Name
    yceffort

Table of Contents

개발 환경

  1. Install Rust로 먼저 Rust를 설치한다.
  2. 그리고 wasm을 만들기 위해, wasm-pack을 설치한다.
cargo install wasm-pack

패키지 만들기

cargo new --lib hello-wasm

이제 아래와 같은 파일이 생성되었을 것이다.

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

일반적으로 단위테스트는 src 디렉토리의 각 파일에 테스트 할 코드와 함께 작성한다. 여기서 사용되는 규칙은 각 파일에 mod tests라는 모듈을 #[cfg(test)]와 함께 선언하고, 그안에 테스트할 코드를 작성하면 된다. #[cfg(test)] 로 선언된 모듈은 cargo test를 할 때만 실행되고, build시에는 컴파일 되지 않는다. 따라서 빌드 시 시간과 공간을 절약할 수 있다.

cfg는 configuration 이라는 뜻이다.

[#test]는 이 함수가 테스트 함수임을 가리키는 역할을 한다.

Rust 작성하기

먼저 Cargo.tomlwasm_bindgen을 의존성 목록에 추가해주자.

[package]
name = "hello-wasm"
version = "0.1.0"
authors = ["yceffort <yceffort@gmail.com>"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
repository = "https://github.com/yceffort/rust-playground/tree/main/wasm/tutorial/hello-wasm"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
// import * from wasm_bindgen/prelude와 같다.
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    pub fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

wasm-bindgen은 자바스크립트와 러스트 사이에 일종의 다리 역할을 한다고 보면 된다. 자바스크립트에서 rust api를 호출하거나, 반대로 rust가 js에서 발생한 예외처리를 하는 등의 처리를 할 수 있도록 해준다.

#[XXX]는 일종의 wrapper를 생성하는 속성 값인데, 이것이 무슨일을 하는지는 이후에 알아보자.

extern 키워드는, 이 것이 rust 외부에 정의된 함수라는 것을 알린다. 외부에 alert라는 함수가 있으며, 이는 문자열 타입의 s 를 받는 다는 것을 의미한다. 눈치 챘을 수도 있지만, 이는 window.alert를 의미한다.

즉, 자바스크립트에 무언가 함수를 호출 하고 싶다면 extern 키워드와 함께 추가하면 된다.

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

이번에는 extern 키워드 대신 다른 것이 나왔다. 이번에는 fn 구문을 wrapping 하고 있다. 이는 rust 함수를 자바스크립트에 의해 호출될 수 있도록 처리한다는 것을 의미한다. 즉 extern과는 반대가 되는 기능이다.

함수를 보면 알겠지만, greet()는 문자열 타입 name을 받고 hello {name}이라는 문자열을 만들고 이를 alert에 넘겨주고 있다.

이제 이 코드를 빌드해보자

빌드하기

wasm-pack build --scope yceffort

마지막 scope는 npm 계정의 아이디를 넣어주면된다.

이 빌드는 다음과 같은 과정을 수행한다.

  1. Rust 코드를 WebAssembly로 컴파일
  2. WebAssembly위에서 wasm-bindgen을 실행하여, WebAssembly가 npm이 이해할 수 있는 모듈로 감싸는 자바스크립트 파일을 생성
  3. pkg 폴더를 만들고, 자바스크립트 파일과 WebAssembly 코드를 그 안으로 옮긴다.
  4. Cargo.toml과 동등한 package.json을 생성
  5. README.md가 있다면 패키지로 복사

빌드가 완료되었다면, pkg 폴더가 생성되어 있는 것을 볼 수 있다.

hello_wasm.js

import * as wasm from './hello_wasm_bg.wasm'
export * from './hello_wasm_bg.js'

package.json

{
  "name": "@yceffort/hello-wasm",
  "collaborators": ["yceffort <yceffort@gmail.com>"],
  "description": "A sample project with wasm-pack",
  "version": "0.1.0",
  "license": "MIT/Apache-2.0",
  "repository": {
    "type": "git",
    "url": "https://github.com/yceffort/rust-playground.git"
  },
  "files": [
    "hello_wasm_bg.wasm",
    "hello_wasm.js",
    "hello_wasm_bg.js",
    "hello_wasm.d.ts"
  ],
  "module": "hello_wasm.js",
  "types": "hello_wasm.d.ts",
  "sideEffects": false
}

빌드한 패키지 사용해보기

이 npm package를 사용할 수 있도록 한번 설정해보자.

{
  "name": "hello-wasm-npm",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "serve": "webpack-dev-server"
  },
  "dependencies": {
    "@yceffort/hello-wasm": "../hello-wasm/pkg"
  },
  "devDependencies": {
    "webpack": "^4.25.1",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.10"
  },
  "author": "",
  "license": "ISC"
}
const path = require('path')
module.exports = {
  entry: './index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js',
  },
  mode: 'development',
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>hello-wasm example</title>
  </head>
  <body>
    <script src="./index.js"></script>
  </body>
</html>
const js = import('./node_modules/@yceffort/hello-wasm/hello_wasm.js')
js.then((js) => {
  js.greet("yceffort's first WebAssembly")
})

first-wasm

wasm-bindgen의 대략적인 원리

wasm-bindgen의 가장 중요한 개념은, wasm module이 ES Module의 한 가지 종류로 인식하고 연동한다는 것이다. pkg에 있는 d.ts를 보면 (타입스크립트 시그니쳐까지..!) 다음과 같이 선언되어 있다.

/* tslint:disable */
/* eslint-disable */
/**
 * @param {string} name
 */
export function greet(name: string): void

WebAssembly는 이러한 처리가 불가능하므로, 이 것을 수행해주는 것이 wasm-bindgen이다. 이 중 자바스크립트 파일은 러스트를 호출할때 사용되는 인터페이스 역할을 하고, *_bg.wasm 파일이 실제로 방금 컴파일한 것과 구현체를 가지고 있다.

hello_wasm_bg.js 파일은 다음과 같이 구현되어 있다.

import * as wasm from './hello_wasm_bg.wasm'

// ...

function getStringFromWasm0(ptr, len) {
  return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len))
}

/**
 * @param {string} name
 */
export function greet(name) {
  var ptr0 = passStringToWasm0(
    name,
    wasm.__wbindgen_malloc,
    wasm.__wbindgen_realloc,
  )
  var len0 = WASM_VECTOR_LEN
  wasm.greet(ptr0, len0)
}

export function __wbg_alert_a5a2f68cc09adc6e(arg0, arg1) {
  alert(getStringFromWasm0(arg0, arg1))
}

wasm.greet(ptr0, len0);를 보면, 이 함수는 문자열이 아닌 포인터와 length를 인수로 받고 있는 것을 알 수 있다.

조금 더 깊이 들어가서, WebAssembly의 greet함수가 러스트 컴파일러에 의해 컴파일되는 시점을 보면 이런식으로 코드가 작성되어 있다.

pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

#[export_name = "greet"]
pub extern fn __wasm_bindgen_generated_greet(arg0_ptr: *mut u8, arg0_len: usize) {
    let arg0 = unsafe { ::std::slice::from_raw_parts(arg0_ptr as *const u8, arg0_len) }
    let arg0 = unsafe { ::std::str::from_utf8_unchecked(arg0) };
    greet(arg0);
}

원래 작성한 코드와 함께, 이상한 이름의 함수와 #[export_name = "greet"]가 붙어 있다. 이는 JS가 던진 pointer와 length를 받는 부분이다. 이 두개 인자를 받아서, greet 함수에 전달한다.

정리하자면, #[wasm_bindgen]는 두개의 wrapper를 생성한다.

  • JS 타입을 받아서 wasm으로 변환 (자바스크립트)
  • wasm 타입을 rust 타입으로 변환 (러스트)

즉, 앞서 언급했던 것 처럼, wasm-bindgen는 자바스크립트 - WASM - 러스트 사이에 다리 역할을 하고 있으며, 이를 위해 많은 일들이 뒷단에서 일어나고 있음을 알 수 있다.