การแก้ไขข้อบกพร่องหน่วยความจำรั่วไหลใน WebAssembly โดยใช้ Emscripten

ถึงแม้ 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

ดูดีขึ้นมากเลย

ภาพหน้าจอของข้อความที่เขียนว่า &quot;การรั่วไหลโดยตรงขนาด 12 ไบต์&quot; มาจากฟังก์ชัน genericBindingType RawImage ::toWireType

สแต็กเทรซบางส่วนจะยังดูไม่ชัดเจนเนื่องจากชี้ไปที่ภายในของ 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
  • ขั้นตอนนี้อาจเห็นได้ชัดเจน แต่เช่นเดียวกับในโค้ดเบสอื่นๆ ให้หลีกเลี่ยงการจัดเก็บสถานะที่เปลี่ยนแปลงได้ในส่วนกลาง ตัวแปร คุณไม่ต้องการแก้ปัญหาเกี่ยวกับการใช้งานซ้ำในการเรียกใช้ต่างๆ หรือแม้แต่ ชุดข้อความ ดังนั้นทางที่ดีที่สุดจึงควรทำให้ข้อมูลครบถ้วนสมบูรณ์ในตัวเองมากที่สุด