ในขณะที่ 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
);
}
คุณพบปัญหาไหม คำแนะนำ: คุณสามารถ ใช้หลังใช้ฟรีได้ แต่อยู่ใน 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
ที่มีอยู่ใช้งานไม่ได้อีกต่อไป และในทางกลับกัน ยอดดูใดๆ ก็ตามที่เนื้อหานั้นหนุนหลัง
ฉันใช้คอนโซลเครื่องมือสำหรับนักพัฒนาเว็บ (หรือ 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 ใหม่ที่เพิ่มเข้ามาเมื่อปีที่แล้วและนำเสนอในการพูดคุยกับ 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
จะเหมาะสำหรับกรณีการใช้งานของไลบรารีอย่างของเรามากกว่า เมื่อคุณต้องการสั่งพิมพ์การรั่วไหลไปยังคอนโซล แต่ให้แอปพลิเคชันทำงานอยู่เสมอ
เราจะเปิดเผยผู้ช่วยคนที่ 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 แต่เราบอกได้ว่าการรั่วไหลมาจาก Conversion RawImage
เป็น "ประเภทสาย" (เป็นค่า JavaScript) โดย Embind อันที่จริง เมื่อเราดูโค้ด จะเห็นว่าเราส่งคืนอินสแตนซ์ RawImage
C++ ไปยัง JavaScript ได้ แต่เราไม่เคยทำให้อินสแตนซ์เหล่านั้นว่างอยู่ทั้ง 2 ฝั่ง
โปรดทราบว่าขณะนี้ยังไม่มีการผสานรวมคอลเล็กชันขยะระหว่าง 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);
การเริ่มต้นตัวแปรทั้งสองเป็นศูนย์ก่อนการเรียกใช้จะแก้ไขปัญหานี้ และตอนนี้โค้ดจะถึงการตรวจสอบการรั่วไหลของหน่วยความจำแทน โชคดีที่การตรวจสอบผ่านเรียบร้อยแล้ว ซึ่งบ่งบอกว่าเราไม่รอยรั่วของตัวแปลงรหัสนี้
ปัญหาเกี่ยวกับสถานะที่แชร์
...หรือเรา
เราทราบว่าการเชื่อมโยงตัวแปลงรหัสของเราจัดเก็บสถานะบางส่วนไว้ รวมถึงส่งผลให้เกิดตัวแปรคงที่ทั่วโลก และ 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);
}
ผมสามารถตามล่าข้อบกพร่องด้านหน่วยความจําเหล่านั้นได้ทีละอย่าง แต่ผมคิดว่าตอนนี้เราเห็นชัดเจนแล้วว่าวิธีการจัดการหน่วยความจำในปัจจุบันนำไปสู่ปัญหาระบบที่น่ารังเกียจบางอย่าง
บางชนิดอาจโดนเจลล้างมือในทันที และบางทีก็ต้องใช้กลอุบายที่ซับซ้อนเพื่อดักจับ สุดท้าย ปัญหาบางอย่างในตอนต้นของโพสต์ คือเราเห็นได้จากบันทึกว่า ตัวฆ่าเชื้อโรคไม่ติดเลย เหตุผลก็คือการใช้งานในทางที่ผิดนั้นเกิดขึ้นในฝั่ง JavaScript ซึ่งตัวล้างทำความสะอาดมองไม่เห็น ปัญหาเหล่านั้นจะเปิดเผยตัวเองในเวอร์ชันที่ใช้งานจริงเท่านั้น หรือหลังจากมีการเปลี่ยนแปลงที่ดูเหมือนว่าจะไม่เกี่ยวข้องกับโค้ดในอนาคต
การสร้าง Wrapper ที่ปลอดภัย
เราลองย้อนกลับไป 2-3 ขั้นตอนแล้วแก้ปัญหาทั้งหมดเหล่านี้แทนโดยการปรับโครงสร้างโค้ดให้ปลอดภัยยิ่งขึ้น ผมจะใช้ Wrapper ของ ImageQuant เป็นตัวอย่างอีกครั้ง แต่กฎรีแฟคเตอร์ที่คล้ายกันจะมีผลกับตัวแปลงรหัสทั้งหมด รวมทั้งฐานของโค้ดอื่นๆ ที่คล้ายคลึงกันด้วย
ก่อนอื่น มาแก้ไขปัญหาการใช้งานหลังใช้ฟรีตั้งแต่ช่วงต้นของโพสต์กัน ด้วยเหตุนี้ เราจำเป็นต้องโคลนข้อมูลจากมุมมองที่สนับสนุน 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;
}
คราวนี้เรามาตรวจสอบกันว่าเราไม่ได้แชร์สถานะใดๆ ในตัวแปรร่วมระหว่างการเรียกใช้ วิธีนี้ช่วยแก้ไขปัญหาบางส่วนที่เราได้พบไปแล้ว รวมทั้งช่วยให้ใช้ตัวแปลงรหัสในสภาพแวดล้อมแบบมัลติเทรดได้ง่ายขึ้นในอนาคต
ในการทำเช่นนั้น เราจะเปลี่ยนโครงสร้างของ Wrapper C++ เพื่อให้แน่ใจว่าการเรียกฟังก์ชันแต่ละครั้งจัดการข้อมูลของตัวเองโดยใช้ตัวแปรในเครื่อง จากนั้น เราจะเปลี่ยนลายเซ็นของฟังก์ชัน 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;
}
โปรดทราบว่าเมื่อมีการเปลี่ยนครั้งเดียว เราทั้ง 2 คนจะตรวจสอบว่าอาร์เรย์ไบต์ที่ได้เป็นของ JavaScript และไม่ได้รับการสนับสนุนโดยหน่วยความจำ WebAssembly และกำจัด RawImage
Wrapper ที่รั่วไหลก่อนหน้านี้ด้วย
ตอนนี้ 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
ที่กําหนดเองในฝั่ง 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 แล้วเก็บข้อมูลไว้
- หากเป็นไปได้ ให้ใช้ภาษาการจัดการหน่วยความจำที่ปลอดภัย หรืออย่างน้อยใช้ Wrapper ประเภทที่ปลอดภัยแทนการดำเนินการบนตัวชี้แบบข้อมูลดิบโดยตรง วิธีนี้ไม่ได้ช่วยป้องกันไม่ให้คุณจากข้อบกพร่องในขอบเขต JavaScript where WebAssembly แต่อย่างน้อยก็จะช่วยลดพื้นที่ของข้อบกพร่องจากโค้ดภาษาแบบคงที่ได้
- ไม่ว่าจะใช้ภาษาใด ให้เรียกใช้โค้ดที่มีตัวทำความสะอาดในระหว่างการพัฒนา เพราะไม่เพียงช่วยแก้ปัญหาโค้ดภาษาแบบคงที่เท่านั้น แต่ยังช่วยแก้ปัญหาบางอย่างในขอบเขต JavaScript \">
WebAssembly เช่น การลืมเรียกใช้
.delete()
หรือส่งตัวชี้ที่ไม่ถูกต้องจากฝั่ง JavaScript ด้วย - หากเป็นไปได้ ให้หลีกเลี่ยงการเปิดเผยข้อมูลและออบเจ็กต์ที่ไม่มีการจัดการจาก WebAssembly ไปจนถึง JavaScript ทั้งหมด JavaScript เป็นภาษาที่รวบรวมแบบขยะ และการจัดการหน่วยความจำด้วยตนเองนั้นไม่เป็นที่นิยมในกลุ่มภาษานี้ กรณีนี้อาจถือเป็นการรั่วไหลนามธรรมของโมเดลหน่วยความจำของภาษาที่ WebAssembly สร้างขึ้น และคุณก็มองข้ามการจัดการที่ไม่ถูกต้องในฐานของโค้ด JavaScript ได้
- กรณีนี้อาจชัดเจนอยู่แล้ว แต่ให้หลีกเลี่ยงการจัดเก็บสถานะที่เปลี่ยนแปลงได้ในตัวแปรร่วม เช่นเดียวกับในฐานของโค้ดอื่นๆ คุณไม่ควรแก้ปัญหาเกี่ยวกับการนำไปใช้ซ้ำในการเรียกใช้หรือชุดข้อความต่างๆ ดังนั้นวิธีที่ดีที่สุดคือทำให้ทุกอย่างมีในตัวเองมากที่สุด