WebAssembly 라이브러리에 JavaScript 코드를 삽입하여 외부와 통신하는 방법을 알아봅니다.
웹과 WebAssembly 통합을 작업할 때는 웹 API 및 서드 파티 라이브러리와 같은 외부 API를 호출하는 방법이 필요합니다. 그런 다음 이러한 API가 반환하는 값과 객체 인스턴스를 저장하는 방법과 나중에 저장된 값을 다른 API에 전달하는 방법이 필요합니다. 비동기 API의 경우 Asyncify를 사용하여 동기식 C/C++ 코드에서 약속을 기다리고 작업이 완료되면 결과를 읽어야 할 수도 있습니다.
Emscripten은 이러한 상호작용을 위한 몇 가지 도구를 제공합니다.
- C++에서 JavaScript 값을 저장하고 운영하기 위한
emscripten::val
- JavaScript 스니펫을 삽입하고 C/C++ 함수로 바인딩하는
EM_JS
EM_JS
와 유사하지만 비동기 JavaScript 스니펫을 더 쉽게 삽입할 수 있는EM_ASYNC_JS
EM_ASM
: 함수를 선언하지 않고 짧은 스니펫을 삽입하고 인라인으로 실행합니다.--js-library
: 여러 JavaScript 함수를 단일 라이브러리로 함께 선언하려는 고급 시나리오에 사용합니다.
이 게시물에서는 유사한 작업에 이러한 함수를 모두 사용하는 방법을 알아봅니다.
emscripten::val 클래스
emcripten::val
클래스는 Embind에서 제공합니다. 전역 API를 호출하고, JavaScript 값을 C++ 인스턴스에 바인딩하고, C++ 유형과 JavaScript 유형 간에 값을 변환할 수 있습니다.
다음은 Asyncify의 .await()
와 함께 이를 사용하여 JSON을 가져오고 파싱하는 방법입니다.
#include <emscripten/val.h>
using namespace emscripten;
val fetch_json(const char *url) {
// Get and cache a binding to the global `fetch` API in each thread.
thread_local const val fetch = val::global("fetch");
// Invoke fetch and await the returned `Promise<Response>`.
val response = fetch(url).await();
// Ask to read the response body as JSON and await the returned `Promise<any>`.
val json = response.call<val>("json").await();
// Return the JSON object.
return json;
}
// Example URL.
val example_json = fetch_json("https://httpbin.org/json");
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
이 코드는 잘 작동하지만 중간 단계를 많이 실행합니다. val
의 각 작업은 다음 단계를 실행해야 합니다.
- 인수로 전달된 C++ 값을 중간 형식으로 변환합니다.
- JavaScript로 이동하여 인수를 읽고 JavaScript 값으로 변환합니다.
- 함수 실행
- JavaScript의 결과를 중간 형식으로 변환합니다.
- 변환된 결과를 C++로 반환하면 C++에서 최종적으로 다시 읽습니다.
각 await()
는 WebAssembly 모듈의 전체 호출 스택을 롤백하고, JavaScript로 돌아가고, 기다린 후, 작업이 완료되면 WebAssembly 스택을 복원하여 C++ 측을 일시중지해야 합니다.
이러한 코드는 C++에서 아무것도 필요하지 않습니다. C++ 코드는 일련의 JavaScript 작업을 위한 드라이버로만 작동합니다. fetch_json
를 JavaScript로 이동하고 동시에 중간 단계의 오버헤드를 줄일 수 있다면 어떨까요?
EM_JS 매크로
EM_JS macro
를 사용하면 fetch_json
를 JavaScript로 이동할 수 있습니다. Emscripten의 EM_JS
를 사용하면 JavaScript 스니펫으로 구현된 C/C++ 함수를 선언할 수 있습니다.
WebAssembly 자체와 마찬가지로 숫자 인수와 반환 값만 지원한다는 제한이 있습니다. 다른 값을 전달하려면 해당 API를 통해 수동으로 변환해야 합니다. 다음은 몇 가지 예입니다.
숫자를 전달할 때는 변환이 필요하지 않습니다.
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
JavaScript와 문자열을 주고받을 때는 preamble.js의 상응하는 변환 및 할당 함수를 사용해야 합니다.
EM_JS(void, log_string, (const char *msg), {
console.log(UTF8ToString(msg));
});
EM_JS(const char *, get_input, (), {
let str = document.getElementById('myinput').value;
// Returns heap-allocated string.
// C/C++ code is responsible for calling `free` once unused.
return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL);
});
마지막으로 더 복잡한 임의의 값 유형의 경우 앞에서 언급한 val
클래스에 JavaScript API를 사용할 수 있습니다. 이를 사용하여 JavaScript 값과 C++ 클래스를 중간 핸들로 변환하고 다시 변환할 수 있습니다.
EM_JS(void, log_value, (EM_VAL val_handle), {
let value = Emval.toValue(val_handle);
console.log(value);
});
EM_JS(EM_VAL, find_myinput, (), {
let input = document.getElementById('myinput');
return Emval.toHandle(input);
});
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
log_value(obj.as_handle()); // logs { x: 1, y: 2 }
val myinput = val::take_ownership(find_input());
// Now you can store the `find_myinput` DOM element for as long as you like, and access it later like:
std::string value = input["value"].as<std::string>();
이러한 API를 염두에 두고 fetch_json
예시를 JavaScript를 벗어나지 않고 대부분의 작업을 실행하도록 다시 작성할 수 있습니다.
EM_JS(EM_VAL, fetch_json, (const char *url), {
return Asyncify.handleAsync(async () => {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
함수의 진입점과 종료점에 여전히 몇 가지 명시적 변환이 있지만 나머지는 이제 일반 JavaScript 코드입니다. 이제 val
등가 항목과 달리 JavaScript 엔진에서 최적화할 수 있으며 모든 비동기 작업에 대해 C++ 측을 한 번만 일시중지하면 됩니다.
EM_ASYNC_JS 매크로
보기에 좋지 않은 부분은 Asyncify.handleAsync
래퍼뿐입니다. Asyncify.handleAsync
래퍼의 유일한 목적은 Asyncify로 async
JavaScript 함수를 실행할 수 있도록 하는 것입니다. 사실 이 사용 사례는 너무나 흔하여 이제 이를 결합하는 전문화된 EM_ASYNC_JS
매크로가 있습니다.
다음은 이를 사용하여 fetch
예시의 최종 버전을 만드는 방법입니다.
EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
EM_ASM
EM_JS
는 JavaScript 스니펫을 선언하는 데 권장되는 방법입니다. 다른 JavaScript 함수 가져오기와 마찬가지로 선언된 스니펫을 직접 바인딩하므로 효율적입니다. 또한 모든 매개변수 유형과 이름을 명시적으로 선언할 수 있어 편리합니다.
하지만 console.log
호출, debugger;
문 또는 유사한 항목에 관한 빠른 스니펫을 삽입하고 싶지만 완전히 별도의 함수를 선언하고 싶지 않은 경우도 있습니다. 드물지만 EM_ASM macros family
(EM_ASM
, EM_ASM_INT
, EM_ASM_DOUBLE
)가 더 간단한 선택일 수 있습니다. 이러한 매크로는 EM_JS
매크로와 유사하지만 함수를 정의하는 대신 삽입된 위치에서 코드를 인라인으로 실행합니다.
함수 프로토타입을 선언하지 않으므로 반환 유형을 지정하고 인수에 액세스하는 다른 방법이 필요합니다.
반환 유형을 선택하려면 올바른 매크로 이름을 사용해야 합니다. EM_ASM
블록은 void
함수처럼 작동해야 하며, EM_ASM_INT
블록은 정수 값을 반환할 수 있고, EM_ASM_DOUBLE
블록은 그에 따라 부동 소수점 수를 반환합니다.
전달된 인수는 JavaScript 본문에서 $0
, $1
등의 이름으로 사용할 수 있습니다. 일반적으로 EM_JS
또는 WebAssembly와 마찬가지로 인수는 숫자 값(정수, 부동 소수점 수, 포인터, 핸들)으로만 제한됩니다.
다음은 EM_ASM
매크로를 사용하여 임의의 JS 값을 콘솔에 로깅하는 방법을 보여주는 예입니다.
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
// convert handle passed under $0 into a JavaScript value
let obj = Emval.fromHandle($0);
console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());
--js-library
마지막으로 Emscripten은 자체 라이브러리 형식으로 별도의 파일에서 JavaScript 코드를 선언하는 것을 지원합니다.
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
그런 다음 C++ 측에서 해당 프로토타입을 수동으로 선언해야 합니다.
extern "C" void log_value(EM_VAL val_handle);
양쪽에서 선언되면 JavaScript 라이브러리는 --js-library option
를 통해 기본 코드와 연결되어 프로토타입을 해당 JavaScript 구현과 연결할 수 있습니다.
그러나 이 모듈 형식은 비표준이므로 주의 깊은 종속 항목 주석이 필요합니다. 따라서 대부분 고급 시나리오에만 사용됩니다.
결론
이 게시물에서는 WebAssembly를 사용할 때 JavaScript 코드를 C++에 통합하는 다양한 방법을 살펴봤습니다.
이러한 스니펫을 포함하면 긴 작업 시퀀스를 더 깔끔하고 효율적인 방식으로 표현하고 서드 파티 라이브러리, 새 JavaScript API, 심지어 C++ 또는 Embind를 통해 아직 표현할 수 없는 JavaScript 문법 기능까지 활용할 수 있습니다.