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. Salah satu hal yang menarik bagi saya (dan banyak pembaca) adalah cara kasar dan sedikit canggung yang harus Anda lakukan untuk mendeklarasikan secara manual fungsi modul wasm yang Anda gunakan. Untuk menyegarkan ingatan Anda, berikut cuplikan kode yang saya maksud:
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 sulit digunakan. Bukankah ada cara yang lebih baik? Tentu saja ada, kalau tidak, artikel ini akan membahas apa?
Pengubahan nama C++
Meskipun pengalaman developer sudah cukup menjadi alasan untuk membuat alat yang membantu
dengan binding ini, sebenarnya ada alasan yang lebih mendesak: Saat Anda mengompilasi kode C
atau C++, setiap file dikompilasi secara terpisah. Kemudian, linker akan menangani
penggabungan semua yang disebut file objek ini dan mengubahnya menjadi file
wasm. Dengan C, nama fungsi masih tersedia dalam file objek
untuk digunakan oleh linker. Yang Anda butuhkan untuk dapat memanggil fungsi C hanyalah nama,
yang kita berikan sebagai string ke cwrap()
.
Di sisi lain, C++ mendukung kelebihan beban fungsi, yang berarti Anda dapat menerapkan
fungsi yang sama beberapa kali selama tanda tangannya berbeda (misalnya,
parameter yang diketik secara berbeda). Di tingkat compiler, nama yang bagus seperti add
akan diubah menjadi sesuatu yang mengenkode tanda tangan dalam nama fungsi untuk linker. Akibatnya, kita tidak dapat lagi mencari fungsi
dengan namanya.
Masukkan embind
embind adalah bagian dari toolchain Emscripten dan menyediakan sejumlah makro C++ yang memungkinkan Anda membuat anotasi kode C++. Anda dapat mendeklarasikan fungsi, enum, class, atau jenis nilai yang akan digunakan dari JavaScript. Mari kita mulai dengan beberapa fungsi sederhana:
#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 saya sebelumnya, kita tidak lagi menyertakan emscripten.h
, 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, image Docker yang sama) seperti pada artikel
sebelumnya. Untuk menggunakan embind,
kita menambahkan tanda --bind
:
$ emcc --bind -O3 add.cpp
Sekarang, yang perlu dilakukan adalah membuat file HTML yang memuat modul wasm yang baru saja kita buat:
<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, kami tidak lagi menggunakan cwrap()
. Fitur ini langsung berfungsi
sejak awal. Namun yang lebih penting, kita tidak perlu khawatir menyalin secara manual
potongan memori untuk membuat string berfungsi. embind memberikannya secara gratis, beserta
pemeriksaan jenis:

Hal ini cukup bagus karena kita dapat menemukan beberapa error lebih awal daripada harus menangani error wasm yang terkadang cukup sulit.
Objek
Banyak konstruktor dan fungsi JavaScript menggunakan objek opsi. Pola ini bagus di JavaScript, tetapi sangat membosankan untuk diwujudkan secara manual di wasm. embind juga dapat membantu di sini.
Misalnya, saya membuat fungsi C++ yang sangat berguna ini yang memproses string saya, dan saya ingin segera menggunakannya di web. Berikut 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
untuk membuat JavaScript melihat
nilai C++ ini sebagai objek. Saya juga dapat menggunakan value_array
jika lebih suka
menggunakan nilai C++ ini sebagai array. Saya juga mengikat fungsi processMessage()
, dan
selebihnya adalah keajaiban embind. 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
Untuk kelengkapan, saya juga harus menunjukkan cara embind memungkinkan Anda mengekspos seluruh class, yang menghadirkan banyak sinergi dengan class ES6. Anda mungkin sudah mulai melihat pola 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, hal 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++, tetapi bukan berarti Anda tidak dapat menautkan ke file C. Untuk mencampur C dan C++, Anda hanya perlu
memisahkan file input ke dalam dua grup: Satu untuk file C dan satu untuk file C++ serta
menambah flag CLI untuk emcc
sebagai 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 dalam pengalaman developer saat bekerja dengan wasm dan C/C++. Artikel ini tidak membahas semua opsi yang ditawarkan embind. Jika Anda tertarik, sebaiknya lanjutkan dengan dokumentasi embind. Perlu diingat bahwa penggunaan embind dapat membuat modul wasm dan kode lem JavaScript Anda lebih besar hingga 11k saat di-gzip — terutama pada modul kecil. Jika hanya memiliki permukaan wasm yang sangat kecil, embind mungkin lebih mahal daripada yang seharusnya di lingkungan produksi. Namun, Anda harus mencobanya.