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, WebAssembly (Wasm) मेमोरी बफ़र का इस्तेमाल करके JavaScript Uint8Array दिखाता है. इसमें byteOffset और byteLength दिए गए पॉइंटर और लंबाई पर सेट होते हैं. यहां खास तौर पर यह कहा जाता है कि यह WebAssembly मेमोरी बफ़र में टाइप किया गया व्यू है, न कि JavaScript के मालिकाना हक वाले डेटा की कॉपी में.

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

इसके अलावा, 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 सैनिटाइज़र टूल को आज़माने का एक अच्छा मौका है, जिसे पिछले साल जोड़ा गया था और 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 लाइब्रेरी के इस्तेमाल के उदाहरणों के लिए ज़्यादा सही है. ऐसा तब होता है, जब आपको कंसोल पर लीक प्रिंट करके, ऐप्लिकेशन को बिना किसी दिक्कत के प्रिंट करना हो.

चलिए, उस दूसरे हेल्पर को 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

यह काफ़ी बेहतर लग रहा है:

जेनेरिकBindingType असल इमेज ::toWireType फ़ंक्शन से आ रही &#39;डायरेक्ट लीक 12 बाइट&#39; को पढ़ने वाले मैसेज का स्क्रीनशॉट

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

हम आपको याद दिलाना चाहते हैं कि फ़िलहाल 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
  );
}

डेटा लीक होने की समस्या ठीक हो जाती है.

सैनिटाइज़र से जुड़ी अन्य समस्याओं का पता लगाएं

सैनिटाइज़र वाले अन्य स्क्वॉश कोडेक बनाने से, दोनों के साथ-साथ कुछ नई समस्याओं का पता चलता है. उदाहरण के लिए, मुझे MozJPEG बाइंडिंग में यह गड़बड़ी मिली है:

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

यहाँ, यह कोई आम बात नहीं है, लेकिन हम किसी तय की गई सीमा से बाहर की किसी याद को लिख रहे हैं {9}

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

सबसे पहले, आइए पोस्ट की शुरुआत से 'इस्तेमाल के बाद' वाली समस्या को ठीक करें. इसके लिए हमें 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);
}

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