- Jawsm เป็นคอมไพเลอร์ JavaScript→WebAssembly ที่เขียนด้วย Rust เป็นเครื่องมือเชิงทดลองสำหรับสร้างไบนารี WASM แบบสแตนด์อโลนที่รันได้โดยไม่ต้องมีอินเทอร์พรีเตอร์
- สร้าง standalone WASM คล้ายกับ porffor แต่มีวิธีการ implement ต่างกัน โดยแปลง syntax ของ JavaScript เป็นคำสั่ง WASM ด้วยการใช้คำสั่งจาก proposal ล่าสุดอย่าง WASM GC, exception handling และ tail call optimization
- ปัจจุบันผ่าน ประมาณ 25% ของ test262 test suite และฟีเจอร์ที่มองว่าสำคัญต่อการตรวจสอบ viability ได้แก่ scopes/closures, try/catch, async/await และ generators ได้ implement แล้ว
- ยังไม่พร้อมสำหรับ production และยังขาดหรือไม่สมบูรณ์ในฟีเจอร์ของภาษาและ built-in types จำนวนมาก รวมถึงยังไม่มี RegExp, builtins ส่วนใหญ่ และ BigInt arithmetic
- ไบนารีที่สร้างขึ้นมี portability ระหว่าง runtime ต่ำ เนื่องจากพึ่งพา WASM proposals ล่าสุด และปัจจุบันใช้วิธีรันบน Chromium หรือ Node ที่ใช้ V8 พร้อม WASIp2 polyfill
เป้าหมายและตำแหน่งของ Jawsm
- Jawsm เป็นคอมไพเลอร์ JavaScript to WebAssembly ที่ออกเสียงเหมือน “awesome”
- เขียนด้วย Rust และมีเป้าหมายเพื่อแปลงโค้ด JavaScript ให้เป็น standalone WASM binary ที่รันได้โดยไม่ต้องมีอินเทอร์พรีเตอร์
- สร้างผลลัพธ์คล้ายกับ porffor แต่แนวทางการ implement ต่างกัน
- ตอนนี้ยังเป็น เครื่องมือเชิงทดลอง และยังไม่พร้อมใช้งานใน production
- ฟีเจอร์ของภาษา JavaScript จำนวนมากยังขาดหายไปหรือไม่สมบูรณ์
- built-in types และ methods จำนวนมากก็ยังขาดหายไปหรือไม่สมบูรณ์เช่นกัน
- เป้าหมายระยะยาวคือรองรับฟีเจอร์ภาษา JavaScript ให้ได้ 100%
ทำไมจึงสร้าง Jawsm
- โปรเจกต์นี้เริ่มต้นขึ้นระหว่างทำงานกับ Crows เครื่องมือ stress test สำหรับรันสถานการณ์ WebAssembly
- ปัจจุบัน Crows รองรับเฉพาะโค้ดที่คอมไพล์จาก Rust เป็น WASM เท่านั้น
- เทสต์ขนาดเล็กมักเขียนด้วย interpreted language ได้ง่ายกว่า แต่แนวทางการรัน scripting language บน WASM ในปัจจุบันยังไม่เหมาะนัก
- หากรวมอินเทอร์พรีเตอร์เข้าไป ขนาดไบนารีจะอย่างน้อยหลาย MB และใช้หน่วยความจำมากขึ้นด้วย
- หรือไม่ก็ต้องใช้ภาษาที่เป็น variant ของภาษาเป้าหมาย เช่น TinyGo, AssemblyScript
- มีเป้าหมายว่าหากใช้ WASM proposals สมัยใหม่ จะสามารถ implement ฟีเจอร์ JavaScript ได้ 100% โดยไม่ต้องมีอินเทอร์พรีเตอร์ที่คอมไพล์มา
- ตั้งอยู่บนสมมติฐานว่า WASM runtime เองก็เป็น อินเทอร์พรีเตอร์ อยู่แล้ว
ฟีเจอร์ที่ทำงานได้ในปัจจุบัน
- ปัจจุบัน Jawsm ผ่าน ประมาณ 25% ของ test262 test suite
- ฟีเจอร์หลัก 4 อย่างสำหรับตรวจสอบ viability ของโปรเจกต์ได้รับการ implement แล้วทั้งหมด
- scopes/closures
- try/catch
- async/await
- generators
- ฟีเจอร์อื่นที่ควรทำงานได้มีดังนี้
- การประกาศและกำหนดค่า
var, let, const
- ลูป
do..while, while, for, for..in, for..of
- คำสั่ง
switch
- การรองรับ
break, continue แบบจำกัด
- string literals และการบวก string literals
- numbers และ operators พื้นฐาน
+, -, *, /
- boolean และ boolean operators พื้นฐาน
- arrays และฟังก์ชันที่เกี่ยวข้องกับ Array ส่วนใหญ่
- object literals
- คีย์เวิร์ด
new
async, await
- การรองรับ
Promise API แบบจำกัด
- generator functions
try/catch
- การรองรับ BigInt ขั้นพื้นฐานมาก
ฟีเจอร์ที่ยังขาดอยู่
- รายการสำคัญที่ยังขาดอยู่ในปัจจุบันมีดังนี้
- builtins ส่วนใหญ่
- methods ส่วนใหญ่ของ builtins ที่มีอยู่
- RegExp expressions
- BigInt arithmetic
- แผนขั้นต่อไปจะเน้นการ implement ฟีเจอร์ต่อไปนี้
- การรองรับ regexp พื้นฐาน
- RegExp literals
- ฟังก์ชันพื้นฐานมากของออบเจ็กต์ RegExp
- BigInt literals และการรองรับ BigInt พื้นฐาน
- automatic casting ที่ดีขึ้นเมื่อใช้ equality checks หรือ operators หลายแบบ
- ฟังก์ชันเพิ่มเติมของ builtins พื้นฐาน เช่น arrays, strings เป็นต้น
สภาพแวดล้อมการรันและข้อจำกัด
- Jawsm ใช้ WASM proposals ที่ค่อนข้างใหม่หลายรายการ ดังนั้นไบนารีที่สร้างขึ้นจึงยังไม่มี portability ระหว่าง runtime สูงนัก
- implementation เป้าหมายคำนึงถึง WASIp2
- Wasmtime เป็น runtime ที่รัน components และ WASIp2 ได้ แต่ยังไม่รองรับบางส่วนของ WASM GC หรือสิ่งอย่าง exception handling ที่ Jawsm ใช้
- เพื่อให้พัฒนาได้ง่ายขึ้นจนกว่า runtime จะตาม proposals ที่ถูก standardize ทัน จึงใช้ V8
- ใช้ V8 ผ่าน Chromium หรือ Node
- ฟีเจอร์ WASIp2 ที่จำเป็นจะชดเชยด้วย JavaScript polyfill
- ใน repository มีสคริปต์
run.js สำหรับรันไบนารีที่ Jawsm สร้างขึ้น
- เป้าหมายสุดท้ายคือให้รันได้บนทุก runtime ที่ implement WASM GC, exception handling และ WASIp2 API
- หรือรวมถึงวิธีใช้ WASIp2 polyfill ด้วย
วิธีใช้งาน
- หากไม่ได้มีจุดประสงค์เพื่อ contribute ปัจจุบันยังไม่แนะนำให้ใช้งาน
- หลังจาก clone repository แล้ว สามารถใช้
execute.sh ได้ดังนี้
./execute.sh --cargo-run path/to/script.js
- คำสั่งนี้จะสร้างไฟล์ WAT, คอมไพล์เป็นไบนารี แล้วรันด้วย Node.js
- เครื่องมือที่ต้องใช้มีดังนี้
cargo ของ Rust
wasm-tools เวอร์ชันค่อนข้างใหม่
- Node.js v23.0.0 ขึ้นไป
- หากส่ง option
--cargo-run เข้าไป จะคอมไพล์โปรเจกต์ด้วย cargo run ก่อนแล้วจึงรัน
- หากรันโดยไม่มี
--cargo-run จะพยายามรัน release build ดังนั้นต้องรัน cargo build --release ก่อน
วิธีทำงานภายใน
- Jawsm แปลง JavaScript syntax เป็น WASM instructions
- กระบวนการแปลงใช้คำสั่งจาก WASM proposals ต่อไปนี้
-
WASM GC
- exception handling
- tail call optimizations
- โค้ด Rust จะแปลงสคริปต์ และใช้ชุด types กับ functions เพื่อย้าย JavaScript semantics ไปเป็น WASM
- WASM instructions ส่วนใหญ่สร้างด้วย tarnik
- tarnik เป็น Rust macro ที่สร้าง WASM instructions โดยอิงจาก syntax แบบ Rust-like
ตัวอย่างการจัดการ scope และ closure
- WASM รองรับ function references, structs, arrays แต่ไม่ได้ให้ scope semantics ของ JavaScript โดยตรง
- Jawsm สร้างโค้ด WASM เพิ่มเติมเพื่อจำลองพฤติกรรม scope ของ JavaScript
- ตัวอย่างโค้ด JavaScript มีดังนี้
let a = "foo";
function bar() {
console.log(a);
}
bar();
- ใน JavaScript การนิยามฟังก์ชันจะสืบทอด scope ที่มันถูกนิยามไว้ ดังนั้น
bar() ต้องสามารถเข้าถึงตัวแปร a ได้
- flow การแปลงโดยคร่าวมีดังนี้
- สร้าง global scope ที่ไม่มี parent
- ประกาศตัวแปร
a ใน scope ปัจจุบันเป็น "foo"
- เมื่อสร้างออบเจ็กต์ฟังก์ชัน
bar จะเก็บ reference ของ scope ที่ฟังก์ชันถูกนิยามไว้ไปด้วย
- เมื่อรันภายในฟังก์ชัน จะสร้าง scope ใหม่แต่ยังคง reference ของ parentScope ไว้
retrieve(scope, "a") จะค้นหา a ใน scope ปัจจุบันและ parent scope ทั้งหมด
- ดึง
bar จาก scope ปัจจุบันแล้วเรียกใช้งาน
ไลเซนส์
- โค้ดเผยแพร่ภายใต้ Apache 2.0 license
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ใช้ ข้อเสนอ WASM GC ได้อย่างชาญฉลาดจริง ๆ
จนถึงตอนนี้คอมไพเลอร์ JS→WASM โดยพื้นฐานแล้วมักใช้วิธีขน JS engine ทั้งตัวไปด้วย แต่นี่เป็นครั้งแรกที่เห็นความพยายามแมปโครงสร้างของ JS เข้ากับความสามารถดั้งเดิมของ WASM โดยตรง
น่าจะน่าสนใจถ้าเอามาเทียบกับ Jaws
ก่อนหน้านี้เคยทำภาษาที่เกือบจะเป็น TypeScript ด้วย คอมไพเลอร์สำหรับ ARM แบบฝังตัว
มันใกล้เคียง TypeScript มากกว่า AssemblyScript เยอะ และเทคนิคบางส่วนที่ใช้ตอนนั้นอาจช่วยได้
https://www.microsoft.com/en-us/research/uploads/prod/2019/09/static-typescript-draft2.pdf
คำพูดที่ว่า “ผมชอบใช้ Rust มากจริง ๆ แต่ก็รู้ว่ามันไม่ใช่ภาษาที่ได้รับความนิยมอย่างกว้างขวาง” นี่ถูกไหม?
Rust ถูกโฆษณาเกินจริงอย่างมาก และดูเหมือนทุกวันนี้ถูกใช้กันทุกที่
ในประเทศของผมคือเอสโตเนีย ถ้าดูจากเว็บประกาศงานท้องถิ่นที่คนใช้กัน งานในพื้นที่มี 0 ตำแหน่ง ดังนั้นในความหมายที่สำคัญจริง ๆ ก็แทบจะพูดไม่ได้เลยว่ามันเป็นที่นิยม
ขึ้นอยู่กับว่านิยามคำว่า “แพร่หลาย” อย่างไร แต่ถ้าดูดัชนีอย่าง StackOverflow, PyPL และสถิติ GitHub ปริมาณการใช้ Rust ดูเหมือนจะอยู่ที่ประมาณ 5~10% ของ JavaScript หรือ Python
ถ้า “ค่อนข้างมั่นใจว่าสุดท้ายจะครอบคลุมสเปก JavaScript ได้ 100%” แล้วมี ผลลัพธ์ test262_runner.rb ไหม?
ผมรู้จัก test262 จากงานนำเสนอของผู้เขียน Porffor และถ้ามีตัวบอกความคืบหน้าใน README แบบ https://github.com/CanadaHonk/porffor?tab=readme-ov-file#test262 ก็น่าจะดี
เป็นโปรเจกต์ที่ดี
ตอนนี้ผ่านเทสต์ประมาณ 12% แต่ยังเหลือส่วนที่ทำได้ง่ายอยู่อีกมาก
โดยเฉพาะเมื่อคิดว่าโปรเจกต์เพิ่งเริ่มมาแค่ 2 สัปดาห์ และแน่นอนว่าไม่ได้หมายความว่าจะไต่ไปถึง 100% ได้แบบเส้นตรง
ยังมี long tail ของ built-in types และฟังก์ชันต่าง ๆ เหลืออยู่ แต่เพราะเริ่มทำจาก “ส่วนที่ยาก” ก่อน จึงยังมีบางส่วนง่าย ๆ ที่ยังไม่ได้ทำ
ตัวอย่างเช่น ในด้านไวยากรณ์ ตอนนี้ทำไว้แค่เงื่อนไขกับลูป while เท่าที่จำเป็นสำหรับรัน test262 harness เท่านั้น ส่วน for, for in, for of, do while และเงื่อนไขอย่าง switch ยังไม่ได้ทำ
สิ่งเหล่านี้สามารถเพิ่มได้โดยแทบจะเหมือนกับ implementation ของ if/else และ while ที่มีอยู่แล้ว
เมื่อทำ implementation ของ
awaitและ generator เสร็จ แนวคิดเชิง semantics ที่ยากชุดสุดท้ายก็จะถูกจัดการ จากนั้นจึงจะไปทำส่วนง่าย ๆ เหล่านี้ต่อยากที่จะบอกว่า coverage จะเพิ่มขึ้นแค่ไหน แต่เช่น ตอนนี้มี 1,200 เทสต์ ที่ล้มเหลวเพราะยังไม่ได้ทำไวยากรณ์
object["foo"]object.fooทำงานได้ แต่object["foo"]ยังทำไม่ได้ไม่ได้หมายความว่า 1,200 เทสต์นั้นจะผ่านทั้งหมดโดยอัตโนมัติ แต่บ่อยครั้งที่เทสต์หลายร้อยรายการล้มเหลวเพราะขาดไวยากรณ์ที่ค่อนข้างเรียบง่ายแบบนี้
อยากใส่กราฟสวย ๆ แบบ Porffor ด้วยแน่นอน
อ่าน README.md ของโปรเจกต์แล้วก็ยังไม่ค่อยเข้าใจว่า รูปแบบการใช้งานที่คาดหวังคืออะไร?
อยากรู้ว่า โค้ด WASM ที่ได้ออกมาจะโต้ตอบกับ runtime แบบไหนและอย่างไร
และอยากรู้ด้วยว่าเป็นเครื่องมือที่เข้ากันได้ทั้งกับเบราว์เซอร์และ WASM runtime อื่น ๆ หรือทำงานได้เฉพาะกับ runtime ที่ผูกมากับโปรเจกต์เท่านั้น
ที่เกี่ยวข้องกัน ถ้าในโค้ด JavaScript เจอ Web API หรือ global identifier ที่นิยามเฉพาะบาง environment เช่น global identifier ของเบราว์เซอร์รุ่นใหม่หรือของ Node.js จะตอบสนองอย่างไร?
ถ้าไม่ได้ target environment เหล่านั้น แล้ว I/O ควรทำอย่างไร?
เป็นคำถามที่ดี และจะเพิ่มรายละเอียดลงใน README ให้มากขึ้น
โปรเจกต์นี้มุ่งเป้าไปที่ การใช้ WebAssembly บนเซิร์ฟเวอร์ เป็นหลัก
การรัน JavaScript ด้วย WebAssembly ภายใน JavaScript เองดูไม่ค่อยมีความหมายมากนัก แต่เมื่อเวลาผ่านไปอาจมีประโยชน์สำหรับการทำ sandbox ให้ปลั๊กอินฝั่งฟรอนต์เอนด์
ไม่ว่าจะรันในเบราว์เซอร์ หรือใน runtime ฝั่ง backend อย่าง WasmTime หรือ WasmEdge การรัน JavaScript ภายใน WebAssembly ตอนนี้ยังไม่ใช่ทางเลือกที่ดีนัก
คุณต้องคอมไพล์ JS engine อย่าง V8 หรือ SpiderMonkey เป็น WASM แล้วรันสคริปต์บนมัน หรือไม่ก็ต้องพอใจกับภาษา “เกือบ JavaScript” อย่าง AssemblyScript
นี่กลายเป็นข้อจำกัดในการรัน workload ฝั่งเซิร์ฟเวอร์
ตัวอย่างเช่น Fastly ใช้ SpiderMonkey สำหรับ WASM worker ซึ่งแม้แต่ hello world ก็ใช้ หน่วยความจำ 5~10MB ต่อ instance
ขณะที่ Shopify ใช้ WASM สำหรับการปรับแต่งฝั่งเซิร์ฟเวอร์ของร้านค้า และจำกัดไบนารี WASM ไว้ที่ ไม่เกิน 250KB ซึ่งด้วยขนาดนี้แทบเป็นไปไม่ได้ที่จะใส่อินเทอร์พรีเตอร์ใด ๆ เข้าไป
ดังนั้นภาษา “ที่แนะนำ” จึงกลายเป็น AssemblyScript และมีการอธิบายเหตุผลไว้ที่นี่: https://shopify.engineering/shopify-webassembly
สถานการณ์นี้เป็นเพราะในอดีต WASM เป็น runtime ที่เรียบง่ายมาก
การคอมไพล์โค้ด C ไปเป็น WASM ค่อนข้างง่าย คล้ายกับคอมไพล์เป็น machine code แต่แม้ WebAssembly เองจะเป็นอินเทอร์พรีเตอร์ชนิดหนึ่ง การตีความภาษาระดับสูงกว่าบนมันก็ไม่ใช่เรื่องง่าย
ตอนนี้เมื่อข้อเสนอใหม่ ๆ อย่าง การรองรับ garbage collection หรือการรองรับ exception handling กำลังถูกทำให้เป็นมาตรฐาน WebAssembly จึงกำลังกลายเป็นอินเทอร์พรีเตอร์ที่ทรงพลังขึ้นมาก พร้อมความสามารถอย่าง struct, array และ function reference
Jaws ใช้ประโยชน์จากจุดนี้โดยแปลงโค้ด JS เป็นโค้ด WASM และให้ WASM ตีความโค้ดผลลัพธ์โดยไม่ต้องใช้เอนจิน JS อย่าง SpiderMonkey
ในทางปฏิบัติ ไบนารีที่ Jaws สร้างขึ้นอาจมีขนาด ต่ำกว่า 50KB และต่างจากวิธีคอมไพล์ SpiderMonkey เป็น WASM แล้วรันสคริปต์บนมันซึ่งมีขนาด 10MB
การใช้หน่วยความจำก็น่าจะลดลงอย่างมากด้วย
สำหรับบริษัทอย่าง Fastly นี่หมายถึงสามารถลดการใช้หน่วยความจำและต้นทุนเซิร์ฟเวอร์ได้เป็นระดับหลักหน่วยของลำดับขนาด และสำหรับบริษัทอย่าง Shopify นี่หมายถึงสามารถให้ผู้เขียนปลั๊กอินฝั่งแบ็กเอนด์ใช้โค้ด JavaScript เดิม เช่น แพ็กเกจ NPM และอีโคซิสเต็ม JavaScript ได้
รู้สึกว่า “การรัน JS โดยไม่มี browser runtime” กำลังใกล้เข้ามาแล้ว
Porffor, Jaws หรือโปรเจกต์อื่นสักตัวน่าจะสำเร็จในที่สุด
ชอบแนวทางนี้มากจริง ๆ
แทนที่จะพยายามสร้างไบนารีโดยตรง ถ้า build โดย target ไปที่ WASM โดยตรง ก็สามารถพึ่งพา WASM GC และการรองรับ async ที่ดูเหมือนจะถูกรวมอยู่ใน WASI 0.3 ได้
จัดการกับความต่างของการเข้ารหัสสตริงและยูทิลิตีที่เกี่ยวข้องอย่างไร?
เท่าที่ผมเข้าใจแบบเลือน ๆ คือ WASM รองรับ UTF-8 ส่วน JS ก็รองรับ UTF-16 ที่อาจเสียหายได้ด้วย
เหมือนกับชุดคำสั่งของ CPU ใน abstract machine ของ WASM ไม่มีแนวคิดเรื่องสตริงหรือการเข้ารหัส
มันเป็นแค่ไบต์ใน linear memory และถ้าต้องการ encoding แบบไหนก็ implement เองได้
WASM ระบุ UTF-8 เป็น encoding ของชื่อในรูปแบบไฟล์ แต่สิ่งนี้ไม่เกี่ยวกับ virtual machine ตอนรันไทม์
บางคนก็เรียกสิ่งนั้นว่า compiler
ยังไงก็ทำออกมาได้ดี
นี่เร็วกว่าการรันโค้ดเดียวกันด้วย JS ตรง ๆ หรือมีไว้เพื่อ interoperability กับภาษาอื่น?
compiler JavaScript สมัยใหม่ optimize เส้นทางที่ถูกรันบ่อยด้วย JIT ได้ค่อนข้างดี
จุดประสงค์ของโปรเจกต์นี้คือทำให้สามารถรัน JavaScript ในสภาพแวดล้อม sandbox ของ WebAssembly ได้
ตัวอย่างเช่น Shopify อนุญาตให้ขยายโค้ดแบ็กเอนด์ด้วย WebAssembly แต่จำกัดขนาดไบนารีไว้ที่ 250KB
ด้วยขนาดระดับนั้น ปัจจุบันใช้ JavaScript ได้ยาก เพราะแม้แต่ interpreter แบบเรียบง่ายอย่าง QuickJS เมื่อคอมไพล์เป็น WASM ก็มีขนาดหลาย MB