2 điểm bởi GN⁺ 2024-11-11 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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

 
GN⁺ 2024-11-11
Ý 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

    • Porffor https://porffor.dev/Static Hermes https://hermesengine.dev/ có vẻ cũng đi theo hướng biên dịch
      Sẽ rất thú vị nếu so sánh với Jaws
    • Đúng vậy. Nhưng nói thật thì đây giống một cách tận dụng thông minh xuất phát từ việc không đủ giỏi để viết cả một interpreter hoàn chỉnh trên WASM
  • 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

    • Tôi vẫn chưa tìm được việc làm Rust không liên quan đến tiền mã hóa nào
      Ở 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
    • Việc được thổi phồng trên mạng xã hội và dễ thấy không nhất thiết có nghĩa là được dùng rộng rãi
      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 await và 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ày
      Khó 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 khai
      object.foo hoạt động, còn object["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

Runtime mà dự án sử dụng **chỉ là WebAssembly**  
Mã được tạo ra nhìn chung gồm khoảng 3 nghìn dòng mã WAT trong tệp này [https://github.com/drogus/jaws/blob/main/src/wat/template.wat](<https://github.com/drogus/jaws/blob/main/src/wat/template.wat>;) và phần mã JS của người dùng đã được chuyển đổi  
Ví dụ, với một chương trình rất đơn giản như `"console.log('foo')"`, toàn bộ phần “được tạo” chỉ có chừng này: [https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928/…](<https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928/…;)  
Về đại khái, nó chuẩn bị đối số bằng `new_static_string` rồi gọi `console.log`  
Hiện tại phía host vẫn cần một ít glue code, nhưng cuối cùng các binary như vậy sẽ có thể chạy trên bất kỳ runtime nào hỗ trợ WASIp2, WASM GC và đề xuất xử lý ngoại lệ

Việc hỗ trợ Web API hoặc các định danh global theo từng môi trường vẫn chưa được triển khai, nhưng có thể nói về cách nó sẽ hoạt động  
**Node.js API** dự kiến sẽ được hỗ trợ thông qua WASI  
WASI là tiêu chuẩn để chương trình WASM giao tiếp với thế giới bên ngoài  
Ví dụ, nó định nghĩa một tập hợp hàm tiêu chuẩn có thể dùng để gửi HTTP request, ghi ra STDOUT, đọc/ghi tệp, v.v.  
Vì vậy khi chạm tới các API như `fetch` hoặc `fs`, chúng sẽ hoạt động trong các runtime hỗ trợ WASI preview2  
Trình duyệt cũng có thể được hỗ trợ bằng polyfill, nhưng trong trường hợp này hỗ trợ I/O sẽ mang tính tùy biến hơn  
Nếu cho phép chương trình WASM đọc hoặc ghi tệp, bạn sẽ phải cung cấp một cơ chế, chẳng hạn lưu vào localStorage, dùng cơ sở dữ liệu SQLite đã được biên dịch sang WASM, hoặc thậm chí gửi tới nơi như S3
  • 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

    • Khi viết tiêu đề, tôi hoàn toàn không nhận ra cách nói hơi kỳ
  • 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?

    • Ở giai đoạn hiện tại thì khó nói, nhưng tôi nghĩ khả năng nó nhanh hơn SpiderMonkey hoặc V8 khi bật JIT là rất thấp
      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