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