Mặc dù JavaScript khá dễ dàng dọn dẹp, nhưng các ngôn ngữ tĩnh chắc chắn không...
Squoosh.app là một PWA minh hoạ số lượng bộ mã hoá và giải mã hình ảnh khác nhau và 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, bản minh hoạ kỹ thuật cho thấy cách bạn có thể sử dụng các thư viện được viết bằng C++ hoặc Rust rồi đưa chúng vào web.
Khả năng chuyển đổi mã từ các hệ sinh thái hiện có là vô cùng quan trọng, nhưng có một số sự khác biệt giữa các ngôn ngữ tĩnh và JavaScript đó. Một trong số đó là theo các cách phương pháp quản lý bộ nhớ.
Mặc dù JavaScript khá dễ dàng dọn dẹp, 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 phải đảm bảo bạn sẽ cung cấp lại sau và không bao giờ sử dụng lại. Nếu điều đó không xảy ra, bạn sẽ bị rò rỉ thông tin... và nó thực sự diễn ra khá thường xuyên. Hãy cùng xem cách bạn có thể gỡ lỗi rò rỉ bộ nhớ và tốt hơn nữa, cách bạn có thể thiết kế mã để tránh chúng lần sau.
Mẫu đáng ngờ
Gần đây, khi bắt đầu làm việc trên Squoosh, tôi không chú ý thấy một hình mẫu thú vị trong Trình bao bọc bộ mã hoá và giải mã C++. Hãy xem trình bao bọc ImageQuant ví dụ (giảm để chỉ hiển thị các phần tạo đối tượng và giải phó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 (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 đề không? Gợi ý: đó là sử dụng sau khi miễn phí, nhưng trong JavaScript!
Trong Emscripten, typed_memory_view
trả về một JavaScript Uint8Array
được WebAssembly (Wasm) hỗ trợ
vùng đệm bộ nhớ, với byteOffset
và byteLength
được đặt thành con trỏ và độ dài đã cho. Chính
đó là một khung hiển thị TypedArray được đưa 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
từ JavaScript, hàm này sẽ gọi một hàm C chuẩn free
để đánh dấu
bộ nhớ này sẵn có cho mọi lượt phân bổ trong tương lai, tức là dữ liệu mà Uint8Array
hiển thị
điểm đế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 free
thậm chí có thể quyết định lấp đầy bộ nhớ đã giải phóng ngay lập tức bằng 0. Chiến lược phát hành đĩa đơn
free
mà Emscripten sử dụng không làm như vậy, nhưng chúng tôi đang dựa vào chi tiết triển khai tại đây
mà chúng tôi không thể đảm bảo.
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
memory.grow
, phương thức này sẽ vô hiệu hoá ArrayBuffer
hiện tại và mọi khung hiển thị theo cách bắc cầu
dựa trên nội dung đó.
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
, tại một thời điểm nào đó, chúng ta vẫn có thể thêm tính năng hỗ trợ đa luồng vào bộ mã hoá và giải mã của mình. 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 tôi sao chép được.
Đang tìm lỗi bộ nhớ
Trong trường hợp này, tôi đã quyết định đi sâu hơn và kiểm tra xem mã này có cho thấy bất kỳ vấn đề nào trong thực tế không. Đây có vẻ là một cơ hội hoàn hảo để dùng thử sản phẩm khử trùng mô phỏng mới dịch vụ hỗ trợ được thêm vào năm ngoái và được trình bày trong bài nói chuyện trên 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 việc
AddressSanitizer,
có thể phát hiện nhiều vấn đề liên quan đến con trỏ và bộ nhớ. Để dùng mã này, chúng tôi cần biên dịch lại bộ mã hoá và giải mã
với -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 ta cũng muốn tìm bộ nhớ tiềm ẩn rò rỉ thông tin. Vì chúng tôi đang sử dụng ImageQuant làm thư viện thay vì chương trình, nên sẽ không có "điểm thoát" vào lúc 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) sẽ cung cấp các hàm
__lsan_do_leak_check
và
__lsan_do_recoverable_leak_check
,
có thể được gọi theo cách thủ công bất cứ khi nào chúng ta mong muốn tất cả bộ nhớ đã được giải phóng và muốn xác thực rằng
giả định. __lsan_do_leak_check
được dùng ở cuối ứng dụng đang chạy, khi bạn
muốn huỷ bỏ quá trình này phòng khi phát hiện thấy bất kỳ sự cố rò rỉ nào, 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 tôi, khi bạn muốn in rò rỉ đến bảng điều khiển, nhưng
duy trì hoạt động của ứng dụng.
Hãy hiển thị trình trợ giúp thứ hai 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 nó từ phía JavaScript sau khi chúng ta hoàn tất với hình ảnh. Thực hiện việc này từ Phía JavaScript, thay vì phía C++, giúp đảm bảo rằng tất cả các phạm vi đều được bị 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 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:
Ồ, 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ả các tên hàm bị hỏng. Hãy biên dịch lại bằng thông tin gỡ lỗi cơ bản để giữ nguyên 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
Giao diện này trông đẹp hơn nhiều:
Một số phần của dấu vết ngăn xếp vẫn trông mờ ám khi trỏ đến phần bên trong Emscripten, nhưng chúng ta có thể
cho biết rằng sự cố rò rỉ bắt nguồn từ lượt chuyển đổi RawImage
thành "loại dây" (thành giá trị JavaScript) bằng cách
Đã liên kết. Thật vậy, khi xem mã, chúng ta có thể thấy rằng chúng ta trả về các phiên bản C++ RawImage
cho
JavaScript, nhưng chúng tôi không bao giờ giải phóng chúng ở cả hai bên.
Xin lưu ý rằng hiện tại, chưa tích hợp tính năng thu thập rác giữa JavaScript và
WebAssembly, mặc dù một công cụ đang được phát triển. Thay vào đó, bạn có
để giải phóng bất kỳ bộ nhớ nào theo cách thủ công và gọi các hàm huỷ từ phía JavaScript sau khi bạn đã hoàn tất
. Riêng đối với Embind, cụm từ chính thức
tài liệu
đề xuất gọi một phương thức .delete()
trên các lớp C++ bị lộ:
Mã JavaScript phải xoá rõ ràng mọi đối tượng C++ xử lý nó đã nhận được hoặc mã Emscripten Vùng nhớ khối xếp sẽ phát triển vô thời 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:
// …
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
);
}
Sự rò rỉ này biến mất như dự kiến.
Phát hiện các vấn đề khác về chất khử trùng
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 vừa tương tự vừa phát hiện một số vấn đề mới. Cho Ví dụ: tôi gặp lỗi này trong các liên kết MozJPEG:
Ở đây, đó không phải là sự rò rỉ dữ liệu, mà là chúng ta ghi vào một bộ nhớ nằm ngoài ranh giới được phân bổ 🤔
Đào sâu vào mã MozJPEG, chúng ta nhận thấy có vấn đề ở đây là jpeg_mem_dest
—
mà chúng ta sử dụng để phân bổ đích bộ nhớ cho JPEG—sử dụng lại các giá trị hiện có của
outbuffer
và outsize
khi chúng lên
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 nó mà không cần khởi chạy một trong hai biến đó, nghĩa là MozJPEG ghi dẫn đến 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 gian của cuộc gọi!
uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);
Việc không khởi chạy cả hai biến trước khi lệnh gọi sẽ giải quyết vấn đề này và giờ đây mã sẽ đạt đến thay vào đó, kiểm tra rò rỉ bộ nhớ. May mắn thay, việc kiểm tra thành công, cho thấy rằng chúng tôi không có rò rỉ trong bộ mã hoá và giải mã này.
Vấn đề về trạng thái được chia sẻ
...hay chúng ta làm gì?
Chúng ta biết rằng các liên kết bộ mã hoá và giải mã của mình lưu trữ một số trạng thái cũng như dẫn đến trạng thái tĩnh toàn cầu nhiều biến 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ố ứng dụng 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 tương lai chạy không? Sau đó, một cuộc gọi riêng lẻ có trình dọn dẹp sẽ không báo cáo các cuộc gọi đó là có vấn đề.
Hãy thử và 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 vậy, bây giờ chúng tôi nhận được báo cáo sau đây:
262.144 byte – có vẻ như toàn bộ hình ảnh mẫu đã bị rò rỉ từ jpeg_finish_compress
!
Sau khi xem các tài liệu và ví dụ chính thức, hoá ra jpeg_finish_compress
không giải phóng bộ nhớ được phân bổ bởi lệnh gọi jpeg_mem_dest
trước đó mà chỉ giải phóng
cấu trúc nén đó, mặc dù cấu trúc nén đó đã biết về bộ nhớ
điểm đến... Hai lần đầu tiên.
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 săn từng lỗi bộ nhớ, nhưng đến giờ thì 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 đề hệ thống khó chịu.
Máy vệ sinh có thể tóm được một vài trong số đó ngay lập tức. Một số trường hợp khác lại đòi hỏi những thủ thuật tinh vi thì mới bị bắt được. Cuối cùng, có một số vấn đề như trong phần đầu của bài đăng, như chúng ta có thể thấy trong nhật ký, công cụ dọn dẹp không hề bị bắt. Lý do là việc sử dụng sai mục đích thực sự xảy ra trên Phía JavaScript mà trình dọn dẹp không có chế độ hiển thị. Những vấn đề đó sẽ tự phát hiện chỉ trong quá trình sản xuất hoặc sau khi những thay đổi có vẻ không liên quan đến mã trong tương lai.
Xây dựng một 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 áp dụng quy tắc tái cấu trúc tương tự vào 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 dùng nữa) ở đầu bài đăng này. Để làm được điều đó, chúng tôi cần để sao chép dữ liệu từ chế độ xem được WebAssembly hỗ trợ 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 chúng ta không chia sẻ trạng thái nào trong các biến toàn cục giữa các lệnh gọi. Chiến dịch này sẽ khắc phục một số vấn đề chúng ta đã gặp phải, đồng thời giúp người dùng dễ dàng sử dụng trong môi trường đa luồng trong tương lai.
Để làm được điều đó, chúng ta sẽ 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 tự quản lý lệnh gọi đó
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
thành
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 tôi đã sử dụng Embind trong Emscripten để tương tác với JavaScript, chúng tôi cũng có thể giúp API này an toàn hơn nữa bằng cách ẩn hoàn toàn chi tiết quản lý bộ nhớ C++!
Để làm được việc đó, hãy di chuyển phần new Uint8ClampedArray(…)
từ JavaScript sang phía C++ bằng
Đã liên kết. Sau đó, chúng ta có thể sử dụng tập dữ liệu này để sao chép dữ liệu vào bộ nhớ JavaScript, thậm chí 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 ý: chỉ với một thay đổi duy nhất, chúng ta đều đảm bảo rằng mảng byte kết quả thuộc sở hữu của JavaScript
và không được bộ nhớ WebAssembly hỗ trợ, đồng thời loại bỏ trình bao bọc RawImage
bị rò rỉ trước đó
của Google.
Bây giờ, JavaScript không 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ư bất kỳ đối tượng được thu thập rác nào 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 ở 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());
}
Về tổng thể, mã bao bọc của chúng tôi nay vừa gọn gàng hơn vừa an toàn hơn.
Sau đó, tôi đã trải qua một số cải tiến nhỏ thêm cho 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, bạn có thể xem PR kết quả tại đây: Sửa lỗi bộ nhớ cho C++ bộ mã hoá và giải mã.
Cướp lại bóng
Chúng ta có thể học hỏi và chia sẻ những bài học nào từ việc tái cấu trúc này và có thể áp dụng cho các cơ sở mã khác?
- Không sử dụng các thành phần hiển thị bộ nhớ được WebAssembly hỗ trợ (bất kể giao diện được tạo bằng ngôn ngữ nào) bên ngoài một lời gọi duy nhất. Bạn không thể dựa vào chúng để 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 pháp thông thường, vì vậy, nếu bạn cần lưu trữ dữ liệu cho sau này, hãy sao chép dữ liệu vào phía JavaScript và lưu trữ mã đó ở đó.
- 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 hoạt động trên con trỏ thô. Việc này sẽ không giúp bạn tránh các lỗi trên JavaScript 📲 WebAssembly ranh giới, nhưng ít nhất nó sẽ làm giảm bề mặt cho các lỗi độc lập bởi mã ngôn ngữ tĩnh.
- Bất kể bạn dùng ngôn ngữ nào, hãy chạy mã bằng công cụ dọn dẹp trong quá trình phát triển vì chúng có thể giúp
không chỉ phát hiện các vấn đề trong mã ngôn ngữ tĩnh, mà còn phát hiện một số vấn đề về JavaScript tránh
Ranh giới 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 để lộ hoàn toàn dữ liệu và đối tượng không được quản lý từ WebAssembly cho JavaScript. JavaScript là ngôn ngữ được thu thập 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à một sự rò rỉ trừu tượng của mô hình bộ nhớ cho ngôn ngữ mà WebAssembly của bạn sử dụng được tạo và hoạt động 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 cũ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 biến. Bạn không muốn gỡ lỗi khi sử dụng lại các lệnh gọi khác nhau hoặc thậm chí luồng, vì vậy, tốt nhất bạn nên giữ luồng độc lập nhất có thể.