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