JS를 Wasm에 결합합니다.
지난 Wasm 기사에서는 웹에서 사용할 수 있도록 C 라이브러리를 wasm으로 컴파일하는 방법에 대해 설명했습니다. 저와 많은 독자들에게 눈에 띄는 한 가지는 사용 중인 wasm 모듈의 기능을 수동으로 선언해야 하는 조잡하고 약간 어색한 방식입니다. 다음은 제가 소개해 드릴 코드 스니펫입니다.
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
여기서는 EMSCRIPTEN_KEEPALIVE
로 표시한 함수의 이름, 반환 유형, 인수 유형을 선언합니다. 그런 다음 api
객체의 메서드를 사용하여 이러한 함수를 호출할 수 있습니다. 그러나 이 방법으로 wasm을 사용하면 문자열을 지원하지 않으며 메모리 청크를 수동으로 이동해야 하므로 많은 라이브러리 API를 사용하기가 매우 지루하게 됩니다. 더 좋은 방법이 없을까? '예'인 이유는 무엇인가요?
그렇지 않다면 이 글이 무엇에 관한 내용일까요?
C++ 이름 맹글링
개발자 환경은 이러한 결합을 지원하는 도구를 빌드하기에 충분한 이유가 될 수 있지만, 실제로 C 또는 C++ 코드를 컴파일할 때 각 파일이 별도로 컴파일된다는 보다 긴급한 이유가 있습니다. 그런 다음 링커는 소위 개체 파일을 모두 하나로 모아 Wasm 파일로 변환합니다. C를 사용하면 링커가 사용할 객체 파일에서 함수 이름을 계속 사용할 수 있습니다. C 함수를 호출할 수 있으려면 cwrap()
에 문자열로 제공되는 이름만 있으면 됩니다.
반면 C++는 함수 오버로드를 지원합니다. 즉, 서명이 다르다면 동일한 함수를 여러 번 구현할 수 있습니다 (예: 유형이 다른 매개변수). 컴파일러 수준에서 add
와 같은 좋은 이름은 링커의 함수 이름에 서명을 인코딩하는 것으로 손상될 수 있습니다. 따라서 더 이상 해당 이름으로 함수를 조회할 수 없습니다.
embind 입력
embind는 Emscripten 도구 모음의 일부이며 C++ 코드에 주석을 달 수 있는 다양한 C++ 매크로를 제공합니다. JavaScript에서 사용할 함수, enum, 클래스 또는 값 유형을 선언할 수 있습니다. 몇 가지 일반 함수로 간단하게 시작해 보겠습니다.
#include <emscripten/bind.h>
using namespace emscripten;
double add(double a, double b) {
return a + b;
}
std::string exclaim(std::string message) {
return message + "!";
}
EMSCRIPTEN_BINDINGS(my_module) {
function("add", &add);
function("exclaim", &exclaim);
}
이전 자료와 달리 더 이상 함수에 EMSCRIPTEN_KEEPALIVE
주석을 달 필요가 없으므로 emscripten.h
는 더 이상 포함하지 않습니다.
대신 자바스크립트에 함수를 노출할 이름을 나열하는 EMSCRIPTEN_BINDINGS
섹션이 있습니다.
이 파일을 컴파일할 때는 이전 문서와 동일한 설정 (또는 원하는 경우 동일한 Docker 이미지)을 사용할 수 있습니다. embind를 사용하기 위해 --bind
플래그를 추가합니다.
$ emcc --bind -O3 add.cpp
이제 남은 작업은 새로 만든 wasm 모듈을 로드하는 HTML 파일을 만드는 것입니다.
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
console.log(Module.add(1, 2.3));
console.log(Module.exclaim("hello world"));
};
</script>
보시다시피 더 이상 cwrap()
를 사용하지 않습니다. 이것은 즉시 작동합니다. 그러나 더 중요한 것은 문자열이 작동하도록 메모리 청크를 수동으로 복사하는 작업에 대해 걱정할 필요가 없다는 것입니다. embind를 사용하면 유형 검사와 함께 이러한 작업이 무료로 제공됩니다.
![인수의 개수가 잘못된 함수를 호출하거나 인수의 유형이 잘못된 경우 DevTools에서 오류가 발생합니다.](https://web.dev/static/articles/embind/image/devtools-errors-you-invo-b853dbd400744.png?authuser=2&hl=ko)
이는 간혹 다루기 힘든 Wasm 오류를 처리하는 대신 일부 오류를 조기에 포착할 수 있으므로 매우 유용합니다.
객체
많은 JavaScript 생성자 및 함수가 옵션 객체를 사용합니다. JavaScript에서 좋은 패턴이지만 wasm에서 수동으로 실현하는 것은 매우 지루합니다. embind도 여기서도 도움이 될 수 있습니다.
예를 들어 문자열을 처리하는 매우 유용한 C++ 함수가 생각났는데 급히 이 함수를 웹에서 사용해 보겠습니다. 그 방법은 다음과 같습니다.
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
struct ProcessMessageOpts {
bool reverse;
bool exclaim;
int repeat;
};
std::string processMessage(std::string message, ProcessMessageOpts opts) {
std::string copy = std::string(message);
if(opts.reverse) {
std::reverse(copy.begin(), copy.end());
}
if(opts.exclaim) {
copy += "!";
}
std::string acc = std::string("");
for(int i = 0; i < opts.repeat; i++) {
acc += copy;
}
return acc;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<ProcessMessageOpts>("ProcessMessageOpts")
.field("reverse", &ProcessMessageOpts::reverse)
.field("exclaim", &ProcessMessageOpts::exclaim)
.field("repeat", &ProcessMessageOpts::repeat);
function("processMessage", &processMessage);
}
processMessage()
함수의 옵션에 관한 구조체를 정의하고 있습니다. EMSCRIPTEN_BINDINGS
블록에서 value_object
를 사용하여 JavaScript가 이 C++ 값을 객체로 인식하도록 할 수 있습니다. 이 C++ 값을 배열로 사용하려면 value_array
를 사용할 수도 있습니다. processMessage()
함수도 바인딩하면 나머지는 매직으로 임베딩됩니다. 이제 상용구 코드 없이 JavaScript에서 processMessage()
함수를 호출할 수 있습니다.
console.log(Module.processMessage(
"hello world",
{
reverse: false,
exclaim: true,
repeat: 3
}
)); // Prints "hello world!hello world!hello world!"
클래스
완전성을 위해 embind를 통해 전체 클래스를 노출하여 ES6 클래스와 함께 많은 시너지 효과를 가져오는 방법도 보여드리겠습니다. 이제부터 패턴이 표시되기 시작할 것입니다.
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
class Counter {
public:
int counter;
Counter(int init) :
counter(init) {
}
void increase() {
counter++;
}
int squareCounter() {
return counter * counter;
}
};
EMSCRIPTEN_BINDINGS(my_module) {
class_<Counter>("Counter")
.constructor<int>()
.function("increase", &Counter::increase)
.function("squareCounter", &Counter::squareCounter)
.property("counter", &Counter::counter);
}
JavaScript 측면에서는 거의 네이티브 클래스와 같습니다.
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const c = new Module.Counter(22);
console.log(c.counter); // prints 22
c.increase();
console.log(c.counter); // prints 23
console.log(c.squareCounter()); // prints 529
};
</script>
C의 경우는 어떨까요?
embind는 C++용으로 작성되었으며 C++ 파일에서만 사용할 수 있지만 C 파일에 링크할 수 없다는 의미는 아닙니다. C와 C++를 함께 사용하려면 입력 파일을 C용 그룹과 C++ 파일용의 두 그룹으로 구분하고 다음과 같이 emcc
용 CLI 플래그를 보강하면 됩니다.
$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp
결론
embind를 사용하면 Wasm 및 C/C++로 작업할 때 개발자 환경을 크게 개선할 수 있습니다. 이 문서에서는 embind에서 제공하는 모든 옵션을 다루지 않습니다. 관심이 있으시면 embind 문서를 계속 참고하시기 바랍니다. embind를 사용하면 gzip으로 압축했을 때 wasm 모듈과 JavaScript 글루 코드를 최대 11,000개까지(특히 작은 모듈에서) 커질 수 있습니다. Wasm 노출 영역이 매우 작으면 프로덕션 환경에서 embind에 비해 비용이 많이 들 수 있습니다. 어쨌든 꼭 한 번 사용해 보아야 합니다.