Khắc phục lỗi rò rỉ bộ nhớ trong WebAssembly bằng Emscripten

Mặc dù JavaScript khá dễ dàng trong việc dọn dẹp, nhưng ngôn ngữ tĩnh chắc chắn thì không...

Squoosh.app là một PWA chỉ minh hoạ số lượng các bộ mã hoá và chế độ cài đặt hình ảnh khác nhau có thể cải thiện kích thước tệp hình ảnh mà không ảnh hưởng đáng kể đến chất lượng. Tuy nhiên, đây cũng là bản minh hoạ kỹ thuật cho thấy cách bạn có thể sử dụng các thư viện viết bằng C++ hoặc Rust rồi đưa lên web.

Việc có thể chuyển mã từ các hệ sinh thái hiện có là vô cùng giá trị, nhưng có một số điểm khác biệt chính giữa các ngôn ngữ tĩnh và JavaScript đó. Một trong số đó là cách tiếp cận khác nhau của chúng đối với việc quản lý bộ nhớ.

Mặc dù JavaScript khá linh hoạt trong việc dọn dẹp sau này, nhưng những ngôn ngữ tĩnh như vậy chắc chắn là không. Bạn cần yêu cầu rõ ràng về bộ nhớ được phân bổ mới và bạn thực sự cần đảm bảo rằng sau này bạn sẽ cung cấp lại bộ nhớ đó và không bao giờ sử dụng lại. Nếu điều đó không xảy ra, thì các bạn sẽ bị lộ thông tin... và việc này thực sự diễn ra khá thường xuyên. Hãy xem cách khắc phục những sự cố rò rỉ bộ nhớ đó và thậm chí tốt hơn nữa là cách thiết kế mã để tránh những lỗi rò rỉ đó trong lần tới.

Mẫu đáng ngờ

Gần đây, khi bắt đầu xử lý Squoosh, tôi nhận thấy một mẫu thú vị trong trình bao bọc bộ mã hoá và giải mã C++. Hãy xem ví dụ về trình bao bọc ImageQuant (được giảm xuống để chỉ hiển thị các phần tạo và giải phóng đối tượng):

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 (tốt, 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
  );
}

Bạn có phát hiện thấy vấn đề nào không? Gợi ý: đây là giá trị use-after-free, nhưng vẫn ở trong JavaScript!

Trong Emscripten, typed_memory_view trả về một Uint8Array JavaScript được hỗ trợ bởi vùng đệm bộ nhớ WebAssembly (Oncem), với byteOffsetbyteLength được đặt thành con trỏ và độ dài đã cho. Điểm chính yếu là đây là một chế độ xem TypedArray chuyển vào vùng đệm bộ nhớ WebAssembly, thay vì bản sao dữ liệu do JavaScript sở hữu.

Khi chúng ta gọi free_result qua JavaScript, hàm này sẽ gọi một hàm C tiêu chuẩn free để đánh dấu bộ nhớ này là có thể sử dụng cho mọi lượt phân bổ sau này, tức là dữ liệu mà khung hiển thị Uint8Array của chúng ta trỏ đến có thể bị ghi đè bằng dữ liệu tuỳ ý bằng bất kỳ lệnh gọi nào trong tương lai vào Wasm.

Hoặc, một số phương thức triển khai của free thậm chí có thể quyết định ngay lập tức giải phóng bộ nhớ đã giải phóng. free mà Emscripten sử dụng không làm được điều đó, nhưng chúng tôi đang dựa vào chi tiết triển khai tại đây nên không thể đảm bảo được.

Hoặc, ngay cả khi bộ nhớ phía sau con trỏ được giữ nguyên, thì quá trình phân bổ mới có thể cần tăng bộ nhớ WebAssembly. Khi WebAssembly.Memory được phát triển thông qua API JavaScript hoặc hướng dẫn memory.grow tương ứng, thao tác này sẽ vô hiệu hoá ArrayBuffer hiện có và mọi khung hiển thị do API này hỗ trợ theo cách bắc cầu.

Hãy để tôi sử dụng bảng điều khiển Công cụ cho nhà phát triển (hoặc Node.js) để minh hoạ hành vi này:

> 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

Cuối cùng, ngay cả khi chúng ta không gọi lại Wasm một cách rõ ràng từ free_result đến new Uint8ClampedArray, thì vào một thời điểm nào đó, chúng ta có thể thêm tính năng hỗ trợ đa luồng vào các bộ mã hoá và giải mã. Trong trường hợp đó, đó có thể là một luồng hoàn toàn khác, ghi đè dữ liệu ngay trước khi chúng ta sao chép được dữ liệu đó.

Đang tìm lỗi bộ nhớ

Để phòng trường hợp này, tôi đã quyết định tìm hiểu sâu hơn để kiểm tra xem mã này có gặp vấn đề nào trong thực tế hay không. Đây có vẻ như là một cơ hội hoàn hảo để dùng thử hỗ trợ trình dọn dẹp Trình mô phỏng mới được thêm vào năm ngoái và được trình bày trong buổi nói chuyện về WebAssembly của chúng tôi tại Hội nghị Nhà phát triển Chrome:

Trong trường hợp này, chúng ta quan tâm đến AddressSanitizer, một công cụ có thể phát hiện nhiều vấn đề liên quan đến con trỏ và bộ nhớ. Để sử dụng mã này, chúng ta cần biên dịch lại bộ mã hoá và giải mã bằng -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

Thao tác này sẽ tự động bật tính năng kiểm tra an toàn cho con trỏ, nhưng chúng tôi cũng muốn tìm các trường hợp rò rỉ bộ nhớ tiềm ẩn. Vì chúng tôi đang sử dụng ImageQuant làm thư viện thay vì chương trình, nên không có "điểm thoát" nào mà Emscripten có thể tự động xác thực rằng tất cả bộ nhớ đã được giải phóng.

Thay vào đó, trong những trường hợp như vậy, LeakSanitizer (có trong AddressSanitizer) cung cấp các hàm __lsan_do_leak_check__lsan_do_recoverable_leak_check. Các hàm này có thể được gọi theo cách thủ công bất cứ khi nào chúng ta muốn giải phóng tất cả bộ nhớ và muốn xác thực việc giả định đó. __lsan_do_leak_check được dùng ở cuối ứng dụng đang chạy, khi bạn muốn huỷ quá trình này trong trường hợp có lỗi rò rỉ nào được phát hiện, trong khi __lsan_do_recoverable_leak_check phù hợp hơn với các trường hợp sử dụng thư viện như của chúng ta, khi bạn muốn in các trường hợp rò rỉ ra bảng điều khiển nhưng vẫn tiếp tục chạy ứng dụng bất kể.

Hãy cho thấy trình trợ giúp thứ hai thông qua Embind để chúng ta có thể gọi trình trợ giúp đó từ JavaScript bất cứ lúc nào:

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

Và gọi mã này từ phía JavaScript sau khi hoàn tất với hình ảnh. Việc thực hiện việc này từ phía JavaScript thay vì từ C++ giúp đảm bảo rằng tất cả các phạm vi đều đã được thoát và tất cả các đối tượng C++ tạm thời đã được giải phóng vào thời điểm chúng tôi chạy các quá trình kiểm tra đó:

  // …

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

Thao tác này sẽ cung cấp cho chúng ta một báo cáo như sau trong bảng điều khiển:

Ảnh chụp màn hình thông báo

Rất tiếc, có một số sự cố rò rỉ nhỏ, nhưng dấu vết ngăn xếp không hữu ích lắm vì tất cả tên hàm đều bị xáo trộn. Hãy biên dịch lại bằng một thông tin gỡ lỗi cơ bản để lưu giữ chúng:

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

Giao diện này trông đẹp hơn nhiều:

Ảnh chụp màn hình thông báo có nội dung &quot;Rò rỉ trực tiếp 12 byte&quot; đến từ hàm GenericBindingType RawImage ::toWireType

Một số phần của dấu vết ngăn xếp trông vẫn chưa rõ ràng khi trỏ đến các thành phần nội bộ Emscripten, nhưng chúng ta có thể biết rằng sự cố rò rỉ xảy ra từ một lượt chuyển đổi RawImage thành "loại dây" (thành một giá trị JavaScript) bằng cách kết hợp. Thực tế, khi xem mã, chúng ta có thể thấy rằng chúng ta trả về các thực thể C++ RawImage về JavaScript, nhưng không bao giờ giải phóng chúng ở cả hai bên.

Xin nhắc lại rằng hiện chưa có chế độ tích hợp thu gom rác nào giữa JavaScript và WebAssembly, mặc dù một chế độ đang được phát triển. Thay vào đó, bạn phải giải phóng mọi bộ nhớ và gọi các hàm khởi tạo theo cách thủ công từ phía JavaScript sau khi hoàn tất đối tượng. Riêng đối với Embind, tài liệu chính thức đề xuất gọi một phương thức .delete() trên các lớp C++ được hiển thị:

Mã JavaScript phải xoá một cách rõ ràng mọi đối tượng C++ đã nhận được, nếu không vùng nhớ khối xếp Emscripten sẽ tăng vô thời hạn.

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

Thật vậy, khi chúng ta làm điều đó trong JavaScript cho lớp của mình:

  // …

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

Vệt rò rỉ biến mất như dự kiến.

Phát hiện thêm các vấn đề liên quan đến trình dọn dẹp vệ sinh

Việc xây dựng các bộ mã hoá và giải mã Squoosh khác bằng trình dọn dẹp cho thấy cả những vấn đề tương tự và một số vấn đề mới. Ví dụ: tôi gặp lỗi này trong liên kết MozJPEG:

Ảnh chụp màn hình thông báo

Ở đây, không phải là rò rỉ, mà là chúng ta ghi vào một bộ nhớ nằm ngoài ranh giới được phân bổ 😂

Khi đào sâu vào mã của MozJPEG, chúng tôi nhận thấy vấn đề ở đây là jpeg_mem_dest (hàm jpeg_mem_dest dùng để phân bổ đích đến của bộ nhớ cho JPEG) sử dụng lại các giá trị hiện có của outbufferoutsize khi các giá trị này khác 0:

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

Tuy nhiên, chúng tôi gọi hàm này mà không khởi tạo bất kỳ biến nào trong số các biến đó. Điều này có nghĩa là MozJPEG ghi kết quả vào một địa chỉ bộ nhớ ngẫu nhiên có thể được lưu trữ trong các biến đó tại thời điểm gọi!

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

Việc không khởi tạo cả hai biến trước khi gọi sẽ giải quyết vấn đề này. Thay vào đó, mã sẽ đạt đến bước kiểm tra rò rỉ bộ nhớ. Rất may là quy trình kiểm tra đã thành công, cho biết rằng chúng tôi không có sự cố rò rỉ nào trong bộ mã hoá và giải mã này.

Vấn đề về trạng thái được chia sẻ

...Hay chúng ta?

Chúng tôi biết rằng các liên kết bộ mã hoá và giải mã của chúng tôi lưu trữ một số trạng thái cũng như kết quả trong các biến tĩnh toàn cục và MozJPEG có một số cấu trúc đặc biệt phức tạp.

uint8_t* last_result;
struct jpeg_compress_struct cinfo;

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

Điều gì sẽ xảy ra nếu một số dữ liệu đó được khởi động từng phần trong lần chạy đầu tiên, sau đó sử dụng lại không đúng cách trong các lần chạy sau này? Sau đó, một lệnh gọi có trình dọn dẹp sẽ không báo cáo các sự cố đó là có sự cố.

Hãy thử xử lý hình ảnh một vài lần bằng cách nhấp ngẫu nhiên ở các mức chất lượng khác nhau trong giao diện người dùng. Thực tế, giờ đây chúng ta nhận được báo cáo sau đây:

Ảnh chụp màn hình thông báo

262.144 byte – có vẻ như toàn bộ hình ảnh mẫu bị rò rỉ từ jpeg_finish_compress!

Sau khi xem tài liệu và các ví dụ chính thức, kết quả cho thấy jpeg_finish_compress không giải phóng bộ nhớ do lệnh gọi jpeg_mem_dest trước đó phân bổ mà chỉ giải phóng cấu trúc nén, mặc dù cấu trúc nén đó đã biết đích đến bộ nhớ của chúng ta ... Sigh.

Chúng ta có thể khắc phục vấn đề này bằng cách giải phóng dữ liệu theo cách thủ công trong hàm 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);
}

Tôi có thể tiếp tục tìm từng lỗi bộ nhớ đó, nhưng tôi nghĩ rằng giờ đây rõ ràng là phương pháp quản lý bộ nhớ hiện tại dẫn đến một số vấn đề có tính hệ thống khó chịu.

Một vài trong số đó có thể bị chất khử trùng phát hiện ngay lập tức. Một số khác yêu cầu thủ thuật phức tạp thì mới bắt được. Cuối cùng, có một số vấn đề như ở đầu bài đăng mà chúng ta có thể thấy từ nhật ký, trình dọn dẹp hoàn toàn không phát hiện được. Lý do là việc sử dụng sai mục đích xảy ra ở phía JavaScript, trong đó trình dọn dẹp không có chế độ hiển thị. Những vấn đề đó sẽ chỉ xuất hiện trong phiên bản chính thức hoặc sau khi có những thay đổi dường như không liên quan đến mã trong tương lai.

Xây dựng trình bao bọc an toàn

Hãy lùi lại một vài bước và thay vào đó, hãy khắc phục tất cả các vấn đề này bằng cách tái cấu trúc mã theo cách an toàn hơn. Tôi sẽ sử dụng lại trình bao bọc ImageQuant làm ví dụ, nhưng các quy tắc tái cấu trúc tương tự sẽ áp dụng cho tất cả các bộ mã hoá và giải mã, cũng như các cơ sở mã tương tự khác.

Trước hết, hãy khắc phục vấn đề use-after-free (không sử dụng sau khi sử dụng) từ đầu bài đăng này. Do đó, chúng ta cần sao chép dữ liệu từ khung hiển thị được hỗ trợ WebAssembly trước khi đánh dấu dữ liệu này là miễn phí ở phía JavaScript:

  // …

  const result = /* … */;

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

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

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

Bây giờ, hãy đảm bảo chúng ta không chia sẻ bất kỳ trạng thái nào trong biến toàn cục giữa các lệnh gọi. Việc này sẽ giúp khắc phục một số vấn đề chúng ta đã gặp phải, đồng thời giúp bạn dễ dàng sử dụng các bộ mã hoá và giải mã trong môi trường đa luồng sau này.

Để làm được điều đó, chúng tôi tái cấu trúc trình bao bọc C++ để đảm bảo rằng mỗi lệnh gọi đến hàm đều quản lý dữ liệu riêng bằng cách sử dụng các biến cục bộ. Sau đó, chúng ta có thể thay đổi chữ ký của hàm free_result để chấp nhận lại con trỏ:

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

Tuy nhiên, vì đã sử dụng Embind trong Emscripten để tương tác với JavaScript, chúng ta cũng có thể làm cho API này an toàn hơn nữa bằng cách ẩn toàn bộ thông tin quản lý bộ nhớ C++!

Để làm được điều đó, hãy chuyển phần new Uint8ClampedArray(…) từ JavaScript sang phía C++ bằng Embind. Sau đó, chúng ta có thể sử dụng đối tượng này để sao chép dữ liệu vào bộ nhớ JavaScript ngay cả trước khi trả về từ hàm:

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

Lưu ý: với một thay đổi duy nhất, chúng ta sẽ đảm bảo rằng mảng byte thu được thuộc sở hữu của JavaScript chứ không phải do bộ nhớ WebAssembly hỗ trợ, đồng thời cũng loại bỏ trình bao bọc RawImage bị rò rỉ trước đó.

Giờ đây, JavaScript không còn phải lo lắng về việc giải phóng dữ liệu nữa và có thể sử dụng kết quả này như bất kỳ đối tượng nào khác được thu thập rác:

  // …

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

Điều này cũng có nghĩa là chúng ta không cần liên kết free_result tuỳ chỉnh ở phía C++ nữa:

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

Nhìn chung, mã trình bao bọc của chúng tôi vừa trở nên sạch hơn, vừa an toàn hơn.

Sau đó, tôi đã thực hiện thêm một số cải tiến nhỏ đối với mã của trình bao bọc ImageQuant và sao chép các bản sửa lỗi quản lý bộ nhớ tương tự cho các bộ mã hoá và giải mã khác. Nếu muốn biết thêm thông tin chi tiết, bạn có thể xem PR kết quả tại đây: Các bản sửa lỗi bộ nhớ cho bộ mã hoá và giải mã C++.

Cướp lại bóng

Chúng tôi có thể rút ra và chia sẻ bài học nào từ quá trình tái cấu trúc này để áp dụng cho các cơ sở mã khác?

  • Đừng sử dụng các khung hiển thị bộ nhớ do WebAssembly hỗ trợ (bất kể ngôn ngữ đó là gì) ngoài một lệnh gọi. Bạn không thể tiếp tục tin tưởng chúng tồn tại lâu hơn thế nữa và bạn sẽ không thể phát hiện các lỗi này bằng các phương thức thông thường. Vì vậy, nếu cần lưu trữ dữ liệu để sử dụng sau này, hãy sao chép dữ liệu vào phía JavaScript và lưu trữ ở đó.
  • Nếu có thể, hãy sử dụng một ngôn ngữ quản lý bộ nhớ an toàn hoặc ít nhất là trình bao bọc kiểu an toàn, thay vì thao tác trực tiếp trên con trỏ thô. Việc này sẽ không giúp bạn tránh được lỗi trên ranh giới JavaScript IANA WebAssembly, nhưng ít nhất việc này sẽ giúp giảm bớt bề mặt của các lỗi độc lập bằng mã ngôn ngữ tĩnh.
  • Bất kể bạn sử dụng ngôn ngữ nào, hãy chạy mã có trình dọn dẹp trong quá trình phát triển – chúng không chỉ giúp phát hiện các vấn đề về mã ngôn ngữ tĩnh mà còn phát hiện một số vấn đề trên ranh giới JavaScript Duyệt WebAssembly, chẳng hạn như quên gọi .delete() hoặc chuyển con trỏ không hợp lệ từ phía JavaScript.
  • Nếu có thể, hãy tránh để lộ toàn bộ dữ liệu và đối tượng không được quản lý từ WebAssembly cho JavaScript. JavaScript là một ngôn ngữ thu gom rác và việc quản lý bộ nhớ thủ công không phổ biến trong ngôn ngữ này. Đây có thể được coi là sự cố rò rỉ trừu tượng của mô hình bộ nhớ bằng ngôn ngữ dùng để tạo WebAssembly và việc quản lý không chính xác rất dễ bị bỏ qua trong cơ sở mã JavaScript.
  • Điều này có thể rõ ràng, nhưng cũng như trong bất kỳ cơ sở mã nào khác, hãy tránh lưu trữ trạng thái có thể thay đổi trong các biến toàn cục. Bạn không muốn gỡ lỗi các vấn đề về việc sử dụng lại trên nhiều lệnh gọi hoặc thậm chí là luồng. Vì vậy, tốt nhất là bạn nên tạo phương pháp riêng biệt nhất có thể.