Webassembly를 Rust로 쉽게 사용해보자

신석진( Seokjin Shin)·2024년 5월 22일
2
post-thumbnail

개요

Webassembly(이하 wasm)은 javascript 환경에서 네이티브 코드를 돌릴 수 있게 하는 기술입니다. 기존 웹 브라우저에 존재하는 가상머신(VM)에서는 JavaScript만을 인식하였지만 wasm 또한 인식하고 코드를 실행할 수 있게 되었습니다.

wasm은 기계어 코드로 컴파일된 바이너리로 VM에 적재된 후 가변 길이의 ArrayBuffer 형태로 메모리에 올라가게 됩니다. 이렇게 메모리에 있는 wasm을 postMessage를 사용하여 window와 service worker간에 공유할 수 있습니다. 앞서 말했듯 VM에 적재된 JavaScript와 wasm을 모두 사용하여 연산을 진행합니다.

JavaScript에서 wasm을 불러오기 위해 glue 코드를 활용합니다. 보통 직접 작성하지 않고 wasm을 지원하는 언어에서는 생성 도구를 제공하고 있습니다. Native code는 C/C++/C#/rust 등 다양한 언어에서 지원하며 이를 wasm으로 빌드하기 위한 여러가지 도구를 제공하고 있습니다.

이번 글에서 wasm을 빌드할 때 사용해볼 언어는 Rust입니다. 실제로 웹 생태계에 많은 영향을 주고 있는 모질라 재단이 처음 발표한 언어로 실제로 웹과의 호환성이 좋다고 알려져 있습니다. MDN 사이트의 공전을 구현하는 예시에서 연산이 있는 부분만 rust로 구현 후 wasm을 빌드하여 사용해보겠습니다.

구현

준비

아래와 같이 명령어를 입력하여 러스트 프로젝트를 생성하고 필요한 툴을 설치합니다.
wasm-pack은 Rust 프로젝트를 빌드하여 wasm 바이너리를 생성하고, wasm bindgen는 rust코드와 javascript 간의 통로 역할을 해줍니다.

cargo new --lib rust_wasm
cargo install wasm-pack wasm-bindgen

Web API를 Rust에서 사용하기 위해 Cargo.toml의 최하단에 아래와 같이 web-sys 의존성들을 명시해줘야합니다. web-sys는 Web API들을 담고 있는 web-bindgen을 제공합니다.

[dependencies.web-sys]
version = "0.3.4"
features = [
  'Document',
  'Element',
  'HtmlElement',
  'Node',
  'Window',
]

Rust 코드 작성

Rust 코드를 간략하게 설명하자면 #[wasm_bindgen(start)]는 wasm이 로드 될 때 호출됩니다. 이 안에서 html 속성들을 찾고 canvas를 삽입합니다.
하위에 공개 함수들은 JavaScript에서 호출할 함수입니다. 원래는 시간을 Rust 내장 함수를 사용하여 구하고 싶었으나 시스템 시간을 가져와야하는 부분에서 예외가 발생하여 javascript에서 밀리초를 넘겨주는 방식으로 계산을 진행하였습니다.

use std::f64::consts::PI;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
fn main() -> Result<(), JsValue> {
    let window = web_sys::window().expect("no global `window` exists");
    let document = window.document().expect("should have a document on window");
    let body = document.body().expect("document should have a body");

    let canvas = document.create_element("canvas")?;
    canvas.set_id("canvas");
    body.append_child(&canvas)?;

    Ok(())
}

#[wasm_bindgen]
pub fn get_earth_time(t: i32) -> f64 {
    let seconds = t / 1000 % 60;
    let milliseconds = t % 1000;

    (2.0 * PI / 60.0) * seconds as f64 + (2.0 * PI / 60_000.0) * milliseconds as f64
}

#[wasm_bindgen]
pub fn get_moon_time(t: i32) -> f64 {
    let seconds = t / 1000 % 60;
    let milliseconds = t % 1000;

    (2.0 * PI / 6.0) * seconds as f64 + (2.0 * PI / 6_000.0) * milliseconds as f64
}

아래와 같이 빌드하면 프로젝트 루트의 pkg 폴더 내부에 wasm과 함께 바인딩 역할을 해줄 JavaScript가 생성됩니다.

wasm-pack build --target web

이 JavaScript를 활용하여 index.js를 만들어줍니다.

HTML/JavaScript 코드 작성

위에서 살펴봤던 get_earth_time, get_moon_time을 생성된 JavaScript에서 가져와줍니다. 그 후 현재 시간을 밀리초 단위로 함수에 전달해주기만 하면 됩니다.

import init, { get_earth_time, get_moon_time } from './pkg/rust_wasm.js';

const sun = new Image();
const moon = new Image();
const earth = new Image();

async function start() {
    await init();

    sun.src = "canvas_sun.png";
    moon.src = "canvas_moon.png";
    earth.src = "canvas_earth.png";
    window.requestAnimationFrame(draw);
}

function draw() {
    const size = Math.min(innerWidth, innerHeight);
    const width = size;
    const height = size;
    const arbit = width / 3;
    const canvas = document.getElementById("canvas");
    canvas.width = width;
    canvas.height = height;

    const ctx = canvas.getContext("2d");

    ctx.globalCompositeOperation = "destination-over";
    ctx.clearRect(0, 0, width, height);

    ctx.fillStyle = "rgb(0 0 0 / 40%)";
    ctx.strokeStyle = "rgb(0 153 255 / 40%)";
    ctx.save();

    ctx.translate(width / 2, height / 2);

    const now = Date.now();

    ctx.rotate(get_earth_time(now));
    ctx.translate(arbit, 0);
    ctx.fillRect(0, -12, 40, 24);
    ctx.drawImage(earth, -12, -12);

    ctx.rotate(get_moon_time(now));
    ctx.translate(0, 28.5);
    ctx.drawImage(moon, -3.5, -3.5);

    ctx.restore();

    ctx.beginPath();
    ctx.arc(width / 2, height / 2, arbit, 0, Math.PI * 2, false);
    ctx.stroke();

    ctx.drawImage(sun, 0, 0, width, height);

    window.requestAnimationFrame(draw);
}

start();

JavaScript를 로드하는 HTML을 작성합니다.

<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            margin: 0;
        }
    </style>
</head>
<body>
    <script type="module" src="/index.js"></script>
</body>
</html>

루트에서 npx http-server .를 실행하여 루트 폴더를 정적으로 제공하면 아래와 같은 결과를 확인해볼 수 있습니다.

마무리

사실 현재 진행한 예시로는 wasm의 이점을 크게 누리지 못했습니다. 연산량이 많고 특히나 자료구조를 활용하는 경우 단순히 많은 양의 Array를 순회해야하는 경우에 성능차이가 많이 날 것입니다. 실제로 rust를 통해 wasm을 생성하여 게임 엔진과 같이 연산이 많고 메모리를 효율적으로 써야하는 분야에 적극적으로 도입해보면 좋을 것 같습니다.

원본 코드

0개의 댓글