การแก้ไขข้อบกพร่องหน่วยความจำรั่วไหลใน 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
  );
}

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