ดูวิธีฝังโค้ด JavaScript ในไลบรารี WebAssembly เพื่อสื่อสารกับโลกภายนอก
ขณะผสานรวม WebAssembly กับเว็บ คุณต้องมีวิธีเรียก API ภายนอก เช่น API ของเว็บและไลบรารีของบุคคลที่สาม จากนั้นคุณจะต้องมีวิธีจัดเก็บค่าและอินสแตนซ์ออบเจ็กต์ที่ API ดังกล่าวแสดงผล และวิธีส่งต่อค่าที่จัดเก็บไว้เหล่านั้นไปยัง API อื่นๆ ในภายหลัง สําหรับ API แบบไม่พร้อมกัน คุณอาจต้องรอให้มีสัญญาในโค้ด C/C++ แบบซิงโครนัสด้วย Asyncify แล้วอ่านผลลัพธ์เมื่อการดำเนินการเสร็จสิ้น
Emscripten มีเครื่องมือหลายอย่างสำหรับการโต้ตอบดังกล่าว ดังนี้
emscripten::val
สำหรับการจัดเก็บและใช้งานค่า JavaScript ใน C++EM_JS
สำหรับการฝังข้อมูลโค้ด JavaScript และเชื่อมโยงเป็นฟังก์ชัน C/C++EM_ASYNC_JS
คล้ายกับEM_JS
แต่ช่วยให้ฝังข้อมูลโค้ด JavaScript แบบไม่พร้อมกันได้ง่ายขึ้นEM_ASM
สำหรับการฝังข้อมูลโค้ดสั้นๆ และการเรียกใช้ในหน้าโดยไม่ต้องประกาศฟังก์ชัน--js-library
สำหรับสถานการณ์ขั้นสูงที่คุณต้องการประกาศฟังก์ชัน JavaScript จำนวนมากรวมกันเป็นไลบรารีเดียว
ในโพสต์นี้ คุณจะได้ดูวิธีใช้สิ่งเหล่านี้ทั้งหมดสําหรับงานที่คล้ายกัน
emscripten::val class
ชั้นเรียน emcripten::val
ให้บริการโดย Embind โดยสามารถเรียกใช้ API ส่วนกลาง เชื่อมโยงค่า JavaScript กับอินสแตนซ์ C++ และแปลงค่าระหว่างประเภท C++ กับ JavaScript
ต่อไปนี้เป็นวิธีใช้กับ .await()
ของ Asyncify เพื่อดึงข้อมูลและแยกวิเคราะห์ JSON บางส่วน
#include <emscripten/val.h>
using namespace emscripten;
val fetch_json(const char *url) {
// Get and cache a binding to the global `fetch` API in each thread.
thread_local const val fetch = val::global("fetch");
// Invoke fetch and await the returned `Promise<Response>`.
val response = fetch(url).await();
// Ask to read the response body as JSON and await the returned `Promise<any>`.
val json = response.call<val>("json").await();
// Return the JSON object.
return json;
}
// Example URL.
val example_json = fetch_json("https://httpbin.org/json");
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
โค้ดนี้ทำงานได้ดี แต่ก็มีขั้นตอนระหว่างกลางหลายอย่าง การดำเนินการแต่ละรายการใน val
จำเป็นต้องทำขั้นตอนต่อไปนี้
- แปลงค่า C++ ที่ส่งผ่านเป็นอาร์กิวเมนต์ให้อยู่ในรูปแบบตัวกลางบางส่วน
- ไปที่ JavaScript อ่านและแปลงอาร์กิวเมนต์เป็นค่า JavaScript
- ดำเนินการฟังก์ชัน
- แปลงผลลัพธ์จาก JavaScript เป็นรูปแบบระดับกลาง
- แสดงผลลัพธ์ที่แปลงแล้วไปยัง C++ และในขั้นสุดท้าย C++ จะอ่านผลลัพธ์อีกครั้ง
await()
แต่ละรายการยังต้องหยุดฝั่ง C++ ชั่วคราวด้วยการคลาย Call Stack ของโมดูล WebAssembly แล้วกลับไปยัง JavaScript, รอ และกู้คืนสแต็ก WebAssembly เมื่อการดำเนินการเสร็จสมบูรณ์
โค้ดดังกล่าวไม่จำเป็นต้องดำเนินการใดๆ จาก C++ โค้ด C++ จะทำหน้าที่เป็นตัวขับเคลื่อนสำหรับชุดการดำเนินการ JavaScript เท่านั้น จะเป็นอย่างไรถ้าคุณสามารถย้าย fetch_json
ไปใช้ JavaScript และลดค่าใช้จ่ายของขั้นตอนระดับกลางได้ในเวลาเดียวกัน
มาโคร EM_JS
EM_JS macro
ให้คุณย้าย fetch_json
ไปยัง JavaScript ได้ EM_JS
ใน Emscripten ช่วยให้คุณประกาศฟังก์ชัน C/C++ ที่ข้อมูลโค้ด JavaScript ใช้งาน
WebAssembly ตรงที่มีข้อจำกัดในการรองรับอาร์กิวเมนต์ตัวเลขและค่าที่ส่งกลับเท่านั้น หากต้องการส่งค่าอื่นๆ คุณจะต้องแปลงค่าเหล่านั้นด้วยตนเองผ่าน API ที่เกี่ยวข้อง ต่อไปนี้เป็นตัวอย่างบางส่วน
การส่งหมายเลขไม่จำเป็นต้องมีการแปลง
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
เมื่อส่งสตริงไปยังและจาก JavaScript คุณต้องใช้ฟังก์ชัน Conversion และการจัดสรรที่เกี่ยวข้องจาก preamble.js ดังนี้
EM_JS(void, log_string, (const char *msg), {
console.log(UTF8ToString(msg));
});
EM_JS(const char *, get_input, (), {
let str = document.getElementById('myinput').value;
// Returns heap-allocated string.
// C/C++ code is responsible for calling `free` once unused.
return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL);
});
และสุดท้าย คุณสามารถใช้ JavaScript API สำหรับคลาส val
ที่กล่าวไปก่อนหน้านี้สำหรับประเภทค่าที่ซับซ้อนมากขึ้นได้ตามต้องการ เมื่อใช้เครื่องมือดังกล่าว คุณจะสามารถแปลงค่า JavaScript และคลาส C++ เป็นแฮนเดิลตรงกลางและย้อนกลับได้ ดังนี้
EM_JS(void, log_value, (EM_VAL val_handle), {
let value = Emval.toValue(val_handle);
console.log(value);
});
EM_JS(EM_VAL, find_myinput, (), {
let input = document.getElementById('myinput');
return Emval.toHandle(input);
});
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
log_value(obj.as_handle()); // logs { x: 1, y: 2 }
val myinput = val::take_ownership(find_input());
// Now you can store the `find_myinput` DOM element for as long as you like, and access it later like:
std::string value = input["value"].as<std::string>();
เมื่อคำนึงถึง API ดังกล่าว ระบบอาจเขียนตัวอย่าง fetch_json
ใหม่เพื่อให้ทำงานส่วนใหญ่ได้โดยไม่ต้องออกจาก JavaScript
EM_JS(EM_VAL, fetch_json, (const char *url), {
return Asyncify.handleAsync(async () => {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
เรายังมี Conversion ที่อาจไม่เหมาะสมอยู่ 2 รายการบริเวณจุดเข้าและออกของฟังก์ชัน แต่ส่วนที่เหลือจะเป็นโค้ด JavaScript ปกติ ตอนนี้เครื่องมือ JavaScript สามารถเพิ่มประสิทธิภาพการทำงานซึ่งแตกต่างจาก val
ที่เทียบเท่ากัน แต่จะหยุดฝั่ง C++ ชั่วคราวเพียงครั้งเดียวสำหรับการดำเนินการที่ไม่พร้อมกันทั้งหมด
มาโคร EM_ASYNC_JS
ส่วนเดียวที่ดูไม่สวยเลยคือ Wrapper Asyncify.handleAsync
ที่มีเพียงวัตถุประสงค์เดียวคืออนุญาตให้เรียกใช้ฟังก์ชัน JavaScript async
ด้วย Asyncify อันที่จริงแล้ว Use Case นี้เป็นเรื่องที่พบได้ทั่วไปจนตอนนี้มีมาโคร EM_ASYNC_JS
เฉพาะทางที่รวมเข้าด้วยกัน
ต่อไปนี้คือวิธีใช้ไฟล์ดังกล่าวเพื่อสร้างตัวอย่าง fetch
เวอร์ชันสุดท้าย
EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
EM_ASM
EM_JS
เป็นวิธีที่แนะนำในการประกาศข้อมูลโค้ด JavaScript แต่มีประสิทธิภาพเนื่องจากเชื่อมโยงข้อมูลโค้ดที่ประกาศไว้โดยตรงเหมือนกับการนำเข้าฟังก์ชัน JavaScript อื่นๆ และยังช่วยให้สามารถประกาศประเภทและชื่อของพารามิเตอร์ทั้งหมดได้อย่างชัดเจน
แต่ในบางกรณี คุณอาจต้องการแทรกข้อมูลโค้ดสั้นๆ สําหรับการเรียกใช้ console.log
, คําสั่ง debugger;
หรือคําที่คล้ายกัน และไม่ต้องการประกาศฟังก์ชันที่แยกกันโดยสิ้นเชิง ในกรณีที่เกิดขึ้นไม่บ่อยนัก EM_ASM macros family
(EM_ASM
, EM_ASM_INT
และ EM_ASM_DOUBLE
) อาจเป็นทางเลือกที่ง่ายกว่า มาโครเหล่านั้นคล้ายกับมาโคร EM_JS
แต่จะเรียกใช้โค้ดแบบในหน้าที่จะถูกแทรก แทนที่จะกำหนดฟังก์ชัน
เนื่องจากไม่ได้ประกาศต้นแบบของฟังก์ชัน จึงต้องหาวิธีอื่นในการระบุประเภทผลลัพธ์และการเข้าถึงอาร์กิวเมนต์
คุณต้องใช้ชื่อมาโครที่ถูกต้องเพื่อเลือกประเภทการแสดงผล บล็อก EM_ASM
ควรทำงานเหมือนกับฟังก์ชัน void
, บล็อก EM_ASM_INT
สามารถแสดงค่าจำนวนเต็ม และ EM_ASM_DOUBLE
บล็อกจะแสดงตัวเลขจุดลอยตัวที่สอดคล้องกัน
อาร์กิวเมนต์ที่ส่งผ่านจะพร้อมใช้งานในชื่อ $0
, $1
และอื่นๆ ในเนื้อหา JavaScript เช่นเดียวกับ EM_JS
หรือ WebAssembly โดยทั่วไป อาร์กิวเมนต์จะจำกัดอยู่ที่ค่าตัวเลขเท่านั้น ได้แก่ จำนวนเต็ม จำนวนจุดลอยตัว ตัวชี้ และแฮนเดิล
ต่อไปนี้คือตัวอย่างวิธีใช้มาโคร EM_ASM
เพื่อบันทึกค่า JS ที่กำหนดเองไปยังคอนโซล
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
// convert handle passed under $0 into a JavaScript value
let obj = Emval.fromHandle($0);
console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());
--js-library
สุดท้าย Emscripten รองรับการประกาศโค้ด JavaScript ในไฟล์แยกต่างหากในรูปแบบไลบรารีที่กำหนดเอง ดังนี้
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
จากนั้นคุณต้องประกาศต้นแบบที่เกี่ยวข้องด้วยตนเองในฝั่ง C++ ดังนี้
extern "C" void log_value(EM_VAL val_handle);
เมื่อประกาศทั้ง 2 ฝั่งแล้ว คุณจะสามารถลิงก์ไลบรารี JavaScript กับโค้ดหลักผ่าน--js-library option
ซึ่งจะเชื่อมต่อต้นแบบกับการติดตั้งใช้งาน JavaScript ที่เกี่ยวข้อง
อย่างไรก็ตาม รูปแบบโมดูลนี้ไม่เป็นมาตรฐานและต้องใช้คำอธิบายประกอบ Dependency อย่างระมัดระวัง ดังนั้น ส่วนมากจะสงวนไว้สำหรับสถานการณ์ขั้นสูง
บทสรุป
ในโพสต์นี้ เราได้พูดถึงวิธีต่างๆ ในการผสานรวมโค้ด JavaScript ลงใน C++ เมื่อทำงานกับ WebAssembly
การใส่ข้อมูลโค้ดดังกล่าวจะช่วยให้คุณแสดงลำดับการดำเนินการยาวๆ ได้อย่างสะอาดตาและมีประสิทธิภาพมากขึ้น รวมถึงใช้ประโยชน์จากไลบรารีของบุคคลที่สาม, JavaScript API ใหม่ และแม้แต่ฟีเจอร์ไวยากรณ์ JavaScript ที่ยังไม่แสดงผ่าน C++ หรือ Embind ได้ด้วย