WebAssembly là gì và đến từ đâu?

Kể từ khi web trở thành một nền tảng không chỉ dành cho tài liệu mà còn cho ứng dụng, một số ứng dụng tiên tiến nhất đã đẩy trình duyệt web đến giới hạn của chúng. Nhiều ngôn ngữ cấp cao hơn có phương pháp tiếp cận "gần gũi hơn" bằng cách giao tiếp với các ngôn ngữ cấp thấp hơn nhằm cải thiện hiệu suất. Ví dụ: Java có Giao diện gốc Java. Đối với JavaScript, ngôn ngữ cấp thấp hơn này là WebAssembly. Trong bài viết này, bạn sẽ khám phá định nghĩa về tập hợp ngôn ngữ và lý do tại sao ngôn ngữ này lại hữu ích trên web. Sau đó, hãy tìm hiểu cách tạo WebAssembly thông qua giải pháp tạm thời asm.js.

Ngôn ngữ tập hợp

Bạn đã bao giờ lập trình bằng ngôn ngữ tập hợp chưa? Trong lập trình máy tính, ngôn ngữ assembly, thường được gọi đơn giản làassembly và thường viết tắt là ASM hoặc asm, là bất kỳ ngôn ngữ lập trình cấp thấp nào có tương ứng rất rõ ràng giữa các hướng dẫn bằng ngôn ngữ đó và hướng dẫn mã máy của cấu trúc.

Ví dụ: khi xem trong Kiến trúc Intel® 64 và IA-32 (PDF), lệnh MUL (đối với phép mul) sẽ thực hiện phép nhân không dấu với toán hạng đầu tiên (toán hạng đích) và toán hạng thứ hai (toán hạng nguồn) và lưu kết quả trong toán hạng đích. Rất đơn giản, toán hạng đích là một toán hạng ngụ ý nằm trong thanh ghi AX, còn toán hạng nguồn nằm trong một thanh ghi đa năng như CX. Kết quả được lưu trữ lại trong thanh ghi AX. Hãy xem xét ví dụ về mã x86 sau:

mov ax, 5  ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx     ; Multiply the value of register AX (5)
           ; and the value of register CX (10), and
           ; store the result in register AX.

Để so sánh, nếu có nhiệm vụ nhân 5 với 10, bạn có thể viết mã tương tự như sau trong JavaScript:

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

Ưu điểm của việc đi vào lộ trình tập hợp là mã cấp thấp và được tối ưu hoá cho máy móc như vậy sẽ hiệu quả hơn nhiều so với mã cấp cao và được tối ưu hoá do con người thực hiện. Trong trường hợp trên, điều này không quan trọng, nhưng bạn có thể hình dung rằng đối với các thao tác phức tạp hơn, sự khác biệt có thể là rất đáng kể.

Đúng như tên gọi, mã x86 phụ thuộc vào kiến trúc x86. Điều gì sẽ xảy ra nếu có cách viết mã tập hợp không phụ thuộc vào một cấu trúc cụ thể, nhưng kế thừa những lợi ích về hiệu suất của việc tập hợp?

asm.js

Bước đầu tiên để viết mã tập hợp mà không có phần phụ thuộc về cấu trúc là asm.js, một tập hợp con JavaScript nghiêm ngặt có thể được dùng làm ngôn ngữ đích cấp thấp, hiệu quả cho trình biên dịch. Ngôn ngữ phụ này đã mô tả hiệu quả một máy ảo có hộp cát cho các ngôn ngữ không an toàn đối với bộ nhớ như C hoặc C++. Việc kết hợp phương thức xác thực tĩnh và động cho phép các công cụ JavaScript sử dụng chiến lược biên dịch tối ưu hoá trước (AOT) cho mã asm.js hợp lệ. Mã được viết bằng các ngôn ngữ nhập tĩnh có quản lý bộ nhớ thủ công (chẳng hạn như C) được dịch bởi một trình biên dịch từ nguồn sang nguồn như mô tả sớm (dựa trên LLVM).

Hiệu suất đã được cải thiện bằng cách giới hạn các tính năng ngôn ngữ ở những tính năng có thể sử dụng ở chế độ AOT. Firefox 22 là trình duyệt đầu tiên hỗ trợ asm.js, được phát hành dưới tên OdinMonkey. Chrome đã thêm tính năng hỗ trợ asm.js trong phiên bản 61. Mặc dù asm.js vẫn hoạt động trong trình duyệt, nhưng nó đã được thay thế bằng WebAssembly. Lý do để sử dụng asm.js vào thời điểm này sẽ là giải pháp thay thế cho các trình duyệt không hỗ trợ WebAssembly.

WebAssembly

WebAssembly là một ngôn ngữ cấp thấp giống như tập hợp với định dạng nhị phân nhỏ gọn, chạy với hiệu suất gần giống bản địa và cung cấp các ngôn ngữ như C/C++ và Rust, cũng như nhiều ngôn ngữ khác có đích biên dịch để chạy trên web. Chúng tôi đang hỗ trợ các ngôn ngữ do bộ nhớ quản lý như Java và Dart đang được phát triển và sẽ sớm được cung cấp, hoặc đã được hỗ trợ như trong trường hợp Kotlin/Wasm. WebAssembly được thiết kế để chạy cùng với JavaScript, cho phép cả hai hoạt động cùng nhau.

Ngoài trình duyệt, các chương trình WebAssembly cũng chạy trong các môi trường thời gian chạy khác nhờ WASI, Giao diện hệ thống WebAssembly, một giao diện hệ thống mô-đun cho WebAssembly. WASI được tạo để có thể di chuyển trên các hệ điều hành, với mục tiêu bảo mật và khả năng chạy trong môi trường hộp cát.

Mã WebAssembly (mã nhị phân, tức là mã byte) được dùng để chạy trên máy tính ngăn xếp ảo di động (VM). Mã byte này được thiết kế để có thể phân tích cú pháp và thực thi nhanh hơn so với JavaScript, đồng thời có tính biểu diễn mã nhỏ gọn.

Việc thực thi hướng dẫn ở dạng khái niệm diễn ra theo cách bộ đếm chương trình truyền thống chuyển qua hướng dẫn. Trong thực tế, hầu hết các công cụ Wasm biên dịch mã byte Wasm thành mã máy rồi thực thi mã đó. Có hai loại nội dung hướng dẫn:

  • Các lệnh kiểm soát tạo thành các cấu trúc điều khiển và đẩy(các) giá trị đối số của chúng ra khỏi ngăn xếp, có thể thay đổi bộ đếm chương trình và đẩy(các) giá trị kết quả vào ngăn xếp.
  • Hướng dẫn đơn giản đẩy(các) giá trị đối số của chúng ra khỏi ngăn xếp, áp dụng một toán tử cho các giá trị, sau đó đẩy(các) giá trị kết quả vào ngăn xếp, tiếp theo là tiến trình ngầm của bộ đếm chương trình.

Quay lại ví dụ trước, mã WebAssembly sau đây sẽ tương đương với mã x86 từ đầu bài viết:

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

Mặc dù asm.js được triển khai hoàn toàn trong phần mềm, tức là mã của asm.js có thể chạy trong bất kỳ công cụ JavaScript nào (ngay cả khi không được tối ưu hoá), nhưng WebAssembly yêu cầu chức năng mới mà tất cả các nhà cung cấp trình duyệt đều đã đồng ý. Được công bố vào năm 2015 và phát hành lần đầu vào tháng 3 năm 2017, WebAssembly trở thành đề xuất W3C vào ngày 5 tháng 12 năm 2019. W3C duy trì tiêu chuẩn với sự đóng góp của tất cả các nhà cung cấp trình duyệt lớn và các bên quan tâm khác. Kể từ năm 2017, trình duyệt hỗ trợ tất cả mọi người.

WebAssembly có hai cách biểu thị: văn bảntệp nhị phân. Những gì bạn thấy ở trên là cách trình bày dạng văn bản.

Trình bày bằng văn bản

Cách trình bày văn bản dựa trên biểu thức S và thường sử dụng đuôi tệp .wat (đối với định dạng WebAtext). Nếu thực sự muốn, bạn có thể viết bằng tay. Lấy ví dụ về phép nhân ở trên và làm cho nó hữu ích hơn bằng cách không còn mã hoá cứng các thừa số nữa, bạn có thể hiểu được đoạn mã sau:

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

Đại diện nhị phân

Định dạng nhị phân sử dụng đuôi tệp .wasm không dành cho hoạt động tiêu thụ của con người, chứ chưa nói đến tác phẩm sáng tạo của con người. Bằng cách sử dụng một công cụ như wat2wasm, bạn có thể chuyển đổi mã trên thành cách biểu diễn nhị phân dưới đây. (Chú thích thường không nằm trong cách biểu diễn nhị phân, nhưng được công cụ wat2 wasm thêm vào để giúp dễ hiểu hơn.)

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

Biên dịch thành WebAssembly

Như bạn thấy, cả .wat.wasm đều không đặc biệt thân thiện với con người. Đây là khi một trình biên dịch như Emscripten phát huy tác dụng. Công cụ này cho phép bạn biên dịch từ các ngôn ngữ cấp cao hơn như C và C++. Ngoài ra còn có các trình biên dịch khác cho các ngôn ngữ khác như Rust và nhiều ngôn ngữ khác. Hãy xem xét mã C sau:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

Thông thường, bạn sẽ biên dịch chương trình C này bằng trình biên dịch gcc.

$ gcc hello.c -o hello

Sau khi cài đặt Emscripten, bạn sẽ biên dịch nó thành WebAssembly bằng cách sử dụng lệnh emcc và gần như các đối số tương tự:

$ emcc hello.c -o hello.html

Thao tác này sẽ tạo một tệp hello.wasm và tệp trình bao bọc HTML hello.html. Khi phân phát tệp hello.html từ máy chủ web, bạn sẽ thấy "Hello World" được in ra bảng điều khiển Công cụ cho nhà phát triển.

Ngoài ra, có một cách để biên dịch thành WebAssembly mà không cần trình bao bọc HTML:

$ emcc hello.c -o hello.js

Như trước đó, thao tác này sẽ tạo tệp hello.wasm, nhưng lần này là tệp hello.js thay vì trình bao bọc HTML. Để kiểm tra, bạn chạy tệp JavaScript kết quả hello.js bằng Node.js, chẳng hạn như:

$ node hello.js
Hello World

Tìm hiểu thêm

Phần giới thiệu ngắn gọn này về WebAssembly chỉ là phần nổi bật. Tìm hiểu thêm về WebAssembly trong tài liệu về WebAssembly về MDN và tham khảo tài liệu về Mô phỏng. Sự thật là khi làm việc với WebAssembly, bạn có thể cảm thấy giống như Cách vẽ meme cú, đặc biệt là vì các nhà phát triển web quen thuộc với HTML, CSS và JavaScript không nhất thiết phải thành thạo các ngôn ngữ sẽ được biên dịch như C. Thật may là có những kênh như thẻ webassembly của StackOverflow. Các chuyên gia thường sẵn lòng trợ giúp nếu bạn đặt câu hỏi lịch sự.

Xác nhận

Bài viết này đã được Jakob Kummerow, Derek SchuffRachel Andrew đánh giá.