ซึ่งจะเชื่อมโยง 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 อาจมีค่าใช้จ่ายมากกว่าที่คุ้มค่าในสภาพแวดล้อมการใช้งานจริง! อย่างไรก็ตาม คุณก็ควรลองใช้ดู