Menyematkan cuplikan JavaScript di C++ dengan Emscripten

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. Anda kemudian memerlukan cara untuk menyimpan nilai dan instance objek yang ditampilkan API tersebut, dan 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 dan membaca hasilnya setelah operasi selesai.

Emscripten menyediakan beberapa alat untuk interaksi tersebut:

  • emscripten::val untuk menyimpan dan beroperasi pada nilai JavaScript di C++.
  • EM_JS untuk menyematkan cuplikan JavaScript dan mengikatnya sebagai fungsi C/C++.
  • EM_ASYNC_JS yang mirip dengan EM_JS, tetapi mempermudah penyematan cuplikan JavaScript asinkron.
  • EM_ASM untuk menyematkan cuplikan pendek 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.

kelas emscripten::val

Class emcripten::val disediakan oleh Embind. Fungsi 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 menjalankan banyak langkah perantara. Setiap operasi di val harus melakukan langkah-langkah berikut:

  1. Mengonversi nilai C++ yang diteruskan sebagai argumen ke dalam beberapa format menengah.
  2. Buka JavaScript, baca dan konversi argumen menjadi nilai JavaScript.
  3. Menjalankan fungsi
  4. Konversi hasil dari JavaScript ke format menengah.
  5. Menampilkan hasil yang telah dikonversi ke C++, dan C++ akhirnya akan membacanya kembali.

Setiap await() juga harus menjeda sisi C++ dengan melepaskan seluruh stack panggilan modul WebAssembly, mengembalikan ke JavaScript, menunggu, dan memulihkan stack WebAssembly saat operasi selesai.

Kode tersebut tidak memerlukan apa pun dari C++. Kode C++ hanya berfungsi 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 sendiri, metode ini memiliki batasan hanya mendukung argumen numerik dan nilai kembalian. Untuk meneruskan nilai lain, Anda harus mengonversinya secara manual melalui API yang sesuai. Berikut ini beberapa contohnya.

Meneruskan angka tidak memerlukan konversi:

// 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 harus 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 yang arbitrer dan lebih kompleks, Anda dapat menggunakan JavaScript API untuk class val yang telah disebutkan sebelumnya. Dengan menggunakannya, Anda bisa mengonversi nilai JavaScript dan class C++ menjadi handle perantara dan kembali:

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>();

Kita masih memiliki beberapa konversi eksplisit di titik masuk dan keluar fungsi, tetapi sisanya sekarang menjadi kode JavaScript reguler. Tidak seperti val yang setara, kode ini kini dapat dioptimalkan oleh mesin JavaScript dan hanya memerlukan jeda sisi C++ sekali untuk semua operasi asinkron.

Makro EM_ASYNC_JS

Satu-satunya hal yang tersisa yang tidak terlihat indah 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 bersama.

Berikut cara menggunakannya untuk menghasilkan versi final dari 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. 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 sesuatu yang serupa dan tidak ingin repot-repot mendeklarasikan fungsi yang sepenuhnya terpisah. Dalam kasus yang jarang terjadi tersebut, EM_ASM macros family (EM_ASM, EM_ASM_INT, dan EM_ASM_DOUBLE) mungkin menjadi pilihan yang lebih sederhana. Makro tersebut mirip dengan makro EM_JS, tetapi makro tersebut mengeksekusi kode inline tempat makro tersebut disisipkan, bukan menentukan fungsi.

Karena fungsi tersebut tidak mendeklarasikan prototipe fungsi, fungsi tersebut memerlukan cara berbeda dalam menentukan jenis nilai yang ditampilkan dan mengakses argumen.

Anda harus menggunakan nama makro yang tepat untuk memilih jenis pengembalian. 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 secara sesuai.

Setiap argumen yang diteruskan akan tersedia dengan nama $0, $1, dan seterusnya di isi JavaScript. Seperti halnya EM_JS atau WebAssembly secara umum, argumen hanya dibatasi untuk nilai numerik—bilangan bulat, angka floating point, pointer, dan tuas.

Berikut adalah contoh cara menggunakan makro EM_ASM untuk mencatat 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 dalam 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 harus 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 untuk menghubungkan prototipe dengan implementasi JavaScript yang sesuai.

Namun, format modul ini non-standar dan memerlukan anotasi dependensi yang cermat. Dengan demikian, sebagian besar dicadangkan 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 rangkaian operasi yang panjang dengan cara yang lebih rapi dan lebih efisien, serta memanfaatkan library pihak ketiga, API JavaScript baru, dan bahkan fitur sintaksis JavaScript yang belum dapat dinyatakan melalui C++ atau Embind.