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

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

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

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

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

نمط مريب

مؤخرًا، أثناء بدء العمل على تطبيق Squoosh، لم أتمكّن من مساعدتك إلا أنّني لاحظت نمطًا مثيرًا للاهتمام في برامج تضمين برنامج ترميز C++. لنلقِ نظرة على حزمة 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 Uint8Array JavaScript مدعومًا بوحدة تخزين ذاكرة WebAssembly (Wasm)، مع ضبط byteOffset وbyteLength على المؤشر والطول المحدَّدَين. النقطة الأساسية هي أنّ هذا عرض TypedArray في ذاكرة تخزين مؤقتة في WebAssembly، وليس نسخة مملوكة من JavaScript للبيانات.

عند استدعاء free_result من JavaScript، فإنّه يستدعي بدوره دالة C عادية free لوضع علامة على هذه الذكرى على أنّها متاحة لأي عمليات تخصيص مستقبلية، ما يعني أنّه يمكن استبدال البيانات التي تشير إليها طريقة عرض Uint8Array ببيانات عشوائية عن طريق أي طلب مستقبلي في Wasm.

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

في هذه الحالة، سنهتم بأداة 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; واردة من دالة GenericBindingType RawImage ::toWireType

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

يؤدي ضبط كلا المتغيّرين على القيمة 0 قبل الاستدعاء إلى حلّ هذه المشكلة، ويصل الرمز البرمجي الآن إلى فحص تسرُّب الذاكرة بدلاً من ذلك. لحسن الحظ، نجح الشيك، مشيرًا إلى أنه ليس لدينا أي تسرب في برنامج الترميز هذا.

مشاكل في الحالة المشتركة

…أم لا؟

نعلم أنّ عمليات ربط برامج الترميز تخزِّن بعض البيانات بالإضافة إلى النتائج في متغيّرات static عموميّة، ويحتوي 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 wrapper و كرّرت إصلاحات إدارة الذاكرة المشابهة لبرامج الترميز الأخرى. إذا كنت مهتمًا بالاطّلاع على مزيد من التفاصيل، يمكنك الاطّلاع على طلب التغيير الناتج هنا: إصلاحات في الذاكرة لبرامج الترميز باستخدام C++.

الخلاصات

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

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