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

क्या आपको कोई समस्या दिख रही है? संकेत: यह इस्तेमाल के बाद मुफ़्त है, लेकिन 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 को साफ़ तौर पर फिर से कॉल न करें, लेकिन हम किसी समय अपने कोडेक में मल्टीथ्रेडिंग की सुविधा जोड़ सकते हैं. ऐसे में, यह एक बिलकुल अलग थ्रेड हो सकता है जो डेटा को क्लोन करने से ठीक पहले ओवरराइट करता है.

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

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