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 đến giới hạn. Phương pháp "gần hơn với phần cứng" bằng cách giao tiếp với các ngôn ngữ cấp thấp hơn để cải thiện hiệu suất được gặp trong nhiều ngôn ngữ cấp cao hơn. 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 ngôn ngữ tập hợp là gì và lý do ngôn ngữ này có thể hữu ích trên web, sau đó tìm hiểu cách tạo WebAssembly thông qua giải pháp tạm thời của asm.js.

Ngôn ngữ tập hợp

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. Đơn giản, toán hạng đích là một toán hạng ngầm ẩn nằm trong thanh ghi AX và toán hạng nguồn nằm trong thanh ghi dùng cho nhiều mục đích như CX. Kết quả được lưu trữ lại trong thanh ghi AX. Hãy xem 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 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 sử dụng phương thức tập hợp là mã cấp thấp và được tối ưu hoá cho máy như vậy hiệu quả hơn nhiều so với mã cấp cao và được tối ưu hoá cho con người. 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ó một 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 sẽ kế thừa các lợi ích về hiệu suất của mã 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 kiến trúc là asm.js, một tập hợp con nghiêm ngặt của JavaScript có thể được dùng làm ngôn ngữ mục tiêu 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 trong hộp cát cho các ngôn ngữ không an toàn về bộ nhớ như C hoặc C++. Việc kết hợp quy trình 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ữ 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).

Cải thiện hiệu suất bằng cách giới hạn các tính năng ngôn ngữ ở những tính năng phù hợp với 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 WebAssembly đã thay thế asm.js. 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ữ cấp thấp giống như ngôn ngữ tập hợp với định dạng tệp 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 như nhiều ngôn ngữ khác có mục tiêu biên dịch để chạy trên web. Chúng tôi đang hỗ trợ các ngôn ngữ được quản lý bộ nhớ như Java và Dart. Tính năng này sẽ sớm ra mắt hoặc đã ra mắt như trong trường hợp của 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 đảm bảo an toàn 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 thiết kế để chạy trên máy ngăn xếp ảo (VM) di động. Mã byte được thiết kế để 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. Trong thực tế, hầu hết các công cụ Wasm đều biên dịch mã byte Wasm thành mã máy, sau đó thực thi mã đó. Có hai loại 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.
  • Lệnh đơn giản đẩy (các) giá trị đối số 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, theo sau là một tiến trình ngầm ẩn 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á), 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 của 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, tính năng hỗ trợ trình duyệt được cung cấp cho mọi trình duyệt.

WebAssembly có hai cách biểu diễn: văn bảntệp nhị phân. Nội dung bạn thấy ở trên là nội dung biểu thị bằng văn bản.

Biểu diễn bằng văn bản

Biểu diễn văn bản dựa trên biểu thức S và thường sử dụng đuôi tệp .wat (dành cho định dạng văn bản tập hợp WebAssembly). Nếu thực sự muốn, bạn có thể viết tay. Lấy ví dụ về phép nhân ở trên và làm cho ví dụ này 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ể hiểu được 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 thành bản trình bày nhị phân sau. (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 sang 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. Trình biên dịch 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:

#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

Khi đã cài đặt Emscripten, bạn sẽ biên dịch Emscripten sang WebAssembly bằ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 vào bảng điều khiển Công cụ cho nhà phát triển.

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

$ emcc hello.c -o hello.js

Giống như trước, thao tác này sẽ tạo một 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 thử, bạn chạy tệp JavaScript hello.js thu được bằng ví dụ như 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 chỉ là phần nổi của tảng băng. 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. Thật sự, việc làm việc với WebAssembly có thể khiến bạn cảm thấy hơi giống như Cách vẽ meme về 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ần biên dịch như C. May mắn là có những kênh như thẻ webassembly của StackOverflow, nơi các chuyên gia thường sẵn sàng trợ giúp nếu bạn hỏi một cách lịch sự.

Lời cảm ơn

Bài viết này đã được Jakob Kummerow, Derek SchuffRachel Andrew xem xét.