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

ซึ่งจะเชื่อมโยง JS กับ 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 จะถูกทำให้อ่านไม่ออกเป็นชื่อที่เข้ารหัสลายเซ็นในชื่อฟังก์ชันสำหรับโปรแกรมลิงก์ ด้วยเหตุนี้ เราจึงค้นหาฟังก์ชันด้วยชื่อไม่ได้อีกต่อไป

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

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

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

วัตถุ

ตัวสร้างและฟังก์ชัน 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++ และเพิ่มแฟล็ก 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 อาจมีค่าใช้จ่ายมากกว่าที่ควรจะเป็นในสภาพแวดล้อมการใช้งานจริง อย่างไรก็ตาม คุณควรลองใช้