Emscripten의 엠바인드

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 이름 맹글링

개발자 경험만 있으면 다른 Google Cloud 제품과의 협업에 도움이 되는 이러한 바인딩에는 실제로 더 중요한 이유가 있습니다. C를 컴파일하면 또는 C 코드에서는 각 파일이 개별적으로 컴파일됩니다. 그런 다음 링커는 이른바 객체 파일을 하나로 묶어 파일에서 참조됩니다. C를 사용하면 함수 이름을 객체 파일에서 계속 사용할 수 있습니다. 를 지정합니다. C 함수를 호출하려면 이름이 cwrap()에 문자열로 제공합니다.

반면에 C 는 함수 오버로드를 지원합니다. 즉, 서명이 다르다면 같은 함수를 여러 번 (예: 여러 매개변수가 있습니다. 컴파일러 수준에서는 add과 같은 적절한 이름 함수에서 서명을 인코딩하는 무언가가 손상될 수 있습니다. 링커의 이름입니다. 결과적으로는 공식을 찾을 수 없어 볼 수 없습니다.

embind 입력

엠바인드 Emscripten 도구 모음의 일부이며 다양한 C 매크로를 제공합니다. 도 있습니다. 어떤 함수, 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.h는 더 이상 포함되지 않습니다. 더 이상 함수에 EMSCRIPTEN_KEEPALIVE 주석을 달 필요가 없습니다. 대신 아래에 있는 이름을 나열하는 EMSCRIPTEN_BINDINGS 섹션이 있습니다. 함수를 JavaScript에 노출해야 합니다.

이 파일을 컴파일하기 위해 동일한 설정 (또는 원하는 경우 동일한 Docker 이미지)에서와 같지만 도움말을 참조하세요. embind를 사용하려면 --bind 플래그를 추가합니다.

$ emcc --bind -O3 add.cpp

이제 마지막으로 wasm 모듈을 생성했습니다.

<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 오류 발생
또는 인수가 잘못된
유형

이 방법은 문제를 처리하는 대신 오류를 조기에 포착할 수 있으므로 매우 유용합니다. 때때로 꽤 관리하기 어려운 것이었습니다.

객체

많은 JavaScript 생성자와 함수가 옵션 객체를 사용합니다. 그것은 좋은 패턴은 있지만 wasm에서 수동으로 실현하는 것은 매우 지루합니다. 엠바인드 도움을 드릴 수 있습니다.

예를 들어, 저는 이 함수를 만든 다음 엄청나게 유용한 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 값을 객체로 제공합니다. 원한다면 value_array를 사용할 수도 있습니다. 이 C 값을 배열로 사용합니다. processMessage() 함수도 바인딩합니다. 나머지는 embind 마법입니다. 이제 다음에서 processMessage() 함수를 호출할 수 있습니다. 상용구 코드가 없는 JavaScript:

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 를 사용할 수 있습니다. 이 도움말에서는 EM바인드 오퍼에 적용되는 모든 옵션을 다루지는 않습니다. 관심이 있으시다면 embind의 문서를 참조하세요. embind를 사용하면 wasm 모듈과 JavaScript 글루 코드는 gzip 실행 시 최대 11k까지 커집니다(작은 경우 가장 두드러짐). 모듈을 마칩니다 Wasm 표면이 매우 작다면 embind가 가치가 있습니다 그럼에도 불구하고 한 번 시도해 보세요.