Emscripten का इस्तेमाल करके, WebAssembly में मेमोरी लीक होने की जानकारी को डीबग करना

JavaScript, अपने बाद की गड़बड़ियों को ठीक करने में काफ़ी मददगार है, लेकिन स्टैटिक भाषाएं ऐसा नहीं करतीं…

Squoosh.app एक PWA है. इससे पता चलता है कि अलग-अलग इमेज कोडेक और सेटिंग, इमेज फ़ाइल के साइज़ को बेहतर बना सकती हैं. ऐसा, इमेज की क्वालिटी पर काफ़ी असर डाले बिना किया जा सकता है. हालांकि, यह एक तकनीकी डेमो भी है. इसमें यह दिखाया गया है कि 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
  );
}

क्या आपको कोई समस्या दिख रही है? अहम जानकारी: यह use-after-free है, लेकिन JavaScript में!

Emscripten में, typed_memory_view एक ऐसा JavaScript Uint8Array दिखाता है जिसे WebAssembly (Wasm) के मेमोरी बफ़र की मदद से चलाया जाता है. इसमें byteOffset और byteLength, दिए गए पॉइंटर और लंबाई पर सेट होते हैं. मुख्य बात यह है कि यह WebAssembly मेमोरी बफ़र में TypedArray व्यू है, न कि डेटा की JavaScript की कॉपी.

जब JavaScript से free_result को कॉल किया जाता है, तो यह स्टैंडर्ड C फ़ंक्शन free को कॉल करता है, ताकि इस मेमोरी को आने वाले समय में किसी भी ऐलोकेशन के लिए उपलब्ध के तौर पर मार्क किया जा सके. इसका मतलब है कि आने वाले समय में Wasm में किसी भी कॉल से, हमारे Uint8Array व्यू पर मौजूद डेटा को किसी भी डेटा से बदला जा सकता है.

इसके अलावा, free को लागू करने के कुछ तरीकों में, खाली हो चुकी मेमोरी को तुरंत शून्य से भरने का विकल्प भी चुना जा सकता है. Emscripten का इस्तेमाल करने वाला free ऐसा नहीं करता. हालांकि, हम यहां लागू करने की ऐसी जानकारी पर भरोसा कर रहे हैं जिसकी गारंटी नहीं दी जा सकती.

इसके अलावा, अगर पॉइंटर के पीछे मौजूद मेमोरी को सुरक्षित रखा जाता है, तो हो सकता है कि नए ऐलोकेशन के लिए, WebAssembly मेमोरी को बढ़ाना पड़े. जब WebAssembly.Memory को JavaScript API या उससे जुड़े memory.grow निर्देश की मदद से बड़ा किया जाता है, तो यह मौजूदा ArrayBuffer को अमान्य कर देता है. साथ ही, इसकी मदद से बनाए गए सभी व्यू को भी अमान्य कर देता है.

इस व्यवहार को दिखाने के लिए, मैं DevTools (या 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

आखिर में, भले ही हम free_result और new Uint8ClampedArray के बीच साफ़ तौर पर Wasm को फिर से कॉल न करें, लेकिन हम किसी समय अपने कोडेक में मल्टीथ्रेडिंग की सुविधा जोड़ सकते हैं. ऐसे में, यह एक पूरी तरह से अलग थ्रेड हो सकती है, जो डेटा को क्लोन करने से ठीक पहले उसे ओवरराइट कर देती है.

मेमोरी से जुड़ी गड़बड़ियां ढूंढना

हमने इस कोड की जांच की है और यह पता लगाया है कि क्या इस कोड में कोई समस्या है. यह Emscripten sanitizers के लिए नए(कुछ हद तक) सहायता को आज़माने का सही मौका है. इसे पिछले साल जोड़ा गया था और Chrome Dev Summit में WebAssembly के बारे में हमारे टॉक में इसे पेश किया गया था:

इस मामले में, हम 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 का इस्तेमाल, लाइब्रेरी के इस्तेमाल के उदाहरणों के लिए किया जाता है. जैसे, जब आपको Console में लीक को प्रिंट करना हो, लेकिन ऐप्लिकेशन को चालू रखना हो.

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 साइड से लागू करें. C++ के बजाय, JavaScript से ऐसा करने से यह पक्का करने में मदद मिलती है कि सभी स्कोप से बाहर निकला जा चुका है और जांच करने के समय सभी अस्थायी 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

यह काफ़ी बेहतर दिखता है:

GenericBindingType RawImage ::toWireType फ़ंक्शन से आने वाले &#39;12 बाइट का सीधा रिसाव&#39; मैसेज का स्क्रीनशॉट

स्टैक ट्रेस के कुछ हिस्से अब भी अस्पष्ट दिखते हैं, क्योंकि वे Emscripten के इंटरनल पर ले जाते हैं. हालांकि, हम यह बता सकते हैं कि लीक, Embind की मदद से RawImage कन्वर्ज़न से "वायर टाइप" (JavaScript वैल्यू) में हो रही है. असल में, कोड को देखने पर पता चलता है कि हम JavaScript को RawImage C++ इंस्टेंस दिखाते हैं, लेकिन हम उन्हें किसी भी तरफ़ से कभी रिलीज़ नहीं करते.

हम आपको याद दिलाना चाहते हैं कि फ़िलहाल, JavaScript और WebAssembly के बीच कोई गै़रबेज कलेक्शन इंटिग्रेशन नहीं है. हालांकि, इसे डेवलप किया जा रहा है. इसके बजाय, ऑब्जेक्ट का इस्तेमाल करने के बाद, आपको मैन्युअल तरीके से मेमोरी खाली करनी होगी और JavaScript से डिस्ट्रॉयर को कॉल करना होगा. खास तौर पर Embind के लिए, आधिकारिक दस्तावेज़ों में, एक्सपोज़ की गई C++ क्लास पर .delete() तरीके को कॉल करने का सुझाव दिया गया है:

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

अगर इनमें से कुछ को पहले रन में आलसी तरीके से शुरू किया जाता है और फिर आने वाले समय में इनका गलत तरीके से फिर से इस्तेमाल किया जाता है, तो क्या होगा? ऐसे में, सैनिटाइज़र का इस्तेमाल करने वाले एक कॉल को समस्या के तौर पर रिपोर्ट नहीं किया जाएगा.

आइए, यूज़र इंटरफ़ेस (यूआई) में अलग-अलग क्वालिटी लेवल पर क्लिक करके, इमेज को कुछ बार प्रोसेस करने की कोशिश करते हैं. असल में, अब हमें यह रिपोर्ट मिलती है:

मैसेज का स्क्रीनशॉट

2,62,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 रैपर का इस्तेमाल करूंगा. हालांकि, रीफ़ैक्टर करने के मिलते-जुलते नियम सभी कोडेक के साथ-साथ, मिलते-जुलते दूसरे कोडबेस पर भी लागू होते हैं.

सबसे पहले, पोस्ट की शुरुआत से ही मुफ़्त में इस्तेमाल करने के बाद पैसे चुकाने की समस्या को ठीक करते हैं. इसके लिए, हमें JavaScript साइड पर इसे मुफ़्त के तौर पर मार्क करने से पहले, WebAssembly के साथ काम करने वाले व्यू से डेटा को क्लोन करना होगा:

  // 

  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);
}

हालांकि, हम JavaScript के साथ इंटरैक्ट करने के लिए, Emscripten में Embind का इस्तेमाल पहले से ही कर रहे हैं. इसलिए, C++ मेमोरी मैनेजमेंट की जानकारी को पूरी तरह से छिपाकर, एपीआई को और भी सुरक्षित बनाया जा सकता है!

इसके लिए, Embind की मदद से new Uint8ClampedArray(…) वाले हिस्से को JavaScript से C++ में ले चलते हैं. इसके बाद, फ़ंक्शन से वापस आने से पहले ही, इसका इस्तेमाल 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);
}

इसका मतलब यह भी है कि अब हमें C++ साइड पर कस्टम free_result बाइंडिंग की ज़रूरत नहीं है:

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 के बीच के बॉर्डर पर मौजूद गड़बड़ियों से नहीं बचाया जा सकेगा. हालांकि, इससे स्टैटिक भाषा कोड में मौजूद गड़बड़ियों की संख्या कम हो जाएगी.
  • कोई भी भाषा इस्तेमाल करने पर, डेवलपमेंट के दौरान कोड को सैनिटाइज़र के साथ चलाएं. इससे, स्टैटिक भाषा के कोड में मौजूद समस्याओं के साथ-साथ, JavaScript ↔ WebAssembly के बीच की कुछ समस्याओं का पता चल सकता है. जैसे, .delete() को कॉल करना भूल जाना या JavaScript साइड से अमान्य पॉइंटर पास करना.
  • अगर हो सके, तो WebAssembly से JavaScript में मैनेज नहीं किए जा सकने वाले डेटा और ऑब्जेक्ट को एक्सपोज़ करने से बचें. JavaScript एक गै़रबेज कलेक्शन वाली भाषा है. इसमें मैन्युअल मेमोरी मैनेजमेंट आम तौर पर नहीं किया जाता. इसे उस भाषा के मेमोरी मॉडल के एब्स्ट्रैक्शन लीक के तौर पर देखा जा सकता है जिससे आपकी WebAssembly बनाई गई थी. साथ ही, JavaScript कोडबेस में गलत मैनेजमेंट को आसानी से अनदेखा किया जा सकता है.
  • यह बात साफ़ तौर पर पता चल सकती है कि किसी भी दूसरे कोडबेस की तरह, बदलाव किए जा सकने वाले स्टेटस को ग्लोबल वैरिएबल में सेव करने से बचें. आपको अलग-अलग कॉल या फिर धागों में, इसके फिर से इस्तेमाल से जुड़ी समस्याओं को डीबग नहीं करना है. इसलिए, इसे ज़्यादा से ज़्यादा अपने-आप काम करने वाला बनाएं.