- Jawsm là trình biên dịch JavaScript→WebAssembly viết bằng Rust, một công cụ thử nghiệm tạo ra các binary WASM độc lập có thể chạy mà không cần interpreter
- Tương tự porffor, công cụ này tạo WASM standalone nhưng cách triển khai khác; nó tận dụng các lệnh từ các đề xuất WASM GC mới nhất, xử lý ngoại lệ và tối ưu hóa tail call để chuyển đổi cú pháp JavaScript thành lệnh WASM
- Hiện vượt qua khoảng 25% bộ kiểm thử test262; các tính năng được xem là quan trọng để xác nhận tính khả thi như scopes/closures, try/catch, async/await, generators đã được triển khai
- Chưa sẵn sàng cho production; nhiều tính năng ngôn ngữ và kiểu dựng sẵn còn thiếu hoặc chưa hoàn chỉnh, đồng thời RegExp, phần lớn builtins và BigInt arithmetic cũng còn thiếu
- Binary được tạo ra có tính di động giữa các runtime thấp do phụ thuộc vào các đề xuất WASM mới nhất; hiện cách chạy được dùng là chạy trên Chromium hoặc Node dựa trên V8 cùng WASIp2 polyfill
Mục tiêu và vị trí của Jawsm
- Jawsm là trình biên dịch JavaScript to WebAssembly, phát âm giống “awesome”
- Được viết bằng Rust, với mục tiêu biến mã JavaScript thành standalone WASM binary có thể chạy mà không cần interpreter
- Tạo ra kết quả tương tự porffor, nhưng cách tiếp cận triển khai khác
- Hiện là công cụ thử nghiệm và chưa sẵn sàng dùng trong production
- Nhiều tính năng của ngôn ngữ JavaScript còn thiếu hoặc chưa hoàn chỉnh
- Nhiều kiểu và phương thức dựng sẵn cũng còn thiếu hoặc chưa hoàn chỉnh
- Mục tiêu dài hạn là hỗ trợ 100% các tính năng của ngôn ngữ JavaScript
Vì sao Jawsm được tạo ra
- Dự án bắt đầu khi tác giả làm việc trên Crows, một công cụ stress test chạy các kịch bản WebAssembly
- Crows hiện chỉ hỗ trợ mã biên dịch từ Rust sang WASM
- Các bài test nhỏ thường dễ viết bằng interpreted language, nhưng cách chạy scripting language trên WASM hiện chưa lý tưởng
- Nếu kèm interpreter, kích thước binary sẽ tối thiểu vài MB và mức sử dụng bộ nhớ cũng lớn hơn
- Hoặc phải dùng một biến thể của ngôn ngữ đích như TinyGo, AssemblyScript
- Mục tiêu là tận dụng các đề xuất WASM hiện đại để có thể triển khai 100% tính năng JavaScript mà không cần interpreter đã biên dịch
- Tiền đề là bản thân WASM runtime vốn đã là một interpreter
Các tính năng hiện hoạt động
- Jawsm hiện vượt qua khoảng 25% bộ kiểm thử test262
- 4 tính năng cốt lõi để xác nhận tính khả thi của dự án đều đã được triển khai
- scopes/closures
- try/catch
- async/await
- generators
- Ngoài ra, các tính năng sau được kỳ vọng là hoạt động
- Khai báo và gán
var, let, const
- Các vòng lặp
do..while, while, for, for..in, for..of
- Câu lệnh
switch
- Hỗ trợ hạn chế cho
break, continue
- String literal và phép cộng string literal
- Số và các toán tử cơ bản
+, -, *, /
- Boolean và các toán tử boolean cơ bản
- Mảng và phần lớn các hàm liên quan đến Array
- Object literals
- Từ khóa
new
async, await
- Hỗ trợ hạn chế cho API
Promise
- Generator functions
try/catch
- Hỗ trợ BigInt rất cơ bản
Các tính năng còn thiếu
- Các hạng mục chính hiện còn thiếu như sau
- Phần lớn builtins
- Phần lớn phương thức của các builtins hiện có
- RegExp expressions
- BigInt arithmetic
- Kế hoạch bước tiếp theo tập trung triển khai các tính năng sau
- Hỗ trợ regexp cơ bản
- RegExp literals
- Các hàm rất cơ bản của đối tượng RegExp
- BigInt literals và hỗ trợ BigInt cơ bản
- Automatic casting tốt hơn khi kiểm tra equality hoặc dùng nhiều toán tử khác nhau
- Nhiều hàm hơn cho các builtins cơ bản như arrays, strings
Môi trường chạy và hạn chế
- Vì Jawsm dùng một số đề xuất WASM tương đối mới, các binary được tạo ra hiện chưa có tính di động cao giữa các runtime
- Triển khai mục tiêu được thiết kế với WASIp2 trong đầu
- Wasmtime là runtime có thể chạy components và WASIp2, nhưng không hỗ trợ một số hạng mục Jawsm sử dụng như WASM GC hoặc exception handling
- Để phát triển dễ hơn trong khi runtime chưa bắt kịp các đề xuất đã được chuẩn hóa, dự án dùng V8
- Dùng V8 thông qua Chromium hoặc Node
- Các tính năng WASIp2 cần thiết được bổ sung bằng JavaScript polyfill
- Repository có script
run.js để chạy binary do Jawsm tạo ra
- Cuối cùng, mục tiêu là có thể chạy trên mọi runtime triển khai WASM GC, exception handling và WASIp2 API
- Hoặc bao gồm cả cách dùng WASIp2 polyfill
Cách sử dụng
- Nếu không nhằm mục đích đóng góp, hiện không khuyến nghị sử dụng
- Sau khi clone repository, có thể dùng
execute.sh như sau
./execute.sh --cargo-run path/to/script.js
- Lệnh này tạo file WAT, biên dịch thành binary rồi chạy bằng Node.js
- Các công cụ cần thiết gồm
cargo của Rust
- Phiên bản
wasm-tools tương đối mới
- Node.js v23.0.0 trở lên
- Khi truyền tùy chọn
--cargo-run, dự án sẽ được biên dịch trước bằng cargo run rồi mới chạy
- Nếu chạy không có
--cargo-run, script sẽ cố chạy release build, vì vậy cần chạy cargo build --release trước
Cách hoạt động bên trong
- Jawsm chuyển đổi JavaScript syntax thành WASM instructions
- Quá trình chuyển đổi tận dụng các lệnh từ những đề xuất WASM sau
-
WASM GC
- exception handling
- tail call optimizations
- Mã Rust chuyển đổi script, đồng thời dùng một tập hợp kiểu và hàm để chuyển JavaScript semantics sang WASM
- Phần lớn WASM instruction được tạo bằng tarnik
- tarnik là một Rust macro tạo WASM instructions dựa trên cú pháp giống Rust
Ví dụ xử lý scope và closure
- WASM hỗ trợ function references, structs, arrays nhưng không trực tiếp cung cấp scope semantics của JavaScript
- Jawsm tạo thêm mã WASM để mô phỏng cách hoạt động của scope trong JavaScript
- Ví dụ mã JavaScript như sau
let a = "foo";
function bar() {
console.log(a);
}
bar();
- Trong JavaScript, định nghĩa hàm kế thừa scope nơi nó được định nghĩa, nên
bar() phải có thể truy cập biến a
- Luồng chuyển đổi đại khái như sau
- Tạo global scope không có parent
- Khai báo biến
a với giá trị "foo" trong scope hiện tại
- Khi tạo đối tượng hàm
bar, lưu kèm tham chiếu scope nơi hàm được định nghĩa
- Khi thực thi bên trong hàm, tạo scope mới nhưng giữ tham chiếu parentScope
retrieve(scope, "a") tìm a trong scope hiện tại và tất cả parent scope
- Lấy
bar từ scope hiện tại rồi gọi
Giấy phép
- Mã được phân phối theo Apache 2.0 license
1 bình luận
Ý kiến trên Hacker News
Đã tận dụng đề xuất WASM GC một cách thực sự thông minh
Từ trước đến nay, các trình biên dịch JS→WASM về cơ bản đều mang theo cả một JS engine, nhưng đây là lần đầu tôi thấy có cách cố gắng ánh xạ trực tiếp cấu trúc JS vào các tính năng nguyên thủy của WASM
Sẽ rất thú vị nếu so sánh với Jaws
Trước đây tôi từng làm một ngôn ngữ gần như TypeScript bằng trình biên dịch ARM nhúng
Nó gần với TypeScript hơn AssemblyScript rất nhiều, và một số kỹ thuật dùng lúc đó có thể hữu ích
https://www.microsoft.com/en-us/research/uploads/prod/2019/09/static-typescript-draft2.pdf
Câu “Tôi rất thích dùng Rust, nhưng cũng biết rằng nó không phải là một ngôn ngữ phổ biến rộng rãi” có đúng không?
Rust đang được thổi phồng dữ dội và dường như ngày nay ở đâu cũng dùng
Ở Estonia, nước tôi, xét theo các bảng tin tuyển dụng địa phương mà mọi người dùng, số việc làm tại chỗ là 0, nên theo nghĩa thực sự quan trọng thì khó có thể nói nó phổ biến chút nào
Còn tùy định nghĩa “rộng rãi” là gì, nhưng nhìn vào các chỉ số như StackOverflow, PyPL và thống kê GitHub thì mức sử dụng Rust có vẻ chỉ khoảng 5–10% so với JavaScript hoặc Python
Nếu “khá tự tin rằng cuối cùng có thể bao phủ 100% đặc tả JavaScript” thì có kết quả test262_runner.rb không?
Tôi biết đến test262 qua một bài trình bày của tác giả Porffor, và sẽ rất tốt nếu README có hiển thị tiến độ kiểu như https://github.com/CanadaHonk/porffor?tab=readme-ov-file#test262
Dự án tốt đấy
Hiện tại mới vượt qua khoảng 12% số bài test, nhưng vẫn còn nhiều phần dễ triển khai chưa làm
Điều này càng đúng nếu xét rằng dự án mới bắt đầu được 2 tuần; tất nhiên không có nghĩa là có thể tiến tuyến tính lên 100%
Vẫn còn một “đuôi dài” các kiểu và hàm built-in, nhưng vì tôi bắt đầu từ các “phần khó” nên một số phần dễ vẫn chưa được làm
Ví dụ, tôi mới triển khai cú pháp ở mức đủ để chạy harness của test262, như câu lệnh điều kiện và vòng lặp while; còn for, for in, for of, do while và các biểu thức điều kiện như switch thì chưa triển khai
Những phần này có thể được thêm vào gần như tương tự với cách triển khai if/else và while hiện có
Khi hoàn tất triển khai
awaitvà generator, các khái niệm ngữ nghĩa khó cuối cùng sẽ được giải quyết, nên sau đó tôi định triển khai các phần dễ nàyKhó nói coverage sẽ tăng bao nhiêu, nhưng ví dụ hiện tại 1200 bài test thất bại chỉ vì cú pháp
object["foo"]chưa được triển khaiobject.foohoạt động, cònobject["foo"]thì chưaĐiều đó không có nghĩa là cả 1200 bài sẽ tự động pass hết, nhưng thường có hàng trăm bài test thất bại vì những thiếu sót cú pháp tương đối đơn giản như vậy
Tôi cũng rất muốn thêm một biểu đồ đẹp như Porffor
Đọc README.md của dự án rồi mà tôi vẫn chưa rõ: cách sử dụng kỳ vọng là gì?
Tôi tò mò mã WASM đầu ra sẽ tương tác với runtime nào và như thế nào
Đây có phải là công cụ tương thích với cả trình duyệt lẫn các WASM runtime khác, hay chỉ chạy được trong runtime gắn với dự án?
Liên quan đến chuyện đó, khi gặp các Web API hoặc định danh global chỉ được định nghĩa trong một môi trường cụ thể bên trong mã JavaScript, chẳng hạn các định danh global của trình duyệt hiện đại hoặc Node.js, thì nó phản ứng thế nào?
Nếu không nhắm đến các môi trường đó thì I/O nên làm ra sao?
Những câu hỏi hay, tôi sẽ bổ sung chi tiết hơn vào README
Dự án này chủ yếu nhắm đến việc dùng WebAssembly trên server
Tôi nghĩ chạy JavaScript bằng WebAssembly bên trong JavaScript không có nhiều ý nghĩa, nhưng theo thời gian có thể sẽ hữu ích cho sandbox plugin phía frontend
Dù chạy trong trình duyệt hay trong runtime backend như WasmTime hoặc WasmEdge, hiện nay việc chạy JavaScript bên trong WebAssembly vẫn chưa lý tưởng
Hoặc phải biên dịch một JS engine như V8 hay SpiderMonkey sang WASM rồi chạy script trên đó, hoặc phải chấp nhận một ngôn ngữ “gần như JavaScript” như AssemblyScript
Đây trở thành yếu tố hạn chế khi chạy workload trên server
Ví dụ Fastly dùng SpiderMonkey cho WASM worker, và chỉ hello world thôi cũng dùng 5–10MB bộ nhớ cho mỗi instance
Trong khi đó Shopify dùng WASM cho tùy biến phía server của cửa hàng và giới hạn binary WASM ở mức dưới 250KB; với kích thước này thì khó nhét bất kỳ interpreter nào vào
Vì vậy ngôn ngữ “được khuyến nghị” trở thành AssemblyScript, và lý do được giải thích ở đây: https://shopify.engineering/shopify-webassembly
Tình huống này về mặt lịch sử là do WASM từng là một runtime rất đơn giản
Biên dịch sang WASM tương đối dễ, giống như biên dịch mã C sang mã máy, nhưng dù bản thân WebAssembly cũng là một dạng interpreter, việc thông dịch các ngôn ngữ cấp cao hơn trên nó lại không dễ
Giờ đây, khi các đề xuất mới như hỗ trợ garbage collection hoặc hỗ trợ xử lý exception được chuẩn hóa, WebAssembly đang trở thành một interpreter mạnh hơn nhiều, có các tính năng như struct, array và function reference
Jaws tận dụng điểm này để chuyển đổi mã JS thành mã WASM, và để WASM diễn giải mã kết quả mà không cần engine JS như SpiderMonkey
Trên thực tế, binary do Jaws tạo ra có lẽ có thể nhỏ hơn 50KB, đối lập với mức 10MB của cách biên dịch SpiderMonkey sang WASM rồi chạy script trên đó
Mức sử dụng bộ nhớ cũng sẽ giảm đáng kể
Với những công ty như Fastly, điều này có nghĩa là có thể giảm mức dùng bộ nhớ và chi phí máy chủ theo bậc độ lớn; còn với những công ty như Shopify, điều này có nghĩa là có thể cho các tác giả plugin backend tận dụng mã JavaScript hiện có, chẳng hạn các gói NPM và hệ sinh thái JavaScript
Có cảm giác “chạy JS không cần runtime trình duyệt” đang đến gần
Có lẽ cuối cùng một trong các dự án như Porffor, Jaws, hoặc một dự án khác sẽ thành công
Tôi rất thích cách tiếp cận này
Thay vì cố tạo binary trực tiếp, nếu build nhắm thẳng tới WASM, bạn có thể dựa vào WASM GC và hỗ trợ async có vẻ sẽ được đưa vào WASI 0.3
Xử lý khác biệt về encoding chuỗi và các utility liên quan như thế nào?
Theo hiểu biết mơ hồ của tôi thì WASM hỗ trợ UTF-8, còn JS có thể hỗ trợ cả UTF-16 có khả năng bị lỗi
Giống như tập lệnh CPU, máy trừu tượng WASM không có khái niệm chuỗi hay encoding
Nó chỉ là các byte trong bộ nhớ tuyến tính, và bạn có thể tự triển khai encoding mong muốn
WASM chỉ định UTF-8 làm encoding cho tên trong định dạng tệp, nhưng điều đó không liên quan đến máy ảo runtime
Một số người cũng gọi thứ đó là compiler
Dù sao thì cũng làm tốt đấy
Cái này có nhanh hơn việc chạy cùng mã đó trực tiếp bằng JS không, hay là để tương tác với các ngôn ngữ khác?
Các compiler JavaScript hiện đại tối ưu hóa khá tốt các đường chạy được thực thi thường xuyên bằng JIT
Mục đích của dự án này là cho phép chạy JavaScript trong môi trường sandbox WebAssembly
Ví dụ, Shopify cho phép mở rộng mã backend bằng WebAssembly nhưng giới hạn kích thước binary ở 250KB
Với kích thước đó hiện rất khó dùng JavaScript, vì ngay cả một interpreter đơn giản như QuickJS khi biên dịch sang WASM cũng sẽ lên tới vài MB