ตราประทับของ Emscripten

ซึ่งจะเชื่อมโยง JS กับ Wasm ของคุณ

ในบทความ Wasm ล่าสุดฉันได้พูด เกี่ยวกับวิธีคอมไพล์ไลบรารี C เป็น Wasm เพื่อให้คุณใช้บนเว็บได้ สิ่งเดียว สิ่งที่โดดเด่นสำหรับฉัน (และผู้อ่านจำนวนมาก) คือวิธีที่หยาบคายและอึดอัดใจเล็กน้อย คุณต้องประกาศด้วยตนเองว่าฟังก์ชันใดของโมดูล Wasm ที่ใช้อยู่ เพื่อความสบายใจของคุณ นี่คือข้อมูลโค้ดที่เรากำลังพูดถึง:

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

ตรงนี้เราจะประกาศชื่อของฟังก์ชันที่เราทำเครื่องหมายด้วย EMSCRIPTEN_KEEPALIVE ประเภทการคืนสินค้าและประเภทการคืนสินค้า ที่มีอยู่ หลังจากนั้น เราจะใช้เมธอดในออบเจ็กต์ api เพื่อเรียกใช้ ฟังก์ชันเหล่านี้ อย่างไรก็ตาม การใช้ Wasm ด้วยวิธีนี้จะไม่รองรับสตริงและ คุณต้องย้ายกลุ่มหน่วยความจำไปรอบๆ ด้วยตนเองซึ่งทำให้ต้องมีคลังจำนวนมาก API ใช้งานยากมาก มีวิธีที่ดีกว่านี้ไหม เหตุผลที่ต้องตอบคือไม่เป็นเช่นนั้น บทความนี้จะมีเนื้อหาเกี่ยวกับอะไร

การจัดการชื่อ C++

แม้ว่าประสบการณ์ของนักพัฒนาซอฟต์แวร์จะเป็นเหตุผลที่ดีพอที่จะสร้างเครื่องมือที่ช่วย ในการผูกข้อมูลเหล่านี้ มีเหตุผลที่ต้องกดดันมากกว่านั้น นั่นก็คือเมื่อคุณคอมไพล์ C หรือ C++ จะมีการคอมไพล์แต่ละไฟล์แยกกัน จากนั้น Linker จะดูแล ปิดเสียงไฟล์ออบเจ็กต์เหล่านี้ทั้งหมดเข้าด้วยกันและเปลี่ยนเป็นไฟล์ Wasm เมื่อใช้ C ชื่อของฟังก์ชันจะอยู่ในไฟล์ออบเจ็กต์ Linker จะใช้ สิ่งที่คุณต้องมีเพื่อให้เรียกฟังก์ชัน C ได้ก็คือชื่อ ซึ่งเราระบุเป็นสตริงให้กับ cwrap()

ส่วน C++ นั้นรองรับฟังก์ชันมากเกินไป ทำให้คุณ ฟังก์ชันเดียวกันหลายครั้งหากลายเซ็นต่างกัน (เช่น พารามิเตอร์ที่พิมพ์แตกต่างกัน) ในระดับคอมไพเลอร์ ชื่อที่ดี เช่น add จะถูกเปลี่ยนแปลงเป็นสิ่งที่เข้ารหัสลายเซ็นในฟังก์ชัน สำหรับ Linker ผลที่ได้คือ เราจึงจะค้นหาฟังก์ชันไม่ได้ ในชื่อนั้นอีกต่อไป

ป้อน embind

embind เป็นส่วนหนึ่งของเครื่องมือ Emscripten และมีมาโคร C++ อยู่มากมาย ที่ให้คุณใส่คำอธิบายประกอบโค้ด C++ ได้ คุณประกาศฟังก์ชัน, Enum, คลาสหรือประเภทค่าที่คุณวางแผนจะใช้จาก JavaScript มาเริ่มกันเลย เรียบง่ายด้วยฟังก์ชันทั่วไป:

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

เมื่อเทียบกับบทความก่อนหน้าของฉัน เราไม่รวม emscripten.h แล้ว เนื่องจาก เราไม่จำเป็นต้องใส่คำอธิบายประกอบในฟังก์ชันด้วย EMSCRIPTEN_KEEPALIVE อีกต่อไป แต่เรามีส่วน EMSCRIPTEN_BINDINGS ซึ่งจะแสดงรายชื่อภายใต้หัวข้อ ซึ่งเราต้องการแสดงฟังก์ชันของเราใน JavaScript

เพื่อคอมไพล์ไฟล์นี้ เราสามารถใช้การตั้งค่าเดิม (หรือวิธีเดียวกัน ถ้าต้องการ อิมเมจ Docker) เช่นเดียวกับใน ก่อนหน้านี้ บทความ หากต้องการใช้ Embind เราจะเพิ่ม Flag --bind:

$ emcc --bind -O3 add.cpp

ทีนี้ก็เหลือไฟล์ HTML ที่โหลดของเรา สร้างโมดูล Wasm แล้ว:

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

คุณจะเห็นว่าเราไม่ได้ใช้ cwrap() แล้ว แค่นี้ก็เรียบร้อย ในกล่อง แต่ที่สำคัญกว่านั้นคือ เราไม่ต้องกังวลเกี่ยวกับการคัดลอกด้วยตัวเอง ชิ้นส่วนหน่วยความจำเพื่อให้สตริงใช้งานได้ embind ให้คุณใช้ฟรี พร้อมทั้ง โดยใช้การตรวจสอบประเภท ดังนี้

ข้อผิดพลาดของเครื่องมือสำหรับนักพัฒนาเว็บเมื่อคุณเรียกใช้ฟังก์ชันที่มีจำนวนอาร์กิวเมนต์ไม่ถูกต้อง
หรืออาร์กิวเมนต์มี
ประเภท

ซึ่งมีประโยชน์มาก เพราะเราสามารถตรวจพบข้อผิดพลาดได้ตั้งแต่เนิ่นๆ แทนที่จะต้องจัดการ ในบางครั้ง ข้อผิดพลาด Wasm ที่ค่อนข้างคลุมเครือ

วัตถุ

ตัวสร้างและฟังก์ชัน JavaScript จำนวนมากจะใช้ออบเจ็กต์ตัวเลือก เยี่ยมเลย ใน JavaScript มากเกินไป แต่ก็น่าเบื่อมากที่จะนำมาใช้ใน Wasm ด้วยตัวเอง Embind ก็ช่วยคุณได้

ยกตัวอย่างเช่น ฉันค้นพบฟังก์ชัน C++ ที่มีประโยชน์อย่างไม่น่าเชื่อ ซึ่งประมวลผลฟังก์ชัน ทำให้ฉันอยากใช้มันบนเว็บอย่างเร่งด่วน โดยทำดังนี้

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

ฉันกำลังกำหนด Struct สำหรับตัวเลือกของฟังก์ชัน processMessage() ใน EMSCRIPTEN_BINDINGS บล็อก ฉันสามารถใช้ value_object เพื่อทำให้ JavaScript เห็น ค่า C++ นี้เป็นออบเจ็กต์ หรือจะใช้ value_array ก็ได้หากต้องการ ใช้ค่า C++ นี้เป็นอาร์เรย์ และเชื่อมโยงฟังก์ชัน processMessage() และ ส่วนที่เหลือจะเป็นเวทมนตร์ ตอนนี้ฉันสามารถเรียกฟังก์ชัน processMessage() จาก JavaScript ที่ไม่มีโค้ดสำเร็จรูป:

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

ชั้นเรียน

เพื่อความครบถ้วนสมบูรณ์ ฉันควรแสดงให้คุณดูด้วยวิธีที่ embind ช่วยให้คุณเปิดเผย ทั้งชั้นเรียน ซึ่งช่วยสร้าง ความสอดคล้องอย่างมากกับชั้นเรียน ES6 คุณอาจ ทีนี้คุณก็จะได้เห็นรูปแบบแล้ว:

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

ในด้าน JavaScript สิ่งนี้จะดูเหมือนเป็นชั้นเรียนเนทีฟ

<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 ล่ะ

embind เขียนขึ้นสำหรับ C++ และใช้ได้เฉพาะในไฟล์ C++ เท่านั้น แต่ไม่ได้ หมายความว่าคุณไม่สามารถเชื่อมโยงกับไฟล์ C ได้ หากต้องการรวม C และ C++ คุณเพียงแค่ แยกไฟล์อินพุตออกเป็น 2 กลุ่ม คือ กลุ่มสำหรับ C และไฟล์ C++ และ เพิ่ม Flag CLI สำหรับ emcc ดังนี้

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

บทสรุป

embind ช่วยปรับปรุงประสบการณ์การใช้งานของนักพัฒนาซอฟต์แวร์ได้เป็นอย่างดีขณะทำงาน ด้วย Wasm และ C/C++ บทความนี้ไม่ได้ครอบคลุมตัวเลือกที่รวมข้อเสนอทั้งหมด หากคุณสนใจ ฉันขอแนะนำให้คุณดำเนินการต่อกับ embind's เอกสารประกอบ โปรดทราบว่าการใช้ embind อาจทำให้ทั้งโมดูล Wasm และ โค้ดกาว JavaScript มีขนาดใหญ่ขึ้นได้สูงสุดถึง 11k เมื่อ gzip'd ซึ่งเห็นได้ชัดเจนที่สุดในขนาดเล็ก โมดูล หากคุณมีเพียงพื้นผิว Wasm ที่มีขนาดเล็กมาก embind อาจมีราคามากกว่า ในสภาพแวดล้อมการผลิต อย่างไรก็ตาม คุณควรให้ มาลองได้เลย