تصحيح أخطاء تسرب الذاكرة في WebAssembly باستخدام Emscripten

وعلى الرغم من أنّ لغة JavaScript تتغاضى عن التنظيف بعد تنفيذها، لا ينطبق ذلك بالتأكيد على اللغات الثابتة...

إنّ Squoosh.app هو تطبيق ويب تقدّمي يوضّح مدى تأثير برامج ترميز وإعدادات الصور المختلفة في تحسين حجم ملف الصورة بدون التأثير بشكل كبير في جودة الجودة. ومع ذلك، فهو أيضًا عرض توضيحي تقني يعرض كيفية أخذ المكتبات المكتوبة بلغة C++ أو Rust وجلبها إلى الويب.

إنّ إمكانية نقل الرموز البرمجية من النظم البيئية الحالية أمر في غاية الأهمية، ولكن هناك بعض الاختلافات الرئيسية بين تلك اللغات الثابتة وJavaScript. أحدها في أساليبهم المختلفة لإدارة الذاكرة.

وعلى الرغم من أنّ لغة JavaScript لا يمكنها التغاضي عن التنظيف بعد تنفيذها، لا ينطبق ذلك بالتأكيد على هذه اللغات الثابتة. عليك طلب ذكرى مخصّصة جديدة صراحةً والحرص على إعادتها لاحقًا وعدم استخدامها مرة أخرى. إذا لم يحدث ذلك، فستحصل على تسريبات ... وقد يحدث ذلك بالفعل بانتظام إلى حد ما. لنلقِ نظرة على كيفية تصحيح الأخطاء التي تسرب في الذاكرة، والأفضل من ذلك، كيف يمكنك تصميم التعليمات البرمجية لتجنب حدوثها في المرة القادمة.

نمط مريب

مؤخرًا، بينما بدأتُ العمل على Squoosh، لم أستطع مساعدتك ولكن لاحظت نمطًا مثيرًا للاهتمام في برامج تضمين برنامج ترميز C++. لنلقِ نظرة على برنامج تضمين ImageQuant كمثال (يتم تقليله إلى عرض أجزاء إنشاء الكائن وDeallocation فقط):

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 عدم ملء الذاكرة التي تم إخلاء جزء منها على الفور. ويُرجى العِلم أنّ ميزة free التي تستخدمها Emscripten لا تتيح ذلك، لكننا نعتمد على تفاصيل التنفيذ هنا، وهي تفاصيل لا يمكن ضمانها.

أو، حتى إذا تم الاحتفاظ بالذاكرة وراء المؤشر، فقد يلزم تخصيص جديد لزيادة ذاكرة WebAssembly. في حال تطوير WebAssembly.Memory إما من خلال واجهة برمجة تطبيقات JavaScript أو من خلال تعليمات 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 في مؤتمر 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 أكثر ملاءمة لحالات استخدام المكتبة مثل حالاتنا، عندما تريد طباعة التسريبات على وحدة التحكّم، مع إبقاء التطبيق قيد التشغيل بغض النظر عن ذلك.

لنعرض المساعد الثاني ذلك عبر 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; من الدالة اطّلِع على استخدام اطّلِع على عامب في نوع RawImage ::toWireType

لا تزال بعض أجزاء تتبُّع تسلسل استدعاء الدوال البرمجية تبدو غامضة لأنّها تشير إلى عناصر Emscripten الداخلية، ولكن يمكننا معرفة أنّ التسريب صادر من تحويل RawImage إلى "Wire type" (إلى قيمة 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 الحالية عندما تكون بقيمة غير صفرية:

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) {
  // …
}

ماذا لو تم إعداد بعضها بطريقة كسول عند التشغيل الأول، ثم تمت إعادة استخدامها بشكل غير صحيح في عمليات التشغيل المستقبلية؟ لن يؤدي إجراء مكالمة واحدة مع مطهِّر إلى الإبلاغ عن ذلك باعتبارها مشكلة.

لنحاول معالجة الصورة عدة مرات من خلال النقر العشوائي على مستويات جودة مختلفة في واجهة المستخدم. في الواقع، نحصل الآن على التقرير التالي:

لقطة شاشة لرسالة

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، حيث لا يظهر للمعقّم. لن يتم الكشف عن هذه المشكلات إلا في مرحلة الإنتاج أو بعد إجراء تغييرات تبدو غير ذات صلة على الكود في المستقبل.

إنشاء برنامج تضمين آمن

لنعد إلى الوراء بضع خطوات، وبدلاً من ذلك نحل كل هذه المشكلات من خلال إعادة هيكلة التعليمات البرمجية بطريقة أكثر أمانًا. وسأستخدم برنامج تضمين 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++ للتأكد من أن كل استدعاء للدالة يدير بياناته الخاصة باستخدام المتغيرات المحلية. بعد ذلك، يمكننا تغيير توقيع دالة 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، يمكننا أيضًا جعل واجهة برمجة التطبيقات أكثر أمانًا عن طريق إخفاء تفاصيل إدارة ذاكرة 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، و نتخلص أيضًا من برنامج تضمين 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());
}

وبوجه عام، أصبح رمز برنامج تضمين البرنامج أكثر وضوحًا وأمانًا في الوقت نفسه.

بعد ذلك، أجريت بعض التحسينات الطفيفة الإضافية على التعليمات البرمجية لبرنامج ImageQuant وأجريت إصلاحات مماثلة لإدارة الذاكرة لبرامج الترميز الأخرى. إذا كنت مهتمًا بمعرفة المزيد من التفاصيل، يمكنك الاطّلاع على العلاقات العامة الناتجة هنا: إصلاحات الذاكرة لبرامج ترميز C++.

الخلاصات

ما الدروس التي يمكننا تعلمها ومشاركتها من عملية إعادة الهيكلة هذه والتي يمكن تطبيقها على قواعد التعليمات البرمجية الأخرى؟

  • لا تستخدم طرق عرض الذاكرة التي تدعمها WebAssembly - بغض النظر عن اللغة التي تم إنشاؤها منها - بخلاف استدعاء واحد. لن تتمكّن من الاعتماد على هذه الأخطاء للبقاء على قيد الحياة لمدة أطول من ذلك، ولن تتمكن من اكتشاف هذه الأخطاء باستخدام الطرق التقليدية، لذا إذا كنت بحاجة إلى تخزين البيانات لوقت لاحق، انسخها إلى جانب JavaScript واحفظها هناك.
  • استخدِم لغة آمنة لإدارة الذاكرة أو على الأقل أنواع برامج تضمين آمنة بدلاً من العمل على المؤشرات الأولية مباشرةً. لن يوفر لك هذا من الأخطاء الموجودة على حدود JavaScript ⬅ WebAssembly ، ولكنه على الأقل سيقلل من سطح الأخطاء بسبب كود اللغة الثابت.
  • وبصرف النظر عن اللغة التي تستخدمها، شغِّل الرموز باستخدام المعقّمات أثناء تطوير الرمز، فهي لا تساعدك في معالجة المشاكل في رمز اللغة الثابت فحسب، بل يمكنها أيضًا معالجة بعض المشاكل المرتبطة بحدود WebAssembly، مثل نسيان استدعاء .delete() أو تمرير مؤشرات غير صالحة من جانب JavaScript.
  • قدر الإمكان، تجنَّب كشف البيانات والكائنات غير المُدارة من WebAssembly إلى JavaScript تمامًا. لغة JavaScript هي لغة يتم جمعها من البيانات غير الضرورية، والإدارة اليدوية للذاكرة غير شائعة فيها. يمكن اعتبار ذلك تسرُّبًا مجرّدًا لنموذج الذاكرة الخاص باللغة التي تم إنشاء WebAssembly منها، ومن السهل التغاضي عن الإدارة غير الصحيحة في قاعدة رموز JavaScript.
  • قد يكون ذلك واضحًا، ولكن كما هو الحال في أي قاعدة رموز برمجية أخرى، تجنَّب تخزين الحالة القابلة للتغيير في المتغيرات العمومية. لست بحاجة إلى تصحيح الأخطاء المرتبطة بإعادة استخدام التطبيق عبر مختلف الاستدعاءات أو حتى سلاسل المحادثات، لذا من الأفضل الاحتفاظ به كخدمة مستقلة قدر الإمكان.