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 dành cho ứng dụng, một số ứng dụng tiên tiến nhất đã đẩy trình duyệt web lên những giới hạn của chúng. Nhiều ngôn ngữ bậc cao có phương pháp "gần gũi hơn với kim loại" thông qua việc giao tiếp với các ngôn ngữ bậc 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ẽ tìm hiểu định nghĩa về ngôn ngữ tập hợp và lý do ngôn ngữ này có thể hữu ích trên web, sau đó tìm hiểu cách WebAssembly được tạo thông qua giải pháp tạm thời của asm.js.

Ngôn ngữ hợp thành

Bạn đã bao giờ lập trình bằng ngôn ngữ hợp thành chưa? Trong lập trình máy tính, ngôn ngữ tập hợp, thường được gọi đơn giản là hội 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ó sự tương ứng rất chặt chẽ giữa hướng dẫn bằng ngôn ngữ đó và hướng dẫn mã máy của kiến trúc.

Ví dụ: khi xem xét tài liệu Kiến trúc Intel® 64 và IA-32 (PDF), lệnh MUL (đối với định dạng mul) thực hiện phép nhân không có chữ ký của 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), đồng thời lưu trữ 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, và toán hạng nguồn nằm trong một thanh ghi đa năng như CX. Kết quả sẽ được lưu trữ lại trong thanh ghi AX. Hãy xem xét ví dụ về mã x86 sau đây:

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 giao nhiệm vụ với mục tiêu nhân 5 và 10, bạn có thể sẽ 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 theo tuyến tập hợp là mã ở cấp thấp và được tối ưu hoá cho máy hiệu quả hơn nhiều so với mã cấp cao mà con người tối ưu hoá. Trong trường hợp trước, đ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 phép toán phức tạp hơn, sự khác biệt có thể là đáng kể.

Như cái tên cho thấy, mã x86 phụ thuộc vào cấu 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 các lợi ích về hiệu suất của tập hợp?

asm.js

Bước đầu tiên để viết mã tập hợp không có phần phụ thuộc 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 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 xác thực tĩnh và động cho phép các công cụ JavaScript sử dụng chiến lược tối ưu hóa trước khi triển khai (AOT) cho mã asm.js hợp lệ. Mã được viết bằng các ngôn ngữ kiểu 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 đến nguồn như Emscripten đầu tiên (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ữ thành những tính năng hỗ trợ 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 các trình duyệt, nhưng nó đã được thay thế bằng WebAssembly. Lý do nên sử dụng asm.js tại thời điểm này là một 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ữ giống tập hợp ở cấp thấp với định dạng nhị phân nhỏ gọn, chạy với hiệu suất gần như gốc và cung cấp các ngôn ngữ như C/C++ và Rust cùng nhiều ngôn ngữ khác có mục tiêu biên dịch để chúng có thể chạy trên web. Tính năng hỗ trợ các ngôn ngữ được quản lý bằng bộ nhớ như Java và Dart đang trong quá trình hoạt động và sẽ sớm ra mắt hoặc đã được triển khai 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 động trên các hệ điều hành, với mục tiêu bảo mật và cho phép chạy trong môi trường hộp cát.

Mã WebAssembly (mã nhị phân, tức là mã byte) được thiết kế để chạy trên máy ngăn xếp ảo (VM) di động. Mã byte đượ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ó cách trình bày mã nhỏ gọn.

Quá trình thực thi khái niệm các lệnh được tiến hành thông qua bộ đếm chương trình truyền thống, tiến lên qua các hướng dẫn. Trên thực tế, hầu hết công cụ Wasm đều 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:

  • Hướng dẫn điều khiển 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 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 triển 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 ở đầ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 nó có thể chạy trong bất kỳ công cụ JavaScript nào (ngay cả khi không được tối ưu hoá), 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 đã đồng ý. Được công bố vào năm 2015 và phát hành lần đầu tiên 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 này nhờ vào sự đóng góp của tất cả các nhà cung cấp trình duyệt chính và các bên quan tâm khác. Kể từ năm 2017, trình duyệt sẽ được hỗ trợ trên toàn cầu.

WebAssembly có hai cách biểu thị: văn bảnnhị phân. Những gì bạn thấy ở trên là nội dung trình bày bằng văn bản.

Diễn tả bằng văn bản

Việc 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 (cho định dạng phần mở rộng WebA đơn giản t). 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 hệ số, bạn có thể có ý nghĩa của 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))
)

Biểu diễn nhị phân

Định dạng nhị phân sử dụng đuôi tệp .wasm không dành cho mục đích sử dụng của con người, chứ chưa nói đến 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 sang cách biểu diễn nhị phân sau đây. (Các nhận xét thường không phải là một phần của cách biểu diễn nhị phân, mà được công cụ wat2wasm thêm vào để giúp người dùng 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 lên 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à lúc trình biên dịch như Emscripten phát huy tác dụng. Thư viện 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++. 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 đây:

#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 tập lệnh này thành WebAssembly bằng lệnh emcc và các đối số gần như giống nhau:

$ 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 vào bảng điều khiển Công cụ cho nhà phát triển.

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

$ emcc hello.c -o hello.js

Như trước đây, 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 hãy chạy tệp JavaScript thu được hello.js với, ví dụ: Node.js:

$ 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 mới chỉ là phần nổi của tảng băng. Hãy tìm hiểu thêm về WebAssembly trong tài liệu về WebAssembly trên MDN và tham khảo tài liệu về Emscripten. Nói thật, làm việc với WebAssembly có thể có vẻ giống như Cách vẽ meme con 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ữ được biên dịch như C. May mắn thay, có những kênh như thẻ webassembly của StackOverflow và các chuyên gia thường rất sẵn lòng trợ giúp nếu bạn hỏi một cách lịch sự.

Xác nhận

Bài viết này do Jakob Kummerow, Derek SchuffRachel Andrew đánh giá.