WebAssembly là cách khác để chạy chương trình trên webpage bên cạnh Javascript. Trước đây, nếu muốn lập trình để trình duyệt tương tác với các thành phần khác nhau trên trang web, lựa chọn duy nhất của lập trình viên là sử dụng Javascript

Vì vậy khi ai đó nói WebAssembly thực thi code nhanh, đối tượng so sánh của họ hiển nhiên là với Javascript. Tuy nhiên WebAssembly không nhắm đến việc thay thế hoàn toàn JavaScript.

Trên thực tế các lập trình viên được kì vọng là sẽ sử dụng cả 2 ngôn ngữ trong cùng một ứng dụng. Kể cả khi bạn không trực tiếp viết WebAssembly  bạn vẫn có thể hưởng lợi ích của nó.

Các modules WebAssembly sẽ định nghĩa hàm có thể được gọi từ JavaScript. Thế nên cũng như hiện giờ bạn download module như lodash qua npm và gọi hàm của nó thông qua một list API, trong tương lai bạn cũng sẽ có thể download modules WebAssembly theo cách tương tự.

Trong bài viết này hãy cùng tìm hiểu cách làm thế nào để tạo một modules WebAssembly , và cách sử dụng nó từ Javascript

WebAssembly ở đâu trong toàn cảnh?

Trong bài viết về assembly, chúng ta đã bàn về cách chương trình dịch biên dịch ngôn ngữ bậc cao sang ngôn ngữ máy.

Diagram showing an intermediate representation between high level languages and assembly languages, with arrows going from high level programming languages to intermediate representation, and then from intermediate representation to assembly language

Vậy WebAssembly ở đâu trong bức tranh này?

Bạn có thể nghĩ nó chỉ là một ngôn ngữ assembly đích khác tương tự x86, ARM. Cũng có một phần đúng, trừ việc mỗi ngôn ngữ đó tương ứng với một kiến trúc máy tính cụ thể.

Khi bạn gửi code đến máy tính của người dùng cuối để thực thi, bạn không thể biết đoạn code đó sẽ chạy trên kiến trúc nào.

Vậy nên WebAssembly  hơi khác biệt một chút so với các ngôn ngữ assembly khác. Nó là ngôn ngữ máy của một cái máy tính trìu tượng, không phải là, một cái máy vật lý.

Vì lý do đó, chỉ lệnh của WebAssembly thỉnh thoảng được gọi là chỉ lệnh ảo. Chúng tương đồng với mã máy hơn một cách đáng kể so với Javascript. Chúng thể hiện một phần nhỏ những chỉ lệnh có thể được thực hiện một cách hiệu quả trên một số phần cứng phổ biến. Nhưng chúng không đồng nhất với một loại mã máy của một kiến trúc cụ thể nào.

Same diagram as above with WebAssembly inserted between the intermediate representation and assembly

Trình duyệt sẽ tải WebAssembly về và dịch ngắn gọn WebAssembly sang mã máy của máy tính đích.

Biên dịch ra .wasm

Bộ công cụ biên dịch hỗ trợ tốt nhất cho WebAssembly hiện tại là LLVM. Có nhiều front-ends và back-ends khác nhau có thể tích hợp vào LLVM.

Note: Hầu hết module WebAssembly lập trình viên sẽ viết bằng ngôn ngữ như  C và Rust rồi sau đó biên dịch sang WebAssembly, nhưng cũng có những cách khác để tạo module WebAssembly. Ví dụ, có một vài tool thử nghiệm giúp bạn tạo module WebAssembly bằng TypeScript, hoặc bạn có thể code trực tiếp bằng WebAssembly.

Giả sử bạn muốn dịch từ C sang WebAssembly. Bạn có thể sử dụng front-end của Clang để dịch C sang biểu diễn trung gian của LLVM. Một khi nó biểu diễn được dưới dạng LLVM’s IR, LLVM sẽ có thể hiểu được nó và có thể thực hiện một số thuật toán tối ưu.

Để dịch từ LLVM’s IR (biểu diễn trung gian- intermediate representation) sang WebAssembly, chúng ta cần một back-end hỗ trợ WebAssembly, có một backends đang được phát triên trong dự án LLVM project. Back-end này đã gần như hoàn thiện và sẽ sớm được công bố. Tuy nhiên, hiện tại bạn sẽ cần một chút công sức để sử dụng nó.

Có một công cụ khác dễ dùng hơn ở thời điểm hiện tại gọi là Emscripten. Nó có back-end của riêng nó và có thể tạo ra module WebAssembly  bằng cách dịch sang một ngôn ngữ đích khác (asm.js) và chuyển nó sang WebAssembly. Về xử lý bên dưới nó cũng sử dụng LLVM, vậy nên, bạn có thể chuyển đổi qua lại giữa 2 back-ends bằng Emscripten.

Diagram of the compiler toolchain

Emscripten bao gồm rất nhiều công cụ và thư viện khác, cho phép chuyển đổi toàn bộ codebases C/C++, nên đúng ra nó giống với SDK hơn là một trình dịch. Ví dụ, một lập trình viên có thể đã quen với việc sử dụng thư viên filesystem để đọc và viết file, nên Emscripten cũng mô phỏng lại thư viện này sử dụng IndexedDB.

Bất kể bạn sử dụng công cụ gì, thì kết quả cuối cùng sẽ là file .wasm. Dưới đây sẽ giải thích kĩ hơn về cấu trúc của file .wasm. Nhưng trước tiên hay xem xét cách sử dụng nó trong JS

Loading module .wasm trong JavaScript

File .wasm là một module WebAssembly , và nó có thể được load trong JavaScript. Ở thời điểm hiện tại, tiến trình loading có một chút hơi phức tạp.


function fetchAndInstantiate(url, importObject) {
  return fetch(url).then(response =>
    response.arrayBuffer()
  ).then(bytes =>
    WebAssembly.instantiate(bytes, importObject)
  ).then(results =>
    results.instance
  );
}

Bạn có thể đọc thêm tài liệu ở đây.

Trong tương lai quá trình này sẽ được đơn giản hóa hơn. Các bộ công cụ hiện tại được kì vọng là sẽ được cải tiến và tích hợp vào các bundlers như webpack và loaders như SystemJS. Khi đó load một module WebAssembly sẽ đơn giản như load JavaScript hiện tại.

Module WebAssembly  có một khác biệt cơ bản so với module JS. Ở thời điểm hiện tại, một hàm WebAssembly  chỉ có thể nhận tham số là kiểu số (số nguyên hoặc số phẩy động)

Diagram showing a JS function calling a C function and passing in an integer, which returns an integer in response

Với bất kì kiểu dữ liệu nào phức tạp hơn, như strings, bạn sẽ phải sử dụng bộ nhớ của module WebAssembly .

Nếu bạn chỉ quan làm việc với JavaScript, có thể bạn không quen với việc truy cập trực tiếp bộ nhớ. Những ngôn ngữ hiệu năng cao như C/C++, Rust, thường quản lý bộ nhớ thủ công. Bộ nhớ của module WebAssembly  giải lập bộ nhớ tương tự như các ngôn ngữ này.

Để làm điều đó, nó sử dụng ArrayBuffer của JavaScript. Mảng buffer là một mảng bytes. Indexes của mảng được sử dụng như địa chỉ bộ nhớ.

Nếu bạn muốn chuyển string qua lại giữa JavaScript và WebAssembly, bạn cần chuyển kí tự sang mã kí tự tương ứng. Rồi ghi nó vào mảng bộ nhớ. Khi đó Indexs của mảng là một số nguyên và có thể chuyển sang hàm WebAssembly . Index của kí tự đầu tiên trong chuỗi có thể sử dụng như con trỏ của chuỗi.

Diagram showing a JS function calling a C function with an integer that represents a pointer into memory, and then the C function writing into memory

Xác xuất lớn là bất kì ai khi phát triển một module WebAssembly cho web, sẽ tạo hàm tiện ích cho nó. Khi đó với tư cách là lập trình viên sử dụng module, bạn không cần phải hiểu rõ về việc quản lý bộ nhớ.

Nếu muốn đọc thêm, bạn có thể đọc thêm tại đây.

Cấu trúc của file .wasm

Nếu bạn viết code bằng ngôn ngữ bậc cao rồi biên dịch nó sang WebAssembly, bạn không cần phải hiểu cấu trúc của module WebAssembly. Nhưng có thể bạn sẽ cần hiểu bản chất.

Nếu bạn vẫn chưa đọc thì có lẽ bạn nên đọc lại bài viết về assembly

Đây là hàm của C sẽ được dịch sang WebAssembly

int add42(int num) {
  return num + 42;
}

Bạn có thể thử WASM Explorer để biên dịch hàm này

Nếu bạn mở file .wasm (và nếu text editor của bạn hỗ trợ hiển thị nó), bạn sẽ thấy thế này.


00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
00 41 2A 6A 0B

Đó là module được biểu diễn dưới dạng “nhị phân”. Nhị phân trong ngoặc kép vì về mặt lý thuyết đây là dạng hexa, nhưng nó có thể chuyển dễ dàng sang dạng nhị phân hoặc dạng mà con người đọc được.

Ví dụ,  num + 42 sẽ biểu diễn như thế này.

Table showing hexadecimal representation of 3 instructions (20 00 41 2A 6A), their binary representation, and then the text representation (get_local 0, i32.const 42, i32.add)

Code chạy như thế nào?  a stack machine

Nếu bạn có thắc mắc như trên, thì đây là điều tập lệnh trên sẽ làm

Diagram showing that get_local 0 gets value of first param and pushes it on the stack, i32.const 42 pushes a constant value on the stack, and i32.add adds the top two values from the stack and pushes the result

Bạn có thể thấy rằng lệnh add không chỉ ra một cách rõ ràng giá trị nào cần add. Vì WebAssembly hoạt động theo cơ chế gọi là stack machine. Tất cả giá trị cần thiết trong câu lệnh đều được load lên stack trước khi lệnh thực hiện.

Những lệnh như add biết chính xác nó cần bao nhiêu giá trị. add thì cần 2, nó sẽ lấy 2 giá trị khỏi đỉnh stack. Dẫn đến lệnh add có thể rất ngắn (1 byte) vì nó không cần chĩ rõ địa chỉ nguồn và đích của dữ liệu. Giúp giảm kích thước của file .wasm, dẫn đến giảm thời gian download.

Mặc dù WebAssembly biểu diễn lệnh đặc biệt dưới dạng stack machine, máy tính vật lý thường không hoạt động theo cách như vậy. Khi trình duyệt dịch WebAssembly  sang mã máy của máy đích nó đang chạy, nó sẽ sử dụng thanh ghi. Và chính vì code của WebAssembly không chỉ rõ tên thanh ghi, nó cho phép trình duyệt được lựa chọn thanh ghi tốt nhất có thể sử dụng trên máy tính đó.

Phân vùng của module

Bên cạnh hàm add42 , có những thành phần khác của file .wasm. Được gọi là phân vùng. Một số phân vùng là bắt buộc với tất cả module, một số là tùy chọn.

Bắt buộc:

  1. Type. Chứa chữ kí của hàm trong module, và các hàm được import trong module.
  2. Function. Chứa index của bất kì functions nào xuất hiện trong module.
  3. Code. chứa code của từng functions trong modules.

Tùy chọn:

  1. Export. Công khai functions, memories, tables, biến toàn cục cho các module WebAssembly khác và cho JavaScript. Cho phép các module được biên dịch độc lập có thể link được đến nhau. Đây là phiên bản .dll của WebAssembly.
  2. Import. Chỉ rõ functions, memories, tables, và biến toàn cục cần import từ module khác(WebAssembly hoặc Javascript).
  3. Start. Hàm sẽ tự động chạy khi module WebAssembly load xong(giống như hàm main).
  4. Global. Định nghĩa biến toàn cục của modules.
  5. Memory. Định nghĩa bộ nhớ sử dụng bởi modules.
  6. Table. cho phép sử dụng giá trị ngoài phạm vi của WebAssembly module, như đối tượng JavaScript. Cực kì có lợi khi gọi hàm ngược, ví dụ như callback.
  7. Data. Bộ nhớ cục bộ hoặc dữ liệu khởi tạo.
  8. Element. bảng cục bộ hoặc ddowcj khởi tạo khi chạy.

Chi tiết hơn, đây là giải thích chuyên sâu về cách phân vùng hoạt động.

Tiếp theo

Giờ bạn đã biết cách làm việc với WebAssembly , Trong phần tiếp theo chúng ta sẽ cùng xem vì sao WebAssembly có thể thực thi nhanh.