- MicroQuickJS(MQuickJS) คือ JavaScript engine ขนาดเล็กมากที่ออกแบบมาสำหรับระบบฝังตัว โดยสามารถทำงานได้ด้วย RAM ราว 10kB และ ROM เพียง 100kB
- ใช้ tracing garbage collector และ การจัดเก็บสตริงแบบ UTF-8 เพื่อลดการใช้หน่วยความจำ ขณะยังคง ความเร็วใกล้เคียง QuickJS
- ภาษาที่รองรับคือ ชุดย่อยที่จำกัดของ JavaScript ใกล้เคียง ES5 และอนุญาตเฉพาะ strict mode ที่ห้ามไวยากรณ์ซึ่งมีโอกาสผิดพลาดสูง
- ผ่าน REPL tool
mqjs สามารถรันสคริปต์ บันทึก bytecode และตั้งค่าขีดจำกัดหน่วยความจำได้ และ bytecode ที่สร้างขึ้นสามารถรันจาก ROM ได้โดยตรง
- เอนจินทั้งหมดและ standard library อยู่ใน ROM ทำให้ได้ การเริ่มต้นทำงานที่รวดเร็วและการใช้หน่วยความจำต่ำ ช่วยเพิ่ม ประสิทธิภาพการรัน JavaScript ในสภาพแวดล้อมแบบฝังตัว
แนะนำ
- MicroQuickJS(MQuickJS) คือ JavaScript engine สำหรับระบบฝังตัว ทำงานได้บน RAM 10kB และ ROM 100kB (รวมโค้ด ARM Thumb-2)
- ความเร็วใกล้เคียงกับ QuickJS
- รองรับเพียง ชุดย่อยที่ใกล้เคียง ES5 และทำงานได้เฉพาะใน strict mode ที่ ห้ามไวยากรณ์ที่ไม่มีประสิทธิภาพหรือมีความเสี่ยงต่อข้อผิดพลาดสูง
- แม้จะแชร์โค้ดบางส่วนกับ QuickJS แต่โครงสร้างภายในถูก ออกแบบใหม่ทั้งหมดเพื่อประหยัดหน่วยความจำ
- ใช้ tracing garbage collector, ไม่ใช้ CPU stack, และ จัดเก็บสตริงแบบ UTF-8
REPL
- คำสั่ง REPL คือ
mqjs รองรับการรันสคริปต์ การประเมินผล โหมดโต้ตอบ การตั้งค่าขีดจำกัดหน่วยความจำ และการบันทึก bytecode
- ตัวอย่าง:
./mqjs --memory-limit 10k tests/mandelbrot.js
- ใช้ตัวเลือก
-o เพื่อบันทึก bytecode ที่คอมไพล์แล้ว ลงไฟล์ได้
- bytecode ที่บันทึกไว้สามารถรันได้ด้วย
./mqjs mandelbrot.bin
- bytecode จะแตกต่างกันตาม endianness และขนาด word ของ CPU (32/64 บิต) และสามารถใช้ตัวเลือก
-m32 เพื่อสร้าง bytecode สำหรับ 32 บิตได้
- ใช้ตัวเลือก
--no-column เพื่อลบ หมายเลขคอลัมน์ในข้อมูลดีบัก ได้
strict mode
- อนุญาตเฉพาะ strict mode เท่านั้น, ไม่สามารถใช้คีย์เวิร์ด
with ได้ และตัวแปร global ต้องประกาศด้วย var เสมอ
- ไม่อนุญาต array hole
- ตัวอย่าง:
a[10] = 2 จะทำให้เกิด TypeError
- หากต้องการอาร์เรย์ที่มีช่องว่าง ให้ใช้ object ทั่วไปแทน
- รองรับเฉพาะ global eval และไม่สามารถเข้าถึงตัวแปรภายในได้
- ไม่รองรับ value boxing (
new Number(1) เป็นต้น)
ชุดย่อยของ JavaScript
- อิงตาม strict mode และเน้น ความเข้ากันได้กับ ES5
- Array object ไม่มี hole และการเข้าถึง index ที่อยู่นอกช่วงจะเป็นข้อผิดพลาด
for in จะวนเฉพาะ พร็อพเพอร์ตีของ object เอง และ for of รองรับเฉพาะอาร์เรย์
- มี global object แต่ไม่รองรับ getter/setter และพร็อพเพอร์ตีที่สร้างขึ้นโดยตรงจะไม่ถูกเปิดเผยเป็นตัวแปร global
- Regexp รองรับการแยกตัวพิมพ์เล็ก-ใหญ่เฉพาะ ASCII และ
/./ จะจับคู่ในระดับ Unicode code point แทน UTF-16
- ฟังก์ชันสตริง จัดการเฉพาะ ASCII (
toLowerCase, toUpperCase)
- Date รองรับเพียง
Date.now()
- ความสามารถเพิ่มเติมที่รองรับ:
for of, Typed arrays, string literal แบบ \u{hex}
- ฟังก์ชัน Math:
imul, clz32, fround, trunc, log2, log10
- operator ยกกำลัง, Regexp flags (s, y, u), ฟังก์ชันสตริง (
replaceAll, trimStart, trimEnd), globalThis
C API
- ลดการพึ่งพา C library ให้เหลือน้อยที่สุด, ไม่ใช้
malloc, free, printf
- ต้อง จัดเตรียม memory buffer เองโดยตรง และเอนจินจะจัดสรรหน่วยความจำภายในบัฟเฟอร์นั้นเท่านั้น
- ตัวอย่าง:
ctx = JS_NewContext(mem_buf, sizeof(mem_buf), &js_stdlib)
- ด้วย วิธีการทำ garbage collection จึงไม่จำเป็นต้องเรียก
JS_FreeValue()
- ที่อยู่ของ object สามารถเปลี่ยนได้ทุกครั้งที่มีการจัดสรร จึง แนะนำให้ใช้ pointer ไปยัง
JSValue
- จัดการการอ้างอิงอย่างปลอดภัยด้วย
JS_PushGCRef() / JS_PopGCRef()
- standard library ถูกคอมไพล์เป็นโครงสร้าง C ที่สามารถเก็บไว้ใน ROM ได้ ทำให้ได้ การเริ่มต้นที่รวดเร็วและใช้ RAM ต่ำ
- การรัน bytecode ทำได้จาก ROM โดยตรง หลังจากย้ายตำแหน่งด้วย
JS_RelocateBytecode() แล้วจึงใช้ JS_LoadBytecode() และ JS_Run() เพื่อรัน
- มี math library (libm.c) และ floating-point emulator ในตัว
โครงสร้างภายในและการเปรียบเทียบกับ QuickJS
- garbage collection: ใช้ GC แบบ tracing และ compacting แทน reference counting
- ป้องกัน memory fragmentation และลดขนาด object
- การแทนค่า: ออกแบบให้สอดคล้องกับขนาด word ของ CPU (32/64 บิต)
- สามารถเก็บจำนวนเต็ม 31 บิต, Unicode code point, จำนวนทศนิยม และ pointer ไปยัง memory block ได้
- สตริงถูกเก็บแบบ UTF-8 มีประสิทธิภาพมากกว่าวิธีใช้อาร์เรย์ 8/16 บิตของ QuickJS
- ฟังก์ชัน C สามารถเก็บเป็นค่าเดี่ยวได้ และไม่สามารถเพิ่มพร็อพเพอร์ตีได้
- standard library อยู่ใน ROM และลด object ใน RAM ให้เหลือน้อยที่สุด ทำให้ เริ่มต้นเอนจินได้รวดเร็ว
- bytecode เป็นแบบ stack-based และทำให้เป็นแบบอ่านอย่างเดียวผ่าน indirect reference table
- บีบอัดหมายเลขบรรทัดและคอลัมน์ด้วย Golomb code
- คอมไพเลอร์ คล้าย QuickJS แต่ใช้ parser แบบไม่เรียกซ้ำ เพื่อจำกัดการใช้ C stack
- สร้าง bytecode แบบ single pass โดยไม่มี parse tree
การทดสอบและเบนช์มาร์ก
- การทดสอบพื้นฐาน:
make test
- QuickJS micro benchmark:
make microbench
- Octane benchmark (เวอร์ชันดัดแปลงสำหรับ strict mode) สามารถดาวน์โหลดแยกได้
ไลเซนส์
- เผยแพร่ภายใต้ MIT License
- ลิขสิทธิ์ซอร์สโค้ดเป็นของ Fabrice Bellard และ Charlie Gordon
2 ความคิดเห็น
ถ้าอยากรู้จัก Fabrice Bellard ลองดูสิ่งที่ผมเคยเขียนไว้ในคอมเมนต์ก่อนหน้านี้ได้เลยครับ คนนี้ทั้งสม่ำเสมอและน่าทึ่งแบบสัตว์ประหลาดจริงๆ..
https://news.hada.io/comment?id=51
ความคิดเห็นจาก Hacker News
ถ้าสิ่งนี้มีมาตั้งแต่ปี 2010 ภาษาสคริปต์ของ Redis ก็น่าจะเป็น JavaScript ไม่ใช่ Lua
Lua ถูกเลือกไม่ใช่เพราะเหตุผลด้านตัวภาษา แต่เพราะข้อจำกัดด้านการติดตั้งใช้งาน (เล็ก เร็ว และอิง ANSI-C)
ไอเดียบางอย่างของ Lua ก็ดี แต่ส่วนตัวรู้สึกว่าการออกห่างจาก ไวยากรณ์สาย Algol นั้นไม่จำเป็น
ความสับสนที่เกิดจากการต้องเรียนรู้แนวคิดเชิงนามธรรมใหม่แบบ SmallTalk หรือ FORTH นั้นคุ้มค่า แต่การเปลี่ยนแปลงของ Lua ดูเหมือนไม่มีเหตุผลมากพอในระดับนั้น
Lua เป็นภาษาขนาดเล็กเพียงภาษาเดียวที่รองรับ tail call optimization(TCO) ทำให้สามารถเขียนโปรแกรมด้วยรีเคอร์ชันล้วน ๆ โดยไม่ต้องมีลูปได้
JavaScript ไม่มีการเพิ่มประสิทธิภาพแบบนี้ จึงทำแบบเดียวกันไม่ได้
Lua ยังเหมาะกับการ เขียนคอมไพเลอร์ เป็นพิเศษ เพราะมีโครงสร้างแบบรีเคอร์ซีฟเยอะ
สำหรับสคริปต์ของ Redis นั้น JS อาจจะเหมาะกว่า แต่ก็น่าเสียดายถ้าจะดูแคลน Lua
ในบราซิล Pascal กับ Ada ถูกใช้อย่างแพร่หลายกว่า C จึงได้รับอิทธิพลจากสองภาษานั้น
Ruby กับ Perl ก็ออกมาในช่วงใกล้เคียงกัน แต่ทดลองเปลี่ยนไวยากรณ์แบบสุดโต่งกว่ามาก
ทั้งที่แยก parser กับ lexer ออกจากกันอยู่แล้ว แต่แทบไม่มีใครลองสลับโทเค็นอย่าง
then/endแทน{}การพูดคุยที่เกี่ยวข้อง: เธรด HN, กระทู้ Reddit
เอนจินนี้จำกัด JS ในแบบที่ผมเคยอยากได้ตอนทำงานกับ JSC
บนเว็บเราทำข้อจำกัดแบบนี้ไม่ได้เพราะเรื่องความเข้ากันได้ แต่ในสภาพแวดล้อมแบบ embedded ข้อจำกัดแบบนี้กลับเป็น การออกแบบที่น่ายินดี ได้
ผมทำ playground สำหรับลองรัน MicroQuickJS ได้ทันทีในเบราว์เซอร์
MicroQuickJS เวอร์ชัน WebAssembly
และแน่นอนว่ามี QuickJS เวอร์ชันต้นฉบับ ด้วย
QuickJS มีขนาด 2.28MB ส่วน MicroQuickJS แค่ 303KB จึงเบากว่ามาก
ถ้าใส่ตัวเลือก
emcc -O3หรือเพิ่ม--closure 1ก็น่าจะลดลงได้อีกQuickJS ถูกปรับแต่งไว้แล้ว ส่วน MicroQuickJS ยังมีพื้นที่ให้ปรับปรุง
อย่างที่ Jeff Atwood เคยพูดไว้ว่า “แอปทุกตัวที่สามารถเขียนด้วย JavaScript ได้ สุดท้ายก็จะถูกเขียนด้วย JavaScript”
ตอนนี้ดูเหมือนว่าคำนั้นจะใช้ได้กับระบบ embedded ด้วย
วิกิของ Jeff Atwood
ลิงก์ JSLinux
เสียดายที่อัปโหลดมาโดยไม่มีประวัติ commit
ผมอยากเห็นว่านักพัฒนาระดับนี้ทำโปรเจกต์เสร็จเร็วแค่ไหน
แต่ยังไงมันก็อิงจาก QuickJS อยู่แล้ว การเทียบตรง ๆ คงไม่มีความหมายมากนัก
สงสัยว่านี่อาจเป็นวิธีที่เบาที่สุดในการแก้ YouTube JS challenge ของ yt-dlp หรือเปล่า
ดู เอกสาร EJS ของ yt-dlp
ตอนนี้รองรับ QuickJS อยู่แล้ว
ปริศนา JS ของ YouTube ซับซ้อนเกินไป ถึงขั้นที่ JS emulator ที่เขียนด้วย Python ยังยอมแพ้
ผมไม่ค่อยรู้เรื่องระบบ embedded แต่สงสัยว่าเอนจินแบบนี้จะทำให้ เขียนโปรแกรม ESP32 หรือ Arduino ด้วย JavaScript ได้ไหม
คล้าย ๆ MicroPython
ส่วน MicroQuickJS รองรับเพียงบางส่วนของ ES5 และไม่มี environment binding มาให้
มันแปลงโค้ด JS เป็น Lua VM bytecode เพื่อรัน ซึ่งเป็นแนวทางที่ฉลาดมาก
ไม่นานมานี้ผมยังลองเขียน Node 0.8 CLI เก่านั้นใหม่ด้วย Rust แต่สุดท้ายอุปกรณ์ก็กลับเข้าไปอยู่ในลิ้นชักเหมือนเดิม
malloc()ซึ่งนี่แหละที่เปิดความเป็นไปได้ใหม่ ๆจังหวะเวลาสำคัญมาก ตอนโพสต์เมื่อคืนแทบไม่มีใครตอบเลย
มีวิธีคือโพสต์ใหม่ช่วงเช้าของอเมริกา หรือรีโพสต์เป็นระยะ ๆ
Fabrice Bellard คือหนึ่งในโปรแกรมเมอร์ที่ทั้งสร้างผลงานได้มากและรอบด้านที่สุดคนหนึ่งที่ยังมีชีวิตอยู่
ผลงานเด่น: FFmpeg, QEMU, JSLinux, TCC, QuickJS
เป็นบุคคลระดับตำนาน
แนวทางการสร้างโปรแกรมที่สมบูรณ์ด้วย dependency และเครื่องมือให้น้อยที่สุดนั้นน่าประทับใจมาก
เพราะถ้าเป็นคนจริงก็คงต้องนอนบ้าง
ts_server, TextSynth
ดูเหมือนเขาจะชอบโครงสร้างที่ผู้ใช้ตั้งค่าพารามิเตอร์ แล้วโปรแกรมทำงานจบในตัว
รายชื่อผู้ชนะ IOCCC
จุดที่น่าประทับใจคือมัน คอมไพล์และรัน JS ได้แม้มี RAM แค่ 10kB
จังหวะนี้เหมาะมากในยุคที่ RAM กำลังแพงขึ้น
สงสัยว่าจะเอาไปใส่ใน Chromium หรือ Electron ได้ไหม