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

การแปลงชื่อ C++

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

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

ป้อน embind

embindเป็นส่วนหนึ่งของชุดเครื่องมือ Emscripten และมีมาโคร C++ จำนวนมากที่ช่วยให้คุณกำกับเนื้อหาโค้ด C++ ได้ คุณสามารถประกาศฟังก์ชัน อาร์เรย์แบบจำกัด ชั้นเรียน หรือประเภทค่าที่วางแผนจะใช้จาก 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);
}

ฉันกําหนดโครงสร้างสําหรับตัวเลือกของฟังก์ชัน processMessage() ในบล็อก EMSCRIPTEN_BINDINGS ฉันสามารถใช้ value_object เพื่อให้ JavaScript เห็นค่า C++ นี้เป็นออบเจ็กต์ ฉันยังใช้ value_array ได้หากต้องการใช้ค่า C++ นี้เป็นอาร์เรย์ ฉันยังเชื่อมโยงฟังก์ชัน processMessage() ด้วย และส่วนที่เหลือคือ Magic ของ embind ตอนนี้ฉันเรียกใช้ฟังก์ชัน 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 กลุ่ม ได้แก่ 1 กลุ่มสำหรับไฟล์ C และอีก 1 กลุ่มสำหรับไฟล์ 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 มีให้ หากสนใจ เราขอแนะนำให้อ่านเอกสารประกอบของ embind ต่อ โปรดทราบว่าการใช้ embind อาจทําให้ทั้งโมดูล wasm และโค้ดกาว JavaScript มีขนาดเพิ่มขึ้นสูงสุด 11 KB เมื่อใช้ gzip โดยเฉพาะอย่างยิ่งในโมดูลขนาดเล็ก หากคุณมีแพลตฟอร์ม WASM เพียงเล็กน้อยเท่านั้น embind อาจใช้ต้นทุนมากกว่าที่ควรจะเป็นในสภาพแวดล้อมที่ใช้งานจริง อย่างไรก็ตาม คุณควรลองใช้