Fungsi ini mengikat JS ke wasm Anda!
Dalam artikel wasm terakhir saya, saya membahas cara mengompilasi library C ke wasm sehingga Anda dapat menggunakannya di web. Satu hal yang menarik bagi saya (dan bagi banyak pembaca) adalah cara yang kasar dan agak canggung Anda harus secara manual mendeklarasikan fungsi mana dari modul wasm yang Anda gunakan. Untuk menyegarkan ingatan Anda, ini adalah cuplikan kode yang sedang saya bicarakan:
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
Di sini kita mendeklarasikan nama fungsi yang kita tandai dengan
EMSCRIPTEN_KEEPALIVE
, jenis nilai yang ditampilkan, dan jenis
argumennya. Setelah itu, kita dapat menggunakan metode pada objek api
untuk memanggil
fungsi ini. Namun, penggunaan wasm dengan cara ini tidak mendukung string dan
mengharuskan Anda memindahkan potongan memori secara manual yang membuat banyak API
library sangat membosankan untuk digunakan. Apa ada cara yang lebih baik? Mengapa ya, jika tidak,
akan dibahas tentang apa artikel ini?
Kesalahan nama C++
Meskipun pengalaman developer akan cukup untuk membuat alat yang membantu
binding ini, sebenarnya ada alasan yang lebih mendesak: Saat Anda mengompilasi kode C
atau C++, setiap file dikompilasi secara terpisah. Kemudian, linker menangani semua file objek yang disebut ini dan mengubahnya menjadi file wasm. Dengan C, nama fungsi masih tersedia dalam file objek untuk digunakan oleh linker. Yang Anda perlukan untuk dapat memanggil fungsi C adalah nama, yang kami sediakan sebagai string ke cwrap()
.
Di sisi lain, C++ mendukung overloading fungsi, yang berarti Anda dapat mengimplementasikan
fungsi yang sama beberapa kali selama tanda tangannya berbeda (misalnya,
parameter yang diketik secara berbeda). Pada level compiler, nama yang bagus seperti add
akan dirusak menjadi sesuatu yang mengenkode tanda tangan dalam nama fungsi untuk linker. Akibatnya, kita tidak akan dapat mencari
fungsi kita dengan namanya lagi.
Masukkan embind
embind adalah bagian dari toolchain Emscripten dan menyediakan berbagai makro C++ yang dapat Anda gunakan untuk menganotasi kode C++. Anda dapat mendeklarasikan fungsi, enum, class, atau jenis nilai yang ingin Anda gunakan dari JavaScript. Mari kita mulai secara sederhana dengan beberapa fungsi biasa:
#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);
}
Dibandingkan dengan artikel sebelumnya, kita tidak menyertakan emscripten.h
lagi, karena kita tidak perlu lagi menganotasi fungsi dengan EMSCRIPTEN_KEEPALIVE
.
Sebagai gantinya, kita memiliki bagian EMSCRIPTEN_BINDINGS
tempat kita mencantumkan nama yang ingin digunakan untuk mengekspos fungsi ke JavaScript.
Untuk mengompilasi file ini, kita dapat menggunakan penyiapan yang sama (atau, jika Anda mau, gambar Docker yang sama) seperti di artikel sebelumnya. Untuk menggunakan embind,
kita menambahkan flag --bind
:
$ emcc --bind -O3 add.cpp
Sekarang yang perlu dilakukan adalah menyiapkan file HTML yang memuat modul wasm yang baru dibuat:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
console.log(Module.add(1, 2.3));
console.log(Module.exclaim("hello world"));
};
</script>
Seperti yang Anda lihat, kita tidak menggunakan cwrap()
lagi. Ini langsung dapat
digunakan. Namun yang lebih penting, kita tidak perlu khawatir menyalin
bagian memori secara manual agar string berfungsi. embind memberikannya secara gratis, beserta
pemeriksaan jenis:
Cara ini cukup bagus karena kita dapat menemukan beberapa error lebih awal daripada menangani error wasm yang terkadang cukup berat.
Objek
Banyak fungsi dan konstruktor JavaScript menggunakan objek opsi. Ini adalah pola yang bagus pada JavaScript, tetapi sangat membosankan untuk disadari secara manual. embind juga dapat membantu di sini.
Misalnya, saya mendapatkan fungsi C++ yang sangat berguna yang memproses string saya, dan saya ingin segera menggunakannya di web. Begini cara saya melakukannya:
#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);
}
Saya menentukan struct untuk opsi fungsi processMessage()
saya. Dalam
blok EMSCRIPTEN_BINDINGS
, saya dapat menggunakan value_object
agar JavaScript melihat
nilai C++ ini sebagai objek. Saya juga bisa menggunakan value_array
jika lebih suka
menggunakan nilai C++ ini sebagai array. Saya juga mengikat fungsi processMessage()
, dan
sisanya adalah embind magic. Sekarang saya dapat memanggil fungsi processMessage()
dari
JavaScript tanpa kode boilerplate:
console.log(Module.processMessage(
"hello world",
{
reverse: false,
exclaim: true,
repeat: 3
}
)); // Prints "hello world!hello world!hello world!"
Class
Demi kelengkapan, saya juga harus menunjukkan bagaimana embind memungkinkan Anda mengekspos seluruh class, yang menghadirkan banyak sinergi dengan class ES6. Anda mungkin dapat mulai melihat polanya sekarang:
#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);
}
Di sisi JavaScript, ini hampir terasa seperti class native:
<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>
Bagaimana dengan C?
embind ditulis untuk C++ dan hanya dapat digunakan dalam file C++, tapi bukan
berarti Anda tidak dapat menautkan ke file C! Untuk mencampur C dan C++, Anda hanya perlu
memisahkan file input menjadi dua grup: Satu untuk file C dan satu lagi untuk file C++, serta
meningkatkan flag CLI untuk emcc
seperti berikut:
$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp
Kesimpulan
embind memberikan peningkatan besar pada pengalaman developer saat menangani wasm dan C/C++. Artikel ini tidak mencakup semua opsi yang ditawarkan embind. Jika tertarik, sebaiknya lanjutkan dengan dokumentasi embind. Perlu diingat bahwa penggunaan embind dapat membuat modul wasm dan kode glue JavaScript lebih besar hingga 11k saat di-gzip — terutama pada modul kecil. Jika Anda hanya memiliki permukaan wasm yang sangat kecil, biaya embind mungkin lebih mahal daripada di lingkungan produksi. Meskipun demikian, Anda pasti harus mencobanya.