Pelajari cara menyematkan kode JavaScript di library WebAssembly untuk berkomunikasi dengan dunia luar.
Saat mengerjakan integrasi WebAssembly dengan web, Anda memerlukan cara untuk memanggil API eksternal seperti API web dan library pihak ketiga. Selanjutnya, Anda memerlukan cara untuk menyimpan nilai dan instance objek yang ditampilkan API tersebut, serta cara untuk meneruskan nilai yang disimpan tersebut ke API lain nanti. Untuk API asinkron, Anda mungkin juga perlu menunggu promise dalam kode C/C++ sinkron dengan Asyncify lalu baca hasilnya setelah operasi selesai.
Emscripten menyediakan beberapa alat untuk interaksi tersebut:
emscripten::val
untuk menyimpan dan mengoperasikan nilai JavaScript di C++.EM_JS
untuk menyematkan cuplikan JavaScript dan mengikatnya sebagai fungsi C/C++.EM_ASYNC_JS
yang mirip denganEM_JS
, tetapi mempermudah penyematan cuplikan JavaScript asinkron.EM_ASM
untuk menyematkan cuplikan singkat dan menjalankannya secara inline, tanpa mendeklarasikan fungsi.--js-library
untuk skenario lanjutan saat Anda ingin mendeklarasikan banyak fungsi JavaScript secara bersamaan sebagai satu library.
Dalam postingan ini, Anda akan mempelajari cara menggunakan semuanya untuk tugas serupa.
Class emscripten::val
Class emcripten::val
disediakan oleh Embind. Library ini dapat memanggil API global, mengikat nilai JavaScript ke instance C++, dan mengonversi nilai antara jenis C++ dan JavaScript.
Berikut cara menggunakannya dengan .await()
Asyncify untuk mengambil dan mengurai beberapa 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>();
Kode ini berfungsi dengan baik, tetapi melakukan banyak langkah perantara. Setiap operasi di val
harus melakukan langkah-langkah berikut:
- Mengonversi nilai C++ yang diteruskan sebagai argumen ke dalam beberapa format perantara.
- Buka JavaScript, baca, dan konversikan argumen menjadi nilai JavaScript.
- Menjalankan fungsi
- Konversikan hasil dari JavaScript ke format perantara.
- Tampilkan hasil yang dikonversi ke C++, dan C++ akhirnya akan membacanya kembali.
Setiap await()
juga harus menjeda sisi C++ dengan menguraikan seluruh stack panggilan modul WebAssembly, kembali ke JavaScript, menunggu, dan memulihkan stack WebAssembly saat operasi selesai.
Kode tersebut tidak memerlukan apa pun dari C++. Kode C++ hanya bertindak sebagai driver untuk serangkaian operasi JavaScript. Bagaimana jika Anda dapat memindahkan fetch_json
ke JavaScript dan mengurangi overhead langkah perantara secara bersamaan?
Makro EM_JS
EM_JS macro
memungkinkan Anda memindahkan fetch_json
ke JavaScript. EM_JS
di Emscripten memungkinkan Anda mendeklarasikan fungsi C/C++ yang diimplementasikan oleh cuplikan JavaScript.
Seperti WebAssembly itu sendiri, ia memiliki batasan hanya mendukung argumen numerik dan nilai yang ditampilkan. Untuk meneruskan nilai lain, Anda perlu mengonversinya secara manual melalui API yang sesuai. Berikut ini beberapa contohnya.
Meneruskan angka tidak memerlukan konversi apa pun:
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
Saat meneruskan string ke dan dari JavaScript, Anda perlu menggunakan fungsi konversi dan alokasi yang sesuai dari 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);
});
Terakhir, untuk jenis nilai arbitrer yang lebih kompleks, Anda dapat menggunakan JavaScript API untuk class val
yang disebutkan sebelumnya. Dengan menggunakannya, Anda dapat mengonversi nilai JavaScript dan class C++ menjadi handle perantara dan sebaliknya:
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>();
Dengan mempertimbangkan API tersebut, contoh fetch_json
dapat ditulis ulang untuk melakukan sebagian besar pekerjaan tanpa keluar dari 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>();
Kami masih memiliki beberapa konversi eksplisit di titik masuk dan keluar fungsi, tetapi sisanya adalah kode JavaScript biasa. Tidak seperti val
yang setara, sekarang dapat dioptimalkan oleh mesin JavaScript dan hanya perlu menjeda sisi C++ sekali untuk semua operasi asinkron.
Makro EM_ASYNC_JS
Satu-satunya bagian yang tersisa yang tidak terlihat bagus adalah wrapper Asyncify.handleAsync
—satu-satunya tujuannya adalah untuk memungkinkan eksekusi fungsi JavaScript async
dengan Asyncify. Bahkan, kasus penggunaan ini sangat umum sehingga kini ada makro EM_ASYNC_JS
khusus yang menggabungkannya.
Berikut cara menggunakannya untuk menghasilkan versi akhir contoh 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
adalah cara yang direkomendasikan untuk mendeklarasikan cuplikan JavaScript. Hal ini efisien karena mengikat cuplikan yang dideklarasikan secara langsung seperti impor fungsi JavaScript lainnya. Ini juga memberikan ergonomi yang baik dengan memungkinkan Anda mendeklarasikan semua jenis dan nama parameter secara eksplisit.
Namun, dalam beberapa kasus, Anda ingin menyisipkan cuplikan cepat untuk panggilan console.log
, pernyataan debugger;
, atau yang serupa dan tidak ingin repot mendeklarasikan seluruh fungsi terpisah. Dalam kasus yang jarang terjadi tersebut, EM_ASM macros family
(EM_ASM
, EM_ASM_INT
, dan EM_ASM_DOUBLE
) mungkin merupakan pilihan yang lebih sederhana. Makro tersebut mirip dengan makro EM_JS
, tetapi mengeksekusi kode inline tempat makro tersebut disisipkan, bukan menentukan fungsi.
Karena tidak mendeklarasikan prototipe fungsi, parameter tersebut memerlukan cara yang berbeda untuk menentukan jenis nilai yang ditampilkan dan argumen akses.
Anda harus menggunakan nama makro yang tepat untuk memilih jenis nilai yang ditampilkan. Blok EM_ASM
diharapkan berfungsi seperti fungsi void
, blok EM_ASM_INT
dapat menampilkan nilai bilangan bulat, dan blok EM_ASM_DOUBLE
menampilkan angka floating point.
Setiap argumen yang diteruskan akan tersedia dengan nama $0
, $1
, dan seterusnya dalam isi JavaScript. Seperti halnya EM_JS
atau WebAssembly pada umumnya, argumen hanya dibatasi untuk nilai numerik—bilangan bulat, angka floating point, pointer, dan tuas.
Berikut adalah contoh cara menggunakan makro EM_ASM
untuk mencatat log nilai JS arbitrer ke konsol:
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
Terakhir, Emscripten mendukung deklarasi kode JavaScript di file terpisah dalam format library kustomnya sendiri:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
Kemudian, Anda perlu mendeklarasikan prototipe yang sesuai secara manual di sisi C++:
extern "C" void log_value(EM_VAL val_handle);
Setelah dideklarasikan di kedua sisi, library JavaScript dapat ditautkan bersama dengan kode utama melalui --js-library option
, yang menghubungkan prototipe dengan implementasi JavaScript yang sesuai.
Namun, format modul ini tidak standar dan memerlukan anotasi dependensi yang cermat. Oleh karena itu, sebagian besar dikhususkan untuk skenario lanjutan.
Kesimpulan
Dalam postingan ini kita telah melihat berbagai cara untuk mengintegrasikan kode JavaScript ke dalam C++ saat bekerja dengan WebAssembly.
Menyertakan cuplikan tersebut memungkinkan Anda mengekspresikan urutan operasi yang panjang dengan cara yang lebih bersih dan lebih efisien, serta memanfaatkan library pihak ketiga, JavaScript API baru, dan bahkan fitur sintaksis JavaScript yang belum dapat diekspresikan melalui C++ atau Embind.