Creating and working with WebAssembly modules

WebAssembly là một cách để chạy các ngôn ngữ lập trình khác ngoài JavaScript trên các trang web. Trong quá khứ khi bạn muốn chạy code trong trình duyệt để tương tác với các phần khác nhau của trang web, lựa chọn duy nhất của bạn là JavaScript. Trong thực tế, các nhà phát triển có thể sử dụng cả WebAssembly và JavaScript trong cùng một ứng dụng. Ngay cả khi bạn không tự mình viết WebAssembly, bạn có thể tận dụng nó từ một nguồn nào đó. Các module WebAssembly định nghĩa các function có thể được sử dụng từ JavaScript. Vì vậy, giống như bạn tải về một module như lodash từ npm và gọi các function là một phần của API của nó, bạn sẽ có thể tải các module WebAssembly trong tương lai. Hãy xem làm thế nào chúng ta có thể tạo các module WebAssembly, và cách chúng ta có thể sử dụng chúng từ JavaScript.

Where does WebAssembly fit?

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

WebAssembly phù hợp với điểm nào trong hình ảnh này?

Bạn có thể nghĩ rằng nó chỉ là một trong những ngôn ngữ assembly mục tiêu. Đó là sự thật, ngoại trừ mỗi một trong những ngôn ngữ đó (x86, ARM) tương ứng với một cấu trúc máy cụ thể.

Khi bạn phân phối code sẽ được thực hiện trên máy của người dùng trên web, bạn không biết kiến ​​trúc mục tiêu của mình là gì. Vì vậy, WebAssembly có một chút khác biệt so với các loại assembly khác. Đó là machine language cho một máy khái niệm chứ không phải là một máy thực tế, máy vật lý. Do đó, instructions của WebAssembly đôi khi được gọi là virtual instructions. Họ có một bản đồ trực tiếp hơn đến machine code so với JavaScript source code. Chúng đại diện cho một loại giao điểm của những gì có thể được thực hiện hiệu quả trên phần cứng phổ biến. Nhưng chúng không phải là ánh xạ trực tiếp tới machine code cụ thể của một phần cứng cụ thể.

Cùng một sơ đồ như trên với WebAssembly chèn giữa đại diện trung gian và assembly.

Trình duyệt tải xuống WebAssembly. Sau đó, nó có thể thực hiện các bước nhảy ngắn từ WebAssembly tới mã assembly của máy đích.

Biên dịch thành .wasm

Chuỗi công cụ biên dịch hiện hỗ trợ nhiều nhất cho WebAssembly được gọi là LLVM. Có nhiều front-ends và back-ends khác nhau có thể được cắm vào LLVM.

Lưu ý: Hầu hết các nhà phát triển module WebAssembly sẽ code bằng các ngôn ngữ như C và Rust và sau đó biên dịch sang WebAssembly, nhưng có nhiều cách khác để tạo một module WebAssembly. Ví dụ: có một công cụ thử nghiệm giúp bạn xây dựng module WebAssembly bằng cách sử dụng TypeScript hoặc bạn có thể code trực tiếp văn bản của WebAssembly.

Khi chúng ta muốn chuyển từ C sang WebAssembly, chúng ta có thể sử dụng clang front-end để đi từ C đến đại diện trung gian (intermediate representation - IR) LLVM. Một khi nó ở IR, LLVM hiểu nó, vì vậy LLVM có thể thực hiện một số tối ưu hóa.

Bất kể toolchain bạn đã sử dụng là gì, kết quả cuối cùng là một tệp kết thúc bằng .wasm. Tôi sẽ giải thích thêm về cấu trúc của tệp .wasm bên dưới. Trước tiên, chúng ta hãy xem cách bạn có thể sử dụng nó trong JS.

Loadding module .wasm trong JavaScript

Tệp .wasm là module WebAssembly, và nó có thể được tải trong JavaScript. Vào thời điểm này, quá trình tải có một chút 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
  );
}

Có một sự khác biệt lớn giữa các module WebAssembly và các module JS. Hiện tại, các chức năng trong WebAssembly chỉ có thể sử dụng số (số nguyên hoặc số dấu chấm động) làm tham số hoặc giá trị trả về.

Đối với bất kỳ loại dữ liệu nào phức tạp hơn, như chuỗi, bạn phải sử dụng bộ nhớ của module WebAssembly.

Nếu bạn chủ yếu làm việc với JavaScript, việc truy cập trực tiếp tới bộ nhớ không phải là quá quen thuộc. Nhiều ngôn ngữ hiệu quả hơn như C, C ++, và Rust, thường có quản lý bộ nhớ bằng tay. Mô phỏng bộ nhớ heap của module WebAssembly bạn sẽ tìm thấy trong các ngôn ngữ đó. Để làm điều này, nó sử dụng một khái niệm trong JavaScript gọi là ArrayBuffer. ArrayBuffer là một mảng các byte. Vì vậy indexes của mảng là các địa chỉ bộ nhớ. Nếu bạn muốn truyền một chuỗi giữa JavaScript và WebAssembly, bạn chuyển đổi các ký tự sang mã ký tự tương đương của chúng. Sau đó viết vào array buffer. Vì các index là số nguyên, nên một indexes có thể được truyền vào hàm WebAssembly. Do đó, indexes của ký tự đầu tiên của chuỗi có thể được sử dụng như một con trỏ. Sơ đồ hiển thị một hàm JS gọi một hàm C với một số nguyên đại diện cho một con trỏ vào bộ nhớ, và sau đó là function C ghi vào bộ nhớ

Cấu trúc của một tệp .wasm

Nếu bạn đang viết mã bằng ngôn ngữ cấp cao hơn và sau đó tổng hợp nó vào WebAssembly, bạn không cần phải biết module WebAssembly được cấu trúc như thế nào. Nhưng nó có thể giúp hiểu những điều cơ bản. Đây là một function viết bởi ngôn ngữ C và chúng ta sẽ chuyển thành WebAssembly:

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

Bạn có thể thử sử dụng WASM Explorer để biên dịch chức năng này. Nếu bạn mở tệp .wasm (và nếu trình soạn thảo của bạn hỗ trợ hiển thị nó), bạn sẽ thấy nó như 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 trong đại diện "nhị phân" của nó. Tôi đặt dấu ngoặc kép quanh nhị phân bởi vì nó thường được hiển thị trong ký tự HEXA, nhưng có thể dễ dàng chuyển đổi sang ký hiệu nhị phân hoặc sang định dạng có thể đọc được của con người.

Ví dụ, num + 42 sẽ giống như thế này:

How the code works: a stack machine

Đây là những gì các hướng dẫn sẽ thực hiện.

Bạn có thể nhận thấy rằng toán tử add đã không chỉ ra địa chỉ của các tham số mà nó sử dụng. Điều này là do WebAssembly là một ví dụ về stack machine. Tất cả các giá trị mà một toán tử cần dùng được sắp xếp lên trên ngăn xếp trước khi toán tử được thực hiện. Các toán tử giống như toán tử add biết rằng nó cần đến bao nhiêu giá trị. Ví dụ như toán tử add, nó sẽ cần đến hai giá trị từ phía trên cùng của stack. Điều này có nghĩa là lệnh add có thể ngắn gọn (một byte đơn), bởi vì lệnh không cần chỉ định các thanh ghi nguồn hoặc đích. Điều này làm giảm kích thước của tệp .wasm, có nghĩa là mất ít thời gian hơn để tải xuống.

Mặc dù WebAssembly được xác định theo cách của một stack machine, đó không phải là cách nó hoạt động trên máy vật lý. Khi trình duyệt dịch WebAssembly thành mã máy cho trình duyệt đang chạy, nó sẽ sử dụng các thanh ghi. Vì mã WebAssembly không chỉ định thanh ghi, nó cho phép trình duyệt linh hoạt hơn để sử dụng thanh ghi tốt nhất phân bổ cho máy đó.