اشکال زدایی نشت حافظه در WebAssembly با استفاده از Emscripten

در حالی که جاوا اسکریپت در پاکسازی پس از خود نسبتاً بخشنده است، زبان های ایستا قطعاً ...

اینگوار استپانیان
Ingvar Stepanyan

Squoosh.app یک PWA است که نشان می دهد کدک ها و تنظیمات مختلف تصویر چقدر می توانند اندازه فایل تصویر را بدون تأثیر قابل توجهی بر کیفیت بهبود دهند. با این حال، این همچنین یک نسخه ی نمایشی فنی است که نشان می دهد چگونه می توانید کتابخانه های نوشته شده در C++ یا Rust را بگیرید و آنها را به وب بیاورید.

توانایی انتقال کد از اکوسیستم‌های موجود بسیار ارزشمند است، اما تفاوت‌های کلیدی بین این زبان‌های ثابت و جاوا اسکریپت وجود دارد. یکی از آنها در رویکردهای متفاوت آنها به مدیریت حافظه است.

در حالی که جاوا اسکریپت در پاکسازی پس از خود نسبتاً بخشنده است، چنین زبان های ایستا قطعاً چنین نیستند. شما باید صریحاً یک حافظه اختصاص داده شده جدید بخواهید و واقعاً باید مطمئن شوید که پس از آن آن را پس داده اید و دیگر هرگز از آن استفاده نکنید. اگر این اتفاق نیفتد، شما نشت می کنید... و در واقع به طور منظم اتفاق می افتد. بیایید نگاهی بیندازیم که چگونه می‌توانید این نشت‌های حافظه را اشکال زدایی کنید و حتی بهتر، چگونه می‌توانید کد خود را طراحی کنید تا دفعه بعد از آنها جلوگیری کنید.

الگوی مشکوک

اخیراً هنگام شروع کار بر روی Squoosh، متوجه الگوی جالبی در بسته‌بندی کدک C++ نشدم. بیایید نگاهی به پوشش ImageQuant به عنوان مثال بیندازیم (کاهش داده شده تا فقط قسمت‌های ایجاد و توزیع شیء را نشان دهد):

liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;

RawImage quantize(std::string rawimage,
                  int image_width,
                  int image_height,
                  int num_colors,
                  float dithering) {
  const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
  int size = image_width * image_height;

  attr = liq_attr_create();
  image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_set_max_colors(attr, num_colors);
  liq_image_quantize(image, attr, &res);
  liq_set_dithering_level(res, dithering);
  uint8_t* image8bit = (uint8_t*)malloc(size);
  result = (uint8_t*)malloc(size * 4);

  // …

  free(image8bit);
  liq_result_destroy(res);
  liq_image_destroy(image);
  liq_attr_destroy(attr);

  return {
    val(typed_memory_view(image_width * image_height * 4, result)),
    image_width,
    image_height
  };
}

void free_result() {
  free(result);
}

جاوا اسکریپت (خوب، TypeScript):

export async function process(data: ImageData, opts: QuantizeOptions) {
  if (!emscriptenModule) {
    emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
  }
  const module = await emscriptenModule;

  const result = module.quantize(/* … */);

  module.free_result();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
}

آیا مشکلی را تشخیص می دهید؟ نکته: پس از استفاده رایگان است، اما در جاوا اسکریپت!

در Emscripten، typed_memory_view یک جاوا اسکریپت Uint8Array برمی‌گرداند که توسط بافر حافظه WebAssembly (Wasm) پشتیبانی می‌شود و byteOffset و byteLength روی اشاره‌گر و طول داده شده تنظیم شده‌اند. نکته اصلی این است که این یک نمای TypedArray در بافر حافظه WebAssembly است، نه یک کپی متعلق به جاوا اسکریپت از داده ها.

هنگامی که free_result از جاوا اسکریپت فراخوانی می کنیم، آن نیز به نوبه خود، یک تابع استاندارد C را free فراخوانی می کند تا این حافظه را به عنوان در دسترس برای هر تخصیص آینده علامت گذاری کند، به این معنی که داده هایی که دیدگاه Uint8Array ما به آن اشاره می کند، می تواند با داده های دلخواه توسط هر فراخوانی آینده در Wasm بازنویسی شود.

یا، برخی از پیاده‌سازی free حتی ممکن است تصمیم بگیرند که بلافاصله حافظه آزاد شده را صفر کنند. free که Emscripten استفاده می کند این کار را نمی کند، اما ما در اینجا به جزئیات پیاده سازی تکیه می کنیم که نمی توان آن را تضمین کرد.

یا حتی اگر حافظه پشت اشاره گر حفظ شود، ممکن است نیاز به تخصیص جدید برای رشد حافظه 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 را فراخوانی نکنیم، ممکن است در برخی مواقع پشتیبانی چند رشته‌ای را به کدک‌های خود اضافه کنیم. در این صورت ممکن است یک رشته کاملاً متفاوت باشد که درست قبل از اینکه بتوانیم آن را شبیه سازی کنیم، داده ها را بازنویسی می کند.

به دنبال اشکالات حافظه

در هر صورت، من تصمیم گرفتم بیشتر بروم و بررسی کنم که آیا این کد در عمل مشکلی را نشان می دهد یا خیر. به نظر می‌رسد این یک فرصت عالی برای آزمایش پشتیبانی جدید (ish) ضد عفونی‌کننده‌های Emscripten است که سال گذشته اضافه شد و در سخنرانی WebAssembly ما در جلسه Chrome Dev Summit ارائه شد:

در این مورد، ما به AddressSanitizer علاقه مند هستیم که می تواند مسائل مختلف مربوط به اشاره گر و حافظه را شناسایی کند. برای استفاده از آن، باید کدک خود را با -fsanitize=address دوباره کامپایل کنیم:

emcc \
  --bind \
  ${OPTIMIZE} \
  --closure 1 \
  -s ALLOW_MEMORY_GROWTH=1 \
  -s MODULARIZE=1 \
  -s 'EXPORT_NAME="imagequant"' \
  -I node_modules/libimagequant \
  -o ./imagequant.js \
  --std=c++11 \
  imagequant.cpp \
  -fsanitize=address \
  node_modules/libimagequant/libimagequant.a

این به طور خودکار بررسی های ایمنی اشاره گر را فعال می کند، اما ما همچنین می خواهیم نشت حافظه احتمالی را پیدا کنیم. از آنجایی که ما از ImageQuant به عنوان کتابخانه به جای برنامه استفاده می کنیم، هیچ "نقطه خروجی" وجود ندارد که Emscripten بتواند به طور خودکار آزاد شدن تمام حافظه را تأیید کند.

درعوض، برای چنین مواردی، LeakSanitizer (که در AddressSanitizer موجود است) توابع __lsan_do_leak_check و __lsan_do_recoverable_leak_check را ارائه می‌کند، که می‌توان هر زمان که انتظار داشتیم تمام حافظه آزاد شود و بخواهیم آن فرض را تأیید کنیم، به صورت دستی فراخوانی می‌شوند. __lsan_do_leak_check برای استفاده در پایان یک برنامه کاربردی در حال اجرا، زمانی که می‌خواهید در صورت شناسایی هرگونه نشتی، فرآیند را متوقف کنید، استفاده می‌شود، در حالی که __lsan_do_recoverable_leak_check برای موارد استفاده کتابخانه‌ای مانند ما مناسب‌تر است، زمانی که می‌خواهید نشت‌ها را در کنسول چاپ کنید، اما برنامه را بدون توجه به اجرا نگه دارید.

بیایید کمک کننده دوم را از طریق Embind در معرض نمایش بگذاریم تا بتوانیم هر زمان که بخواهیم آن را از جاوا اسکریپت فراخوانی کنیم:

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

این گزارشی مانند زیر را در کنسول به ما می دهد:

اسکرین شات از یک پیام

اوه-اوه، چند نشت کوچک وجود دارد، اما stacktrace چندان مفید نیست زیرا همه نام‌های توابع مخدوش هستند. بیایید با یک اطلاعات اولیه اشکال زدایی دوباره کامپایل کنیم تا آنها را حفظ کنیم:

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 بایت" از تابع GenericBindingType RawImage ::toWireType

برخی از قسمت‌های stacktrace همچنان مبهم به نظر می‌رسند زیرا به قسمت‌های داخلی Emscripten اشاره می‌کنند، اما می‌توانیم بگوییم که نشت از تبدیل RawImage به "نوع سیم" (به یک مقدار جاوا اسکریپت) توسط Embind است. در واقع، وقتی به کد نگاه می‌کنیم، می‌بینیم که نمونه‌های RawImage C++ را به جاوا اسکریپت برمی‌گردانیم، اما هرگز آن‌ها را در هر دو طرف آزاد نمی‌کنیم.

به عنوان یادآوری، در حال حاضر هیچ ادغام جمع آوری زباله بین جاوا اسکریپت و WebAssembly وجود ندارد، اگرچه یکی در حال توسعه است . در عوض، پس از اتمام کار با شی، باید به صورت دستی هر حافظه و مخرب‌کننده‌هایی را از سمت جاوا اسکریپت فراخوانی کنید. به طور خاص برای Embind، اسناد رسمی پیشنهاد می‌کنند که یک متد .delete() را در کلاس‌های C++ در معرض نمایش فراخوانی کنید:

کد جاوا اسکریپت باید صراحتاً هر دسته شیء 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
  );
}

نشتی همانطور که انتظار می رفت برطرف می شود.

کشف مشکلات بیشتر با مواد ضدعفونی کننده

ساخت دیگر کدک‌های Squoosh با ضدعفونی‌کننده‌ها هم مشکلات مشابه و هم برخی از مسائل جدید را آشکار می‌کند. به عنوان مثال، من این خطا را در اتصالات MozJPEG دارم:

اسکرین شات از یک پیام

اینجا لو رفتن نیست، بلکه ما به خاطره ای خارج از مرزهای اختصاص داده شده می نویسیم 😱

با کاوش در کد MozJPEG، متوجه می‌شویم که مشکل اینجاست که jpeg_mem_dest - تابعی که برای تخصیص یک مقصد حافظه برای JPEG استفاده می‌کنیم - از مقادیر موجود outbuffer استفاده می‌کند و زمانی که آنها غیر صفر هستند، outsize استفاده می‌کند :

if (*outbuffer == NULL || *outsize == 0) {
  /* Allocate initial buffer */
  dest->newbuffer = *outbuffer = (unsigned char *) malloc(OUTPUT_BUF_SIZE);
  if (dest->newbuffer == NULL)
    ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10);
  *outsize = OUTPUT_BUF_SIZE;
}

با این حال، ما آن را بدون مقداردهی اولیه هیچ یک از آن متغیرها فراخوانی می کنیم، به این معنی که MozJPEG نتیجه را در یک آدرس حافظه بالقوه تصادفی می نویسد که اتفاقاً در آن متغیرها در زمان فراخوانی ذخیره می شود!

uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);

صفر کردن هر دو متغیر قبل از فراخوانی این مشکل را حل می کند و اکنون کد به جای آن به بررسی نشت حافظه می رسد. خوشبختانه، بررسی با موفقیت انجام شد، که نشان می دهد ما هیچ گونه نشتی در این کدک نداریم.

مسائل مربوط به حالت مشترک

... یا ما؟

ما می دانیم که پیوندهای کدک ما مقداری از حالت را ذخیره می کند و همچنین منجر به متغیرهای ثابت جهانی می شود و MozJPEG ساختارهای پیچیده ای دارد.

uint8_t* last_result;
struct jpeg_compress_struct cinfo;

val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
  // …
}

چه می‌شود اگر برخی از آن‌ها در اولین اجرا به‌طور تنبلی مقداردهی اولیه شوند، و سپس به‌طور نادرست در اجراهای بعدی دوباره استفاده شوند؟ سپس یک تماس با یک ضد عفونی کننده آنها را به عنوان مشکل ساز گزارش نمی کند.

بیایید سعی کنیم تصویر را چند بار با کلیک کردن تصادفی روی سطوح مختلف کیفیت در رابط کاربری پردازش کنیم. در واقع، اکنون گزارش زیر را دریافت می کنیم:

اسکرین شات از یک پیام

262144 بایت—به نظر می رسد که کل تصویر نمونه از 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 به عنوان مثال استفاده خواهم کرد، اما قوانین refactoring مشابه برای همه کدک ها و همچنین سایر پایگاه های کد مشابه اعمال می شود.

اول از همه، بیایید مشکل استفاده پس از رایگان را از ابتدای پست برطرف کنیم. برای این کار، باید داده ها را از نمای WebAssembly-backed قبل از علامت گذاری به عنوان رایگان در سمت جاوا اسکریپت کلون کنیم:

  // 

  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++ wrapper را تغییر می دهیم تا مطمئن شویم که هر فراخوانی به تابع داده های خود را با استفاده از متغیرهای محلی مدیریت می کند. سپس، می‌توانیم امضای تابع 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);
}

اما، از آنجایی که ما در حال حاضر از Embind در Emscripten برای تعامل با جاوا اسکریپت استفاده می‌کنیم، ممکن است API را با پنهان کردن کامل جزئیات مدیریت حافظه C++ ایمن‌تر کنیم!

برای آن، اجازه دهید بخش new Uint8ClampedArray(…) را از جاوا اسکریپت به سمت C++ با Embind منتقل کنیم. سپس، می‌توانیم از آن برای کلون کردن داده‌ها در حافظه جاوا اسکریپت حتی قبل از بازگشت از تابع استفاده کنیم:

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

این همچنین به این معنی است که ما دیگر نیازی به اتصال free_result سفارشی در سمت C++ نداریم:

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 .

غذای آماده

چه درس‌هایی می‌توانیم یاد بگیریم و از این بازسازی مجدد به اشتراک بگذاریم که می‌تواند در سایر پایگاه‌های کد اعمال شود؟

  • از نماهای حافظه ای که توسط WebAssembly پشتیبانی می شود - مهم نیست که از کدام زبان ساخته شده است - فراتر از یک فراخوان استفاده نکنید. شما نمی توانید به زنده ماندن آنها بیشتر از این تکیه کنید و نمی توانید این اشکالات را با روش های معمولی پیدا کنید، بنابراین اگر نیاز به ذخیره داده ها برای بعد دارید، آن را در سمت جاوا اسکریپت کپی کنید و در آنجا ذخیره کنید.
  • در صورت امکان، به جای کار کردن مستقیم با اشاره گرهای خام، از یک زبان مدیریت حافظه ایمن یا حداقل از نوع پوشش های امن استفاده کنید. این شما را از اشکالات موجود در مرز JavaScript ↔ WebAssembly نجات نمی دهد، اما حداقل سطح اشکالات را کاهش می دهد که توسط کد زبان ثابت وجود دارند.
  • مهم نیست از چه زبانی استفاده می‌کنید، کد را با ضدعفونی‌کننده‌ها در طول توسعه اجرا کنید—آنها می‌توانند نه تنها مشکلات کد زبان استاتیک، بلکه برخی مشکلات را در سراسر مرز JavaScript ↔ WebAssembly، مانند فراموش کردن فراخوانی .delete() یا ارسال نشانگرهای نامعتبر از سمت جاوا اسکریپت، کمک کنند.
  • در صورت امکان، از قرار دادن داده ها و اشیاء مدیریت نشده از WebAssembly به طور کلی در معرض جاوا اسکریپت خودداری کنید. جاوا اسکریپت یک زبان جمع آوری زباله است و مدیریت دستی حافظه در آن رایج نیست. این را می توان نشت انتزاعی از مدل حافظه زبانی در نظر گرفت که WebAssembly شما از آن ساخته شده است، و مدیریت نادرست به راحتی در یک پایگاه کد جاوا اسکریپت نادیده گرفته می شود.
  • این ممکن است بدیهی باشد، اما، مانند هر پایگاه کد دیگری، از ذخیره حالت تغییرپذیر در متغیرهای سراسری خودداری کنید. شما نمی‌خواهید مشکلات مربوط به استفاده مجدد از آن را در فراخوان‌ها یا حتی رشته‌های مختلف اشکال‌زدایی کنید، بنابراین بهتر است تا حد ممکن آن را مستقل نگه دارید.
،

در حالی که جاوا اسکریپت در پاکسازی پس از خود نسبتاً بخشنده است، زبان های ایستا قطعاً ...

اینگوار استپانیان
Ingvar Stepanyan

Squoosh.app یک PWA است که نشان می دهد کدک ها و تنظیمات مختلف تصویر چقدر می توانند اندازه فایل تصویر را بدون تأثیر قابل توجهی بر کیفیت بهبود دهند. با این حال، این همچنین یک نسخه ی نمایشی فنی است که نشان می دهد چگونه می توانید کتابخانه های نوشته شده در C++ یا Rust را بردارید و آنها را به وب بیاورید.

توانایی انتقال کد از اکوسیستم‌های موجود بسیار ارزشمند است، اما تفاوت‌های کلیدی بین این زبان‌های ثابت و جاوا اسکریپت وجود دارد. یکی از آنها در رویکردهای متفاوت آنها به مدیریت حافظه است.

در حالی که جاوا اسکریپت در پاکسازی پس از خود نسبتاً بخشنده است، چنین زبان های ایستا قطعاً چنین نیستند. شما باید صریحاً یک حافظه اختصاص داده شده جدید بخواهید و واقعاً باید مطمئن شوید که پس از آن آن را پس داده اید و دیگر هرگز از آن استفاده نکنید. اگر این اتفاق نیفتد، شما نشت می کنید... و در واقع به طور منظم اتفاق می افتد. بیایید نگاهی بیندازیم که چگونه می‌توانید این نشت‌های حافظه را اشکال زدایی کنید و حتی بهتر، چگونه می‌توانید کد خود را طراحی کنید تا دفعه بعد از آنها جلوگیری کنید.

الگوی مشکوک

اخیراً هنگام شروع کار بر روی Squoosh، متوجه الگوی جالبی در بسته‌بندی کدک C++ نشدم. بیایید نگاهی به پوشش ImageQuant به عنوان مثال بیندازیم (کاهش داده شده تا فقط قسمت‌های ایجاد و توزیع شیء را نشان دهد):

liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;

RawImage quantize(std::string rawimage,
                  int image_width,
                  int image_height,
                  int num_colors,
                  float dithering) {
  const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
  int size = image_width * image_height;

  attr = liq_attr_create();
  image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_set_max_colors(attr, num_colors);
  liq_image_quantize(image, attr, &res);
  liq_set_dithering_level(res, dithering);
  uint8_t* image8bit = (uint8_t*)malloc(size);
  result = (uint8_t*)malloc(size * 4);

  // …

  free(image8bit);
  liq_result_destroy(res);
  liq_image_destroy(image);
  liq_attr_destroy(attr);

  return {
    val(typed_memory_view(image_width * image_height * 4, result)),
    image_width,
    image_height
  };
}

void free_result() {
  free(result);
}

جاوا اسکریپت (خوب، TypeScript):

export async function process(data: ImageData, opts: QuantizeOptions) {
  if (!emscriptenModule) {
    emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
  }
  const module = await emscriptenModule;

  const result = module.quantize(/* … */);

  module.free_result();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
}

آیا مشکلی را تشخیص می دهید؟ نکته: پس از استفاده رایگان است، اما در جاوا اسکریپت!

در Emscripten، typed_memory_view یک جاوا اسکریپت Uint8Array برمی‌گرداند که توسط بافر حافظه WebAssembly (Wasm) پشتیبانی می‌شود و byteOffset و byteLength روی اشاره‌گر و طول داده شده تنظیم شده‌اند. نکته اصلی این است که این یک نمای TypedArray در بافر حافظه WebAssembly است، نه یک کپی متعلق به جاوا اسکریپت از داده ها.

هنگامی که free_result از جاوا اسکریپت فراخوانی می کنیم، آن نیز به نوبه خود، یک تابع استاندارد C را free فراخوانی می کند تا این حافظه را به عنوان در دسترس برای هر تخصیص آینده علامت گذاری کند، به این معنی که داده هایی که دیدگاه Uint8Array ما به آن اشاره می کند، می تواند با داده های دلخواه توسط هر فراخوانی آینده در Wasm بازنویسی شود.

یا، برخی از پیاده‌سازی free حتی ممکن است تصمیم بگیرند که بلافاصله حافظه آزاد شده را صفر کنند. free که Emscripten استفاده می کند این کار را نمی کند، اما ما در اینجا به جزئیات پیاده سازی تکیه می کنیم که نمی توان آن را تضمین کرد.

یا حتی اگر حافظه پشت اشاره گر حفظ شود، ممکن است نیاز به تخصیص جدید برای رشد حافظه 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 را فراخوانی نکنیم، ممکن است در برخی مواقع پشتیبانی چند رشته‌ای را به کدک‌های خود اضافه کنیم. در این صورت ممکن است یک رشته کاملاً متفاوت باشد که درست قبل از اینکه بتوانیم آن را شبیه سازی کنیم، داده ها را بازنویسی می کند.

به دنبال اشکالات حافظه

در هر صورت، من تصمیم گرفتم بیشتر بروم و بررسی کنم که آیا این کد در عمل مشکلی را نشان می دهد یا خیر. به نظر می‌رسد این یک فرصت عالی برای آزمایش پشتیبانی جدید (ish) ضد عفونی‌کننده‌های Emscripten است که سال گذشته اضافه شد و در سخنرانی WebAssembly ما در جلسه Chrome Dev Summit ارائه شد:

در این مورد، ما به AddressSanitizer علاقه مند هستیم که می تواند مسائل مختلف مربوط به اشاره گر و حافظه را شناسایی کند. برای استفاده از آن، باید کدک خود را با -fsanitize=address دوباره کامپایل کنیم:

emcc \
  --bind \
  ${OPTIMIZE} \
  --closure 1 \
  -s ALLOW_MEMORY_GROWTH=1 \
  -s MODULARIZE=1 \
  -s 'EXPORT_NAME="imagequant"' \
  -I node_modules/libimagequant \
  -o ./imagequant.js \
  --std=c++11 \
  imagequant.cpp \
  -fsanitize=address \
  node_modules/libimagequant/libimagequant.a

این به طور خودکار بررسی های ایمنی اشاره گر را فعال می کند، اما ما همچنین می خواهیم نشت حافظه احتمالی را پیدا کنیم. از آنجایی که ما از ImageQuant به عنوان کتابخانه به جای برنامه استفاده می کنیم، هیچ "نقطه خروجی" وجود ندارد که Emscripten بتواند به طور خودکار آزاد شدن تمام حافظه را تأیید کند.

درعوض، برای چنین مواردی، LeakSanitizer (که در AddressSanitizer موجود است) توابع __lsan_do_leak_check و __lsan_do_recoverable_leak_check را ارائه می‌کند، که می‌توان هر زمان که انتظار داشتیم تمام حافظه آزاد شود و بخواهیم آن فرض را تأیید کنیم، به صورت دستی فراخوانی می‌شوند. __lsan_do_leak_check برای استفاده در پایان یک برنامه کاربردی در حال اجرا، زمانی که می‌خواهید در صورت شناسایی هرگونه نشتی، فرآیند را متوقف کنید، استفاده می‌شود، در حالی که __lsan_do_recoverable_leak_check برای موارد استفاده کتابخانه‌ای مانند ما مناسب‌تر است، زمانی که می‌خواهید نشت‌ها را در کنسول چاپ کنید، اما برنامه را بدون توجه به اجرا نگه دارید.

بیایید کمک کننده دوم را از طریق Embind در معرض نمایش بگذاریم تا بتوانیم هر زمان که بخواهیم آن را از جاوا اسکریپت فراخوانی کنیم:

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

این گزارشی مانند زیر را در کنسول به ما می دهد:

اسکرین شات از یک پیام

اوه-اوه، چند نشت کوچک وجود دارد، اما stacktrace چندان مفید نیست زیرا همه نام‌های توابع مخدوش هستند. بیایید با یک اطلاعات اولیه اشکال زدایی دوباره کامپایل کنیم تا آنها را حفظ کنیم:

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 بایت" از تابع GenericBindingType RawImage ::toWireType

برخی از قسمت‌های stacktrace همچنان مبهم به نظر می‌رسند زیرا به قسمت‌های داخلی Emscripten اشاره می‌کنند، اما می‌توانیم بگوییم که نشت از تبدیل RawImage به "نوع سیم" (به یک مقدار جاوا اسکریپت) توسط Embind است. در واقع، وقتی به کد نگاه می‌کنیم، می‌بینیم که نمونه‌های RawImage C++ را به جاوا اسکریپت برمی‌گردانیم، اما هرگز آن‌ها را در هر دو طرف آزاد نمی‌کنیم.

به عنوان یادآوری، در حال حاضر هیچ ادغام جمع آوری زباله بین جاوا اسکریپت و WebAssembly وجود ندارد، اگرچه یکی در حال توسعه است . در عوض، پس از اتمام کار با شی، باید به صورت دستی هر حافظه و مخرب‌کننده‌هایی را از سمت جاوا اسکریپت فراخوانی کنید. به طور خاص برای Embind، اسناد رسمی پیشنهاد می‌کنند که یک متد .delete() را در کلاس‌های C++ در معرض نمایش فراخوانی کنید:

کد جاوا اسکریپت باید صراحتاً هر دسته شیء 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
  );
}

نشتی همانطور که انتظار می رفت برطرف می شود.

کشف مشکلات بیشتر با مواد ضدعفونی کننده

ساخت دیگر کدک‌های Squoosh با ضدعفونی‌کننده‌ها هم مشکلات مشابه و هم برخی از مسائل جدید را آشکار می‌کند. به عنوان مثال، من این خطا را در اتصالات MozJPEG دارم:

اسکرین شات از یک پیام

در اینجا، این یک نشت نیست، بلکه ما به خاطره ای خارج از مرزهای اختصاص داده شده می نویسیم.

با کاوش در کد MozJPEG، متوجه می‌شویم که مشکل اینجاست که jpeg_mem_dest - تابعی که برای تخصیص یک مقصد حافظه برای JPEG استفاده می‌کنیم - از مقادیر موجود outbuffer استفاده می‌کند و زمانی که آنها غیر صفر هستند، outsize استفاده می‌کند :

if (*outbuffer == NULL || *outsize == 0) {
  /* Allocate initial buffer */
  dest->newbuffer = *outbuffer = (unsigned char *) malloc(OUTPUT_BUF_SIZE);
  if (dest->newbuffer == NULL)
    ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10);
  *outsize = OUTPUT_BUF_SIZE;
}

با این حال، ما آن را بدون مقداردهی اولیه هیچ یک از آن متغیرها فراخوانی می کنیم، به این معنی که MozJPEG نتیجه را در یک آدرس حافظه بالقوه تصادفی می نویسد که اتفاقاً در آن متغیرها در زمان فراخوانی ذخیره می شود!

uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);

صفر کردن هر دو متغیر قبل از فراخوانی این مشکل را حل می کند و اکنون کد به جای آن به بررسی نشت حافظه می رسد. خوشبختانه، بررسی با موفقیت انجام شد، که نشان می دهد ما هیچ گونه نشتی در این کدک نداریم.

مسائل مربوط به حالت مشترک

... یا ما؟

ما می دانیم که پیوندهای کدک ما مقداری از حالت را ذخیره می کند و همچنین منجر به متغیرهای ثابت جهانی می شود و MozJPEG ساختارهای پیچیده ای دارد.

uint8_t* last_result;
struct jpeg_compress_struct cinfo;

val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
  // …
}

چه می‌شود اگر برخی از آن‌ها در اولین اجرا به‌طور تنبلی مقداردهی اولیه شوند، و سپس به‌طور نادرست در اجراهای بعدی دوباره استفاده شوند؟ سپس یک تماس با یک ضد عفونی کننده آنها را به عنوان مشکل ساز گزارش نمی کند.

بیایید سعی کنیم تصویر را چند بار با کلیک کردن تصادفی روی سطوح مختلف کیفیت در رابط کاربری پردازش کنیم. در واقع، اکنون گزارش زیر را دریافت می کنیم:

اسکرین شات از یک پیام

262144 بایت—به نظر می رسد که کل تصویر نمونه از 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 به عنوان مثال استفاده خواهم کرد، اما قوانین refactoring مشابه برای همه کدک ها و همچنین سایر پایگاه های کد مشابه اعمال می شود.

اول از همه، بیایید مشکل استفاده پس از رایگان را از ابتدای پست برطرف کنیم. برای این کار، باید داده ها را از نمای WebAssembly-backed قبل از علامت گذاری به عنوان رایگان در سمت جاوا اسکریپت کلون کنیم:

  // 

  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++ wrapper را تغییر می دهیم تا مطمئن شویم که هر فراخوانی به تابع داده های خود را با استفاده از متغیرهای محلی مدیریت می کند. سپس، می‌توانیم امضای تابع 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);
}

اما، از آنجایی که ما در حال حاضر از Embind در Emscripten برای تعامل با جاوا اسکریپت استفاده می‌کنیم، ممکن است API را با پنهان کردن کامل جزئیات مدیریت حافظه C++ ایمن‌تر کنیم!

برای آن، اجازه دهید بخش new Uint8ClampedArray(…) را از جاوا اسکریپت به سمت C++ با Embind منتقل کنیم. سپس، می‌توانیم از آن برای کلون کردن داده‌ها در حافظه جاوا اسکریپت حتی قبل از بازگشت از تابع استفاده کنیم:

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

این همچنین به این معنی است که ما دیگر نیازی به اتصال free_result سفارشی در سمت C++ نداریم:

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 .

غذای آماده

چه درس‌هایی می‌توانیم یاد بگیریم و چه درس‌هایی را از این refactoring به اشتراک بگذاریم که می‌تواند در سایر پایگاه‌های کد اعمال شود؟

  • از نماهای حافظه ای که توسط WebAssembly پشتیبانی می شود - مهم نیست که از کدام زبان ساخته شده است - فراتر از یک فراخوان استفاده نکنید. شما نمی توانید به زنده ماندن آنها بیشتر از این تکیه کنید و نمی توانید این اشکالات را با روش های معمولی پیدا کنید، بنابراین اگر نیاز به ذخیره داده ها برای بعد دارید، آن را در سمت جاوا اسکریپت کپی کنید و در آنجا ذخیره کنید.
  • در صورت امکان، به جای کار کردن مستقیم با اشاره گرهای خام، از یک زبان مدیریت حافظه ایمن یا حداقل از نوع پوشش های امن استفاده کنید. این شما را از اشکالات موجود در مرز JavaScript ↔ WebAssembly نجات نمی دهد، اما حداقل سطح اشکالات را کاهش می دهد که توسط کد زبان ثابت وجود دارند.
  • مهم نیست از چه زبانی استفاده می‌کنید، کد را با ضدعفونی‌کننده‌ها در طول توسعه اجرا کنید—آنها می‌توانند نه تنها مشکلات کد زبان استاتیک، بلکه برخی مشکلات را در سراسر مرز JavaScript ↔ WebAssembly، مانند فراموش کردن فراخوانی .delete() یا ارسال نشانگرهای نامعتبر از سمت جاوا اسکریپت، کمک کنند.
  • در صورت امکان، از قرار دادن داده ها و اشیاء مدیریت نشده از WebAssembly به طور کلی در معرض جاوا اسکریپت خودداری کنید. جاوا اسکریپت یک زبان جمع آوری زباله است و مدیریت دستی حافظه در آن رایج نیست. این را می توان نشت انتزاعی از مدل حافظه زبانی در نظر گرفت که WebAssembly شما از آن ساخته شده است، و مدیریت نادرست به راحتی در یک پایگاه کد جاوا اسکریپت نادیده گرفته می شود.
  • این ممکن است بدیهی باشد، اما، مانند هر پایگاه کد دیگری، از ذخیره حالت تغییرپذیر در متغیرهای سراسری خودداری کنید. شما نمی‌خواهید مشکلات مربوط به استفاده مجدد از آن را در فراخوان‌ها یا حتی رشته‌های مختلف اشکال‌زدایی کنید، بنابراین بهتر است تا حد امکان آن را مستقل نگه دارید.
،

در حالی که جاوا اسکریپت در پاکسازی پس از خود نسبتاً بخشنده است، زبان های ایستا قطعاً ...

اینگوار استپانیان
Ingvar Stepanyan

Squoosh.app یک PWA است که نشان می دهد کدک ها و تنظیمات مختلف تصویر چقدر می توانند اندازه فایل تصویر را بدون تأثیر قابل توجهی بر کیفیت بهبود دهند. با این حال، این همچنین یک نسخه ی نمایشی فنی است که نشان می دهد چگونه می توانید کتابخانه های نوشته شده در C++ یا Rust را بگیرید و آنها را به وب بیاورید.

توانایی انتقال کد از اکوسیستم‌های موجود بسیار ارزشمند است، اما تفاوت‌های کلیدی بین این زبان‌های ثابت و جاوا اسکریپت وجود دارد. یکی از آنها در رویکردهای متفاوت آنها به مدیریت حافظه است.

در حالی که جاوا اسکریپت در پاکسازی پس از خود نسبتاً بخشنده است، چنین زبان های ایستا قطعاً چنین نیستند. شما باید صریحاً یک حافظه اختصاص داده شده جدید بخواهید و واقعاً باید مطمئن شوید که پس از آن آن را پس داده اید و دیگر هرگز از آن استفاده نکنید. اگر این اتفاق نیفتد، شما نشت می کنید... و در واقع به طور منظم اتفاق می افتد. بیایید نگاهی بیندازیم که چگونه می‌توانید این نشت‌های حافظه را اشکال زدایی کنید و حتی بهتر، چگونه می‌توانید کد خود را طراحی کنید تا دفعه بعد از آنها جلوگیری کنید.

الگوی مشکوک

اخیراً هنگام شروع کار بر روی Squoosh، متوجه الگوی جالبی در بسته‌بندی کدک C++ نشدم. بیایید نگاهی به پوشش ImageQuant به عنوان مثال بیندازیم (کاهش داده شده تا فقط قسمت‌های ایجاد و توزیع شیء را نشان دهد):

liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;

RawImage quantize(std::string rawimage,
                  int image_width,
                  int image_height,
                  int num_colors,
                  float dithering) {
  const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
  int size = image_width * image_height;

  attr = liq_attr_create();
  image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_set_max_colors(attr, num_colors);
  liq_image_quantize(image, attr, &res);
  liq_set_dithering_level(res, dithering);
  uint8_t* image8bit = (uint8_t*)malloc(size);
  result = (uint8_t*)malloc(size * 4);

  // …

  free(image8bit);
  liq_result_destroy(res);
  liq_image_destroy(image);
  liq_attr_destroy(attr);

  return {
    val(typed_memory_view(image_width * image_height * 4, result)),
    image_width,
    image_height
  };
}

void free_result() {
  free(result);
}

جاوا اسکریپت (خوب، TypeScript):

export async function process(data: ImageData, opts: QuantizeOptions) {
  if (!emscriptenModule) {
    emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
  }
  const module = await emscriptenModule;

  const result = module.quantize(/* … */);

  module.free_result();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
}

آیا مشکلی را تشخیص می دهید؟ نکته: پس از استفاده رایگان است، اما در جاوا اسکریپت!

در Emscripten، typed_memory_view یک جاوا اسکریپت Uint8Array برمی‌گرداند که توسط بافر حافظه WebAssembly (Wasm) پشتیبانی می‌شود و byteOffset و byteLength روی اشاره‌گر و طول داده شده تنظیم شده‌اند. نکته اصلی این است که این یک نمای TypedArray در بافر حافظه WebAssembly است، نه یک کپی متعلق به جاوا اسکریپت از داده ها.

هنگامی که free_result از جاوا اسکریپت فراخوانی می کنیم، آن نیز به نوبه خود، یک تابع استاندارد C را free فراخوانی می کند تا این حافظه را به عنوان در دسترس برای هر تخصیص آینده علامت گذاری کند، به این معنی که داده هایی که دیدگاه Uint8Array ما به آن اشاره می کند، می تواند با داده های دلخواه توسط هر فراخوانی آینده در Wasm بازنویسی شود.

یا، برخی از پیاده‌سازی free حتی ممکن است تصمیم بگیرند که بلافاصله حافظه آزاد شده را صفر کنند. free که Emscripten استفاده می کند این کار را نمی کند، اما ما در اینجا به جزئیات پیاده سازی تکیه می کنیم که نمی توان آن را تضمین کرد.

یا حتی اگر حافظه پشت اشاره گر حفظ شود، ممکن است نیاز به تخصیص جدید برای رشد حافظه 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 را فراخوانی نکنیم، ممکن است در برخی مواقع پشتیبانی چند رشته‌ای را به کدک‌های خود اضافه کنیم. در این صورت ممکن است یک رشته کاملاً متفاوت باشد که درست قبل از اینکه بتوانیم آن را شبیه سازی کنیم، داده ها را بازنویسی می کند.

به دنبال اشکالات حافظه

در هر صورت، من تصمیم گرفتم بیشتر بروم و بررسی کنم که آیا این کد در عمل مشکلی را نشان می دهد یا خیر. به نظر می‌رسد این یک فرصت عالی برای آزمایش پشتیبانی جدید (ish) ضد عفونی‌کننده‌های Emscripten است که سال گذشته اضافه شد و در سخنرانی WebAssembly ما در جلسه Chrome Dev Summit ارائه شد:

در این مورد، ما به AddressSanitizer علاقه مند هستیم که می تواند مسائل مختلف مربوط به اشاره گر و حافظه را شناسایی کند. برای استفاده از آن، باید کدک خود را با -fsanitize=address دوباره کامپایل کنیم:

emcc \
  --bind \
  ${OPTIMIZE} \
  --closure 1 \
  -s ALLOW_MEMORY_GROWTH=1 \
  -s MODULARIZE=1 \
  -s 'EXPORT_NAME="imagequant"' \
  -I node_modules/libimagequant \
  -o ./imagequant.js \
  --std=c++11 \
  imagequant.cpp \
  -fsanitize=address \
  node_modules/libimagequant/libimagequant.a

این به طور خودکار بررسی های ایمنی اشاره گر را فعال می کند، اما ما همچنین می خواهیم نشت حافظه احتمالی را پیدا کنیم. از آنجایی که ما از ImageQuant به عنوان کتابخانه به جای برنامه استفاده می کنیم، هیچ "نقطه خروجی" وجود ندارد که Emscripten بتواند به طور خودکار آزاد شدن تمام حافظه را تأیید کند.

درعوض، برای چنین مواردی، LeakSanitizer (که در AddressSanitizer موجود است) توابع __lsan_do_leak_check و __lsan_do_recoverable_leak_check را ارائه می‌کند، که می‌توان هر زمان که انتظار داشتیم تمام حافظه آزاد شود و بخواهیم آن فرض را تأیید کنیم، به صورت دستی فراخوانی می‌شوند. __lsan_do_leak_check به معنای استفاده در انتهای یک برنامه در حال اجرا است ، هنگامی که می خواهید روند را در صورت تشخیص هرگونه نشت سقط کنید ، در حالی که __lsan_do_recoverable_leak_check مناسب تر برای استفاده از کتابخانه ها مانند ما است ، وقتی می خواهید نشتی را به کنسول چاپ کنید ، اما برنامه را با توجه به اجرای آن ادامه دهید.

بیایید آن یاور دوم را از طریق Embind در معرض دید قرار دهیم تا بتوانیم در هر زمان از JavaScript تماس بگیریم:

#include <sanitizer/lsan_interface.h>

// …

void free_result() {
  free(result);
}

EMSCRIPTEN_BINDINGS(my_module) {
  function("zx_quantize", &zx_quantize);
  function("version", &version);
  function("free_result", &free_result);
  function("doLeakCheck", &__lsan_do_recoverable_leak_check);
}

و هنگامی که با تصویر تمام شد ، آن را از سمت جاوا اسکریپت فراخوانی کنید. انجام این کار از طرف JavaScript ، به جای C ++ ، به اطمینان حاصل می شود که تمام دامنه ها از آن خارج شده اند و تمام اشیاء 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
  );
}

این گزارشی مانند موارد زیر در کنسول به ما می دهد:

تصویر یک پیام

اوه اوه ، برخی از نشت های کوچک وجود دارد ، اما StackTrace خیلی مفید نیست زیرا تمام نام های عملکردی در آن قرار گرفته اند. بیایید با یک اطلاعات اشکال زدایی اساسی برای حفظ آنها دوباره جمع شویم:

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 genericbindingtype آمده است :: عملکرد TowireType

برخی از قسمت های StackTrace هنوز هم مبهم به نظر می رسند ، زیرا به داخلی های داخلی اشاره می کنند ، اما می توانیم بگوییم که نشت از تبدیل RawImage به "نوع سیم" (به مقدار جاوا اسکریپت) توسط Embind می رسد. در واقع ، وقتی به کد نگاه می کنیم ، می بینیم که نمونه های RawImage C ++ را به JavaScript برمی گردانیم ، اما هرگز آنها را از هر طرف آزاد نمی کنیم.

به عنوان یک یادآوری ، در حال حاضر هیچ ادغام جمع آوری زباله بین JavaScript و WebAnsembly وجود ندارد ، اگرچه یکی در حال توسعه است . درعوض ، شما باید هر خاطره ای را به صورت دستی آزاد کنید و پس از اتمام کار با شیء ، از طرف جاوا اسکریپت تماس بگیرید. برای EMBIND به طور خاص ، اسناد رسمی پیشنهاد می کنند که یک روش .delete() را در کلاسهای C ++ در معرض تماس بگیرید:

کد JavaScript باید صریحاً هر نوع دستگیره C ++ را که دریافت کرده است حذف کند ، یا پشته emscripten به طور نامحدود رشد می کند.

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

در واقع ، هنگامی که ما این کار را در JavaScript برای کلاس خود انجام می دهیم:

  // 

  const result = opts.zx
    ? module.zx_quantize(data.data, data.width, data.height, opts.dither)
    : module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);

  module.free_result();
  result.delete();
  module.doLeakCheck();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
}

نشت همانطور که انتظار می رفت از بین می رود.

کشف مسائل بیشتر با ضد عفونی کننده

ساخت سایر کدک های اسکواش با ضد عفونی کننده ، هم مشابه و هم برخی از موضوعات جدید را نشان می دهد. به عنوان مثال ، من این خطا را در اتصال Mozjpeg کردم:

تصویر یک پیام

در اینجا ، این یک نشت نیست ، اما ما در خارج از مرزهای اختصاص یافته به یک خاطره می نویسیم

با حفر کد Mozjpeg ، می فهمیم که مشکل در اینجا این است که jpeg_mem_dest عملکردی که ما برای اختصاص یک مقصد حافظه برای JPEG— استفاده می کنیم ، در صورت عدم صفر ، مقادیر موجود از outbuffer را انجام می دهد و outsize .

if (*outbuffer == NULL || *outsize == 0) {
  /* Allocate initial buffer */
  dest->newbuffer = *outbuffer = (unsigned char *) malloc(OUTPUT_BUF_SIZE);
  if (dest->newbuffer == NULL)
    ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10);
  *outsize = OUTPUT_BUF_SIZE;
}

با این حال ، ما آن را بدون شروع هر یک از این متغیرها ، به این معنی است که Mozjpeg نتیجه را در یک آدرس حافظه بالقوه تصادفی می نویسد که اتفاقاً در زمان تماس در آن متغیرها ذخیره می شود!

uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);

صفر شروع هر دو متغیر قبل از فراخوانی این مسئله را حل می کند ، و اکنون کد به جای آن به بررسی نشت حافظه می رسد. خوشبختانه ، چک با موفقیت می گذرد و نشان می دهد که ما هیچ نشتی در این کدک نداریم.

مسائل مربوط به دولت مشترک

… یا ما؟

ما می دانیم که اتصالات کدک ما برخی از ایالت ها را ذخیره می کند و همچنین منجر به متغیرهای استاتیک جهانی می شود ، و Mozjpeg دارای ساختارهای پیچیده ای است.

uint8_t* last_result;
struct jpeg_compress_struct cinfo;

val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
  // …
}

چه می شود اگر برخی از آنها در اولین اجرا تنبل شوند و سپس به طور نادرست در دوران آینده مورد استفاده مجدد قرار بگیرند؟ سپس یک تماس واحد با ضد عفونی کننده آنها را به عنوان مشکل ساز گزارش نمی دهد.

بیایید با کلیک تصادفی در سطوح مختلف کیفیت در 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);
}

من می توانم یک به یک شکار آن اشکالات حافظه را ادامه دهم ، اما فکر می کنم اکنون به اندازه کافی واضح است که رویکرد فعلی برای مدیریت حافظه منجر به برخی از مسائل سیستماتیک نامطبوع می شود.

برخی از آنها را می توان بلافاصله توسط ضد عفونی کننده گرفتار کرد. برخی دیگر نیاز به ترفندهای پیچیده ای برای گرفتار شدن دارند. سرانجام ، مواردی مانند ابتدای پست وجود دارد که ، همانطور که از سیاههها می بینیم ، به هیچ وجه توسط ضد عفونی کننده گرفتار نمی شوند. دلیل این امر این است که استفاده نادرست واقعی در سمت جاوا اسکریپت اتفاق می افتد ، که در آن ضد عفونی کننده هیچ دیداری ندارد. این موضوعات فقط در تولید یا بعد از تغییرات به ظاهر نامربوط در کد در آینده آشکار می شوند.

ساخت یک بسته بندی ایمن

بیایید چند قدم به عقب برداریم و در عوض با تغییر ساختار کد به روشی ایمن تر ، همه این مشکلات را برطرف کنیم. من دوباره به عنوان نمونه از Prapper ImageQuant استفاده می کنم ، اما قوانین مشابه اصلاح مجدد در مورد همه کدک ها و همچنین سایر کد های مشابه اعمال می شود.

اول از همه ، بیایید مسئله استفاده-پس از آن را از ابتدای پست برطرف کنیم. برای این کار ، ما باید قبل از علامت گذاری به صورت رایگان در سمت JavaScript ، داده ها را از نمای تحت حمایت WebAssembly کلون کنیم:

  // 

  const result = /*  */;

  const imgData = new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );

  module.free_result();
  result.delete();
  module.doLeakCheck();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
  return imgData;
}

حال ، بیایید اطمینان حاصل کنیم که ما هیچ وضعیتی را در متغیرهای جهانی بین دعوت ها به اشتراک نمی گذاریم. این هر دو برخی از مواردی را که قبلاً دیده ایم برطرف می کند ، و همچنین استفاده از کدک های ما در یک محیط چند رشته ای در آینده را آسان تر می کند.

برای انجام این کار ، ما بسته بندی C ++ را دوباره اصلاح می کنیم تا اطمینان حاصل کنیم که هر تماس به عملکرد با استفاده از متغیرهای محلی داده های خود را مدیریت می کند. سپس ، ما می توانیم امضای عملکرد free_result خود را تغییر دهیم تا نشانگر را بپذیرد:

liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;

RawImage quantize(std::string rawimage,
                  int image_width,
                  int image_height,
                  int num_colors,
                  float dithering) {
  const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
  int size = image_width * image_height;

  attr = liq_attr_create();
  image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_attr* attr = liq_attr_create();
  liq_image* image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_set_max_colors(attr, num_colors);
  liq_result* res = nullptr;
  liq_image_quantize(image, attr, &res);
  liq_set_dithering_level(res, dithering);
  uint8_t* image8bit = (uint8_t*)malloc(size);
  result = (uint8_t*)malloc(size * 4);
  uint8_t* result = (uint8_t*)malloc(size * 4);

  // 
}

void free_result() {
void free_result(uint8_t *result) {
  free(result);
}

اما ، از آنجا که ما در حال حاضر از Empind در Emscripten برای تعامل با JavaScript استفاده می کنیم ، ممکن است API را با مخفی کردن جزئیات مدیریت حافظه C ++ به طور کلی ایمن تر کنیم!

برای این کار ، بیایید قسمت new Uint8ClampedArray(…) را از JavaScript به سمت C ++ با Embind منتقل کنیم. سپس ، ما می توانیم از آن برای کلون کردن داده ها در حافظه JavaScript حتی قبل از بازگشت از عملکرد استفاده کنیم:

class RawImage {
 public:
  val buffer;
  int width;
  int height;

  RawImage(val b, int w, int h) : buffer(b), width(w), height(h) {}
};
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");

RawImage quantize(/*  */) {
val quantize(/*  */) {
  // 
  return {
    val(typed_memory_view(image_width * image_height * 4, result)),
    image_width,
    image_height
  };
  val js_result = Uint8ClampedArray.new_(typed_memory_view(
    image_width * image_height * 4,
    result
  ));
  free(result);
  return js_result;
}

توجه داشته باشید که چگونه ، با یک تغییر واحد ، ما هر دو اطمینان می دهیم که آرایه بایت حاصل متعلق به JavaScript است و از حافظه WebAssembly پشتیبانی نمی شود و از بسته بندی RawImage که قبلاً درز شده است نیز خلاص می شویم.

اکنون JavaScript دیگر نیازی به نگرانی در مورد آزاد کردن داده ها نیست و می تواند از نتیجه مانند هر شیء جمع آوری شده زباله استفاده کند:

  // 

  const result = /*  */;

  const imgData = new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );

  module.free_result();
  result.delete();
  // module.doLeakCheck();

  return imgData;
  return new ImageData(result, result.width, result.height);
}

این همچنین بدان معنی است که ما دیگر نیازی به اتصال free_result سفارشی در سمت C ++ نداریم:

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 ++ .

غذای آماده

چه درسهایی را می توانیم از این بازپرداخت که می تواند برای سایر پایگاه های کد استفاده شود ، یاد بگیریم و به اشتراک بگذاریم؟

  • از نمای حافظه با پشتیبانی WebAssembly استفاده نکنید - هیچ ماده ای از کدام زبان ساخته شده است - از یک دعوت واحد استفاده کنید. شما نمی توانید بیش از این به آنها زنده بمانید ، و قادر نخواهید بود این اشکالات را به وسیله معمولی بدست آورید ، بنابراین اگر نیاز به ذخیره داده ها برای بعداً دارید ، آن را در سمت JavaScript کپی کرده و آن را در آنجا ذخیره کنید.
  • در صورت امکان ، به جای اینکه مستقیماً روی نشانگرهای خام کار کنید ، از یک زبان مدیریت حافظه ایمن یا حداقل بسته های نوع ایمن استفاده کنید. این امر شما را از اشکالات موجود در مرز JavaScript ↔ WebAssembly نجات نمی دهد ، اما حداقل باعث می شود سطح اشکالات مربوط به کد زبان استاتیک کاهش یابد.
  • مهم نیست که از کدام زبانی استفاده می کنید ، کد را با ضد عفونی کننده در حین توسعه اجرا کنید - آنها می توانند نه تنها مشکلات موجود در کد زبان استاتیک ، بلکه برخی از مسائل مربوط به مرز JavaScript ↔ WebAnsembly ، مانند فراموش کردن تماس با .delete() یا عبور در شاعران نامعتبر را از سمت جاوا اسکریپت به شما کمک کنند.
  • در صورت امکان ، از افشای داده ها و اشیاء بدون کنترل از WebAssembly تا JavaScript به طور کلی خودداری کنید. JavaScript یک زبان جمع آوری زباله است و مدیریت حافظه دستی در آن رایج نیست. این را می توان نشت انتزاع از مدل حافظه زبانی که WebAnsembly شما از آن ساخته شده است ، در نظر گرفت و مدیریت نادرست به راحتی در یک پایگاه کد JavaScript نادیده می گیرد.
  • این ممکن است واضح باشد ، اما ، مانند هر پایگاه کد دیگر ، از ذخیره وضعیت قابل تغییر در متغیرهای جهانی خودداری کنید. شما نمی خواهید با استفاده مجدد از آن در میان دعوت های مختلف یا حتی موضوعات ، مشکلات را اشکال زدایی کنید ، بنابراین بهتر است آن را تا حد امکان خود حفظ کنید.
،

در حالی که JavaScript در تمیز کردن بعد از خودش نسبتاً بخشنده است ، اما زبانهای استاتیک قطعاً ...

اینگوار استپانیان
Ingvar Stepanyan

Squoosh.App یک PWA است که نشان می دهد چقدر کدک ها و تنظیمات مختلف تصویر می توانند اندازه فایل تصویر را بدون تأثیر قابل توجهی بر کیفیت بهبود بخشند. با این حال ، این یک نسخه ی نمایشی فنی است که نشان می دهد چگونه می توانید کتابخانه ها را به صورت C ++ یا Rust نوشته و آنها را به وب بیاورید.

قادر به ارسال کد از اکوسیستم های موجود فوق العاده ارزشمند است ، اما تفاوتهای اساسی بین آن زبانهای استاتیک و جاوا اسکریپت وجود دارد. یکی از این موارد در رویکردهای مختلف خود برای مدیریت حافظه است.

در حالی که JavaScript در تمیز کردن بعد از خودش نسبتاً بخشنده است ، چنین زبانهای استاتیک قطعاً اینگونه نیستند. شما باید صریحاً از یک حافظه اختصاصی جدید بخواهید و واقعاً باید مطمئن شوید که پس از آن به آن بازگردید و دیگر هرگز از آن استفاده نکنید. اگر این اتفاق نیفتد ، نشت می کنید ... و در واقع به طور منظم اتفاق می افتد. بیایید نگاهی بیندازیم که چگونه می توانید آن نشت حافظه را اشکال زدایی کنید و حتی بهتر ، چگونه می توانید کد خود را طراحی کنید تا دفعه دیگر از آنها جلوگیری شود.

الگوی مشکوک

به تازگی ، در حالی که شروع به کار روی Squoosh می کنم ، من نمی توانم به الگوی جالب توجه در بسته های کدک C ++ توجه کنم. بیایید به عنوان نمونه نگاهی به یک بسته بندی ImageQuant نگاهی بیندازیم (برای نشان دادن فقط قطعات ایجاد شیء و DealLocation کاهش یافته است):

liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;

RawImage quantize(std::string rawimage,
                  int image_width,
                  int image_height,
                  int num_colors,
                  float dithering) {
  const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
  int size = image_width * image_height;

  attr = liq_attr_create();
  image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_set_max_colors(attr, num_colors);
  liq_image_quantize(image, attr, &res);
  liq_set_dithering_level(res, dithering);
  uint8_t* image8bit = (uint8_t*)malloc(size);
  result = (uint8_t*)malloc(size * 4);

  // …

  free(image8bit);
  liq_result_destroy(res);
  liq_image_destroy(image);
  liq_attr_destroy(attr);

  return {
    val(typed_memory_view(image_width * image_height * 4, result)),
    image_width,
    image_height
  };
}

void free_result() {
  free(result);
}

JavaScript (خوب ، TypeScript):

export async function process(data: ImageData, opts: QuantizeOptions) {
  if (!emscriptenModule) {
    emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
  }
  const module = await emscriptenModule;

  const result = module.quantize(/* … */);

  module.free_result();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
}

آیا مسئله ای را مشاهده می کنید؟ نکته: این بدون استفاده است ، اما در JavaScript!

در emscriptten ، typed_memory_view یک JavaScript Uint8Array را که توسط بافر حافظه WebAssembly (WASM) پشتیبانی می شود ، با byteOffset و تنظیم بایت و byteLength تنظیم می کند. نکته اصلی این است که این یک نمای TypedArray به یک بافر حافظه WebAssembly است ، نه یک کپی متعلق به JavaScript از داده ها.

هنگامی که ما از JavaScript free_result تماس می گیریم ، به نوبه خود ، یک عملکرد C استاندارد را free می نامد تا این حافظه را برای هرگونه تخصیص آینده در دسترس قرار دهد ، این بدان معنی است که داده هایی که نمای Uint8Array ما به آن اشاره می کند ، می تواند با داده های خودسرانه توسط هر تماس آینده به WASM بازنویسی شود.

یا ، برخی از اجرای free حتی ممکن است تصمیم بگیرند که حافظه آزاد شده بلافاصله صفر را پر کنند. free که Emscripten از آن استفاده می کند این کار را نمی کند ، اما ما در اینجا به جزئیات اجرای تکیه می کنیم که نمی توان تضمین کرد.

یا حتی اگر حافظه پشت نشانگر حفظ شود ، ممکن است تخصیص جدید برای رشد حافظه WebAnsembly نیاز داشته باشد. هنگامی که WebAssembly.Memory یا از طریق API JavaScript یا 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 تماس نگیریم ، در بعضی مواقع ممکن است پشتیبانی چندتایی را به کدک های خود اضافه کنیم. در این حالت می تواند یک موضوع کاملاً متفاوت باشد که درست قبل از اینکه بتوانیم آن را کلون کنیم ، داده ها را بازنویسی می کند.

به دنبال اشکالات حافظه

در مورد ، من تصمیم گرفتم که بیشتر بروم و بررسی کنم که آیا این کد در عمل هیچ مشکلی را نشان می دهد یا خیر. به نظر می رسد این یک فرصت مناسب برای امتحان کردن پشتیبانی از ضد عفونی کننده های جدید (ISH) Emscripten است که سال گذشته اضافه شد و در گفتگوی WebAnsembly ما در اجلاس Dev Chrome ارائه شده است:

در این حالت ، ما به آدرس دهنده دهنده علاقه مندیم ، که می تواند موضوعات مختلف مربوط به نشانگر و حافظه را تشخیص دهد. برای استفاده از آن ، ما باید کدک خود را با -fsanitize=address دوباره جمع کنیم:

emcc \
  --bind \
  ${OPTIMIZE} \
  --closure 1 \
  -s ALLOW_MEMORY_GROWTH=1 \
  -s MODULARIZE=1 \
  -s 'EXPORT_NAME="imagequant"' \
  -I node_modules/libimagequant \
  -o ./imagequant.js \
  --std=c++11 \
  imagequant.cpp \
  -fsanitize=address \
  node_modules/libimagequant/libimagequant.a

این به طور خودکار بررسی های ایمنی اشاره گر را فعال می کند ، اما ما همچنین می خواهیم نشت حافظه بالقوه پیدا کنیم. از آنجا که ما از ImageQuant به عنوان یک کتابخانه به جای یک برنامه استفاده می کنیم ، هیچ "نقطه خروج" وجود ندارد که در آن Emscripten به طور خودکار تأیید کند که تمام حافظه آزاد شده است.

در عوض ، برای چنین مواردی ، LeaksAnitizer (موجود در آدرس دهنده) توابع __lsan_do_leak_check و __lsan_do_recoverable_leak_check را فراهم می کند ، که هر وقت انتظار داریم همه حافظه آزاد شود و بخواهیم آن فرض را تأیید کنیم ، می توان به صورت دستی فراخوانی کرد. __lsan_do_leak_check به معنای استفاده در انتهای یک برنامه در حال اجرا است ، هنگامی که می خواهید روند را در صورت تشخیص هرگونه نشت سقط کنید ، در حالی که __lsan_do_recoverable_leak_check مناسب تر برای استفاده از کتابخانه ها مانند ما است ، وقتی می خواهید نشتی را به کنسول چاپ کنید ، اما برنامه را با توجه به اجرای آن ادامه دهید.

بیایید آن یاور دوم را از طریق Embind در معرض دید قرار دهیم تا بتوانیم در هر زمان از JavaScript تماس بگیریم:

#include <sanitizer/lsan_interface.h>

// …

void free_result() {
  free(result);
}

EMSCRIPTEN_BINDINGS(my_module) {
  function("zx_quantize", &zx_quantize);
  function("version", &version);
  function("free_result", &free_result);
  function("doLeakCheck", &__lsan_do_recoverable_leak_check);
}

و هنگامی که با تصویر تمام شد ، آن را از سمت جاوا اسکریپت فراخوانی کنید. انجام این کار از طرف JavaScript ، به جای C ++ ، به اطمینان حاصل می شود که تمام دامنه ها از آن خارج شده اند و تمام اشیاء 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
  );
}

این گزارشی مانند موارد زیر در کنسول به ما می دهد:

تصویر یک پیام

اوه اوه ، برخی از نشت های کوچک وجود دارد ، اما StackTrace خیلی مفید نیست زیرا تمام نام های عملکردی در آن قرار گرفته اند. بیایید با یک اطلاعات اشکال زدایی اساسی برای حفظ آنها دوباره جمع شویم:

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 genericbindingtype آمده است :: عملکرد TowireType

برخی از قسمت های StackTrace هنوز هم مبهم به نظر می رسند ، زیرا به داخلی های داخلی اشاره می کنند ، اما می توانیم بگوییم که نشت از تبدیل RawImage به "نوع سیم" (به مقدار جاوا اسکریپت) توسط Embind می رسد. در واقع ، وقتی به کد نگاه می کنیم ، می بینیم که نمونه های RawImage C ++ را به JavaScript برمی گردانیم ، اما هرگز آنها را از هر طرف آزاد نمی کنیم.

به عنوان یک یادآوری ، در حال حاضر هیچ ادغام جمع آوری زباله بین JavaScript و WebAnsembly وجود ندارد ، اگرچه یکی در حال توسعه است . درعوض ، شما باید هر خاطره ای را به صورت دستی آزاد کنید و پس از اتمام کار با شیء ، از طرف جاوا اسکریپت تماس بگیرید. برای EMBIND به طور خاص ، اسناد رسمی پیشنهاد می کنند که یک روش .delete() را در کلاسهای C ++ در معرض تماس بگیرید:

کد JavaScript باید صریحاً هر نوع دستگیره C ++ را که دریافت کرده است حذف کند ، یا پشته emscripten به طور نامحدود رشد می کند.

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

در واقع ، هنگامی که ما این کار را در JavaScript برای کلاس خود انجام می دهیم:

  // 

  const result = opts.zx
    ? module.zx_quantize(data.data, data.width, data.height, opts.dither)
    : module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);

  module.free_result();
  result.delete();
  module.doLeakCheck();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
}

نشت همانطور که انتظار می رفت از بین می رود.

کشف مسائل بیشتر با ضد عفونی کننده

ساخت سایر کدک های اسکواش با ضد عفونی کننده ، هم مشابه و هم برخی از موضوعات جدید را نشان می دهد. به عنوان مثال ، من این خطا را در اتصال Mozjpeg کردم:

تصویر یک پیام

در اینجا ، این یک نشت نیست ، اما ما در خارج از مرزهای اختصاص یافته به یک خاطره می نویسیم

با حفر کد Mozjpeg ، می فهمیم که مشکل در اینجا این است که jpeg_mem_dest عملکردی که ما برای اختصاص یک مقصد حافظه برای JPEG— استفاده می کنیم ، در صورت عدم صفر ، مقادیر موجود از outbuffer را انجام می دهد و outsize .

if (*outbuffer == NULL || *outsize == 0) {
  /* Allocate initial buffer */
  dest->newbuffer = *outbuffer = (unsigned char *) malloc(OUTPUT_BUF_SIZE);
  if (dest->newbuffer == NULL)
    ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10);
  *outsize = OUTPUT_BUF_SIZE;
}

با این حال ، ما آن را بدون شروع هر یک از این متغیرها ، به این معنی است که Mozjpeg نتیجه را در یک آدرس حافظه بالقوه تصادفی می نویسد که اتفاقاً در زمان تماس در آن متغیرها ذخیره می شود!

uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);

صفر شروع هر دو متغیر قبل از فراخوانی این مسئله را حل می کند ، و اکنون کد به جای آن به بررسی نشت حافظه می رسد. خوشبختانه ، چک با موفقیت می گذرد و نشان می دهد که ما هیچ نشتی در این کدک نداریم.

مسائل مربوط به دولت مشترک

… یا ما؟

ما می دانیم که اتصالات کدک ما برخی از ایالت ها را ذخیره می کند و همچنین منجر به متغیرهای استاتیک جهانی می شود ، و Mozjpeg دارای ساختارهای پیچیده ای است.

uint8_t* last_result;
struct jpeg_compress_struct cinfo;

val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
  // …
}

چه می شود اگر برخی از آنها در اولین اجرا تنبل شوند و سپس به طور نادرست در دوران آینده مورد استفاده مجدد قرار بگیرند؟ سپس یک تماس واحد با ضد عفونی کننده آنها را به عنوان مشکل ساز گزارش نمی دهد.

بیایید با کلیک تصادفی در سطوح مختلف کیفیت در 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);
}

من می توانم یک به یک شکار آن اشکالات حافظه را ادامه دهم ، اما فکر می کنم اکنون به اندازه کافی واضح است که رویکرد فعلی برای مدیریت حافظه منجر به برخی از مسائل سیستماتیک نامطبوع می شود.

برخی از آنها را می توان بلافاصله توسط ضد عفونی کننده گرفتار کرد. برخی دیگر نیاز به ترفندهای پیچیده ای برای گرفتار شدن دارند. سرانجام ، مواردی مانند ابتدای پست وجود دارد که ، همانطور که از سیاههها می بینیم ، به هیچ وجه توسط ضد عفونی کننده گرفتار نمی شوند. دلیل این امر این است که استفاده نادرست واقعی در سمت جاوا اسکریپت اتفاق می افتد ، که در آن ضد عفونی کننده هیچ دیداری ندارد. این موضوعات فقط در تولید یا بعد از تغییرات به ظاهر نامربوط در کد در آینده آشکار می شوند.

ساخت یک بسته بندی ایمن

بیایید چند قدم به عقب برداریم و در عوض با تغییر ساختار کد به روشی ایمن تر ، همه این مشکلات را برطرف کنیم. من دوباره به عنوان نمونه از Prapper ImageQuant استفاده می کنم ، اما قوانین مشابه اصلاح مجدد در مورد همه کدک ها و همچنین سایر کد های مشابه اعمال می شود.

اول از همه ، بیایید مسئله استفاده-پس از آن را از ابتدای پست برطرف کنیم. برای این کار ، ما باید قبل از علامت گذاری به صورت رایگان در سمت JavaScript ، داده ها را از نمای تحت حمایت WebAssembly کلون کنیم:

  // 

  const result = /*  */;

  const imgData = new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );

  module.free_result();
  result.delete();
  module.doLeakCheck();

  return new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );
  return imgData;
}

حال ، بیایید اطمینان حاصل کنیم که ما هیچ وضعیتی را در متغیرهای جهانی بین دعوت ها به اشتراک نمی گذاریم. این هر دو برخی از مواردی را که قبلاً دیده ایم برطرف می کند ، و همچنین استفاده از کدک های ما در یک محیط چند رشته ای در آینده را آسان تر می کند.

برای انجام این کار ، ما بسته بندی C ++ را دوباره اصلاح می کنیم تا اطمینان حاصل کنیم که هر تماس به عملکرد با استفاده از متغیرهای محلی داده های خود را مدیریت می کند. سپس ، ما می توانیم امضای عملکرد free_result خود را تغییر دهیم تا نشانگر را بپذیرد:

liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;

RawImage quantize(std::string rawimage,
                  int image_width,
                  int image_height,
                  int num_colors,
                  float dithering) {
  const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
  int size = image_width * image_height;

  attr = liq_attr_create();
  image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_attr* attr = liq_attr_create();
  liq_image* image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
  liq_set_max_colors(attr, num_colors);
  liq_result* res = nullptr;
  liq_image_quantize(image, attr, &res);
  liq_set_dithering_level(res, dithering);
  uint8_t* image8bit = (uint8_t*)malloc(size);
  result = (uint8_t*)malloc(size * 4);
  uint8_t* result = (uint8_t*)malloc(size * 4);

  // 
}

void free_result() {
void free_result(uint8_t *result) {
  free(result);
}

اما ، از آنجا که ما در حال حاضر از Empind در Emscripten برای تعامل با JavaScript استفاده می کنیم ، ممکن است API را با مخفی کردن جزئیات مدیریت حافظه C ++ به طور کلی ایمن تر کنیم!

برای این کار ، بیایید قسمت new Uint8ClampedArray(…) را از JavaScript به سمت C ++ با Embind منتقل کنیم. سپس ، ما می توانیم از آن برای کلون کردن داده ها در حافظه JavaScript حتی قبل از بازگشت از عملکرد استفاده کنیم:

class RawImage {
 public:
  val buffer;
  int width;
  int height;

  RawImage(val b, int w, int h) : buffer(b), width(w), height(h) {}
};
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");

RawImage quantize(/*  */) {
val quantize(/*  */) {
  // 
  return {
    val(typed_memory_view(image_width * image_height * 4, result)),
    image_width,
    image_height
  };
  val js_result = Uint8ClampedArray.new_(typed_memory_view(
    image_width * image_height * 4,
    result
  ));
  free(result);
  return js_result;
}

توجه داشته باشید که چگونه ، با یک تغییر واحد ، ما هر دو اطمینان می دهیم که آرایه بایت حاصل متعلق به JavaScript است و از حافظه WebAssembly پشتیبانی نمی شود و از بسته بندی RawImage که قبلاً درز شده است نیز خلاص می شویم.

اکنون JavaScript دیگر نیازی به نگرانی در مورد آزاد کردن داده ها نیست و می تواند از نتیجه مانند هر شیء جمع آوری شده زباله استفاده کند:

  // 

  const result = /*  */;

  const imgData = new ImageData(
    new Uint8ClampedArray(result.view),
    result.width,
    result.height
  );

  module.free_result();
  result.delete();
  // module.doLeakCheck();

  return imgData;
  return new ImageData(result, result.width, result.height);
}

این همچنین بدان معنی است که ما دیگر نیازی به اتصال free_result سفارشی در سمت C ++ نداریم:

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 ++ .

غذای آماده

چه درسهایی را می توانیم از این بازپرداخت که می تواند برای سایر پایگاه های کد استفاده شود ، یاد بگیریم و به اشتراک بگذاریم؟

  • از نمای حافظه با پشتیبانی WebAssembly استفاده نکنید - هیچ ماده ای از کدام زبان ساخته شده است - از یک دعوت واحد استفاده کنید. شما نمی توانید بیش از این به آنها زنده بمانید ، و قادر نخواهید بود این اشکالات را به وسیله معمولی بدست آورید ، بنابراین اگر نیاز به ذخیره داده ها برای بعداً دارید ، آن را در سمت JavaScript کپی کرده و آن را در آنجا ذخیره کنید.
  • در صورت امکان ، به جای اینکه مستقیماً روی نشانگرهای خام کار کنید ، از یک زبان مدیریت حافظه ایمن یا حداقل بسته های نوع ایمن استفاده کنید. این امر شما را از اشکالات موجود در مرز JavaScript ↔ WebAssembly نجات نمی دهد ، اما حداقل باعث می شود سطح اشکالات مربوط به کد زبان استاتیک کاهش یابد.
  • مهم نیست که از کدام زبانی استفاده می کنید ، کد را با ضد عفونی کننده در حین توسعه اجرا کنید - آنها می توانند نه تنها مشکلات موجود در کد زبان استاتیک ، بلکه برخی از مسائل مربوط به مرز JavaScript ↔ WebAnsembly ، مانند فراموش کردن تماس با .delete() یا عبور در شاعران نامعتبر را از سمت جاوا اسکریپت به شما کمک کنند.
  • در صورت امکان ، از افشای داده ها و اشیاء بدون کنترل از WebAssembly تا JavaScript به طور کلی خودداری کنید. JavaScript یک زبان جمع آوری زباله است و مدیریت حافظه دستی در آن رایج نیست. این را می توان نشت انتزاع از مدل حافظه زبانی که WebAnsembly شما از آن ساخته شده است ، در نظر گرفت و مدیریت نادرست به راحتی در یک پایگاه کد JavaScript نادیده می گیرد.
  • این ممکن است واضح باشد ، اما ، مانند هر پایگاه کد دیگر ، از ذخیره وضعیت قابل تغییر در متغیرهای جهانی خودداری کنید. شما نمی خواهید با استفاده مجدد از آن در میان دعوت های مختلف یا حتی موضوعات ، مشکلات را اشکال زدایی کنید ، بنابراین بهتر است آن را تا حد امکان خود حفظ کنید.