mkbitmap을 WebAssembly로 컴파일

WebAssembly란 무엇이며 어디에서 왔나요?에서 오늘 WebAssembly를 완성하게 된 과정을 설명해 드렸습니다. 이 도움말에서는 기존 C 프로그램인 mkbitmap를 WebAssembly로 컴파일하는 접근 방식을 보여줍니다. 이 예제에는 파일 작업, WebAssembly 및 JavaScript 영역 간 통신, 캔버스에 그리기가 포함되기 때문에 hello world 예제보다 더 복잡하지만, 부담을 주지 않을 만큼 충분히 관리할 수 있습니다.

이 도움말은 WebAssembly를 배우려는 웹 개발자를 위해 작성되었으며 mkbitmap 등을 WebAssembly로 컴파일하려는 경우 방법을 단계별로 보여줍니다. 경고로, 처음 실행할 때 앱이나 라이브러리가 컴파일되지 않는 것은 완전히 정상적인 현상입니다. 그렇기 때문에 아래에 설명된 일부 단계가 작동하지 않았기 때문에 다시 추적하여 다르게 다시 시도해야 했습니다. 이 문서에서는 최종 컴파일 명령어가 마치 하늘에서 떨어진 것처럼 보이는 마법의 최종 컴파일 명령어를 보여주지 않고 실제 진행 상황과 약간의 불만을 설명합니다.

mkbitmap 정보

mkbitmap C 프로그램은 이미지를 읽고 이미지에 역전, 고역 필터링, 확장, 임곗값 순서로 하나 이상의 작업을 적용합니다. 각 작업은 개별적으로 제어하고 켜거나 끌 수 있습니다. mkbitmap의 주요 용도는 색상 또는 그레이 스케일 이미지를 다른 프로그램, 특히 SVGcode의 기반을 형성하는 추적 프로그램 potrace에 적합한 형식으로 변환하는 것입니다. 사전 처리 도구인 mkbitmap는 특히 만화나 필기 텍스트와 같이 스캔한 라인아트를 고해상도 2레벨 이미지로 변환하는 데 유용합니다.

여러 옵션과 하나 이상의 파일 이름을 전달하여 mkbitmap를 사용합니다. 자세한 내용은 도구의 설명서 페이지를 참조하세요.

$ mkbitmap [options] [filename...]
컬러 만화 이미지입니다.
원본 이미지 (소스).
전처리 후 그레이 스케일로 변환된 만화 이미지
첫 번째 확장 후 기준점: mkbitmap -f 2 -s 2 -t 0.48 (소스)

코드 가져오기

첫 번째 단계는 mkbitmap의 소스 코드를 가져오는 것입니다. 프로젝트 웹사이트에서 찾을 수 있습니다. 이 문서의 작성 시점을 기준으로는 potrace-1.16.tar.gz가 최신 버전입니다.

로컬에서 컴파일 및 설치

다음 단계는 도구를 로컬에서 컴파일하고 설치하여 어떻게 작동하는지 파악하는 것입니다. INSTALL 파일에는 다음 안내가 포함되어 있습니다.

  1. cd를 패키지의 소스 코드가 포함된 디렉터리로 이동하고 ./configure를 입력하여 시스템에 패키지를 구성합니다.

    configure를 실행하는 데 다소 시간이 걸릴 수 있습니다. 실행 중에는 확인 중인 기능을 알려주는 일부 메시지가 출력됩니다.

  2. make을 입력하여 패키지를 컴파일합니다.

  3. 선택적으로, make check를 입력하여 일반적으로 방금 빌드한 제거된 바이너리를 사용하여 패키지와 함께 제공되는 자체 테스트를 실행합니다.

  4. make install를 입력하여 프로그램과 데이터 파일 및 문서를 설치합니다. 루트가 소유한 프리픽스에 설치할 때는 일반 사용자로 패키지를 구성 및 빌드하고 make install 단계만 루트 권한으로 실행하는 것이 좋습니다.

이 단계를 수행하면 potracemkbitmap라는 두 개의 실행 파일이 생성됩니다. 이 도움말에서는 두 가지 실행 파일을 중점적으로 다룹니다. mkbitmap --version를 실행하여 올바르게 작동하는지 확인할 수 있습니다. 다음은 간결하게 표시하기 위해 내 컴퓨터의 네 가지 단계 모두의 출력입니다.

1단계, ./configure:

 $ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands

2단계, make:

$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I..     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.

3단계, make check:

$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS:  8
# SKIP:  0
# XFAIL: 0
# FAIL:  0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.

4단계, sudo make install:

$ sudo make install
Password:
Making install in src
 .././install-sh -c -d '/usr/local/bin'
  /bin/sh ../libtool   --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.

작동하는지 확인하려면 mkbitmap --version를 실행합니다.

$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

버전 세부정보를 가져오면 mkbitmap를 성공적으로 컴파일하고 설치한 것입니다. 그런 다음 동일한 단계를 WebAssembly에서 실행합니다.

WebAssembly로 mkbitmap 컴파일

Emscripten은 C/C++ 프로그램을 WebAssembly로 컴파일하는 도구입니다. Emscripten의 프로젝트 빌드 문서에는 다음과 같이 설명되어 있습니다.

Emscripten을 사용하면 대규모 프로젝트를 손쉽게 구축할 수 있습니다. Emscripten은 gcc의 드롭인 대체로 emcc를 사용하도록 makefile을 구성하는 간단한 스크립트 두 개를 제공합니다. 대부분의 경우 프로젝트의 현재 빌드 시스템은 변경되지 않은 상태로 유지됩니다.

그러면 문서가 계속 진행됩니다 (간결성을 위해 약간 수정됨).

일반적으로 다음 명령어로 빌드하는 경우를 생각해 보겠습니다.

./configure
make

Emscripten으로 빌드하려면 다음 명령어를 대신 사용합니다.

emconfigure ./configure
emmake make

따라서 기본적으로 ./configureemconfigure ./configure가 되고 makeemmake make가 됩니다. 다음은 mkbitmap를 사용하여 이 작업을 실행하는 방법을 보여줍니다.

0단계, make clean:

$ make clean
Making clean in src
 rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo

1단계, emconfigure ./configure:

$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands

2단계, emmake make:

$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I..     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.

모든 것이 잘 진행되었다면 디렉터리 어딘가에 .wasm 파일이 있을 것입니다. find . -name "*.wasm"를 실행하여 찾을 수 있습니다.

$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm

마지막 두 개는 유망해 보이므로 src/ 디렉터리로 cd합니다. 이제 상응하는 두 개의 새 파일 mkbitmappotrace도 있습니다. 이 도움말에서는 mkbitmap만 관련이 있습니다. .js 확장자가 없다는 사실이 조금 헷갈리긴 하지만, 사실은 간단한 head 호출로 확인할 수 있는 JavaScript 파일입니다.

$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define   var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};

// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)

mv mkbitmap mkbitmap.js (원하는 경우 각각 mv potrace potrace.js)를 호출하여 JavaScript 파일의 이름을 mkbitmap.js로 바꿉니다. 이제 node mkbitmap.js --version를 실행하여 명령줄에서 Node.js로 파일을 실행하여 첫 번째 테스트가 작동하는지 확인합니다.

$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

mkbitmap을(를) WebAssembly로 컴파일했습니다. 이제 다음 단계는 브라우저에서 작동하도록 하는 것입니다.

브라우저에서 WebAssembly로 mkbitmap

mkbitmap.jsmkbitmap.wasm 파일을 mkbitmap이라는 새 디렉터리에 복사하고 mkbitmap.js JavaScript 파일을 로드하는 index.html HTML 상용구 파일을 만듭니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>mkbitmap</title>
  </head>
  <body>
    <script src="mkbitmap.js"></script>
  </body>
</html>

mkbitmap 디렉터리를 제공하는 로컬 서버를 시작하고 브라우저에서 엽니다. 입력을 요청하는 메시지가 표시됩니다. 이는 도구의 man 페이지에 따르면 '[i]파일 이름 인수가 제공되지 않으면 mkbitmap이 표준 입력에서 읽는 필터로 작동'하고 Emscripten의 경우 기본적으로 prompt()이기 때문에 이는 예상된 결과입니다.

입력을 요청하는 프롬프트를 보여주는 mkbitmap 앱

자동 실행 방지

mkbitmap가 즉시 실행되는 것을 중지하고 사용자 입력을 기다리도록 하려면 Emscripten의 Module 객체를 이해해야 합니다. Module는 Emscripten이 생성한 코드가 실행의 여러 지점에서 호출하는 속성이 포함된 전역 JavaScript 객체입니다. Module 구현을 제공하여 코드 실행을 제어할 수 있습니다. Emscripten 애플리케이션은 시작될 때 Module 객체의 값을 보고 적용합니다.

mkbitmap의 경우 Module.noInitialRuntrue로 설정하여 메시지가 표시되도록 한 최초 실행을 방지합니다. script.js라는 스크립트를 만들고 index.html<script src="mkbitmap.js"></script> 에 포함하고 다음 코드를 script.js에 추가합니다. 이제 앱을 새로고침하면 프롬프트가 사라질 것입니다.

var Module = {
  // Don't run main() at page load
  noInitialRun: true,
};

추가 빌드 플래그를 사용하여 모듈식 빌드 만들기

앱에 입력을 제공하려면 Module.FS에서 Emscripten의 파일 시스템 지원을 사용하면 됩니다. 문서의 파일 시스템 지원 포함 섹션에는 다음과 같은 내용이 있습니다.

Emscripten은 파일 시스템 지원을 자동으로 포함할지 결정합니다. 대부분의 프로그램은 파일이 필요하지 않고 파일 시스템 지원 크기가 무시할 수 없으므로 Emscripten은 특별한 이유가 없을 때 파일을 포함하지 않도록 합니다. 즉, C/C++ 코드가 파일에 액세스하지 않으면 FS 객체 및 기타 파일 시스템 API가 출력에 포함되지 않습니다. 반면에 C/C++ 코드에서 파일을 사용하면 파일 시스템 지원이 자동으로 포함됩니다.

안타깝게도 mkbitmap은 Emscripten이 파일 시스템 지원을 자동으로 포함하지 않는 경우 중 하나이므로 이를 명시적으로 지시해야 합니다. 즉, 앞에서 설명한 emconfigureemmake 단계를 따라야 하며 CFLAGS 인수를 통해 플래그를 몇 개 더 설정해야 합니다. 다음 플래그는 다른 프로젝트에도 유용할 수 있습니다.

또한 이 경우 --host 플래그를 wasm32로 설정하여 WebAssembly용으로 컴파일하고 있음을 configure 스크립트에 알려야 합니다.

최종 emconfigure 명령어는 다음과 같습니다.

$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'

emmake make를 다시 실행하고 새로 만든 파일을 mkbitmap 폴더에 복사해야 합니다.

ES 모듈 script.js만 로드하도록 index.html를 수정합니다. 여기에서 mkbitmap.js 모듈을 가져옵니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>mkbitmap</title>
  </head>
  <body>
    <!-- No longer load `mkbitmap.js` here -->
    <script src="script.js" type="module"></script>
  </body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  console.log(Module);
};

run();

이제 브라우저에서 앱을 열면 DevTools 콘솔에 로깅된 Module 객체가 표시되고 프롬프트가 사라집니다. 시작 시 mkbitmapmain() 함수가 더 이상 호출되지 않기 때문입니다.

DevTools 콘솔에 로깅된 모듈 객체를 보여주는 흰색 화면의 mkbitmap 앱

수동으로 기본 함수 실행

다음 단계는 Module.callMain()를 실행하여 mkbitmapmain() 함수를 수동으로 호출하는 것입니다. callMain() 함수는 명령줄에서 전달하는 내용에 대해 하나씩 일치하는 인수 배열을 취합니다. 명령줄에서 mkbitmap -v을 실행하는 경우 브라우저에서 Module.callMain(['-v'])를 호출합니다. 그러면 DevTools 콘솔에 mkbitmap 버전 번호가 기록됩니다.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  Module.callMain(['-v']);
};

run();

DevTools 콘솔에 로깅된 mkbitmap 버전 번호를 보여주는 흰색 화면의 mkbitmap 앱

표준 출력 리디렉션

표준 출력 (stdout)은 기본적으로 콘솔입니다. 그러나 출력을 변수에 저장하는 함수와 같은 다른 항목으로 리디렉션할 수 있습니다. 즉, Module.print 속성을 설정하여 HTML에 출력을 추가할 수 있습니다.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  let consoleOutput = 'Powered by ';
  const Module = await loadWASM({
    print: (text) => (consoleOutput += text),
  });
  Module.callMain(['-v']);
  document.body.textContent = consoleOutput;
};

run();

mkbitmap 버전 번호를 보여주는 mkbitmap 앱

입력 파일을 메모리 파일 시스템으로 가져오기

입력 파일을 메모리 파일 시스템으로 가져오려면 명령줄에 mkbitmap filename에 상응하는 명령어가 필요합니다. 이 작업에 접근하는 방식을 이해하려면 먼저 mkbitmap가 입력을 예상하고 출력을 생성하는 방법에 관한 배경 지식이 필요합니다.

mkbitmap의 지원되는 입력 형식은 PNM (PBM, PGM, PPM) 및 BMP입니다. 출력 형식은 비트맵의 경우 PBM이고 그레이맵의 경우 PGM입니다. filename 인수가 지정되면 mkbitmap는 기본적으로 접미사를 .pbm로 변경하여 입력 파일 이름에서 이름을 가져온 출력 파일을 만듭니다. 예를 들어 입력 파일 이름이 example.bmp이면 출력 파일 이름은 example.pbm입니다.

Emscripten은 로컬 파일 시스템을 시뮬레이션하는 가상 파일 시스템을 제공하므로, 동기 파일 API를 사용하는 네이티브 코드를 거의 또는 전혀 변경하지 않고 컴파일하고 실행할 수 있습니다. mkbitmap가 입력 파일을 filename 명령줄 인수로 전달된 것처럼 읽으려면 Emscripten에서 제공하는 FS 객체를 사용해야 합니다.

FS 객체는 메모리 내 파일 시스템 (일반적으로 MEMFS라고 함)에서 지원되며 가상 파일 시스템에 파일을 쓰는 데 사용하는 writeFile() 함수가 있습니다. 다음 코드 샘플과 같이 writeFile()를 사용합니다.

파일 쓰기 작업이 작동하는지 확인하려면 '/' 매개변수를 사용하여 FS 객체의 readdir() 함수를 실행합니다. example.bmp항상 자동으로 생성되는 여러 기본 파일이 표시됩니다.

버전 번호를 출력하기 위한 이전 Module.callMain(['-v']) 호출은 삭제되었습니다. 이는 Module.callMain()가 일반적으로 한 번만 실행될 것으로 예상되는 함수이기 때문입니다.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  console.log(Module.FS.readdir('/'));
};

run();

example.bmp를 포함한 메모리 파일 시스템의 파일 배열을 보여주는 mkbitmap 앱

첫 번째 실제 실행

모든 것이 준비되면 Module.callMain(['example.bmp'])을 실행하여 mkbitmap를 실행합니다. MEMFS의 '/' 폴더의 콘텐츠를 로깅하면 example.bmp 입력 파일 옆에 새로 만든 example.pbm 출력 파일이 표시됩니다.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  Module.callMain(['example.bmp']);
  console.log(Module.FS.readdir('/'));
};

run();

메모리 파일 시스템의 파일 배열(example.bmp, example.pbm 등)을 보여주는 mkbitmap 앱

메모리 파일 시스템에서 출력 파일을 가져옵니다.

FS 객체의 readFile() 함수를 사용하면 메모리 파일 시스템의 마지막 단계에서 생성된 example.pbm를 가져올 수 있습니다. 이 함수는 File 객체로 변환하여 디스크에 저장하는 Uint8Array를 반환합니다. 브라우저에서는 일반적으로 브라우저에서 직접 보는 PBM 파일을 지원하지 않기 때문입니다. (보다 우아한 파일 저장 방법이 있지만 동적으로 생성된 <a download>를 사용하는 것이 가장 널리 지원되는 방법입니다.) 파일이 저장되면 자주 사용하는 이미지 뷰어에서 열 수 있습니다.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  Module.callMain(['example.bmp']);
  const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
  const file = new File([output], 'example.pbm', {
    type: 'image/x-portable-bitmap',
  });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(file);
  a.download = file.name;
  a.click();
};

run();

입력 .bmp 파일 및 출력 .pbm 파일의 미리보기가 표시된 macOS Finder

대화형 UI 추가

지금까지 입력 파일이 하드코딩되고 mkbitmap기본 매개변수로 실행됩니다. 마지막 단계는 사용자가 동적으로 입력 파일을 선택하고 mkbitmap 매개변수를 조정한 다음 선택한 옵션으로 도구를 실행하도록 하는 것입니다.

// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);

PBM 이미지 형식은 파싱하기가 그리 어렵지 않으므로 일부 JavaScript 코드를 사용하면 출력 이미지의 미리보기를 표시할 수도 있습니다. 이렇게 하는 한 가지 방법은 아래 삽입된 데모소스 코드를 참고하세요.

결론

축하합니다. mkbitmap를 WebAssembly로 컴파일하여 브라우저에서 제대로 작동하도록 했습니다. 막다른 부분이 있었고 제대로 작동할 때까지 도구를 여러 번 컴파일해야 했지만 위에서 설명한 것처럼 이는 이 과정의 일부입니다. 또한 문제가 발생하면 StackOverflow의 webassembly 태그를 기억하세요. 즐겁게 컴파일하세요!

감사의 말씀

이 도움말은 샘 클레그레이첼 앤드류가 검토했습니다.