ตราประทับของ 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 เดียวกันก็ได้ หากต้องการ) กับในบทความก่อนหน้า ในการใช้ Embin เราจะเพิ่มแฟล็ก --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 ด้วยตนเอง embined จะช่วยคุณได้เช่นกัน!

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