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

Mặc dù JavaScript khá dễ tính trong việc dọn dẹp sau khi tự dọn dẹp, nhưng các ngôn ngữ tĩnh thì chắc chắn không phải vậy…

Squoosh.app là một PWA minh hoạ mức độ các bộ mã hoá và giải mã hình ảnh cũng như chế độ cài đặt 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à một bản minh hoạ kỹ thuật cho thấy cách bạn có thể lấy các thư viện được viết bằng C++ hoặc Rust và đưa các thư viện đó lên web.

Việc có thể chuyển mã từ các hệ sinh thái hiện có là vô cùng có 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ác phương pháp khác nhau để quản lý bộ nhớ.

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

Mẫu đáng ngờ

Gần đây, khi bắt đầu làm việc trên Squoosh, tôi không thể không nhận thấy một mẫu thú vị trong các trình bao bọc bộ mã hoá và giải mã C++. Hãy xem xét trình bao bọc ImageQuant làm ví dụ (được giảm để 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 (vâng, 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 vấn đề nào không? Gợi ý: đó là lỗi sử dụng sau khi giải phóng, nhưng trong JavaScript!

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

Khi chúng ta gọi free_result từ JavaScript, hàm này sẽ gọi hàm C chuẩn free để đánh dấu bộ nhớ này là có sẵn cho mọi hoạt động phân bổ trong tương lai. Điều này có nghĩa là dữ liệu mà thành phần hiển thị Uint8Array của chúng ta trỏ đến có thể được 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ố cách triển khai free thậm chí có thể quyết định ngay lập tức điền giá trị 0 vào bộ nhớ được giải phóng. free mà Emscripten sử dụng không làm như vậy, nhưng chúng tôi đang dựa vào một chi tiết triển khai tại đây mà không thể đảm bảo.

Hoặc ngay cả khi bộ nhớ đằng sau con trỏ được giữ nguyên, 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 lệnh memory.grow tương ứng, ArrayBuffer hiện tại sẽ mất hiệu lực và theo đó, mọi thành phần hiển thị được hỗ trợ bởi WebAssembly.Memory cũng sẽ mất hiệu lực.

Hãy để tôi sử dụng bảng điều khiển DevTools (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 không gọi lại Wasm một cách rõ ràng giữa free_resultnew Uint8ClampedArray, tại một thời điểm nào đó, chúng ta có thể thêm tính năng hỗ trợ nhiều luồng vào 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 sẽ ghi đè dữ liệu ngay trước khi chúng ta có thể sao chép luồng đó.

Tìm lỗi bộ nhớ

Phòng trường hợp có vấn đề, tôi quyết định kiểm tra thêm để xem mã này có gặp vấn đề gì trong thực tế hay không. Đây có vẻ là cơ hội hoàn hảo để thử tính năng hỗ trợ trình dọn dẹp Emscripten mới(khá 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 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. Công cụ này có thể phát hiện nhiều vấn đề liên quan đến con trỏ và bộ nhớ. Để sử dụng, 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 con trỏ, nhưng chúng ta cũng muốn tìm các sự cố rò rỉ bộ nhớ tiềm ẩn. Vì chúng ta đang sử dụng ImageQuant dưới dạng 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 đó, đối với 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. Bạn có thể gọi các hàm này theo cách thủ công bất cứ khi nào chúng ta dự kiến tất cả bộ nhớ sẽ được giải phóng và muốn xác thực giả định đó. __lsan_do_leak_check được dùng ở cuối một ứng dụng đang chạy, khi bạn muốn huỷ quy trình trong trường hợp phát hiện thấy rò rỉ, 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ư trường hợp của chúng ta, khi bạn muốn in rò rỉ vào bảng điều khiển nhưng vẫn giữ cho ứng dụng chạy.

Hãy hiển thị 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 hàm này từ phía JavaScript sau khi chúng ta hoàn tất hình ảnh. Việc thực hiện việc này từ phía JavaScript thay vì C++ giúp đảm bảo rằng tất cả các phạm vi đã 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 ta chạy các bước 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 của một thông báo

Ồ, có một số 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 thông tin gỡ lỗi cơ bản để giữ lại các thông tin đó:

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

Cách này trông đẹp hơn nhiều:

Ảnh chụp màn hình thông báo &quot;Direct leak of 12 bytes&quot; (Rò rỉ trực tiếp 12 byte) từ hàm GenericBindingType RawImage ::toWireType

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

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

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

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

Thật vậy, khi chúng ta thực hiện việc đó 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
 
);
}

Lỗi rò rỉ sẽ biến mất như dự kiến.

Khám phá thêm vấn đề về trình dọn dẹp

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

Ảnh chụp màn hình của một thông báo

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

Khi tìm hiểu mã của MozJPEG, chúng tôi nhận thấy vấn đề ở đây là jpeg_mem_dest – hàm mà chúng ta sử dụng để phân bổ đích đến 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 ta gọi hàm này mà không khởi tạo bất kỳ biến nào trong số đó, nghĩa là MozJPEG sẽ ghi kết quả vào một địa chỉ bộ nhớ có thể ngẫu nhiên đã đượ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 đặt giá trị ban đầu là 0 cho cả hai biến trước khi gọi sẽ giải quyết vấn đề này và giờ đây, mã sẽ chuyển đến bước kiểm tra rò rỉ bộ nhớ. May mắn là quy trình kiểm tra đã thành công, cho biết rằng chúng ta không có bất kỳ rò rỉ nào trong bộ mã hoá và giải mã này.

Vấn đề về trạng thái dùng chung

…Hay chúng ta có?

Chúng ta biết rằng các liên kết bộ mã hoá và giải mã của chúng ta 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ố trong số đó được khởi chạy từng phần trong lần chạy đầu tiên, sau đó được 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 duy nhất với trình dọn dẹp sẽ không báo cáo các lỗi đó là có vấn đề.

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

Ảnh chụp màn hình của một 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, hóa ra jpeg_finish_compress không giải phóng bộ nhớ được phân bổ bằng lệnh gọi jpeg_mem_dest trước đó – lệnh gọi này chỉ giải phóng cấu trúc nén, mặc dù cấu trúc nén đó đã biết về đích đến bộ nhớ của chúng ta… Thở dài.

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 kiếm từng lỗi bộ nhớ đó, nhưng tôi nghĩ giờ đây đã đủ rõ ràng rằng phương pháp quản lý bộ nhớ hiện tại dẫn đến một số vấn đề hệ thống khó chịu.

Một số lỗi có thể được trình dọn dẹp phát hiện ngay lập tức. Một số khác cần có thủ thuật phức tạp để phát hiện. Cuối cùng, có một số vấn đề như ở đầu bài đăng mà chúng ta có thể thấy trong nhật ký, trình dọn dẹp không phát hiện được. Lý do là việc sử dụng sai thực tế 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 bản phát hành chính thức hoặc sau khi có những thay đổi có vẻ như không liên quan đến mã trong tương lai.

Tạo trình bao bọc an toàn

Hãy quay lại một vài bước và 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 trình bao bọc ImageQuant làm ví dụ một lần nữa, nhưng các quy tắc tái cấu trúc tương tự áp dụng cho tất cả bộ mã hoá và giải mã, cũng như các cơ sở mã tương tự khác.

Trước tiên, hãy khắc phục vấn đề sử dụng sau khi giải phóng từ đầu bài đăng. Để làm được điều đó, chúng ta cần sao chép dữ liệu từ thành phần hiển thị được hỗ trợ WebAssembly trước khi đánh dấu dữ liệu đó 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 rằng chúng ta không chia sẻ bất kỳ trạng thái nào trong các 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 đề mà chúng ta đã thấy, đồng thời giúp bạn dễ dàng sử dụng bộ mã hoá và giải mã trong môi trường đa luồng trong tương lai.

Để làm việc đó, chúng ta 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 sẽ quản lý dữ liệu riêng bằ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 con trỏ trở lại:

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ì chúng ta đang sử dụng Embind trong Emscripten để tương tác với JavaScript, nên chúng ta cũng có thể làm cho API 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 di chuyển phần new Uint8ClampedArray(…) từ JavaScript sang phía C++ bằng Embind. Sau đó, chúng ta có thể sử dụng hàm này để nhân bản 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 ý cách thức chỉ với một thay đổi, chúng ta vừa đảm bảo rằng mảng byte thu được thuộc sở hữu của JavaScript và không được bộ nhớ WebAssembly hỗ trợ, 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ả như mọi đối tượng thu gom rác khá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 nữa ở phía 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());
}

Tóm lại, mã trình bao bọc của chúng ta vừa sạch sẽ vừa an toàn hơn.

Sau đó, tôi đã thực hiện một số cải tiến nhỏ khác đố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 quan tâm đến thông tin chi tiết hơn, bạn có thể xem nội dung PR sau đây tại đây: Sửa lỗi bộ nhớ cho bộ mã hoá và giải mã C++.

Cướp lại bóng

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

  • Đừng sử dụng chế độ xem bộ nhớ do WebAssembly hỗ trợ (bất kể ngôn ngữ tạo ra chế độ xem đó) ngoài một lệnh gọi duy nhất. Bạn không thể dựa vào các biến này tồn tại lâu hơn 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 bạn cần lưu trữ dữ liệu để sử dụng sau, hãy sao chép dữ liệu đó sang phía JavaScript và lưu trữ ở đó.
  • Nếu có thể, hãy sử dụng ngôn ngữ quản lý bộ nhớ an toàn hoặc ít nhất là trình bao bọc loại an toàn thay vì trực tiếp thao tác trên con trỏ thô. Điều này sẽ không giúp bạn tránh được lỗi trên ranh giới JavaScript ↔ WebAssembly, nhưng ít nhất nó sẽ giảm thiểu khả năng lỗi xảy ra do mã ngôn ngữ tĩnh.
  • Bất kể bạn sử dụng ngôn ngữ nào, hãy chạy mã bằng trình dọn dẹp trong quá trình phát triển. Các trình dọn dẹp này không chỉ giúp phát hiện vấn đề trong mã ngôn ngữ tĩnh mà còn phát hiện một số vấn đề trên ranh giới JavaScript ↔ WebAssembly, chẳng hạn như quên gọi .delete() hoặc truyền con trỏ không hợp lệ từ phía JavaScript.
  • Nếu có thể, hãy tránh hiển thị 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ớ theo cách thủ công không phổ biến trong ngôn ngữ này. Đây có thể được coi là sự rò rỉ trừu tượng của mô hình bộ nhớ của ngôn ngữ mà WebAssembly được tạo ra, 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ể hiển nhiên, nhưng giố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 đề khi sử dụng lại trong nhiều lệnh gọi hoặc thậm chí là luồng, vì vậy, tốt nhất bạn nên giữ cho luồng này càng độc lập càng tốt.