WebAssembly thường được giới thiệu như cách chạy C hoặc Rust trong browser. Cách nói đó đúng nhưng chưa chạm đến ý tưởng quan trọng hơn: WebAssembly, hay Wasm, là một đích biên dịch nhỏ gọn, có kiểu dữ liệu cùng semantics kiểm tra và thực thi được định nghĩa rõ. Compiler của nhiều ngôn ngữ có thể sinh Wasm; browser, server runtime, plugin host, edge platform hoặc embedded engine đều có thể instantiate module đó.
Tính khả chuyển không có nghĩa một module tự nó là ứng dụng chạy được ở mọi nơi. Core Wasm có phép tính, linear memory, table, global và function, nhưng không có sẵn file, socket, clock, DOM hay console. Mọi hiệu ứng đều đi qua contract do host cung cấp. Hiểu contract này, bao gồm chi phí copy và trust boundary, hữu ích hơn nhiều so với học thuộc binary format.
Từ source code đến stack machine đã được kiểm tra
Wasm binary lưu instruction có type trong cấu trúc control flow rõ ràng. WebAssembly Text format, viết tắt WAT, biểu diễn cùng một mô hình bằng S-expression để con người đọc được. Phần lớn instruction lấy operand từ một stack khái niệm rồi đẩy kết quả trở lại. Function sau không cần temporary variable có tên: hai lệnh local.get đẩy operand, còn i32.mul lấy hai giá trị đó và đẩy lại một kết quả i32.
(module
(func (export "square") (param i32) (result i32)
local.get 0
local.get 0
i32.mul))
Gọi Wasm là stack machine nhằm mô tả semantics của instruction, không khẳng định machine code cuối cùng luôn dùng memory stack vật lý. Engine decode và validate module, sau đó interpreter, baseline compiler, optimizing JIT hoặc ahead-of-time compiler ánh xạ stack value sang register và instruction của CPU. Engine production không cần thực hiện một lần push vào RAM cho từng operand.
Validation diễn ra trước execution. Validator kiểm tra type của instruction, function signature, branch target, structured block, local đã được khởi tạo, index trong metadata và hình dạng stack tại các điểm nối control flow còn reachable. Chẳng hạn, function khai báo trả về i32 không thể kết thúc với một f64. Quá trình tuyến tính này cho phép engine compile code đã decode mà không phải suy luận lại control flow tùy ý.
Validation không phải program verification. Nó chứng minh module đúng cấu trúc và đúng type theo luật Wasm; nó không chứng minh một index mang ý nghĩa nghiệp vụ, phép tính không overflow hay thuật toán chắc chắn dừng.
Note
File .wasm là distribution format. WAT là dạng text tương đương, phù hợp để học, tạo test, inspect và viết module nhỏ. Ứng dụng thực tế thường compile từ một source language thay vì bảo trì file WAT lớn bằng tay.
Module, import, export, memory và table
Module là code tĩnh cùng các declaration. Instantiation ghép module với import rồi tạo runtime state như memory, table và mutable global. Vì vậy nhiều instance của cùng một compiled module có thể giữ state tách biệt; embedder cũng có thể chủ động cấp shared import nếu muốn chia sẻ.
Module WAT hoàn chỉnh dưới đây import một logging function, export một memory, đặt hai function vào table và cung cấp operation dùng cả hai. Có thể assemble nó bằng lệnh wat2wasm math.wat -o math.wasm.
(module
(type $unary (func (param i32) (result i32)))
(import "env" "log_i32" (func $log_i32 (param i32)))
(memory (export "memory") 1 4)
(table (export "functions") 2 funcref)
(func $square (type $unary) (param $value i32) (result i32)
local.get $value
local.get $value
i32.mul)
(func $double (type $unary) (param $value i32) (result i32)
local.get $value
i32.const 2
i32.mul)
(elem (i32.const 0) $square $double)
(func (export "run")
(param $value i32) (param $which i32) (result i32)
(local $result i32)
local.get $value
local.get $which
call_indirect (type $unary)
local.tee $result
call $log_i32
local.get $result)
(func (export "sum_i32")
(param $ptr i32) (param $length i32) (result i32)
(local $end i32)
(local $sum i32)
local.get $ptr
local.get $length
i32.const 4
i32.mul
i32.add
local.set $end
block $done
loop $next
local.get $ptr
local.get $end
i32.ge_u
br_if $done
local.get $sum
local.get $ptr
i32.load
i32.add
local.set $sum
local.get $ptr
i32.const 4
i32.add
local.set $ptr
br $next
end
end
local.get $sum))
Memory là một mảng byte liên tục, chia thành page 64 KiB. Ví dụ này bắt đầu với một page và có thể tăng tối đa thành bốn. Lệnh load và store dùng địa chỉ byte; chúng trap nếu vùng truy cập vượt ra ngoài memory. Engine không biết byte nào là array, string, object hay giá trị đang được owner nào giữ. Source-language ABI phải quy định các khái niệm đó.
Table là collection reference được truy cập bằng index. Nó hỗ trợ function pointer, virtual dispatch, callback và nhiều mô hình dynamic linking. call_indirect kiểm tra lúc runtime rằng entry được chọn có tồn tại và đúng signature $unary. Truyền index 2 vào table chỉ có hai entry sẽ trap thay vì nhảy đến một địa chỉ tùy ý. Import và export cũng có type, được link theo cặp module/name trong lúc instantiate.
Đi qua ranh giới JavaScript và chia sẻ linear memory
Browser cung cấp API WebAssembly để compile và instantiate. instantiateStreaming có thể validate và compile ngay khi byte đang về, với điều kiện server gửi Content-Type: application/wasm. Có thể fallback qua response.arrayBuffer() nếu không sửa được MIME type của server, nhưng cách đó mất lợi ích streaming.
type MathExports = {
memory: WebAssembly.Memory;
run(value: number, which: number): number;
sum_i32(pointer: number, length: number): number;
};
const imports = {
env: {
log_i32(value: number): void {
console.log('Kết quả từ Wasm:', value);
},
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/math.wasm'),
imports,
);
const wasm = instance.exports as unknown as MathExports;
console.log(wasm.run(12, 0)); // bình phương: 144
console.log(wasm.run(12, 1)); // nhân đôi: 24
const values = new Int32Array([10, 20, 30, 40]);
const pointer = 0; // Pointer Wasm là byte offset trong linear memory.
let memoryView = new Int32Array(wasm.memory.buffer);
memoryView.set(values, pointer / Int32Array.BYTES_PER_ELEMENT);
console.log(wasm.sum_i32(pointer, values.length)); // 100
wasm.memory.grow(1);
memoryView = new Int32Array(wasm.memory.buffer); // Tạo lại view sau khi grow.
Các scalar call chuyển JavaScript number thành numeric parameter của Wasm. Array call khác hẳn: memoryView.set copy bốn integer từ array do JavaScript sở hữu sang Wasm memory; function chỉ nhận pointer và length. Với image, tensor, archive hay audio buffer lớn, copy và marshaling có thể tốn thời gian hơn operation cần tăng tốc.
Đối với non-shared memory, memory.grow thay ArrayBuffer được expose và detach buffer cũ, nên mọi typed-array view đã cache phải được tạo lại. Module thực tế cũng cần allocation protocol. Host có thể gọi malloc và free được export, hoặc binding generator có thể quản lý temporary buffer. Hai phía phải thống nhất byte order, alignment, encoding của string, ownership và bên nào release memory. Wasm memory dùng little-endian. Pointer do code không đáng tin truyền vào phải được kiểm tra range trước khi host import đọc dữ liệu.
Binding do wasm-bindgen, Emscripten hoặc component-model toolchain sinh ra che phần lớn ABI plumbing. Chúng cải thiện ergonomics nhưng không biến data transfer thành miễn phí. Hãy profile số lần crossing boundary, batch operation quá nhỏ và giữ hot loop cùng dữ liệu của nó ở một phía nếu có thể.
Warning
Linear memory được bounds check nhưng không có logical type. Nếu hai range hợp lệ vô tình overlap, hoặc stale pointer trỏ đến vùng allocator đã tái sử dụng, engine vẫn chỉ thấy byte access hợp lệ. Memory bug của source language vẫn có thể làm hỏng state bên trong module dù không thể trực tiếp đọc arbitrary host memory.
WASI, capability và component model
Core Wasm chủ ý không có operating-system API. Trong browser, JavaScript import có thể expose một số operation của DOM hoặc network. Ngoài browser, WebAssembly System Interface, tức WASI, định nghĩa interface khả chuyển cho stream, clock, random data và file. Runtime có thể cấp cho command quyền vào một preopened directory mà không mở toàn bộ filesystem, hoặc không cấp network capability nào.
Đây là mô hình hướng capability: code nhận handle và function đại diện cho đúng quyền nó cần thay vì mặc nhiên có ambient access. Điểm phân biệt này rất quan trọng. Module compile cho browser, target wasm32-unknown-unknown thuần và target WASI đều có thể chứa core Wasm hợp lệ nhưng yêu cầu tập import không tương thích. Binary instruction set khả chuyển; ABI xung quanh và capability được cấp mới quyết định program có chạy được hay không.
| Target hoặc contract | Host thường gặp | Module mong đợi |
|---|---|---|
| Core Wasm với custom import | Browser, plugin host, embedded engine | Function và memory riêng của ứng dụng |
| Binding hướng JavaScript | Browser hoặc JavaScript server runtime | JS glue được sinh, Web API và binding ABI |
| WASI command | Wasm runtime hoặc nền tảng giống container | WASI interface chuẩn cùng resource được cấp rõ ràng |
| Component | Runtime hỗ trợ component | Typed interface bằng WIT, resource và canonical ABI adapter |
Component model bổ sung contract cấp cao nằm trên core module. WebAssembly Interface Types, hay WIT, mô tả record, variant, string, list, resource và toàn bộ import/export của một world. Canonical ABI adapter lower các giá trị đó thành scalar call cùng dữ liệu trong core memory, rồi lift kết quả về interface value. Nhờ vậy component viết bằng nhiều ngôn ngữ có thể ghép với nhau mà không buộc từng cặp tự thống nhất một private ABI kiểu C.
Mức hỗ trợ component model và WASI vẫn khác nhau giữa language toolchain và runtime. Deployment nên pin version và interface đã kiểm thử. Đây là interoperability layer, không phải lời hứa rằng mọi library đều compile lại được mà không xử lý assumption về platform.
Module đã validate bắt đầu trong sandbox: nó không thể tự gọi system call hay đọc host memory nếu embedder không cấp đường đi. Tuy nhiên sandbox chỉ hẹp bằng chính các import. Host function nhận path chưa kiểm tra, gọi HTTP không hạn chế hoặc trả secret có thể tạo lỗi confused deputy và exfiltration. Hãy giới hạn capability, kiểm tra cặp pointer/length, đặt trần CPU và memory, áp deadline, đồng thời xem module và compiler toolchain là supply-chain input. Bounds check cũng không loại bỏ denial of service, logic bug, side channel kiểu Spectre hay vulnerability trong heap của unsafe language.
Hiệu năng, toolchain và debugging
Wasm cung cấp low-level operation dễ dự đoán, binary gọn, SIMD và memory tường minh. Nó rất phù hợp với codec, compression, cryptography, parser, emulator, numerical kernel và native library đã tồn tại. Engine có thể cache compiled code, dùng tiered compilation để startup nhanh rồi optimize dần, hoặc compile ahead of time trên server.
Điều đó không làm Wasm tự động nhanh hơn JavaScript hay native code. JavaScript JIT hiện đại tối ưu code có type ổn định rất mạnh. Wasm vẫn trả chi phí compile, boundary conversion, allocation, bounds check và đôi khi data copy. So với native binary, nó có thể thiếu lựa chọn instruction tối ưu riêng cho platform hoặc integration với library trưởng thành. Thread yêu cầu shared memory và host support; trong browser còn cần cross-origin isolation. SIMD cùng feature mới phải được runtime detect hoặc quy định trong deployment baseline.
Cần đo end-to-end latency và throughput với input thật. Cold startup và warm execution là hai workload khác nhau. Download module, compilation, instantiation, copy host-to-Wasm, execution và copy kết quả cũng là các phase riêng. Microbenchmark đặt dữ liệu sẵn trong memory trước khi bật timer có thể che đúng chi phí lớn nhất ở production.
Một vòng inspect ngắn phát hiện được nhiều lỗi contract:
wat2wasm math.wat -o math.wasm
wasm-tools validate math.wasm
wasm-objdump -x math.wasm
wasm2wat math.wasm -o roundtrip.wat
WABT cung cấp wat2wasm, wasm2wat và wasm-objdump; wasm-tools validate, print, compose và inspect module lẫn component; wasm-opt của Binaryen tối ưu sau link. Native-language toolchain có thể sinh DWARF debug information, còn browser developer tools hoặc runtime debugger có thể map instruction về source nếu metadata được giữ lại. Nên lưu unstripped artifact để điều tra, sinh release binary deterministic, inspect import trước deployment và profile cả generated Wasm lẫn host glue.
Không nên chọn Wasm chỉ vì bài toán có tính toán. Hãy dùng host language khi phần lớn thời gian nằm ở DOM manipulation, database hay network wait, hàng loạt host call rất nhỏ hoặc dữ liệu vốn là rich object của host. Tránh Wasm khi bundle size và cold startup lớn hơn lợi ích của phép tính ngắn, runtime thiếu feature cần thiết hoặc team chưa thể debug và vá native toolchain bổ sung. Với trusted server service không cần portability hay isolation, native process có thể đơn giản và nhanh hơn. Với một web form thông thường, JavaScript gần như luôn rõ ràng hơn.
Những điểm cần nhớ
- WebAssembly là đích biên dịch có type và được validate. Stack semantics sẽ thành machine code thực; validation kiểm tra cấu trúc và type chứ không chứng minh business logic đúng.
- Core module khai báo import/export và sở hữu state của từng instance. Linear memory chứa byte không có type, còn table cung cấp indirect reference và function dispatch được kiểm tra.
- Host call là ABI boundary. Quy ước pointer/length, allocation, encoding, memory growth và data copy phải được thiết kế rồi đo tường minh.
- WASI cung cấp system interface khả chuyển theo capability; component model và WIT thêm contract giàu kiểu dữ liệu ở tầng trên core module.
- Sandbox loại bỏ ambient access, không loại bỏ mọi rủi ro. Host import, resource exhaustion, unsafe-language bug, side channel và supply-chain compromise vẫn quan trọng.
- Wasm hiệu quả nhất khi lượng tính toán đủ lớn và dữ liệu ở lại trong module. Startup, chiến lược JIT/AOT, feature support và boundary overhead quyết định performance thực tế.
- Dùng WAT cùng binary tool để inspect contract, giữ debug metadata và chọn Wasm vì portability, isolation hoặc low-level code có thể tái sử dụng, không phải vì tính mới lạ.