แม้ว่า JavaScript จะค่อนข้างให้อภัยในการล้างข้อมูลหลังจากตัวเอง แต่ภาษาแบบคงที่นั้นไม่แน่นอน
Squoosh.app เป็น PWA ที่แสดงให้เห็นว่าโปรแกรมเปลี่ยนไฟล์รูปภาพและการตั้งค่าต่างๆ ช่วยปรับปรุงขนาดไฟล์รูปภาพได้มากเพียงใดโดยไม่ส่งผลต่อคุณภาพอย่างมีนัยสำคัญ แต่ก็ยังเป็นการสาธิตทางเทคนิคที่แสดงให้เห็นวิธีนําคลังเขียนด้วย C++ หรือ Rust ไปใช้ในเว็บ
การพอร์ตโค้ดจากระบบนิเวศที่มีอยู่มีประโยชน์อย่างยิ่ง แต่ภาษาแบบคงที่เหล่านั้นและ JavaScript มีความแตกต่างที่สำคัญบางอย่าง หนึ่งในนั้นคือแนวทางการจัดการหน่วยความจำที่แตกต่างกัน
แม้ว่า JavaScript จะค่อนข้างให้อภัยในการล้างข้อมูลหลังจากตัวเอง แต่ภาษาแบบคงที่ดังกล่าวจะไม่ให้อภัยอย่างแน่นอน คุณต้องขอหน่วยความจำใหม่อย่างชัดเจน และจะต้องคืนหน่วยความจำดังกล่าวหลังจากนั้นและจะไม่ใช้อีก หากไม่เกิดขึ้น คุณจะพบปัญหาการรั่วไหล ซึ่งเกิดขึ้นค่อนข้างบ่อย มาดูวิธีแก้ไขข้อบกพร่องหน่วยความจำที่รั่วไหลเหล่านั้น และวิธีออกแบบโค้ดเพื่อหลีกเลี่ยงข้อบกพร่องดังกล่าวในครั้งถัดไป
รูปแบบที่น่าสงสัย
เมื่อเร็วๆ นี้ขณะเริ่มทํางานกับ Squoosh เราสังเกตเห็นรูปแบบที่น่าสนใจใน Wrapper ตัวแปลงรหัส C++ มาดูตัวอย่าง Wrapper ของ ImageQuant (ย่อให้แสดงเฉพาะส่วนของการสร้างและการจัดสรรพื้นที่ใหม่ของออบเจ็กต์)
liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;
RawImage quantize(std::string rawimage,
int image_width,
int image_height,
int num_colors,
float dithering) {
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
int size = image_width * image_height;
attr = liq_attr_create();
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_set_max_colors(attr, num_colors);
liq_image_quantize(image, attr, &res);
liq_set_dithering_level(res, dithering);
uint8_t* image8bit = (uint8_t*)malloc(size);
result = (uint8_t*)malloc(size * 4);
// …
free(image8bit);
liq_result_destroy(res);
liq_image_destroy(image);
liq_attr_destroy(attr);
return {
val(typed_memory_view(image_width * image_height * 4, result)),
image_width,
image_height
};
}
void free_result() {
free(result);
}
JavaScript (หรือ TypeScript)
export async function process(data: ImageData, opts: QuantizeOptions) {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
}
const module = await emscriptenModule;
const result = module.quantize(/* … */);
module.free_result();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
คุณพบปัญหาไหม เคล็ดลับ: use-after-free แต่อยู่ใน JavaScript
ใน Emscripten typed_memory_view
จะแสดงผล JavaScript Uint8Array
ที่รองรับโดยบัฟเฟอร์หน่วยความจำ WebAssembly (Wasm) โดยตั้งค่า byteOffset
และ byteLength
เป็นพอยน์เตอร์และความยาวที่ระบุ ประเด็นหลักคือนี่คือมุมมอง TypedArray ไปยังบัฟเฟอร์หน่วยความจำ WebAssembly ไม่ใช่สำเนาข้อมูลของ JavaScript
เมื่อเราเรียก free_result
จาก JavaScript ฟังก์ชันดังกล่าวจะเรียกฟังก์ชัน C มาตรฐาน free
เพื่อทําเครื่องหมายหน่วยความจํานี้ว่าพร้อมสําหรับการจัดสรรในอนาคต ซึ่งหมายความว่าข้อมูลที่เราชี้ไปยังมุมมอง Uint8Array
สามารถเขียนทับด้วยข้อมูลใดก็ได้โดยการเรียกใช้ Wasm ในอนาคต
หรือการใช้งาน free
บางรายการอาจตัดสินใจที่จะกรอกค่า 0 ลงในหน่วยความจำที่ว่างเปล่าทันที free
ที่ Emscripten ใช้ไม่ได้ทำเช่นนั้น แต่เราอาศัยรายละเอียดการใช้งานที่นี่ซึ่งไม่สามารถรับประกันได้
หรือแม้ว่าหน่วยความจำที่อยู่หลังพอยน์เตอร์จะได้รับการสงวนไว้ แต่การจัดสรรใหม่อาจต้องเพิ่มหน่วยความจำ WebAssembly เมื่อเพิ่ม WebAssembly.Memory
ผ่าน JavaScript API หรือคำสั่ง memory.grow
ที่เกี่ยวข้อง ระบบจะทำให้ ArrayBuffer
ที่มีอยู่ใช้งานไม่ได้ และทำให้มุมมองที่ ArrayBuffer
รองรับใช้งานไม่ได้ด้วย
เราจะใช้คอนโซลเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ (หรือ Node.js) เพื่อสาธิตลักษณะการทำงานนี้
> memory = new WebAssembly.Memory({ initial: 1 })
Memory {}
> view = new Uint8Array(memory.buffer, 42, 10)
Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// ^ all good, we got a 10 bytes long view at address 42
> view.buffer
ArrayBuffer(65536) {}
// ^ its buffer is the same as the one used for WebAssembly memory
// (the size of the buffer is 1 WebAssembly "page" == 64KB)
> memory.grow(1)
1
// ^ let's say we grow Wasm memory by +1 page to fit some new data
> view
Uint8Array []
// ^ our original view is no longer valid and looks empty!
> view.buffer
ArrayBuffer(0) {}
// ^ its buffer got invalidated as well and turned into an empty one
สุดท้ายนี้ แม้ว่าเราจะไม่ได้เรียกใช้ Wasm อย่างชัดเจนอีกในช่วงระหว่าง free_result
ถึง new
Uint8ClampedArray
แต่เราก็อาจเพิ่มการรองรับการแยกหลายเธรดลงในโปรแกรมเปลี่ยนรหัสของเราในบางจุด ในกรณีนี้ อาจเป็นเธรดอื่นที่เขียนทับข้อมูลก่อนที่เราจะคัดลอกได้
มองหาข้อบกพร่องเกี่ยวกับหน่วยความจํา
เพื่อเป็นการป้องกันไว้ก่อน เราจึงตัดสินใจตรวจสอบเพิ่มเติมว่ารหัสนี้มีปัญหาเกิดขึ้นจริงหรือไม่ ดูเหมือนว่านี่จะเป็นโอกาสที่ดีในการลองใช้การรองรับโปรแกรมตรวจสอบ Emscripten เวอร์ชันใหม่(ish) ที่เพิ่มเข้ามาเมื่อปีที่แล้วและนำเสนอในการพูดคุยเรื่อง WebAssembly ที่ Chrome Dev Summit
ในกรณีนี้ เราสนใจ AddressSanitizer ซึ่งสามารถตรวจหาปัญหาต่างๆ เกี่ยวกับตัวชี้และหน่วยความจำ หากต้องการใช้ เราต้องคอมไพล์ตัวแปลงรหัสใหม่ด้วย -fsanitize=address
โดยทำดังนี้
emcc \
--bind \
${OPTIMIZE} \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="imagequant"' \
-I node_modules/libimagequant \
-o ./imagequant.js \
--std=c++11 \
imagequant.cpp \
-fsanitize=address \
node_modules/libimagequant/libimagequant.a
ซึ่งจะเปิดใช้การตรวจสอบความปลอดภัยของพอยน์เตอร์โดยอัตโนมัติ แต่เราต้องการค้นหาการรั่วไหลของหน่วยความจำที่อาจเกิดขึ้นด้วย เนื่องจากเราใช้ ImageQuant เป็นไลบรารีแทนโปรแกรม จึงไม่มี "จุดสิ้นสุด" ที่ Emscripten จะตรวจสอบได้อัตโนมัติว่ามีการคืนหน่วยความจำทั้งหมดแล้ว
ในกรณีเช่นนี้ LeakSanitizer (รวมอยู่ใน AddressSanitizer) จะมีฟังก์ชัน __lsan_do_leak_check
และ __lsan_do_recoverable_leak_check
ซึ่งเรียกใช้ด้วยตนเองได้เมื่อใดก็ตามที่เราคาดว่าจะมีการปลดปล่อยหน่วยความจำทั้งหมดและต้องการยืนยันสมมติฐานดังกล่าว __lsan_do_leak_check
มีไว้เพื่อใช้เมื่อแอปพลิเคชันที่ทำงานอยู่สิ้นสุดลง เมื่อคุณต้องการหยุดกระบวนการในกรณีที่ตรวจพบการรั่วไหล ส่วน __lsan_do_recoverable_leak_check
เหมาะสําหรับ Use Case ของไลบรารีมากกว่า เช่น เมื่อคุณต้องการพิมพ์การรั่วไหลไปยังคอนโซล แต่ให้แอปพลิเคชันทำงานต่อไป
มาแสดงตัวช่วยที่ 2 ผ่าน Embind เพื่อให้เราเรียกใช้จาก JavaScript ได้ทุกเมื่อ
#include <sanitizer/lsan_interface.h>
// …
void free_result() {
free(result);
}
EMSCRIPTEN_BINDINGS(my_module) {
function("zx_quantize", &zx_quantize);
function("version", &version);
function("free_result", &free_result);
function("doLeakCheck", &__lsan_do_recoverable_leak_check);
}
และเรียกใช้จากฝั่ง JavaScript เมื่อเราจัดการกับรูปภาพเสร็จแล้ว การทำเช่นนี้จากฝั่ง JavaScript แทนที่จะเป็นฝั่ง C++ จะช่วยให้มั่นใจได้ว่าสโคปทั้งหมดจะออกและมีการคืนค่าออบเจ็กต์ C++ ชั่วคราวทั้งหมดแล้วเมื่อเราเรียกใช้การตรวจสอบเหล่านั้น
// …
const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);
module.free_result();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
ซึ่งจะให้รายงานดังต่อไปนี้ในคอนโซล
อ๊ะ มีการรั่วไหลเล็กน้อย แต่สแต็กเทรซไม่ค่อยมีประโยชน์เนื่องจากชื่อฟังก์ชันทั้งหมดถูกทำให้อ่านไม่ออก มาคอมไพล์อีกครั้งพร้อมข้อมูลการแก้ไขข้อบกพร่องพื้นฐานเพื่อเก็บข้อมูลไว้กัน
emcc \
--bind \
${OPTIMIZE} \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="imagequant"' \
-I node_modules/libimagequant \
-o ./imagequant.js \
--std=c++11 \
imagequant.cpp \
-fsanitize=address \
-g2 \
node_modules/libimagequant/libimagequant.a
ข้อความนี้ดูดีกว่ามาก
บางส่วนของสแต็กเทรซยังดูคลุมเครือเนื่องจากชี้ไปยังข้อมูลภายในของ Emscripten แต่เราสามารถบอกได้ว่าการรั่วไหลมาจากRawImage
การเปลี่ยนเป็น "ประเภทการเดินสาย" (เป็นค่า JavaScript) โดย Embind เมื่อตรวจสอบโค้ดแล้ว เราพบว่าเราส่งคืนRawImage
อินสแตนซ์ C++ ไปยัง JavaScript แต่ไม่เคยยกเลิกการจองอินสแตนซ์ดังกล่าวในฝั่งใดฝั่งหนึ่ง
โปรดทราบว่าขณะนี้ยังไม่มีการผสานรวมการเก็บขยะระหว่าง JavaScript กับ WebAssembly แม้ว่าจะอยู่ระหว่างการพัฒนา แต่คุณต้องเพิ่มหน่วยความจําและเรียกตัวทำลายจากฝั่ง JavaScript ด้วยตนเองเมื่อใช้ออบเจ็กต์เสร็จแล้ว สำหรับ Embind โดยเฉพาะ เอกสารอย่างเป็นทางการแนะนำให้เรียกใช้เมธอด .delete()
ในคลาส C++ ที่เปิดเผย
โค้ด JavaScript ต้องลบตัวแฮนเดิลออบเจ็กต์ C++ ที่ได้รับอย่างชัดเจน มิฉะนั้นกอง Emscripten จะเติบโตอย่างไม่จำกัด
var x = new Module.MyClass;
x.method();
x.delete();
เมื่อเราทำเช่นนั้นใน JavaScript สำหรับชั้นเรียน
// …
const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);
module.free_result();
result.delete();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
การรั่วไหลหายไปตามที่คาดไว้
พบปัญหาเพิ่มเติมเกี่ยวกับน้ำยาฆ่าเชื้อ
การสร้างตัวแปลงรหัส Squoosh อื่นๆ ด้วยโปรแกรมตรวจสอบพบปัญหาที่คล้ายกันและปัญหาใหม่ๆ ตัวอย่างเช่น ฉันได้รับข้อผิดพลาดนี้ในการเชื่อมโยง MozJPEG
ในกรณีนี้ ไม่ได้เป็นการรั่วไหล แต่เป็นการเขียนไปยังหน่วยความจําที่อยู่นอกขอบเขตที่กําหนด 😱
เมื่อตรวจสอบโค้ดของ MozJPEG เราพบว่าปัญหาคือ jpeg_mem_dest
ซึ่งเป็นฟังก์ชันที่เราใช้เพื่อจัดสรรปลายทางหน่วยความจำสำหรับ JPEG ใช้ค่าที่มีอยู่ของ outbuffer
และ outsize
ซ้ำเมื่อค่าเหล่านั้นไม่ใช่ 0
if (*outbuffer == NULL || *outsize == 0) {
/* Allocate initial buffer */
dest->newbuffer = *outbuffer = (unsigned char *) malloc(OUTPUT_BUF_SIZE);
if (dest->newbuffer == NULL)
ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10);
*outsize = OUTPUT_BUF_SIZE;
}
อย่างไรก็ตาม เราเรียกใช้โดยไม่เริ่มต้นตัวแปรใดตัวแปรหนึ่ง ซึ่งหมายความว่า MozJPEG จะเขียนผลลัพธ์ไปยังที่อยู่หน่วยความจำแบบสุ่มที่อาจจัดเก็บไว้ในตัวแปรเหล่านั้น ณ เวลาเรียกใช้
uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);
การเริ่มต้นค่าทั้งสองตัวแปรเป็น 0 ก่อนการเรียกใช้จะช่วยแก้ปัญหานี้ได้ และตอนนี้โค้ดจะไปถึงการตรวจสอบการรั่วไหลของหน่วยความจำแทน โชคดีที่การตรวจสอบผ่านการตรวจสอบเรียบร้อยแล้ว ซึ่งหมายความว่าเราไม่มีการรั่วไหลในโค้ดนี้
ปัญหาเกี่ยวกับสถานะที่แชร์
…หรือว่าเราจะ
เราทราบดีว่าการเชื่อมโยงโค้ดของเราจัดเก็บสถานะบางส่วนและผลลัพธ์ไว้ในตัวแปรแบบคงที่ส่วนกลาง และ MozJPEG มีโครงสร้างที่ซับซ้อนมาก
uint8_t* last_result;
struct jpeg_compress_struct cinfo;
val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
// …
}
จะเกิดอะไรขึ้นหากมีการเริ่มต้นบางส่วนแบบเลื่อนเวลาในการเรียกใช้ครั้งแรก แล้วนํากลับมาใช้อย่างไม่เหมาะสมในการเรียกใช้ครั้งต่อๆ ไป ดังนั้นการโทรครั้งเดียวที่มีการใช้โปรแกรมฆ่าเชื้อจะไม่รายงานว่าเป็นปัญหา
ลองประมวลผลรูปภาพ 2-3 ครั้งโดยคลิกแบบสุ่มที่ระดับคุณภาพต่างๆ ใน UI ตอนนี้เราได้รับรายงานดังต่อไปนี้
262,144 ไบต์ - ดูเหมือนว่ารูปภาพตัวอย่างทั้งหมดจะรั่วไหลจาก jpeg_finish_compress
หลังจากตรวจสอบเอกสารและตัวอย่างอย่างเป็นทางการ พบว่า jpeg_finish_compress
ไม่ได้เพิ่มพื้นที่ว่างให้กับหน่วยความจำที่เรียกใช้โดย jpeg_mem_dest
ก่อนหน้านี้ แต่จะเพิ่มพื้นที่ว่างให้กับโครงสร้างการบีบอัดเท่านั้น แม้ว่าโครงสร้างการบีบอัดนั้นจะทราบปลายทางหน่วยความจำของเราอยู่แล้วก็ตาม…
เราแก้ไขปัญหานี้ได้โดยการล้างข้อมูลด้วยตนเองในฟังก์ชัน free_result
โดยทำดังนี้
void free_result() {
/* This is an important step since it will release a good deal of memory. */
free(last_result);
jpeg_destroy_compress(&cinfo);
}
เราอาจไล่ล่าข้อบกพร่องเกี่ยวกับหน่วยความจำทีละข้อ แต่ตอนนี้ก็ชัดเจนแล้วว่าแนวทางการจัดการหน่วยความจำในปัจจุบันทำให้เกิดปัญหาระบบที่น่ารำคาญ
สารฆ่าเชื้อสามารถจับไวรัสบางชนิดได้ทันที ส่วนบางรายต้องใช้กลโกงที่ซับซ้อนในการจับ สุดท้าย ยังมีปัญหาอื่นๆ เช่น ปัญหาในตอนต้นของโพสต์ ซึ่งเราเห็นว่าจากบันทึก Sanitizer ไม่ได้ตรวจพบปัญหาดังกล่าวเลย สาเหตุคือการใช้ในทางที่ผิดเกิดขึ้นที่ฝั่ง JavaScript ซึ่งตัวกรองไม่สามารถเข้าถึงได้ ปัญหาเหล่านี้จะปรากฏขึ้นเมื่ออยู่ในเวอร์ชันที่ใช้งานจริงเท่านั้น หรือหลังจากการเปลี่ยนแปลงโค้ดที่ดูเหมือนจะไม่เกี่ยวข้องในอนาคต
การสร้าง Wrapper ที่ปลอดภัย
เรามาย้อนกลับไป 2-3 ขั้นตอนและแก้ไขปัญหาทั้งหมดเหล่านี้ด้วยการปรับเปลี่ยนโครงสร้างโค้ดให้ปลอดภัยยิ่งขึ้นแทน เราจะใช้ ImageQuant Wrapper เป็นตัวอย่างอีกครั้ง แต่กฎการจัดระเบียบใหม่แบบเดียวกันนี้ใช้กับตัวแปลงรหัสทั้งหมด รวมถึงโค้ดเบสอื่นๆ ที่คล้ายกัน
ก่อนอื่นมาแก้ไขปัญหาการใช้หลังจากการปลดปล่อยตั้งแต่ต้นโพสต์กัน ด้วยเหตุนี้ เราจึงต้องโคลนข้อมูลจากมุมมองที่รองรับ WebAssembly ก่อนทำเครื่องหมายเป็น "ฟรี" ฝั่ง JavaScript โดยทำดังนี้
// …
const result = /* … */;
const imgData = new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
module.free_result();
result.delete();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
return imgData;
}
ตอนนี้มาตรวจสอบว่าเราไม่แชร์สถานะใดๆ ในตัวแปรส่วนกลางระหว่างการเรียกใช้ ซึ่งจะช่วยแก้ไขปัญหาบางอย่างที่เราพบแล้ว และทำให้การใช้ตัวแปลงรหัสในสภาพแวดล้อมแบบหลายเธรดในอนาคตง่ายขึ้น
ในการดำเนินการดังกล่าว เราจึงปรับโครงสร้างของ C++ wrapper เพื่อให้แน่ใจว่าการเรียกใช้ฟังก์ชันแต่ละครั้งจะจัดการข้อมูลของตนเองโดยใช้ตัวแปรภายใน จากนั้นเราจะเปลี่ยนลายเซ็นของฟังก์ชัน free_result
ให้รับพอยน์เตอร์กลับได้ดังนี้
liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;
RawImage quantize(std::string rawimage,
int image_width,
int image_height,
int num_colors,
float dithering) {
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
int size = image_width * image_height;
attr = liq_attr_create();
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_attr* attr = liq_attr_create();
liq_image* image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_set_max_colors(attr, num_colors);
liq_result* res = nullptr;
liq_image_quantize(image, attr, &res);
liq_set_dithering_level(res, dithering);
uint8_t* image8bit = (uint8_t*)malloc(size);
result = (uint8_t*)malloc(size * 4);
uint8_t* result = (uint8_t*)malloc(size * 4);
// …
}
void free_result() {
void free_result(uint8_t *result) {
free(result);
}
แต่เนื่องจากเราใช้ Embind ใน Emscripten เพื่อโต้ตอบกับ JavaScript อยู่แล้ว เราจึงอาจทำให้ API ปลอดภัยยิ่งขึ้นด้วยการซ่อนรายละเอียดการจัดการหน่วยความจำ C++ ทั้งหมด
ด้วยเหตุนี้ เรามาย้ายส่วน new Uint8ClampedArray(…)
จาก JavaScript ไปไว้ฝั่ง C++ ด้วย Embind จากนั้นเราจะใช้เพื่อโคลนข้อมูลลงในหน่วยความจำ JavaScript ได้ก่อนที่ระบบจะแสดงผลจากฟังก์ชัน
class RawImage {
public:
val buffer;
int width;
int height;
RawImage(val b, int w, int h) : buffer(b), width(w), height(h) {}
};
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
RawImage quantize(/* … */) {
val quantize(/* … */) {
// …
return {
val(typed_memory_view(image_width * image_height * 4, result)),
image_width,
image_height
};
val js_result = Uint8ClampedArray.new_(typed_memory_view(
image_width * image_height * 4,
result
));
free(result);
return js_result;
}
โปรดสังเกตว่าการเปลี่ยนแปลงเพียงครั้งเดียวนี้ช่วยให้เรามั่นใจได้ว่า JavaScript จะเป็นเจ้าของอาร์เรย์ไบต์ที่ได้ และไม่ได้สำรองข้อมูลโดยหน่วยความจำ WebAssembly และกำจัด Wrapper RawImage
ที่รั่วไหลก่อนหน้านี้ด้วย
ตอนนี้ JavaScript ไม่ต้องกังวลเกี่ยวกับการเพิ่มพื้นที่ว่างสำหรับข้อมูลอีกต่อไป และสามารถใช้ผลลัพธ์ได้เช่นเดียวกับออบเจ็กต์อื่นๆ ที่ระบบเก็บขยะ
// …
const result = /* … */;
const imgData = new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
module.free_result();
result.delete();
// module.doLeakCheck();
return imgData;
return new ImageData(result, result.width, result.height);
}
ซึ่งหมายความว่าเราไม่จำเป็นต้องมี free_result
Binding ที่กําหนดเองอีกต่อไปในฝั่ง C++
void free_result(uint8_t* result) {
free(result);
}
EMSCRIPTEN_BINDINGS(my_module) {
class_<RawImage>("RawImage")
.property("buffer", &RawImage::buffer)
.property("width", &RawImage::width)
.property("height", &RawImage::height);
function("quantize", &quantize);
function("zx_quantize", &zx_quantize);
function("version", &version);
function("free_result", &free_result, allow_raw_pointers());
}
โดยรวมแล้ว โค้ด Wrapper ของเราทั้งสะอาดขึ้นและปลอดภัยขึ้นในเวลาเดียวกัน
หลังจากนี้ เราได้ทำการปรับปรุงโค้ดของ Wrapper ของ ImageQuant เพิ่มเติมเล็กน้อยและจำลองการแก้ไขการจัดการหน่วยความจำที่คล้ายกันสำหรับโปรแกรมเปลี่ยนไฟล์อื่นๆ หากสนใจรายละเอียดเพิ่มเติม โปรดดู PR ที่ได้ในส่วนการแก้ไขหน่วยความจําสําหรับตัวแปลงรหัส C++
สรุปประเด็นสำคัญ
เราเรียนรู้และแชร์บทเรียนอะไรได้บ้างจากการปรับโครงสร้างนี้ซึ่งนำไปใช้กับโค้ดเบสอื่นๆ ได้
- อย่าใช้มุมมองหน่วยความจําที่ WebAssembly รองรับ ไม่ว่าจะสร้างจากภาษาใดก็ตาม เกินกว่าการเรียกใช้ครั้งเดียว คุณไม่สามารถคาดหวังให้ข้อมูลเหล่านี้อยู่ได้นานกว่านั้น และคุณจะจับข้อบกพร่องเหล่านี้ด้วยวิธีเดิมๆ ไม่ได้ ดังนั้นหากต้องการจัดเก็บข้อมูลไว้ใช้ภายหลัง ให้คัดลอกข้อมูลดังกล่าวไปไว้ฝั่ง JavaScript แล้วจัดเก็บไว้ที่นั่น
- หากเป็นไปได้ ให้ใช้ภาษาการจัดการหน่วยความจำที่ปลอดภัย หรืออย่างน้อยก็ใช้ตัวห่อประเภทที่ปลอดภัยแทนการดำเนินการกับพอยน์เตอร์ดิบโดยตรง วิธีนี้จะไม่ช่วยคุณกำจัดข้อบกพร่องในขอบเขต JavaScript ↔ WebAssembly แต่อย่างน้อยก็ช่วยลดโอกาสที่จะเกิดข้อบกพร่องที่เกิดจากโค้ดภาษาแบบคงที่
- ไม่ว่าจะใช้ภาษาใดก็ตาม ให้เรียกใช้โค้ดด้วยโปรแกรมตรวจสอบระหว่างการพัฒนา ซึ่งจะช่วยตรวจจับปัญหาได้ไม่เพียงในโค้ดภาษาแบบคงที่เท่านั้น แต่ยังช่วยตรวจจับปัญหาบางอย่างในขอบเขต JavaScript ↔ WebAssembly ด้วย เช่น การลืมเรียก
.delete()
หรือการส่งผ่านพอยน์เตอร์ที่ไม่ถูกต้องจากฝั่ง JavaScript - หากเป็นไปได้ ให้หลีกเลี่ยงการแสดงข้อมูลและออบเจ็กต์ที่ไม่ได้รับการจัดการจาก WebAssembly ไปยัง JavaScript ทั้งหมด JavaScript เป็นภาษาที่มีการรวบรวมขยะ และการจัดการหน่วยความจําด้วยตนเองนั้นไม่พบบ่อยนัก กรณีนี้อาจถือเป็นการรั่วไหลของการแยกแยะรูปแบบหน่วยความจำของภาษาที่ใช้สร้าง WebAssembly และการจัดการที่ไม่ถูกต้องนั้นมองข้ามได้ง่ายในโค้ดฐาน JavaScript
- เรื่องนี้อาจดูชัดเจน แต่เช่นเดียวกับโค้ดเบสอื่นๆ โปรดหลีกเลี่ยงการจัดเก็บสถานะที่เปลี่ยนแปลงได้ในตัวแปรส่วนกลาง คุณไม่ต้องการแก้ไขข้อบกพร่องเกี่ยวกับการใช้ซ้ำในคำเรียกใช้ต่างๆ หรือแม้แต่ในเธรดต่างๆ ดังนั้นคุณควรทำให้ฟังก์ชันนี้ทำงานได้ด้วยตัวเองมากที่สุด