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

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

ইঙ্গভার স্টেপানিয়ান
Ingvar Stepanyan

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 ভিউ , ডাটার জাভাস্ক্রিপ্ট-মালিকানাধীন কপির পরিবর্তে।

যখন আমরা JavaScript থেকে 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);
}

কিন্তু, যেহেতু আমরা ইতিমধ্যেই জাভাস্ক্রিপ্টের সাথে ইন্টারঅ্যাক্ট করার জন্য এমস্ক্রিপ্টে এম্বিন্ড ব্যবহার করছি, তাই আমরা হয়তো 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() কল করতে ভুলে যাওয়া বা JavaScript পাশ থেকে অবৈধ পয়েন্টার পাস করা।
  • সম্ভব হলে, WebAssembly থেকে জাভাস্ক্রিপ্টে অব্যবস্থাপিত ডেটা এবং অবজেক্টগুলিকে সম্পূর্ণভাবে প্রকাশ করা এড়িয়ে চলুন। জাভাস্ক্রিপ্ট একটি আবর্জনা-সংগৃহীত ভাষা, এবং ম্যানুয়াল মেমরি ম্যানেজমেন্ট এতে সাধারণ নয়। এটিকে আপনার WebAssembly যে ভাষা থেকে তৈরি করা হয়েছিল তার মেমরি মডেলের একটি বিমূর্ততা ফাঁস হিসাবে বিবেচনা করা যেতে পারে এবং একটি জাভাস্ক্রিপ্ট কোডবেসে ভুল ব্যবস্থাপনা উপেক্ষা করা সহজ।
  • এটি সুস্পষ্ট হতে পারে, তবে, অন্য যেকোন কোডবেসের মতো, গ্লোবাল ভেরিয়েবলে পরিবর্তনযোগ্য অবস্থা সংরক্ষণ করা এড়িয়ে চলুন। আপনি বিভিন্ন আহ্বান বা এমনকি থ্রেড জুড়ে এটির পুনঃব্যবহারের সাথে সমস্যাগুলি ডিবাগ করতে চান না, তাই এটি যতটা সম্ভব স্বয়ংসম্পূর্ণ রাখা ভাল।
,

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

ইঙ্গভার স্টেপানিয়ান
Ingvar Stepanyan

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 ভিউ , ডাটার জাভাস্ক্রিপ্ট-মালিকানাধীন কপির পরিবর্তে।

যখন আমরা JavaScript থেকে 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);
}

কিন্তু, যেহেতু আমরা ইতিমধ্যেই জাভাস্ক্রিপ্টের সাথে ইন্টারঅ্যাক্ট করার জন্য এমস্ক্রিপ্টে এম্বিন্ড ব্যবহার করছি, তাই আমরা হয়তো 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() কল করতে ভুলে যাওয়া বা JavaScript পাশ থেকে অবৈধ পয়েন্টার পাস করা।
  • সম্ভব হলে, WebAssembly থেকে জাভাস্ক্রিপ্টে অব্যবস্থাপিত ডেটা এবং অবজেক্টগুলিকে সম্পূর্ণভাবে প্রকাশ করা এড়িয়ে চলুন। জাভাস্ক্রিপ্ট একটি আবর্জনা-সংগৃহীত ভাষা, এবং ম্যানুয়াল মেমরি ম্যানেজমেন্ট এতে সাধারণ নয়। এটিকে আপনার WebAssembly যে ভাষা থেকে তৈরি করা হয়েছিল তার মেমরি মডেলের একটি বিমূর্ততা ফাঁস হিসাবে বিবেচনা করা যেতে পারে এবং একটি জাভাস্ক্রিপ্ট কোডবেসে ভুল ব্যবস্থাপনা উপেক্ষা করা সহজ।
  • এটি সুস্পষ্ট হতে পারে, তবে, অন্য যেকোন কোডবেসের মতো, গ্লোবাল ভেরিয়েবলে পরিবর্তনযোগ্য অবস্থা সংরক্ষণ করা এড়িয়ে চলুন। আপনি বিভিন্ন আহ্বান বা এমনকি থ্রেড জুড়ে এটির পুনঃব্যবহারের সাথে সমস্যাগুলি ডিবাগ করতে চান না, তাই এটি যতটা সম্ভব স্বয়ংসম্পূর্ণ রাখা ভাল।
,

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

ইঙ্গভার স্টেপানিয়ান
Ingvar Stepanyan

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 ভিউ , ডাটার জাভাস্ক্রিপ্ট-মালিকানাধীন কপির পরিবর্তে।

যখন আমরা JavaScript থেকে 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);
}

এবং একবার আমরা চিত্রটি সম্পন্ন করার পরে এটি জাভাস্ক্রিপ্ট দিক থেকে অনুরোধ করুন। জাভাস্ক্রিপ্ট দিক থেকে এটি করা, সি ++ এর পরিবর্তে, সমস্ত স্কোপগুলি বেরিয়ে এসেছে এবং সমস্ত অস্থায়ী সি ++ অবজেক্টগুলি আমরা সেই চেকগুলি চালানোর সময় থেকে মুক্তি পেয়েছিল তা নিশ্চিত করতে সহায়তা করে:

  // 

  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

এটি আরও ভাল দেখাচ্ছে:

জেনারিকবাইন্ডিং টাইপ রাডিমেজ থেকে আসা '12 বাইটের সরাসরি ফাঁস' পড়ার একটি বার্তার স্ক্রিনশট :: টোয়েরটাইপ ফাংশন

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

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

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

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

ফুটো প্রত্যাশার মতো চলে যায়।

স্যানিটাইজারদের সাথে আরও সমস্যা আবিষ্কার করা

স্যানিটাইজারগুলির সাথে অন্যান্য স্কুওশ কোডেকগুলি তৈরি করা উভয়ই অনুরূপ পাশাপাশি কিছু নতুন সমস্যা প্রকাশ করে। উদাহরণস্বরূপ, আমি মোজেজপেগ বাইন্ডিংগুলিতে এই ত্রুটিটি পেয়েছি:

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

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

মোজজেপেগের কোডটি খনন করে আমরা দেখতে পেলাম যে এখানে সমস্যাটি হ'ল jpeg_mem_dest আমরা জেপিগের জন্য একটি মেমরি গন্তব্য বরাদ্দ করতে ব্যবহার করি এমন ফাংশন -যখন তারা অ-শূন্য হয় তখন 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);

অনুরোধটি এই সমস্যাটি সমাধান করার আগে উভয় ভেরিয়েবল শূন্য-প্রাথমিককরণ এবং এখন কোডটি পরিবর্তে একটি মেমরি ফাঁস চেক পৌঁছেছে। ভাগ্যক্রমে, চেকটি সফলভাবে পাস করে, ইঙ্গিত দেয় যে এই কোডেকটিতে আমাদের কোনও ফাঁস নেই।

ভাগ করা রাষ্ট্রের সাথে সমস্যাগুলি

… নাকি আমরা করি?

আমরা জানি যে আমাদের কোডেক বাইন্ডিংগুলি কিছু রাজ্যের পাশাপাশি বিশ্বব্যাপী স্ট্যাটিক ভেরিয়েবলগুলির ফলাফল সংরক্ষণ করে এবং মোজজেপেগের কিছু বিশেষ জটিল কাঠামো রয়েছে।

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

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

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

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

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

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

  // 

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

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

এটি করার জন্য, আমরা ফাংশনে প্রতিটি কল স্থানীয় ভেরিয়েবলগুলি ব্যবহার করে নিজস্ব ডেটা পরিচালনা করে তা নিশ্চিত করার জন্য আমরা সি ++ র‌্যাপারটিকে রিফ্যাক্টর করি। তারপরে, আমরা পয়েন্টারটিকে পিছনে গ্রহণ করতে আমাদের 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);
}

তবে, যেহেতু আমরা ইতিমধ্যে জাভাস্ক্রিপ্টের সাথে ইন্টারঅ্যাক্ট করার জন্য এমএসক্রিপ্টনে এম্বাইন্ড ইন এমসক্রিপ্টে ব্যবহার করছি, আমরা পাশাপাশি সি ++ মেমরি পরিচালনার বিশদটি পুরোপুরি লুকিয়ে এপিআইকে আরও নিরাপদ করে তুলতে পারি!

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

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

কীভাবে, একক পরিবর্তনের সাথে আমরা উভয়ই নিশ্চিত করি যে ফলস্বরূপ বাইট অ্যারে জাভাস্ক্রিপ্টের মালিকানাধীন এবং ওয়েবসাম্বলি মেমরি দ্বারা সমর্থিত নয় এবং পূর্বে ফাঁস হওয়া 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);
}

এর অর্থ হ'ল আমাদের আর সি ++ পাশে কাস্টম 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());
}

সব মিলিয়ে, আমাদের মোড়ক কোড একই সাথে ক্লিনার এবং নিরাপদ উভয়ই হয়ে উঠেছে।

এর পরে আমি ইমেজকুয়ান্ট র‌্যাপারের কোডে আরও কিছু সামান্য উন্নতি করেছি এবং অন্যান্য কোডেকের জন্য অনুরূপ মেমরি ম্যানেজমেন্ট ফিক্সগুলির প্রতিলিপি করেছি। আপনি যদি আরও বিশদে আগ্রহী হন তবে আপনি এখানে ফলাফলটি দেখতে পাবেন: সি ++ কোডেকগুলির জন্য মেমরি ফিক্সগুলি

Takeaways

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

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

যদিও জাভাস্ক্রিপ্টটি নিজের পরে পরিষ্কার করার ক্ষেত্রে মোটামুটি ক্ষমা করছে, স্থির ভাষা অবশ্যই নয় ...

ইঙ্গভার স্টেপানিয়ান
Ingvar Stepanyan

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

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

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

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

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

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

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

এমস্ক্রিপ্টনে, typed_memory_view একটি জাভাস্ক্রিপ্ট Uint8Array ফেরত দেয় যা ওয়েবসেম্বলি (ডাব্লুএএসএম) মেমরি বাফার দ্বারা সমর্থিত, byteOffset এবং byteLength সাথে প্রদত্ত পয়েন্টার এবং দৈর্ঘ্যে সেট করে। মূল বক্তব্যটি হ'ল এটি ডেটা জাভাস্ক্রিপ্ট-মালিকানাধীন অনুলিপিটি না করে একটি ওয়েবসেম্বলি মেমরি বাফারে টাইপডারে ভিউ

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

অথবা, কিছু free বাস্তবায়ন এমনকি অবিলম্বে মুক্ত মেমরিটি শূন্য-পূরণ করার সিদ্ধান্ত নিতে পারে। এমস্ক্রিপ্টেন যে free ব্যবহার করে তা এটি করে না, তবে আমরা এখানে একটি বাস্তবায়নের বিশদ উপর নির্ভর করছি যা গ্যারান্টিযুক্ত হতে পারে না।

বা, এমনকি যদি পয়েন্টারের পিছনে মেমরিটি সংরক্ষণ করা হয় তবে নতুন বরাদ্দের জন্য ওয়েবসেমি মেমরিটি বাড়ানোর প্রয়োজন হতে পারে। যখন WebAssembly.Memory হয় জাভাস্ক্রিপ্ট এপিআই বা সংশ্লিষ্ট memory.grow ArrayBuffer

এই আচরণটি প্রদর্শনের জন্য আমাকে ডিভটুলস (বা নোড.জেএস) কনসোলটি ব্যবহার করতে দিন:

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

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

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

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

এটি স্বয়ংক্রিয়ভাবে পয়েন্টার সুরক্ষা চেকগুলি সক্ষম করবে, তবে আমরা সম্ভাব্য মেমরি ফাঁসও খুঁজে পেতে চাই। যেহেতু আমরা কোনও প্রোগ্রামের পরিবর্তে ইমেজকুয়ান্টকে গ্রন্থাগার হিসাবে ব্যবহার করছি, তাই কোনও "প্রস্থান পয়েন্ট" নেই যেখানে এমস্ক্রিপ্টেন স্বয়ংক্রিয়ভাবে যাচাই করতে পারে যে সমস্ত স্মৃতি মুক্ত হয়েছে।

পরিবর্তে, এই জাতীয় ক্ষেত্রে লিকসানিটাইজার (অ্যাড্রেসসানিটাইজার অন্তর্ভুক্ত) ফাংশন সরবরাহ করে __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);
}

এবং একবার আমরা চিত্রটি সম্পন্ন করার পরে এটি জাভাস্ক্রিপ্ট দিক থেকে অনুরোধ করুন। জাভাস্ক্রিপ্ট দিক থেকে এটি করা, সি ++ এর পরিবর্তে, সমস্ত স্কোপগুলি বেরিয়ে এসেছে এবং সমস্ত অস্থায়ী সি ++ অবজেক্টগুলি আমরা সেই চেকগুলি চালানোর সময় থেকে মুক্তি পেয়েছিল তা নিশ্চিত করতে সহায়তা করে:

  // 

  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

এটি আরও ভাল দেখাচ্ছে:

জেনারিকবাইন্ডিং টাইপ রাডিমেজ থেকে আসা '12 বাইটের সরাসরি ফাঁস' পড়ার একটি বার্তার স্ক্রিনশট :: টোয়েরটাইপ ফাংশন

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

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

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

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

ফুটো প্রত্যাশার মতো চলে যায়।

স্যানিটাইজারদের সাথে আরও সমস্যা আবিষ্কার করা

স্যানিটাইজারগুলির সাথে অন্যান্য স্কুওশ কোডেকগুলি তৈরি করা উভয়ই অনুরূপ পাশাপাশি কিছু নতুন সমস্যা প্রকাশ করে। উদাহরণস্বরূপ, আমি মোজেজপেগ বাইন্ডিংগুলিতে এই ত্রুটিটি পেয়েছি:

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

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

মোজজেপেগের কোডটি খনন করে আমরা দেখতে পেলাম যে এখানে সমস্যাটি হ'ল jpeg_mem_dest আমরা জেপিগের জন্য একটি মেমরি গন্তব্য বরাদ্দ করতে ব্যবহার করি এমন ফাংশন -যখন তারা অ-শূন্য হয় তখন 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);

অনুরোধটি এই সমস্যাটি সমাধান করার আগে উভয় ভেরিয়েবল শূন্য-প্রাথমিককরণ এবং এখন কোডটি পরিবর্তে একটি মেমরি ফাঁস চেক পৌঁছেছে। ভাগ্যক্রমে, চেকটি সফলভাবে পাস করে, ইঙ্গিত দেয় যে এই কোডেকটিতে আমাদের কোনও ফাঁস নেই।

ভাগ করা রাষ্ট্রের সাথে সমস্যাগুলি

… নাকি আমরা করি?

আমরা জানি যে আমাদের কোডেক বাইন্ডিংগুলি কিছু রাজ্যের পাশাপাশি বিশ্বব্যাপী স্ট্যাটিক ভেরিয়েবলগুলির ফলাফল সংরক্ষণ করে এবং মোজজেপেগের কিছু বিশেষ জটিল কাঠামো রয়েছে।

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

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

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

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

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

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

  // 

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

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

এটি করার জন্য, আমরা ফাংশনে প্রতিটি কল স্থানীয় ভেরিয়েবলগুলি ব্যবহার করে নিজস্ব ডেটা পরিচালনা করে তা নিশ্চিত করার জন্য আমরা সি ++ র‌্যাপারটিকে রিফ্যাক্টর করি। তারপরে, আমরা পয়েন্টারটিকে পিছনে গ্রহণ করতে আমাদের 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);
}

তবে, যেহেতু আমরা ইতিমধ্যে জাভাস্ক্রিপ্টের সাথে ইন্টারঅ্যাক্ট করার জন্য এমএসক্রিপ্টনে এম্বাইন্ড ইন এমসক্রিপ্টে ব্যবহার করছি, আমরা পাশাপাশি সি ++ মেমরি পরিচালনার বিশদটি পুরোপুরি লুকিয়ে এপিআইকে আরও নিরাপদ করে তুলতে পারি!

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

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

কীভাবে, একক পরিবর্তনের সাথে আমরা উভয়ই নিশ্চিত করি যে ফলস্বরূপ বাইট অ্যারে জাভাস্ক্রিপ্টের মালিকানাধীন এবং ওয়েবসাম্বলি মেমরি দ্বারা সমর্থিত নয় এবং পূর্বে ফাঁস হওয়া 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);
}

এর অর্থ হ'ল আমাদের আর সি ++ পাশে কাস্টম 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());
}

সব মিলিয়ে, আমাদের মোড়ক কোড একই সাথে ক্লিনার এবং নিরাপদ উভয়ই হয়ে উঠেছে।

এর পরে আমি ইমেজকুয়ান্ট র‌্যাপারের কোডে আরও কিছু সামান্য উন্নতি করেছি এবং অন্যান্য কোডেকের জন্য অনুরূপ মেমরি ম্যানেজমেন্ট ফিক্সগুলির প্রতিলিপি করেছি। আপনি যদি আরও বিশদে আগ্রহী হন তবে আপনি এখানে ফলাফলটি দেখতে পাবেন: সি ++ কোডেকগুলির জন্য মেমরি ফিক্সগুলি

Takeaways

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

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