Emscripten’s

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:

Error DevTools saat Anda memanggil fungsi dengan jumlah argumen yang salah
atau argumen yang salah
(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.