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 sedikit canggung untuk mendeklarasikan secara manual 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, 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? Mengapa ya ada, jika tidak, akan membahas apa artikel ini?

Kesalahan nama C++

Meskipun pengalaman developer akan menjadi alasan yang cukup untuk membuat alat yang membantu binding ini, sebenarnya ada alasan yang lebih mendesak: Saat Anda mengompilasi kode C atau C++, setiap file akan dikompilasi secara terpisah. Kemudian, linker menangani semua file objek yang disebut ini secara bersamaan dan mengubahnya menjadi file wasm. Dengan C, nama fungsi masih tersedia di file objek untuk digunakan oleh linker. 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 asalkan 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 mencari fungsi lagi 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 sederhana dengan beberapa fungsi polos:

#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 menyertakan emscripten.h lagi karena tidak perlu menganotasi fungsi lagi dengan EMSCRIPTEN_KEEPALIVE. Sebagai gantinya, kita memiliki bagian EMSCRIPTEN_BINDINGS yang mencantumkan nama yang diinginkan 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 flag --bind:

$ emcc --bind -O3 add.cpp

Sekarang kita hanya akan 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 dapat Anda lihat, kita tidak lagi menggunakan cwrap(). Fitur ini langsung berfungsi sejak awal. Namun yang lebih penting, kita tidak perlu khawatir menyalin bagian memori secara manual agar string berfungsi. embind memberi Anda hal itu secara gratis, bersama pemeriksaan jenis:

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

Cara ini sangat bagus karena kita dapat menemukan beberapa error lebih awal daripada menangani error wasm yang terkadang cukup berat.

Objek

Banyak konstruktor dan fungsi JavaScript menggunakan objek opsi. Ini adalah pola yang bagus dalam JavaScript, tetapi sangat membosankan jika menyadarinya 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(). Di blok EMSCRIPTEN_BINDINGS, saya dapat menggunakan value_object untuk membuat 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 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

Demi kelengkapan, saya juga harus menunjukkan kepada Anda bagaimana embind memungkinkan Anda mengekspos seluruh class, yang menghasilkan banyak sinergi dengan class ES6. Anda mungkin bisa mulai melihat sebuah 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, 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 itu bukan berarti Anda tidak dapat menautkan ke file C. Untuk menggabungkan C dan C++, Anda hanya perlu memisahkan file input menjadi dua grup: Satu untuk C dan satu untuk file C++, lalu tambahkan 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 luar biasa pada pengalaman developer saat menggunakan wasm dan C/C++. Artikel ini tidak mencakup semua opsi embind yang ditawarkan. Jika Anda tertarik, sebaiknya lanjutkan dengan dokumentasi embind. Perlu diingat bahwa menggunakan embind dapat membuat modul wasm dan kode glue JavaScript Anda lebih besar hingga 11k jika dilakukan 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.