Sự kết hợp của Emscripten

Hàm này liên kết JS với wasm!

Trong bài viết wasm gần đây nhất, tôi đã nói về cách biên dịch thư viện C thành wasm để bạn có thể sử dụng trên web. Một điều điều nổi bật đối với tôi (và với nhiều độc giả) là cách nói thô thiển và hơi vụng về bạn phải khai báo theo cách thủ công các chức năng của mô-đun wasm mà bạn đang sử dụng. Để giúp bạn đầu óc, sau đây là đoạn mã mà tôi đang nói đến:

const api = {
    version: Module.cwrap('version', 'number', []),
    create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
    destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};

Ở đây, chúng ta khai báo tên của các hàm mà chúng ta đánh dấu EMSCRIPTEN_KEEPALIVE, loại dữ liệu trả về và loại dữ liệu trả về đối số. Sau đó, chúng ta có thể dùng các phương thức trên đối tượng api để gọi các hàm này. Tuy nhiên, sử dụng wasm theo cách này không hỗ trợ chuỗi và yêu cầu bạn di chuyển thủ công các phần bộ nhớ khiến nhiều thư viện Các API rất tẻ nhạt khi sử dụng. Không phải có cách tốt hơn phải không? Tại sao lại có, nếu không thì có nội dung bài viết này là gì?

Rút gọn tên C++

Mặc dù trải nghiệm của nhà phát triển là đủ để xây dựng một công cụ hữu ích với những liên kết này, thực sự có một lý do cấp bách hơn: Khi bạn biên dịch C hoặc mã C++, mỗi tệp sẽ được biên dịch riêng. Sau đó, trình liên kết sẽ xử lý kết hợp tất cả những tệp được gọi là tệp đối tượng này lại với nhau rồi biến chúng thành một wasm tệp. Với C, tên của các hàm vẫn có trong tệp đối tượng để trình liên kết sử dụng. Tất cả những gì bạn cần để có thể gọi hàm C là tên, mà chúng tôi sẽ cung cấp dưới dạng một chuỗi cho cwrap().

Mặt khác, C++ hỗ trợ nạp chồng hàm, nghĩa là bạn có thể triển khai cùng một hàm nhiều lần, miễn là chữ ký khác nhau (ví dụ: các tham số được nhập khác nhau). Ở cấp độ trình biên dịch, một tên hay như add sẽ được đưa vào nội dung mã hoá chữ ký trong hàm tên cho trình liên kết. Do đó, chúng tôi sẽ không thể tra cứu hàm của mình với tên của doanh nghiệp nữa.

Nhập embind

liên kết là một phần của chuỗi công cụ Emscripten và cung cấp cho bạn một loạt các macro C++ cho phép bạn chú thích mã C++. Bạn có thể khai báo hàm, enum, hoặc loại giá trị mà bạn định sử dụng từ JavaScript. Bắt đầu đơn giản bằng một số hàm đơn giản:

#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);
}

So với bài viết trước, chúng tôi không sử dụng emscripten.h nữa, vì chúng ta không phải chú thích các hàm bằng EMSCRIPTEN_KEEPALIVE nữa. Thay vào đó, chúng ta có phần EMSCRIPTEN_BINDINGS trong đó chúng ta liệt kê các tên theo mà chúng ta muốn hiển thị các hàm của mình cho JavaScript.

Để biên dịch tệp này, chúng ta có thể sử dụng cùng một thiết lập (hoặc nếu muốn, bạn cũng hình ảnh Docker) như trong trước bài viết. Cách dùng embind: chúng ta sẽ thêm cờ --bind:

$ emcc --bind -O3 add.cpp

Giờ đây, phần còn lại chỉ là sử dụng một tệp HTML tải đã tạo mô-đun wasm:

<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
    console.log(Module.add(1, 2.3));
    console.log(Module.exclaim("hello world"));
};
</script>

Như bạn có thể thấy, chúng tôi không còn sử dụng cwrap() nữa. Tính năng này hoạt động rất hiệu quả hộp. Nhưng quan trọng hơn là chúng tôi không phải lo lắng về việc sao chép theo cách thủ công các phần bộ nhớ để chuỗi hoạt động! embind cung cấp cho bạn tính năng đó miễn phí, cùng với với kiểm tra loại:

Lỗi Công cụ cho nhà phát triển khi bạn gọi một hàm có số lượng đối số không chính xác
hoặc các đối số có sai số
lượt chuyển đổi

Điều này khá tuyệt vời vì chúng ta có thể sớm phát hiện được một số lỗi thay vì xử lý các lỗi wasm đôi khi khá khó sử dụng.

Đối tượng

Nhiều hàm khởi tạo và hàm JavaScript sử dụng các đối tượng tuỳ chọn. Thật tuyệt trong JavaScript, nhưng cực kỳ tẻ nhạt khi nhận ra trong wasm theo cách thủ công. embind cũng có thể giúp bạn!

Ví dụ: tôi đã tìm ra hàm C++ hữu ích cực kỳ hữu ích này giúp xử lý chuỗi và tôi khẩn cấp muốn sử dụng tính năng này trên web. Đây là cách tôi đã làm điều đó:

#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);
}

Tôi đang xác định một cấu trúc cho các tuỳ chọn của hàm processMessage(). Trong EMSCRIPTEN_BINDINGS khối, tôi có thể sử dụng value_object để tạo JavaScript xem giá trị C++ này dưới dạng đối tượng. Tôi cũng có thể dùng value_array nếu muốn hãy sử dụng giá trị C++ này dưới dạng mảng. Tôi cũng liên kết hàm processMessage() và phần còn lại là liên kết kỳ diệu. Bây giờ, tôi có thể gọi hàm processMessage() từ JavaScript mà không cần bất kỳ mã nguyên mẫu nào:

console.log(Module.processMessage(
    "hello world",
    {
    reverse: false,
    exclaim: true,
    repeat: 3
    }
)); // Prints "hello world!hello world!hello world!"

Lớp

Để cho hoàn chỉnh, tôi cũng sẽ cho bạn thấy cách embind cho phép bạn phơi bày toàn bộ các lớp, điều này mang lại nhiều sức mạnh tổng hợp với các lớp ES6. Bạn có thể hãy bắt đầu thấy một mẫu ngay bây giờ:

#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);
}

Về phía JavaScript, đây gần như là một lớp gốc:

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

Còn C thì sao?

embind được viết cho C++ và chỉ có thể được sử dụng trong các tệp C++, nhưng không được có nghĩa là bạn không thể liên kết chống lại các tệp C! Để kết hợp C và C++, bạn chỉ cần tách các tệp đầu vào thành hai nhóm: một nhóm cho các tệp C và một nhóm cho các tệp C++ và tăng cường các cờ CLI cho emcc như sau:

$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp

Kết luận

embind mang đến cho bạn những cải tiến tuyệt vời về trải nghiệm của nhà phát triển khi làm việc bằng wasm và C/C++. Bài viết này không đề cập đến tất cả các lựa chọn được liên kết với các ưu đãi. Nếu bạn quan tâm, tôi khuyên bạn nên tiếp tục với embind's . Lưu ý rằng việc sử dụng embind có thể tạo cả mô-đun wasm và Mã keo JavaScript lớn hơn lên tới 11k khi gzip'd — đáng chú ý nhất trên tệp nhỏ các mô-đun. Nếu bạn chỉ có bề mặt wasm rất nhỏ, thì embind có thể tốn hơn điều này rất hữu ích trong môi trường sản xuất! Tuy nhiên, chắc chắn bạn nên cung cấp hãy thử.