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 các ứ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. Phương pháp "tiến gần đến phần cứng" bằng cách tương tác với các ngôn ngữ cấp thấp hơn để cải thiện hiệu suất được sử dụng trong nhiều ngôn ngữ cấp cao hơn. Ví dụ: Java có Giao diện gốc của Java. Đối với JavaScript, ngôn ngữ cấp thấp này là WebAssembly. Trong bài viết này, bạn sẽ khám phá ngôn ngữ hợp ngữ 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 WebAssembly được tạo thông qua giải pháp tạm thời của asm.js.
Hợp ngữ
Bạn đã từng lập trình bằng ngôn ngữ hợp ngữ chưa? Trong lập trình máy tính, ngôn ngữ hợp ngữ (thường được gọi đơn giản là Hợp ngữ và thường được 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ó mối tương quan rất chặt chẽ giữa các chỉ dẫn trong ngôn ngữ và chỉ dẫn mã máy của cấu trúc.
Ví dụ: khi xem Cấu trúc Intel® 64 và IA-32 (PDF), chỉ dẫn MUL
(để nhân) thực hiện phép nhân không dấu 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 kết quả vào toán hạng đích. Rất đơn giản, toán hạng đích là một toán hạng ngầm 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ả đượ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ụ nhân 5 với 10, có lẽ bạn 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 ngôn ngữ lập trình hợp ngữ là mã được tối ưu hoá cho máy và ở cấp độ thấp như vậy sẽ hiệu quả hơn nhiều so với mã được tối ưu hoá cho con người và ở cấp độ cao. 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 thao tác phức tạp hơn, sự khác biệt có thể đáng kể.
Đúng như tên gọi, mã x86 phụ thuộc vào kiến trúc x86. Nếu có một cách viết mã hợp ngữ không phụ thuộc vào một cấu trúc cụ thể, nhưng vẫn kế thừa được lợi ích về hiệu suất của hợp ngữ thì sao?
asm.js
Bước đầu tiên để viết mã hợp ngữ mà không có sự phụ thuộc vào 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 hiệu quả, cấp thấp cho trình biên dịch. Ngôn ngữ phụ này mô tả hiệu quả một máy ảo được cách ly cho các ngôn ngữ có bộ nhớ không an toàn như C hoặc C++. Sự kết hợp giữa 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 thời gian (AOT) cho mã asm.js hợp lệ. Mã được viết bằng các ngôn ngữ được nhập tĩnh có tính năng quản lý bộ nhớ thủ công (chẳng hạn như C) được dịch bằng trình biên dịch từ nguồn sang nguồn, chẳng hạn như Emscripten ban đầu (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 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 các trình duyệt, nhưng WebAssembly đã thay thế asm.js. Lý do sử dụng asm.js tại thời điểm này là để thay thế cho những trình duyệt không hỗ trợ WebAssembly.
WebAssembly
WebAssembly là một ngôn ngữ giống như hợp ngữ cấp thấp có định dạng nhị phân nhỏ gọn, chạy với hiệu suất gần như nguyên bản 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 chạy trên web. Chúng tôi đang phát triển tính năng hỗ trợ các ngôn ngữ được quản lý bộ nhớ như Java và Dart, đồng thời tính năng này sẽ sớm ra mắt hoặc đã ra mắt như 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 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 ra để có thể di chuyển giữa các hệ điều hành, với mục tiêu là bảo mật và có 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ột máy ảo (VM) ngăn xếp ảo di động. Mã byte được thiết kế để phân tích cú pháp và thực thi nhanh hơn JavaScript, đồng thời có một bản trình bày mã nhỏ gọn.
Việc thực thi các chỉ dẫn theo khái niệm diễn ra thông qua một bộ đếm chương trình truyền thống, tiến hành thông qua các chỉ dẫn. Trên 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, rồi thực thi mã đó. Hướng dẫn được chia thành 2 danh mục:
- 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 sẽ bật (pop) (các) giá trị đối số của chúng từ ngăn xếp, áp dụng một toán tử cho các giá trị, rồi đẩy (push) (các) giá trị kết quả vào ngăn xếp, sau đó là một bước tiế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 chưa đượ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 đề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 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 nào cũng được hỗ trợ.
WebAssembly có 2 biểu diễn: dạng văn bản và dạng nhị phân. Những gì bạn thấy ở trên là cách trình bày bằng văn bản.
Biểu diễn bằng văn bản
Biểu diễn bằng văn bản dựa trên S-expression và thường dùng đuôi tệp .wat
(đối với định dạng văn bản WebAssembly). 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 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 đ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))
)
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 người dùng, chứ đừng nói đến việc người dùng tạo ra. 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 biểu diễn nhị phân sau đây. (Các nhận xét thường không thuộc biểu diễn nhị phân, nhưng được công cụ wat2wasm thêm vào để 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
và .wasm
đều không thân thiện với người dùng. Đây là lúc 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 Emscripten thành WebAssembly bằng lệnh emcc
và gần như cùng các đối số:
$ emcc hello.c -o hello.html
Thao tác này sẽ tạo 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ộ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.
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
Như trước đây, 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ụ: 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à một phần nhỏ.
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ành thật mà nói, việc sử dụng WebAssembly có thể khiến bạn cảm thấy hơi giống với meme Cách vẽ một 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ông thạo các ngôn ngữ sẽ được 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 lò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 Schuff và Rachel Andrew xem xét.