ตราประทับของ 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 ชื่อของฟังก์ชันจะยังคงอยู่ในไฟล์ออบเจ็กต์ เพื่อให้ลิงเกอร์ใช้งานได้ สิ่งที่คุณต้องมีในการเรียกใช้ฟังก์ชัน C คือชื่อ ซึ่งเราจะระบุเป็นสตริงให้กับ cwrap()

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

ป้อน 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 Image เดียวกันก็ได้) กับในบทความก่อนหน้า หากต้องการใช้ Embind ให้เพิ่มแฟล็ก --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);
}

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

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++ และ เพิ่มแฟล็ก 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 มีให้ หากสนใจ เราขอแนะนำให้ศึกษาเอกสารประกอบของ embind ต่อไป โปรดทราบว่าการใช้ embind อาจทำให้ทั้งโมดูล wasm และโค้ดกาว JavaScript มีขนาดใหญ่ขึ้นสูงสุด 11k เมื่อใช้ gzip โดยเฉพาะอย่างยิ่งในโมดูลขนาดเล็ก หากคุณมีพื้นผิว wasm ขนาดเล็กมาก embind อาจมีค่าใช้จ่ายมากกว่า คุ้มค่าในสภาพแวดล้อมการใช้งานจริง อย่างไรก็ตาม คุณควรลองใช้ ดู