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