Fungsi ini mengikat JS ke wasm Anda.
Dalam artikel wasm terakhir, saya membahas tentang cara mengompilasi pustaka 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 Anda harus secara manual mendeklarasikan fungsi mana dari modul wasm yang Anda gunakan. Untuk menyegarkan pikiran Anda, ini adalah cuplikan kode yang 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
, apa jenis nilai yang ditampilkan, dan apa jenisnya
argumen, Setelah itu, kita dapat menggunakan metode pada objek api
untuk memanggil
fungsi-fungsi ini. Namun, menggunakan wasm dengan cara ini tidak mendukung string dan
mengharuskan Anda untuk memindahkan potongan memori secara manual yang membuat banyak
API yang sangat membosankan untuk digunakan. Bukankah ada cara yang lebih baik? Mengapa ya ada, jika tidak
mengenai apa yang akan dibahas dalam artikel ini?
Kesalahan nama C++
Meskipun pengalaman developer akan menjadi alasan yang cukup
untuk membuat alat yang membantu
dengan pengikatan ini, sebenarnya ada alasan yang lebih mendesak: Ketika Anda mengompilasi C
atau kode C++, setiap file
dikompilasi secara terpisah. Kemudian, {i>link <i}akan menangani
mengumpulkan semua file objek yang disebut ini dan mengubahnya menjadi wasm
. Dengan C, nama fungsi masih tersedia di file objek
yang akan digunakan penaut. Yang Anda butuhkan agar dapat memanggil
fungsi C adalah nama,
yang kita sediakan sebagai string untuk cwrap()
.
Di sisi lain, C++ mendukung kelebihan beban fungsi, yang berarti Anda dapat mengimplementasikan
fungsi yang sama beberapa kali asalkan tanda tangannya berbeda (mis.
parameter dengan jenis yang berbeda). Di tingkat compiler, nama yang bagus seperti add
akan dirusak menjadi sesuatu yang mengenkode tanda tangan dalam fungsi
untuk penaut. Akibatnya, kita tidak bisa
mencari {i>function<i} kita
dengan namanya lagi.
Masukkan embind
tambahkan merupakan bagian dari toolchain Emscripten dan menyediakan banyak makro C++ yang memungkinkan Anda untuk memberi anotasi pada kode C++. Anda dapat mendeklarasikan fungsi, enum, atau jenis nilai yang ingin Anda gunakan dari JavaScript. Mari kita mulai 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 saya sebelumnya, kami tidak menyertakan emscripten.h
lagi, karena
kita tidak perlu menganotasi fungsi lagi dengan EMSCRIPTEN_KEEPALIVE
.
Sebagai gantinya, kita memiliki bagian EMSCRIPTEN_BINDINGS
tempat kita mencantumkan nama pada
yang ingin kita ekspos fungsi ke JavaScript.
Untuk mengompilasi file ini, kita bisa menggunakan
pengaturan yang sama (atau, jika Anda mau,
image Docker) seperti pada versi sebelumnya
artikel ini. Untuk menggunakan embind,
kita menambahkan flag --bind
:
$ emcc --bind -O3 add.cpp
Sekarang yang tersisa adalah menyiapkan file HTML yang memuat membuat modul wasm:
<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 lagi menggunakan cwrap()
. Ini langsung berhasil
dari kotak. Tapi yang lebih penting, kita tidak perlu
khawatir menyalin secara manual
sepotong memori untuk membuat {i>string<i} berfungsi! embind memberi Anda secara gratis,
dengan pemeriksaan jenis:
Cara ini sangat bagus karena kita dapat menemukan beberapa {i>error<i} lebih awal daripada berurusan dengan {i>error <i}yang kadang-kadang cukup berat.
Objek
Banyak fungsi dan konstruktor JavaScript yang menggunakan objek opsi. Enak sekali di JavaScript, tapi sangat membosankan untuk menyadarinya secara manual. embind juga bisa membantu Anda!
Misalnya, saya mendapatkan fungsi C++ yang sangat berguna ini yang memproses {i>string<i}, dan saya sangat ingin 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()
. Di kolom
EMSCRIPTEN_BINDINGS
, saya dapat menggunakan value_object
untuk membuat JavaScript terlihat
nilai C++ ini sebagai objek. Saya juga dapat menggunakan value_array
jika mau
menggunakan nilai C++ ini sebagai array. Saya juga mengikat fungsi processMessage()
, dan
sisanya ada keajaiban. 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 kepada Anda bagaimana embind memungkinkan Anda mengekspos seluruh class, yang membawa banyak sinergi dengan class ES6. Anda mungkin bisa sekarang mulai melihat sebuah pola:
#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 tidak
berarti Anda tidak dapat
menautkan ke file C! Untuk mencampur C dan C++, Anda hanya perlu
pisahkan file input Anda menjadi dua kelompok: satu untuk C dan satu untuk file C++ dan
meningkatkan 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 besar dalam pengalaman developer saat bekerja dengan wasm dan C/C++. Artikel ini tidak mencakup semua opsi yang menyertakan penawaran. Jika Anda berminat, sebaiknya lanjutkan dengan dokumentasi tambahan. Perlu diingat bahwa menggunakan embind dapat membuat modul wasm dan Perekat kode JavaScript lebih besar hingga 11k ketika menggunakan {i>gzip<i} — terutama pada modul. Jika Anda hanya memiliki permukaan wasm yang sangat kecil, embind mungkin memerlukan biaya lebih dari itu layak dalam lingkungan produksi. Meskipun demikian, Anda harus memberikan cobalah.