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 sedikit canggung yang harus Anda lakukan untuk mendeklarasikan fungsi modul wasm mana yang Anda gunakan secara manual. Untuk mengulang materi, 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, menggunakan wasm dengan cara ini tidak mendukung string dan
memerlukan Anda untuk memindahkan potongan memori secara manual, yang membuat banyak API
library sangat merepotkan untuk digunakan. Bukankah ada cara yang lebih baik? Ya, ada. Jika tidak,
apa yang akan dibahas dalam artikel ini?
Penggabungan 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, penaut akan menangani
penggabungan semua file objek yang disebut ini dan mengubahnya menjadi file
wasm. Dengan C, nama fungsi masih tersedia dalam file objek
untuk digunakan oleh penaut. Yang Anda perlukan untuk dapat memanggil fungsi C adalah namanya,
yang kami berikan 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 dengan jenis yang berbeda). Pada tingkat compiler, nama yang bagus seperti add
akan diubah menjadi sesuatu yang mengenkode tanda tangan dalam nama
fungsi untuk penaut. Akibatnya, kita tidak akan dapat lagi mencari fungsi kita dengan namanya.
Masukkan embind
embind adalah bagian dari toolchain Emscripten dan menyediakan banyak makro C++ yang memungkinkan Anda menganotasi kode C++. Anda dapat mendeklarasikan fungsi, enum, class, atau jenis nilai yang ingin Anda gunakan 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 kita ekspos ke JavaScript.
Untuk mengompilasi file ini, kita dapat menggunakan penyiapan yang sama (atau, jika Anda mau, image Docker yang sama) seperti dalam artikel
sebelumnya. Untuk menggunakan embind,
kita menambahkan flag --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 dapat Anda lihat, kita tidak lagi menggunakan cwrap()
. Fitur ini langsung berfungsi
sejak awal. Namun, yang lebih penting, kita tidak perlu khawatir untuk menyalin
sebagian memori secara manual agar string berfungsi. embind memberikannya kepada Anda secara gratis, beserta
pemeriksaan jenis:
Hal ini sangat bagus karena kita dapat menemukan beberapa error lebih awal, bukan menangani error wasm yang terkadang cukup sulit digunakan.
Objek
Banyak konstruktor dan fungsi JavaScript menggunakan objek opsi. Ini adalah pola yang bagus di JavaScript, tetapi sangat merepotkan untuk diwujudkan dalam wasm secara manual. embind juga dapat membantu di sini.
Misalnya, saya menemukan fungsi C++ yang sangat berguna ini yang memproses string saya, dan saya ingin segera menggunakannya di web. Berikut cara 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. Di 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 memilih
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 memberikan banyak sinergi dengan class ES6. Anda mungkin 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++, tetapi hal itu tidak
berarti Anda tidak dapat menautkan ke file C. Untuk menggabungkan C dan C++, Anda hanya perlu memisahkan file input menjadi dua grup: Satu untuk file C dan satu untuk file C++ dan menambahkan 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 memberi Anda peningkatan yang signifikan dalam pengalaman developer saat menggunakan 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 glue JavaScript Anda menjadi lebih besar hingga 11 ribu saat di-gzip — terutama pada modul kecil. Jika Anda hanya memiliki platform wasm yang sangat kecil, embind mungkin harganya lebih mahal daripada nilainya di lingkungan produksi. Meskipun demikian, Anda harus mencobanya.