Emscripten’s

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:

Error DevTools saat Anda memanggil fungsi dengan jumlah argumen yang salah
atau argumen memiliki jenis
yang salah

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.