Emscripten ব্যবহার করে WebAssembly-এ ডিবাগিং মেমরি লিক

যদিও জাভাস্ক্রিপ্ট নিজের পরে পরিষ্কার করার ক্ষেত্রে মোটামুটি ক্ষমাশীল, স্ট্যাটিক ভাষাগুলি অবশ্যই নয়…

Squoosh.app হল একটি PWA যা চিত্রের গুণমানকে উল্লেখযোগ্যভাবে প্রভাবিত না করে কতটা ভিন্ন ইমেজ কোডেক এবং সেটিংস ইমেজ ফাইলের আকার উন্নত করতে পারে তা ব্যাখ্যা করে। যাইহোক, এটি একটি প্রযুক্তিগত ডেমোও প্রদর্শন করে যে আপনি কীভাবে C++ বা রাস্টে লেখা লাইব্রেরিগুলিকে ওয়েবে আনতে পারেন।

বিদ্যমান ইকোসিস্টেম থেকে কোড পোর্ট করতে সক্ষম হওয়া অবিশ্বাস্যভাবে মূল্যবান, তবে সেই স্ট্যাটিক ভাষা এবং জাভাস্ক্রিপ্টের মধ্যে কিছু মূল পার্থক্য রয়েছে। তাদের মধ্যে একটি মেমরি ব্যবস্থাপনা তাদের বিভিন্ন পদ্ধতির মধ্যে.

যদিও জাভাস্ক্রিপ্ট নিজের পরে পরিষ্কার করার ক্ষেত্রে মোটামুটি ক্ষমাশীল, এই ধরনের স্ট্যাটিক ভাষা অবশ্যই নয়। আপনাকে স্পষ্টভাবে একটি নতুন বরাদ্দ করা মেমরির জন্য জিজ্ঞাসা করতে হবে এবং আপনাকে সত্যিই নিশ্চিত করতে হবে যে আপনি এটি পরে ফিরিয়ে দিয়েছেন এবং এটি আর কখনও ব্যবহার করবেন না। যদি তা না হয়, তাহলে আপনি ফাঁস পাবেন... এবং এটি আসলে মোটামুটি নিয়মিতভাবে ঘটে। আসুন দেখে নেওয়া যাক আপনি কীভাবে সেই মেমরি লিকগুলি ডিবাগ করতে পারেন এবং আরও ভাল, পরের বার সেগুলি এড়াতে আপনি কীভাবে আপনার কোড ডিজাইন করতে পারেন।

সন্দেহজনক প্যাটার্ন

সম্প্রতি, Squosh-এ কাজ শুরু করার সময়, আমি 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);
}

জাভাস্ক্রিপ্ট (ভাল, টাইপস্ক্রিপ্ট):

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

আপনি একটি সমস্যা স্পট? ইঙ্গিত: এটি ব্যবহার-পরে-মুক্ত , কিন্তু জাভাস্ক্রিপ্টে!

Emscripten-এ, typed_memory_view একটি JavaScript Uint8Array প্রদান করে যা WebAssembly (Wasm) মেমরি বাফার দ্বারা সমর্থিত, প্রদত্ত পয়েন্টার এবং দৈর্ঘ্যে byteOffset এবং byteLength সেট করে। মূল বিষয় হল এটি একটি WebAssembly মেমরি বাফারে একটি TypedArray ভিউ , ডাটার জাভাস্ক্রিপ্ট-মালিকানাধীন কপির পরিবর্তে।

যখন আমরা জাভাস্ক্রিপ্ট থেকে free_result কল করি, তখন এটি একটি স্ট্যান্ডার্ড C ফাংশনকে free কল করে এই মেমরিটিকে ভবিষ্যতের যেকোন বরাদ্দের জন্য উপলব্ধ হিসাবে চিহ্নিত করতে, যার অর্থ আমাদের Uint8Array ভিউ যে ডেটার দিকে নির্দেশ করে, ভবিষ্যতের যে কোনও কলের দ্বারা নির্বিচারে ডেটা দিয়ে ওভাররাইট করা যেতে পারে। Wasm মধ্যে

অথবা, free কিছু বাস্তবায়ন এমনকি মুক্ত করা মেমরিকে অবিলম্বে শূন্য-পূর্ণ করার সিদ্ধান্ত নিতে পারে। এমস্ক্রিপ্টেন যে 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-এ স্পষ্টভাবে কল না করি, কোনো সময়ে আমরা আমাদের কোডেকগুলিতে মাল্টিথ্রেডিং সমর্থন যোগ করতে পারি। সেই ক্ষেত্রে এটি একটি সম্পূর্ণ ভিন্ন থ্রেড হতে পারে যা আমরা এটিকে ক্লোন করতে পরিচালনা করার ঠিক আগে ডেটা ওভাররাইট করে।

মেমরি বাগ খুঁজছেন

ঠিক সেই ক্ষেত্রে, আমি আরও এগিয়ে যাওয়ার সিদ্ধান্ত নিয়েছি এবং এই কোডটি অনুশীলনে কোনও সমস্যা প্রদর্শন করে কিনা তা পরীক্ষা করে দেখব। এটি গত বছর যুক্ত করা নতুন (ইশ) এমস্ক্রিপ্টেন স্যানিটাইজার সমর্থন চেষ্টা করার একটি নিখুঁত সুযোগ বলে মনে হচ্ছে এবং ক্রোম ডেভ সামিটে আমাদের ওয়েব অ্যাসেম্বলি আলোচনায় উপস্থাপন করা হয়েছে:

এই ক্ষেত্রে, আমরা অ্যাড্রেস স্যানিটাইজারে আগ্রহী, যা বিভিন্ন পয়েন্টার- এবং মেমরি-সম্পর্কিত সমস্যা সনাক্ত করতে পারে। এটি ব্যবহার করার জন্য, আমাদের কোডেক পুনরায় কম্পাইল করতে হবে -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 স্বয়ংক্রিয়ভাবে যাচাই করতে পারে যে সমস্ত মেমরি মুক্ত করা হয়েছে।

পরিবর্তে, এই ধরনের ক্ষেত্রে লিকস্যানিটাইজার (অ্যাড্রেস স্যানিটাইজারে অন্তর্ভুক্ত) ফাংশনগুলি প্রদান করে __lsan_do_leak_check এবং __lsan_do_recoverable_leak_check , যেগুলি ম্যানুয়ালি আহ্বান করা যেতে পারে যখনই আমরা আশা করি সমস্ত মেমরি মুক্ত করা হবে এবং সেই অনুমানকে যাচাই করতে চাই। __lsan_do_leak_check একটি চলমান অ্যাপ্লিকেশনের শেষে ব্যবহার করা বোঝায়, যখন আপনি কোনো লিক সনাক্ত করা হলে প্রক্রিয়াটি বাতিল করতে চান, যখন __lsan_do_recoverable_leak_check আমাদের মতো লাইব্রেরি ব্যবহারের ক্ষেত্রে আরও উপযুক্ত, যখন আপনি কনসোলে লিক প্রিন্ট করতে চান , কিন্তু নির্বিশেষে অ্যাপ্লিকেশন চলমান রাখা.

আসুন এম্বিন্ডের মাধ্যমে সেই দ্বিতীয় সহায়কটিকে প্রকাশ করি যাতে আমরা যেকোন সময় জাভাস্ক্রিপ্ট থেকে কল করতে পারি:

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

এবং জাভাস্ক্রিপ্ট দিক থেকে এটি আহ্বান করুন একবার আমরা ইমেজটি সম্পন্ন করে ফেলি। 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

এটি অনেক ভাল দেখায়:

GenericBindingType RawImage ::toWireType ফাংশন থেকে আসছে '12 বাইটের ডাইরেক্ট লিক' লেখা একটি বার্তার স্ক্রিনশট

স্ট্যাকট্রেসের কিছু অংশ এখনও অস্পষ্ট দেখায় কারণ তারা এমস্ক্রিপ্টেন ইন্টারনালের দিকে নির্দেশ করে, কিন্তু আমরা বলতে পারি যে ফাঁসটি এম্বিন্ডের "ওয়্যার টাইপ" (একটি জাভাস্ক্রিপ্ট মান) থেকে একটি RawImage রূপান্তর থেকে আসছে। প্রকৃতপক্ষে, যখন আমরা কোডটি দেখি, তখন আমরা দেখতে পাই যে আমরা জাভাস্ক্রিপ্টে RawImage C++ ইন্সট্যান্স ফিরিয়ে দিই, কিন্তু আমরা কখনই সেগুলিকে উভয় দিকে মুক্ত করি না।

একটি অনুস্মারক হিসাবে, বর্তমানে JavaScript এবং WebAssembly এর মধ্যে কোন আবর্জনা সংগ্রহের একীকরণ নেই, যদিও একটি তৈরি করা হচ্ছে । পরিবর্তে, আপনি অবজেক্টটি সম্পন্ন করার পরে আপনাকে ম্যানুয়ালি কোনো মেমরি মুক্ত করতে হবে এবং জাভাস্ক্রিপ্টের দিক থেকে ডেস্ট্রাক্টরকে কল করতে হবে। বিশেষভাবে এমবিন্ডের জন্য, অফিসিয়াল ডক্স উন্মুক্ত C++ ক্লাসে একটি .delete() পদ্ধতি কল করার পরামর্শ দেয়:

JavaScript কোডটি অবশ্যই প্রাপ্ত যেকোনো C++ অবজেক্ট হ্যান্ডেলকে স্পষ্টভাবে মুছে ফেলতে হবে, নতুবা Emscripten হিপ অনির্দিষ্টকালের জন্য বৃদ্ধি পাবে।

var x = new Module.MyClass;
x.method();
x.delete();

প্রকৃতপক্ষে, যখন আমরা আমাদের ক্লাসের জন্য জাভাস্ক্রিপ্টে এটি করি:

  // …

  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 বাইন্ডিংয়ে এই ত্রুটিটি পেয়েছি:

একটি বার্তার স্ক্রিনশট

এখানে, এটি একটি ফাঁস নয়, আমরা বরাদ্দকৃত সীমানার বাইরে একটি স্মৃতিতে লিখছি 😱

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

যাইহোক, আমরা সেই ভেরিয়েবলগুলির মধ্যে কোনোটিই শুরু না করেই এটি চালু করি, যার মানে মোজজেপিইজি ফলাফলটিকে একটি সম্ভাব্য এলোমেলো মেমরি ঠিকানাতে লিখে যা কলের সময় সেই ভেরিয়েবলগুলিতে সংরক্ষণ করা হয়েছিল!

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

তাদের মধ্যে কিছু যদি প্রথম রানে অলসভাবে সূচনা হয় এবং তারপরে ভবিষ্যতের রানে ভুলভাবে পুনরায় ব্যবহার করা হয় তবে কী হবে? তারপরে স্যানিটাইজার সহ একটি একক কল তাদের সমস্যাযুক্ত হিসাবে রিপোর্ট করবে না।

চলুন UI-তে বিভিন্ন মানের স্তরে এলোমেলোভাবে ক্লিক করে ছবিটিকে কয়েকবার প্রসেস করার চেষ্টা করি। প্রকৃতপক্ষে, এখন আমরা নিম্নলিখিত প্রতিবেদনটি পাই:

একটি বার্তার স্ক্রিনশট

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

আমি সেই মেমরি বাগগুলিকে একের পর এক শিকার করতে পারি, কিন্তু আমি মনে করি এখন পর্যন্ত এটি যথেষ্ট পরিষ্কার যে মেমরি পরিচালনার বর্তমান পদ্ধতিটি কিছু বাজে পদ্ধতিগত সমস্যার দিকে নিয়ে যায়।

তাদের মধ্যে কিছু এখনই স্যানিটাইজার দ্বারা ধরা যেতে পারে। অন্যদের ধরার জন্য জটিল কৌশল প্রয়োজন। অবশেষে, পোস্টের শুরুতে এমন কিছু সমস্যা রয়েছে যা আমরা লগগুলি থেকে দেখতে পাচ্ছি, স্যানিটাইজার দ্বারা মোটেও ধরা পড়ে না। কারণ হল যে জাভাস্ক্রিপ্টের দিকে প্রকৃত অপব্যবহার ঘটে, যেখানে স্যানিটাইজারটির কোন দৃশ্যমানতা নেই। এই সমস্যাগুলি কেবলমাত্র উত্পাদনে বা ভবিষ্যতে কোডে আপাতদৃষ্টিতে সম্পর্কহীন পরিবর্তনের পরে নিজেকে প্রকাশ করবে।

একটি নিরাপদ মোড়ক নির্মাণ

চলুন কয়েক ধাপ পিছিয়ে যাই, এবং এর পরিবর্তে কোডটিকে আরও নিরাপদ উপায়ে পুনর্গঠন করে এই সমস্ত সমস্যার সমাধান করি। আমি আবার একটি উদাহরণ হিসাবে ImageQuant wrapper ব্যবহার করব, কিন্তু অনুরূপ রিফ্যাক্টরিং নিয়ম সমস্ত কোডেকের পাশাপাশি অন্যান্য অনুরূপ কোডবেসে প্রযোজ্য।

প্রথমত, পোস্টের শুরু থেকেই ব্যবহার-পর-মুক্ত সমস্যাটি ঠিক করা যাক। এর জন্য, জাভাস্ক্রিপ্ট সাইডে ফ্রি হিসেবে চিহ্নিত করার আগে আমাদের ওয়েব অ্যাসেম্বলি-ব্যাকড ভিউ থেকে ডেটা ক্লোন করতে হবে:

  // …

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

কিন্তু, যেহেতু আমরা ইতিমধ্যেই জাভাস্ক্রিপ্টের সাথে ইন্টারঅ্যাক্ট করার জন্য Emscripten-এ Embind ব্যবহার করছি, তাই আমরা হয়তো C++ মেমরি ম্যানেজমেন্টের বিশদ সম্পূর্ণভাবে লুকিয়ে রেখে API-কে আরও নিরাপদ করতে পারি!

এর জন্য, new Uint8ClampedArray(…) অংশটিকে জাভাস্ক্রিপ্ট থেকে এম্বিন্ডের সাথে C++ দিকে নিয়ে যাওয়া যাক। তারপরে, আমরা ফাংশন থেকে ফিরে আসার আগেও জাভাস্ক্রিপ্ট মেমরিতে ডেটা ক্লোন করতে এটি ব্যবহার করতে পারি:

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

লক্ষ্য করুন কিভাবে, একটি একক পরিবর্তনের মাধ্যমে, আমরা উভয়েই নিশ্চিত করি যে ফলাফলপ্রাপ্ত বাইট অ্যারে জাভাস্ক্রিপ্টের মালিকানাধীন এবং WebAssembly মেমরি দ্বারা সমর্থিত নয় এবং পূর্বে ফাঁস হওয়া RawImage র‍্যাপার থেকেও মুক্তি পাব।

এখন জাভাস্ক্রিপ্টকে আর ডেটা মুক্ত করার বিষয়ে চিন্তা করতে হবে না, এবং ফলাফলটি অন্য যে কোনও আবর্জনা-সংগৃহীত বস্তুর মতো ব্যবহার করতে পারে:

  // …

  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 র‍্যাপারের কোডে আরও কিছু ছোটখাটো উন্নতি করেছি এবং অন্যান্য কোডেকের জন্য অনুরূপ মেমরি ম্যানেজমেন্ট ফিক্সগুলি প্রতিলিপি করেছি। আপনি যদি আরও বিশদ বিবরণে আগ্রহী হন, আপনি এখানে ফলাফল PR দেখতে পারেন: C++ কোডেকগুলির জন্য মেমরি সংশোধন

Takeaways

এই রিফ্যাক্টরিং থেকে আমরা কোন পাঠ শিখতে এবং ভাগ করতে পারি যা অন্যান্য কোডবেসে প্রয়োগ করা যেতে পারে?

  • WebAssembly-এর দ্বারা সমর্থিত মেমরি ভিউ ব্যবহার করবেন না - এটি যে ভাষা থেকে তৈরি করা হোক না কেন - একটি একক আহ্বানের বাইরে৷ আপনি এর থেকে বেশি সময় বেঁচে থাকার উপর নির্ভর করতে পারবেন না, এবং আপনি প্রচলিত উপায়ে এই বাগগুলি ধরতে পারবেন না, তাই আপনার যদি পরবর্তী সময়ের জন্য ডেটা সংরক্ষণ করার প্রয়োজন হয় তবে এটি জাভাস্ক্রিপ্টের পাশে কপি করুন এবং সেখানে সংরক্ষণ করুন।
  • যদি সম্ভব হয়, একটি নিরাপদ মেমরি ম্যানেজমেন্ট ল্যাঙ্গুয়েজ ব্যবহার করুন বা, অন্তত, নিরাপদ টাইপের মোড়ক, সরাসরি কাঁচা পয়েন্টারগুলিতে কাজ করার পরিবর্তে। এটি আপনাকে JavaScript ↔ WebAssembly বাউন্ডারিতে বাগ থেকে বাঁচাতে পারবে না, তবে অন্তত এটি স্ট্যাটিক ল্যাঙ্গুয়েজ কোড দ্বারা স্বয়ংসম্পূর্ণ বাগগুলির জন্য পৃষ্ঠকে কমিয়ে দেবে।
  • আপনি যে ভাষাই ব্যবহার করুন না কেন, ডেভেলপমেন্টের সময় স্যানিটাইজার দিয়ে কোড চালান—এগুলি শুধুমাত্র স্ট্যাটিক ল্যাঙ্গুয়েজ কোডে সমস্যাই নয়, জাভাস্ক্রিপ্ট ↔ ওয়েব অ্যাসেম্বলি সীমানা জুড়ে কিছু সমস্যাও ধরতে সাহায্য করতে পারে, যেমন .delete() কল করতে ভুলে যাওয়া বা পাস করা জাভাস্ক্রিপ্ট পাশ থেকে অবৈধ পয়েন্টার.
  • সম্ভব হলে, WebAssembly থেকে জাভাস্ক্রিপ্টে অব্যবস্থাপিত ডেটা এবং অবজেক্টগুলিকে সম্পূর্ণভাবে প্রকাশ করা এড়িয়ে চলুন। জাভাস্ক্রিপ্ট একটি আবর্জনা-সংগৃহীত ভাষা, এবং ম্যানুয়াল মেমরি ম্যানেজমেন্ট এতে সাধারণ নয়। এটিকে আপনার WebAssembly যে ভাষা থেকে তৈরি করা হয়েছিল তার মেমরি মডেলের একটি বিমূর্ততা ফাঁস হিসাবে বিবেচনা করা যেতে পারে এবং একটি জাভাস্ক্রিপ্ট কোডবেসে ভুল ব্যবস্থাপনা উপেক্ষা করা সহজ।
  • এটি সুস্পষ্ট হতে পারে, তবে, অন্য যেকোন কোডবেসের মতো, গ্লোবাল ভেরিয়েবলে পরিবর্তনযোগ্য অবস্থা সংরক্ষণ করা এড়িয়ে চলুন। আপনি বিভিন্ন আহ্বান বা এমনকি থ্রেড জুড়ে এটির পুনঃব্যবহারের সাথে সমস্যাগুলি ডিবাগ করতে চান না, তাই এটি যতটা সম্ভব স্বয়ংসম্পূর্ণ রাখা ভাল।