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 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:

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

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 menggunakan 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.