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