เขียน Bun ใหม่ด้วย Rust
(bun.com)- Bun ซึ่งเริ่มต้นด้วย Zig เติบโตเป็น runtime ที่มีการดาวน์โหลดมากกว่า 22 ล้านครั้งต่อเดือน แต่ปัญหาความเสถียรที่เกิดซ้ำจากการผสานกันระหว่างเอนจิน JavaScript แบบ GC กับการจัดการหน่วยความจำด้วยตนเอง กลายเป็นเหตุผลให้เปลี่ยนไปใช้ Rust
- แทนที่จะให้คนย้ายโค้ด Zig จำนวน 535,496 บรรทัดตลอด 1 ปี ทีมรัน dynamic workflow ของ Claude Code ประมาณ 50 รายการ และ Claude instance สูงสุด 64 ตัวแบบขนานเป็นเวลา 11 วัน
- การ port ถูกตรวจสอบด้วย
PORTING.md,LIFETIMES.tsv, ผู้ implement 1 คนกับ reviewer เชิงปฏิปักษ์ อย่างน้อย 2 คน และชุดทดสอบ TypeScript เดิม โดยผ่าน CI 6 แพลตฟอร์ม 100% - หลังเปลี่ยนเป็น Rust, Bun v1.4.0 แก้ บั๊ก 128 รายการ ที่ทำซ้ำได้ใน v1.3.14 แก้ memory leak ที่ instrument ได้ทั้งหมด และลดขนาด binary บน Linux·Windows ลงประมาณ 20%
- Bun v1.3.14 เป็นเวอร์ชัน Zig สุดท้าย และ v1.4.0 เป็นเวอร์ชัน Rust แรกที่ให้ใช้ใน canary โดยทีมใช้ borrow checker, Miri, LeakSanitizer และ fuzzing แบบ 24/7 บนฐาน coverage เป็นเครื่องมือปรับปรุงความเสถียร
Bun ที่เริ่มต้นด้วย Zig และปัญหาความเสถียร
- Bun เริ่มต้นจากโปรเจกต์ที่ port แบบทีละบรรทัด transpiler สำหรับ JavaScript·TypeScript ของ esbuild จาก Go ไปเป็น Zig
- โค้ด Zig ชุดแรกเขียนขึ้นเมื่อวันที่ 16 เมษายน 2021 และการควบคุมระดับต่ำกับการออกแบบที่เน้นประสิทธิภาพของ Zig ทำให้การ implement Bun ช่วงแรกเป็นไปได้
- Bun ช่วงแรกถูกเขียนด้วย Zig โดยคนคนเดียวตลอด 1 ปี และมีขอบเขตกว้างมาก
- transpiler·minifier·bundler สำหรับ JavaScript, TypeScript, CSS
- package manager ที่เข้ากันได้กับ npm
- test runner คล้าย Jest
- การ resolve module ที่เข้ากันได้กับ Node.js·TypeScript
- client สำหรับ HTTP/1.1·WebSocket
- การ implement Node.js API เช่น
fs,net,tls
- ปัจจุบัน Bun CLI ถูกดาวน์โหลดมากกว่า 22 ล้านครั้งต่อเดือน ถูกใช้เป็น runtime โดย Claude Code และ OpenCode และมี Vercel, Railway, DigitalOcean เป็นต้นที่ให้การรองรับแบบ 1st-party
บั๊กด้านความปลอดภัยหน่วยความจำที่เกิดซ้ำ
- ตัวอย่างบั๊กที่แก้ใน Bun v1.3.14 รวมถึง use-after-free, double-free, memory leak, การเข้าถึง out-of-bounds และ race condition
- heap-use-after-free จากการเรียก
.reset()ระหว่าง async.write()ของnode:zlib - use-after-free ที่ JS callback แบบ reentrant ของ
node:http2ทำให้เกิด hashmap rehash จน pointer ของ stream ภายในเป็นโมฆะ - ปัญหาที่ callback
valueOf()หรือtoString()detachArrayBufferในUDPSocket.send()และsendMany() - crash และ out-of-bounds read จากการ detach หรือ resize
ArrayBufferระหว่างการ coercion argument ในBuffer#copy,Buffer#fill - memory leak ที่เกี่ยวข้องกับ
crypto.scrypt,tlsSocket.setSession(),fs.watch() - double-free ระหว่างการจัดการ vendor prefix และ multi-layer background ของ CSS parser
- crash จาก race condition ของ
MessageEventระหว่างการเข้าถึงพร้อมกันของBroadcastChannelหรือMessagePort
- heap-use-after-free จากการเรียก
- ก่อนหน้านี้ก็ใช้กลไกหลายอย่างเพื่อเสริมความเสถียร
- patch ให้ Zig compiler รองรับ Address Sanitizer และรันชุดทดสอบ ASAN ในทุก commit
- บน Windows แจกจ่าย build แบบ Zig safety-checked ReleaseSafe
- fuzz API ของ Bun runtime แบบ 24/7 ด้วย Fuzzilli
- ดำเนินการทดสอบ memory leak แบบ end-to-end จำนวนมาก
- ไม่ได้มีจุดยืนว่า Zig เองเป็นปัญหา แต่ข้อกำหนดที่ต้องจัดการค่าของ GC ร่วมกับหน่วยความจำที่จัดการเอง เป็นแหล่งหลักของปัญหาความเสถียร
เหตุผลที่เลือก Rust
- JavaScript เป็นภาษาแบบ GC และเอนจินอย่าง JavaScriptCore กับ V8 มีกฎที่เข้มงวดเกี่ยวกับ exception handling และ GC
- Zig ไม่ได้จัดการหน่วยความจำอัตโนมัติเหมือน C ไม่มี constructor·destructor และส่วนใหญ่ต้องระบุ cleanup ด้วย
deferที่แต่ละ call site - ใน Bun การจัดการ lifetime ของค่า GC และค่าที่จัดการด้วยตนเองให้ถูกต้อง เป็นแหล่งใหญ่ของปัญหาความเสถียร
- ต้องตรวจสอบว่า byte ที่ allocate ถูก free ที่ไหน
- ต้องรับประกันว่า free เพียงครั้งเดียว
- ต้องตรวจสอบการจัดการ JavaScript exception ให้ถูกต้อง
- ต้องตรวจสอบว่า GC pointer มองเห็นได้จาก conservative stack scanner หรือไม่
- วิธี cleanup ของ Zig คือ
defer,errdeferแบบชัดเจน ส่วน C++ ใช้ destructor และ move ขณะที่ Rust ใช้Drop - ในโค้ด Zig เดิมของ Bun มีการผสมกันระหว่าง arena lifetime, reference counting และการ review อย่างละเอียด
- แม้จะบังคับกฎ ownership ด้วย style guide และ code review ได้ แต่ในโค้ดที่ปลอดภัยของ Rust, use-after-free, double-free และ free ที่ตกหล่นใน error path จะกลายเป็น compiler error
- โค้ดของ Bun ประมาณ 20% เป็น C++ และฝังไลบรารี C/C++ หลายตัว
- JavaScriptCore
- uWebSockets และ usockets
- lshpack และ lsquic
- BoringSSL
- SQLite
- C++ ก็เป็นตัวเลือกได้ แต่ยังคงต้องพึ่ง style guide และ code review และแม้มี ASAN ก็ยังอาจเกิด memory corruption และ leak ได้
กลยุทธ์การเขียนใหม่: ทำทีเดียวแบบเชิงกล
- โค้ด Zig เดิมของ Bun มี 535,496 บรรทัด ไม่รวมคอมเมนต์ และการเขียนใหม่แบบดั้งเดิมถูกประเมินว่าเป็นงานที่ทีมวิศวกรขนาดเล็กต้องใช้เวลาประมาณ 1 ปี
- เพราะไม่สามารถหยุดแก้บั๊ก แก้ความปลอดภัย และพัฒนาฟีเจอร์เป็นเวลา 1 ปีได้ จึงเลือก การ port แบบเชิงกล ที่ลดการเปลี่ยนแปลงพฤติกรรมผู้ใช้ให้เหลือน้อยที่สุดเป็นแนวทางที่เสี่ยงต่ำที่สุด
- ชุดทดสอบของ Bun เขียนด้วย TypeScript จึงไม่ขึ้นกับภาษาที่ใช้ implement runtime
- การเขียนใหม่แบบ incremental ถูกมองว่าจะเจ็บปวดในระยะสั้นและกลาง เพราะต้องสร้างโค้ดชั่วคราวและหวังว่าจะลบทิ้งได้ภายหลัง จึงย้ายทั้งหมดในครั้งเดียว
- โค้ด Rust ถูกเขียนให้ดูเหมือน transpile มาจากโค้ด Zig และเลือกแนวทางค่อย ๆ ลด
unsafeหลัง Bun v1.4 พร้อม refactor ไปเป็น Rust ที่ idiomatic มากขึ้น
Dynamic workflow ของ Claude Code
- ในการเขียนใหม่ด้วย Rust มี dynamic workflow ประมาณ 50 รายการใน Claude Code ที่รันต่อเนื่องเป็นเวลา 11 วัน
- workflow ไล่ตั้งแต่การเขียนคู่มือการ port, แปลงไฟล์, แก้ compiler error, กู้คืน subcommand, ทำให้ชุดทดสอบทั้งหมดผ่าน ไปจนถึง cleanup ขนาดใหญ่
- สร้างคู่มือการ port ที่ map pattern และ type ของ Zig ไปเป็น pattern และ type ของ Rust
- port ไฟล์
.zigทั้งหมดเป็นไฟล์.rsแบบเชิงกลตามPORTING.mdและLIFETIMES.tsv - แก้ compiler error แยกตาม crate
- กู้คืนการทำงานของ subcommand เช่น
bun test,bun build - ทำให้ชุดทดสอบทั้งหมดผ่าน
- refactor และ cleanup ขนาดใหญ่
- ตลอดเวลาส่วนใหญ่ คนอ่าน output ของ workflow เพื่อตรวจสอบปัญหาและบั๊ก พร้อมปรับ prompt เพื่อให้ Claude แก้ loop
- เป็นงานเตรียมการ มีการหารือกับ Claude ประมาณ 3 ชั่วโมงเกี่ยวกับวิธี map pattern ใน codebase Zig ไปเป็น Rust และผลลัพธ์นี้ถูก serialize เป็น
PORTING.md - เพื่อเพิ่ม lifetime ของ Rust ให้โค้ดจัดการหน่วยความจำด้วยตนเอง ได้รัน workflow ที่วิเคราะห์ lifetime ของทุก struct field
- ค้นหา field ที่มี lifetime ซับซ้อน
- เสนอ lifetime
- agent reviewer เชิงปฏิปักษ์ 2 ตัวตรวจสอบ
- นำ feedback มาปรับและบันทึกเป็น
LIFETIMES.tsv
วิธีรีวิวแบบปฏิปักษ์
- มี Claude ผู้รีวิวแบบปฏิปักษ์ แยกอยู่คนละ context window กับ Claude ที่ทำ implementation แต่ละตัว โดยผู้รีวิวได้รับคำสั่งให้ดูเฉพาะ diff และสมมติว่าโค้ดผิดเพื่อค้นหา bug
- โครงสร้างพื้นฐานประกอบด้วยผู้ implement 1 คน, ผู้รีวิวแบบปฏิปักษ์อย่างน้อย 2 คน และ fixer 1 คน
- bug ที่ผู้รีวิวจับได้จริงทั้งหมดเป็นโค้ดที่ compile ผ่าน แต่มีปัญหาด้านพฤติกรรม
uv_closeเป็น asynchronous แต่Box<uv::Pipe>ถูก drop ที่ท้าย match arm ทำให้ libuv ถือ freed memory อยู่ จนเกิด use-after-free และ double-free- ข้อผิดพลาดของ timespec ที่ทำให้เกิด
nsecติดลบ เมื่อใช้trunc()กับ file time ค่าลบที่ไม่ใช่จำนวนเต็ม - ข้อผิดพลาดที่
unwrap_orประเมิน argument แบบ eager ทำให้เกิด panic ในกรณีละเว้น percentage ของcolor-mix()
- เช่นเดียวกับการรีวิวโดยมนุษย์ มีการแยก context ของผู้เขียนกับผู้รีวิว เพื่อลด bias ที่อาจเกิดจากการที่ผู้ implement อยากให้ merge
การทำ port ขนาดใหญ่และ parallelization
- ก่อนย้ายไฟล์
.zigทั้งหมด 1,448 ไฟล์ ได้ตรวจสอบขั้นตอนกับไฟล์ 3 ไฟล์ก่อน- ผู้ implement 1 คนเขียนไฟล์
.rs - ผู้รีวิว 2 คนตรวจสอบว่าพฤติกรรมตรงกับ
.zigและทำตามPORTING.md,LIFETIMES.tsvหรือไม่ - fixer 1 คนนำข้อเสนอไปปรับใช้
- ผู้ implement 1 คนเขียนไฟล์
- ช่วงต้นของการ port ไฟล์ทั้งหมด Claude หลายตัวรัน
git stash,git stash pop,git reset HEAD --hardจนชนกันเอง - หลังจากนั้นจึงเพิ่มกฎใน workflow เพื่อห้าม
git stash,git reset, คำสั่งgitที่ไม่ใช่การ commit ไฟล์เฉพาะ และคำสั่งช้าอย่างcargo - สุดท้ายใช้ workflow shard 4 ชุดและ worktree 4 ชุด โดยในแต่ละ shard มี Claude 16 ตัว commit และ push ไฟล์
- ด้วย parallelization และการเตรียมการล่วงหน้า ช่วง peak Claude เขียนโค้ดได้ประมาณ 1,300 บรรทัดต่อนาที
- commit ใน port branch ที่ไม่รวม merge มี 6,502 commit, ชั่วโมง peak มี 695 commits และ landed diff สุดท้ายคือ +1,009,272 บรรทัด
- ยังมีปัญหาที่ไม่ได้เพิ่ม IOPS พื้นฐานของ EC2 instance ทำให้เพียง
grepช้า ๆ ครั้งเดียวก็ทำให้การอ่าน/เขียนดิสก์หยุดไปหลายนาที
ข้อผิดพลาด compile และการแยก crate
- หลังจากเขียนโค้ดทั้งหมดแล้ว Claude workflow จึงแก้ compiler error
- codebase Zig โดยพื้นฐานแล้วเป็น compilation unit เดียว ขณะที่โค้ด Rust ตั้งใจจะแบ่งเป็นประมาณ 100 crate เพื่อให้ compile เร็วขึ้น
- ประเภทข้อผิดพลาดที่ยากที่สุดคือ circular dependency
- แค่ PR แยก crate ก่อนการ rewrite เป็น Rust ยังไม่เพียงพอ
- workflow แยกต่างหากทำหน้าที่จัดประเภทและบันทึกว่าโค้ดที่มี circular dependency ควรอยู่ที่ไหน
- workflow อีกชุดทำ refactoring ดังกล่าว
- หลังแก้ circular dependency แล้ว ก็เผยให้เห็น compiler error ประมาณ 16,000 รายการ
- ข้อผิดพลาดเหล่านี้ถูกประมวลผลแบบขนานแยกตาม crate
- รัน
cargo checkในแต่ละ crate - จัดกลุ่ม output ตามไฟล์แล้วบันทึกไว้
- แก้ compiler error ของ crate นั้น
- ผู้รีวิวแบบปฏิปักษ์ 2 คนตรวจสอบการเปลี่ยนแปลง
- fixer 1 คนนำการแก้ไขไปปรับใช้
- รัน
- เคยมี false start ที่ Claude ตีความ “ทำให้ทุก crate compile ได้” เป็นการสร้าง function stub
- เมื่อเกิด pattern ที่พยายามใช้ comment อธิบายยาว ๆ เพื่อ justify workaround จึงเพิ่มกฎรีวิวว่า “ถ้าต้องใช้ comment ยาวระดับย่อหน้า แปลว่าโค้ดผิดและต้องแก้โค้ด”
กระบวนการจนทดสอบผ่าน
- หลัง
cargo checkผ่านแล้ว จึงแก้ link error, panic ทันทีหลังเริ่มต้น, การรันbun --version,bun test <file>ตามลำดับ - ใช้ workflow ที่บันทึก stacktrace ความล้มเหลวของแต่ละ CLI subcommand เป็นไฟล์ แล้วแก้ผ่าน loop ผู้ implement·ผู้รีวิว·fixer
- workflow สำหรับไฟล์ทดสอบ shard ไฟล์ทดสอบแบบสุ่มประมาณ 100 ไฟล์ไปยัง worktree 4 ชุด และบันทึก stacktrace กับ error ของแต่ละ failure เพื่อแก้ไข
- test suite มีการทดสอบ memory leak และ integration test ที่อาจ timeout ใน debug build
- การทดสอบที่รัน
next devและตรวจว่า hot module reloading ตรวจพบการเปลี่ยนแปลง 100 ครั้ง - stress test ที่ใช้จำนวน TCP socket จนถึงขีดจำกัดสูงสุด
- การทดสอบอ่าน/เขียนดิสก์ระดับกิกะไบต์
- การทดสอบที่ spawn process ประมาณ 10,000 process
- การทดสอบที่รัน
- เพื่อ isolation จึงใช้
systemd-runและ cgroups จำกัดการใช้ memory/CPU และแยก pid namespace - ถึงอย่างนั้นเครื่องก็ crash หลายครั้งเพราะพื้นที่ดิสก์ไม่พอ
- สองวันหลัง CI ครั้งแรก จำนวนไฟล์ทดสอบที่ล้มเหลวลดจาก 972 เหลือ 23 และอีกหนึ่งวันครึ่งหลังจากนั้น Linux ก็ green ทั้งหมด
- สุดท้าย CI test ทั้งหมดผ่านครบ 6 แพลตฟอร์ม
- macOS x64
- macOS arm64
- Linux x64
- Linux arm64
- Windows x64
- Windows arm64
- หลังทดสอบผ่าน 100% มนุษย์ตรวจด้วยตนเองว่าการทดสอบถูกรันจริงและไม่ได้ถูก skip แล้วจึง merge
- จุดที่ merge เข้า
mainยังไม่ใช่ versioned release และยังไม่มั่นใจพอที่จะ release แต่เป็นระดับความมั่นใจที่พอจะทุ่มเทกับ rewrite ต่อได้
ขนาดและต้นทุนการทดสอบ
- ในช่วง 11 วัน ตั้งแต่ May 3 ถึง merge วันที่ May 14 มีการสร้าง 6,778 commits
- test ไม่ได้ถูกลบหรือ skip
- ขนาดการทดสอบแยกตามแพลตฟอร์มมีดังนี้
- Debian 13 x64:
expect()1,386,826 ครั้ง, test 60,624 รายการ, ไฟล์ 4,174 ไฟล์ - macOS 14 arm64:
expect()1,259,953 ครั้ง, test 58,850 รายการ, ไฟล์ 4,175 ไฟล์ - Windows 2019 x64:
expect()1,007,544 ครั้ง, test 57,337 รายการ, ไฟล์ 4,173 ไฟล์
- Debian 13 x64:
- งานก่อน merge ใช้ uncached input token 5.9 พันล้าน, output token 690 ล้าน และ cached input token read 72 พันล้าน
- คิดตามราคา API มีค่าใช้จ่ายประมาณ 165,000 ดอลลาร์
- ประเมินว่าถ้ามนุษย์ทำเอง วิศวกร 3 คนที่มี context ของ codebase ทั้งหมดน่าจะใช้เวลาประมาณ 1 ปี
- model ที่ใช้คือ pre-release Claude Fable 5 และมี disclosure ว่า Bun ถูก Anthropic เข้าซื้อในเดือนธันวาคม 2025
รีวิวความปลอดภัย, fuzzing และสถานะของ unsafe
- หลัง merge port Rust แล้ว ได้ทำ security review 11 รอบ ด้วย Claude Code Security และจัดการ findings แล้ว
- เพิ่ม fuzzing แบบ coverage-based ตลอด 24/7 ให้กับ parser ทั้งหมดของ Bun
- JavaScript
- TypeScript
- JSX
- CSS
- JSON5
- JSONC
- TOML
- YAML
- Markdown
- INI
- Bun Shell scripts
- semver ranges
- ไฟล์
.patch - CSS colors
- fuzzer ส่ง bug ที่พบไปให้ Claude เพื่อส่ง PR ที่รวมการ reproduce และการแก้ไข ส่วนมนุษย์รีวิว PR
- จนถึงตอนนี้ parser ถูก execute ไปแล้ว 100,000 ล้านครั้ง และนำไปสู่ PR ประมาณ 15 รายการ
- ณ เวลาที่เขียน โค้ด Rust ประมาณ 4% อยู่ใน
unsafeblock- keyword
unsafeประมาณ 13,000 รายการ - ประมาณ 27,000 บรรทัด / จากทั้งหมดประมาณ 780,000 บรรทัด
- 78% ของ
unsafeblock เป็นแบบบรรทัดเดียว และเป็น pointer ที่มาจาก C++ หรือการเรียก C library
- keyword
- ระบุว่าเพราะยังใช้ library C/C++ อย่าง JavaScriptCore ต่อไป
unsafeจึงจะมากกว่าโปรเจกต์ Rust ล้วนเสมอ
Regression ที่พบหลังเปลี่ยนมาใช้ Rust
- Rust rewrite เป็นการเปลี่ยนแปลงครั้งใหญ่ จึงทำให้เกิด regression ที่ทราบแล้ว 19 รายการ และทั้งหมดได้รับการแก้ไขแล้ว
- ส่วนใหญ่เกิดจากโค้ดที่ไวยากรณ์ของทั้งสองภาษาคล้ายกัน แต่ความหมายต่างกัน
-
side effect ภายใน
debug_assert!assertของ Zig เป็นฟังก์ชัน ดังนั้น argument จะถูกเรียกใช้ในทุก builddebug_assert!ของ Rust เป็น macro ดังนั้นใน release build นิพจน์ทั้งหมดจะถูกลบออก- การเรียก
insert_staleหายไปใน release build ทำให้เคส HMR บางกรณีของโปรเจกต์ HTML route ที่ใช้ React พัง - issue ที่เกี่ยวข้อง: #30678
-
slice ที่มีความยาวเป็นเลขคี่
- Zig helper
reinterpretSlice(u16, bytes)ของ Bun ใช้@divTruncจึงละเว้น trailing odd byte bytemuck::cast_sliceของ Rust จะ panic เมื่อความยาวเป็นเลขคี่- มี regression ที่
Blob.text()ซึ่งมีไบต์เลขคี่ตามหลัง UTF-16 BOM ไม่คืนค่า string แต่ทำให้ process panic - การแก้ไขคือกลับไปละเว้น odd byte ด้วย
&buf[..buf.len() & !1] - issue ที่เกี่ยวข้อง: #31188
- Zig helper
-
Bounds checks
- โค้ด Zig บน macOS และ Linux ถูกคอมไพล์ด้วย
ReleaseFastทำให้ bounds check ถูกลบออก ส่วน Rust release build ยังคง bounds check ไว้ - ขนาด overflow block ของ Bun module resolver ยังเป็น placeholder
64อยู่ ทำให้ ceiling ลดจาก 8.4 ล้าน interned filenames เหลือ 270,272 ptrs[4095]ที่ port มาเกิด off-by-one และกลายเป็นเคสที่โปรเจกต์จริงเข้าถึงได้ โดย Rust จะ panic แทนการเขียนออกนอกขอบเขต- issue ที่เกี่ยวข้อง: #31503
- โค้ด Zig บน macOS และ Linux ถูกคอมไพล์ด้วย
-
format strings แบบ
comptimeOutput.prettyของ Zig มีfmtเป็นcomptimeดังนั้น color marker อย่าง<r>,<d>จะถูกแปลงเป็น ANSI escape ก่อนแทนที่ argument- ฟังก์ชัน Rust ไม่มี comptime parameter จึงประมวลผล marker ใน string ที่เสร็จแล้ว และ rewrite ไปถึง argument อย่างผิดพลาด
- ใน
bun update -iการจบ OSC 8 hyperlink ชนกับ trailing<r>marker ทำให้rถูกพิมพ์ออกมาเป็นข้อความ - ใน Rust จำเป็นต้องใช้ macro
bun_core::pretty!("<r>{}<r>", hyperlink) - issue ที่เกี่ยวข้อง: #30693
บั๊กและ memory leak ที่แก้ไขแล้ว
- Bun v1.4.0 แก้ไข 128 บั๊ก ที่ reproduce ได้ใน v1.3.14
- ครอบคลุมตั้งแต่ memory leak, crash ไปจนถึง help text ที่ลงสีผิด
Dropของ Rust จะเรียกฟังก์ชันdropโดยอัตโนมัติเมื่อค่าออกนอก scope- ใน Zig ต้องเพิ่ม
deferในแต่ละ call site จึงเกิดการลืม cleanup หรือ cleanup ซ้ำได้ง่าย Dropของ Rust เป็นการเลือกยอมรับ hidden control flow เพื่อแลกกับการลด footgun ที่พบบ่อยDropแก้ไข memory leak ที่เกี่ยวกับ file path ใน error handling code ได้หลายจุด- การผสานรวม LeakSanitizer ของ Bun ได้รับการปรับปรุง ทำให้ติดตาม native code memory allocations ทั้งหมดได้
- instrumentable memory leak ทั้งหมดได้รับการแก้ไขแล้ว
-
ปรับปรุง leak ใน
Bun.build()- ใน Bun v1.3.14 เดิม การเรียก
Bun.build()แบบ in-process แต่ละครั้งทำให้ parsed source text และ AST symbol table มีอายุอยู่นานกว่าอายุของ build และ leak ทีละหลาย MB - ในการทดสอบที่ bundle โปรเจกต์ 60-module เดิม 2,000 ครั้งใน process เดียว v1.3.14 leak ต่อเนื่องประมาณ 3MB ต่อ build
- ใน Bun v1.4.0 การใช้หน่วยความจำทรงตัว
- | Builds | Bun v1.3.14 | Bun v1.4.0 |
- | --- | ---: | ---: |
- | 500 | 1,914 MB | 526 MB |
- | 1,000 | 3,506 MB | 586 MB |
- | 1,500 | 5,097 MB | 608 MB |
- | 2,000 | 6,745 MB | 609 MB |
- ใน Bun v1.3.14 เดิม การเรียก
ขนาด binary, การใช้ stack และประสิทธิภาพ
- แค่การเปลี่ยนแปลงช่วงต้นของ Rust rewrite ก็ทำให้ขนาด binary ลดลง
- Windows: ลดลง 3.8 MB
- macOS: ลดลง 5.5 MB
- Linux: ลดลง 6.8 MB
- สาเหตุหลักคือการใช้
comptimeมากเกินไปในโค้ด Zig - หลังจากนั้นยังมีการใช้ Identical Code Folding, ลบ unused data ของ ICU และทำให้บางส่วนของ libicu ถูกคลายการบีบอัดแบบ lazy ด้วย zstd dictionary
- เมื่อนำ Rust rewrite, การเปลี่ยนแปลง ICU และ identical code folding มารวมกัน ขนาด binary ของ Bun บน Linux และ Windows ลดลงประมาณ 20%
| Version | Platform | Size |
|---|---|---|
| Bun v1.4.0 canary | Windows | 76 MB |
| Bun v1.3.14 | Windows | 94 MB |
| Bun v1.4.0 canary | Linux | 70 MB |
| Bun v1.3.14 | Linux | 88 MB |
- TOML parser และ recursive-descent parser ของ Bun ใช้ stack space น้อยลง
- LLVM IR codegen ของ Rust ปล่อย intrinsic
llvm.lifetime.startและllvm.lifetime.endให้กับ stack variable ทำให้ LLVM สามารถนำ stack slot กลับมาใช้ซ้ำได้ - ก่อนหน้านี้ต้อง refactor ฟังก์ชันขนาดใหญ่เป็นฟังก์ชันเล็กหลายตัวด้วยมือ โดยเฉพาะเพื่อเลี่ยง open issue ของ Zig
- Rust รองรับ cross-language link-time optimization ระหว่าง C/C++ กับ Rust ทำให้สามารถ inline ข้ามภาษาได้
-
benchmark บน Linux x64
- เปรียบเทียบ Bun v1.3.14 กับ Bun v1.4.0 บน Linux x64 EC2 Xeon Platinum 8488C
- วัด HTTP throughput ด้วย oha และ app workload ด้วย hyperfine
- | server | Bun v1.3.14 | Bun v1.4.0 | Δ |
- | --- | ---: | ---: | ---: |
- | Bun.serve | 169.6k req/s | 177.7k req/s | +4.8% |
- | node:http | 103.8k req/s | 108.5k req/s | +4.5% |
- | Elysia | 158.9k req/s | 163.3k req/s | +2.8% |
- | express | 64.5k req/s | 66.6k req/s | +3.2% |
- | fastify | 91.5k req/s | 95.9k req/s | +4.8% |
- | workload | Bun v1.3.14 | Bun v1.4.0 | Δ |
- | --- | ---: | ---: | ---: |
- | next build | 13.62 s | 13.03 s | +4.5% |
- | vite build | 1.69 s | 1.65 s | +2.2% |
- |
tsc -b --force| 0.94 s | 0.89 s | +4.7% |
กรณีการใช้งานจริงและสถานะการเผยแพร่
- Prisma เปิดตัว public beta ของ Prisma Compute บน Rust rewrite ของ Bun
- ฝั่ง Prisma ระบุว่าได้ทดสอบ failure mode อย่าง connection pool ที่ไม่สามารถกู้คืนได้หลัง VM pause/resume และ memory leak บน Rust rewrite แล้ว และสามารถจัดการ failure mode ดังกล่าวได้ดี
- Claude Code v2.1.181 และเวอร์ชันหลังจากรีลีสวันที่ 17 มิถุนายน ใช้ Bun ที่พอร์ตเป็น Rust
- Linux startup ของ Claude Code เร็วขึ้น 10% และนอกเหนือจากนั้น ผู้ใช้ส่วนใหญ่แทบไม่สังเกตเห็นความแตกต่าง
- Bun v1.3.14 เป็น Bun เวอร์ชันสุดท้ายที่เขียนด้วย Zig
- Bun v1.4.0 เป็น Bun เวอร์ชันแรกที่เขียนด้วย Rust และให้ใช้งานในรูปแบบ canary
เครื่องมือที่ทีมได้รับและงานที่ยังเหลือ
- codebase Rust ใหม่ยังคงมีรูปแบบใกล้เคียงกับ codebase Zig เดิมมาก
- เขียนขึ้นเพื่อให้คนที่เข้าใจโค้ด Zig เดิมสามารถเข้าใจโค้ด Rust ที่แปลแบบเชิงกลได้เช่นกัน
- การรีวิว PR ของ Rust rewrite ดำเนินไปโดยตรวจสอบว่า adversarial review agent สามารถจับความไม่สอดคล้องระหว่าง Zig กับ Rust การปฏิบัติตาม porting guide และ lifetime guide ได้อย่างถูกต้องหรือไม่ และให้คนอ่านโค้ดจำนวนมากแบบ side-by-side
- Bun v1.4 ทำให้ Bun เร็วขึ้น เล็กลง ใช้หน่วยความจำน้อยลง และมอบเครื่องมือสำหรับการปรับปรุงเสถียรภาพ
- Rust borrow checker
- Miri
- LeakSanitizer
- coverage-guided fuzzing ตลอด 24/7 สำหรับ parser
- ยังมีส่วนที่ต้อง refactor เหลืออยู่ และมีการเชื่อมต่อ bun-unsafe-audit
- วิศวกรหนึ่งคนเฝ้าติดตาม Fable และ Claude Code อย่างใกล้ชิด จนไปถึงสถานะที่ test suite ทั้งหมดผ่านบนทุกแพลตฟอร์มได้ภายในเวลาเพียง 11 วัน
1 ความคิดเห็น
ความคิดเห็นบน Lobste.rs
extern "C"ออกไปได้มาก แต่ก็ยังต้องพึ่ง style guide ที่บังคับผ่าน code review อยู่ดี และแม้มี ASAN ก็ยังคงเกิด memory corruption และ memory leak ต่อไปน่าสนใจที่ Node.js ทำงานได้ดีแม้ใช้ C++ แต่ผมไม่เคยมอง Bun เป็นโปรเจกต์จริงจัง ตอนนี้มันดูเหมือน test bench ของฝ่ายการตลาด Anthropic เลยคิดว่าจะถอยห่างต่อไป
สปอยล์คือ ในความเป็นจริงไม่ได้ระวังกันขนาดนั้น และก็มีการทำพลาดด้วย
unsafeและ 78% ในนั้นเป็นบล็อกบรรทัดเดียว” ดูเหมือนเป็นคำพูดเพื่อให้สบายใจ แต่การที่บล็อกunsafeมีบรรทัดเดียวหรือไม่ไม่ใช่ประเด็น ถ้าข้างในนั้นทำลายการรับประกันความปลอดภัย โค้ดทั้งหมดนอกบล็อกก็อาจมี soundness ที่ถูกทำลายได้ เช่นกันการ merge ช่วงแรกของพอร์ต Rust ของ Bun มี unsoundness ชัดเจนแบบนี้อยู่: https://github.com/oven-sh/bun/issues/30719
issue นั้นได้รับการรับมือโดยที่ผู้ดูแลเปิดใช้เครื่องมือ Miri ของ Rust ใน CI และในส่วน “What's Next” ของบทความก็มี Miri (which runs for a growing chunk of code in CI) รวมอยู่ด้วย ดังนั้นดูเหมือนว่ากำลังทำงานไปในทิศทางนั้น ซึ่งเป็นเรื่องดี
ถ้าพูดอย่างเป็นธรรม Rust ที่มีการละเมิดความปลอดภัยก็ยังอาจดูแลง่ายกว่าได้ ขึ้นอยู่กับคุณภาพของโค้ด Zig ที่มันเข้ามาแทนที่ ถึงอย่างนั้น จำนวนบรรทัดโค้ดต่อบล็อก unsafe ก็ไม่ใช่ตัวชี้วัดคุณภาพ โดยเฉพาะอย่างยิ่งถ้าบล็อกเหล่านั้นไม่มีแนวปฏิบัติการเขียนโค้ดอื่น ๆ ความเชี่ยวชาญ หรือการตรวจสอบอัตโนมัติประกอบด้วย
unsafeไม่ได้เกิดจากกระบวนการพอร์ต แต่เกิดจาก ข้อกำหนดของโปรเจกต์ มากกว่า ถ้าเรียกใช้ไลบรารี C ก็จำเป็นต้องมีบล็อกunsafeและไม่มีทางกำจัดได้ด้วยการ refactor อย่างเดียวแน่นอนว่าถ้าเขียนไลบรารี C นั้นใหม่ด้วยก็เป็นไปได้ แต่นั่นอาจค่อยพิจารณาภายหลัง
ในประกาศ “Bun is joining Anthropic” Jarred บอกว่าจะจ้างวิศวกรเพิ่มเพื่อทำงานกับ Bun แต่ถ้าดูแค่ GitHub ทีม Bun กลับดูเหมือนเล็กลงด้วยซ้ำ ไม่แน่ใจว่าจะสรุปอะไรได้จากตรงนี้ แค่ประมาณว่า “Bun เป็นทีมเล็ก” เท่านั้น
วิธีการเองน่าสนใจ แต่บทความอ่านแล้วเหมือนบทความการตลาด ขาดการวิเคราะห์ว่าต้นทุนเท่าไร ไม่มีความเสี่ยงของการเขียนใหม่ด้วย Rust และคำอธิบายเชิงรูปธรรมว่าทำไมถึงเกิดการเขียนใหม่ตั้งแต่แรกก็อ่อน เดาว่าอาจเป็นเพราะ no ai policy ของ Zig หรืออาจมีนโยบายภายใน Anthropic ที่ต้องการโฟกัสกับ Rust
ส่วนเหตุผลที่เขียนใหม่นั้น ผมว่าบทความเล่าเรื่องได้ค่อนข้างสอดคล้อง Bun ยังคง crash ต่อเนื่องแม้จะมีมาตรการเพื่อจับปัญหาแล้ว และนักพัฒนาก็ต้องการวิธีป้องกันปัญหาเหล่านี้อย่างเป็นระบบมากขึ้น
แผนแรกคือบังคับใช้สไตล์การเขียนโค้ดบางแบบให้เข้มขึ้น และนำ smart pointer มาใช้ แต่ Jared มองว่า smart pointer ที่ทำเองใช้งานแย่กว่า Rust และไม่มีการรับประกันเท่า Rust จากนั้นจึงกลายเป็นว่า “ลองทดสอบสักสัปดาห์ไหมว่าโมเดลใหม่ของ Anthropic จะเขียน Bun ใหม่เป็น Rust ได้หรือเปล่า?” และเมื่ออัตราการผ่าน test suite สูงขึ้น กระแสก็ดูเหมือนเปลี่ยนจาก “น่าลอง” เป็น “จะ merge”
กล่าวคือ ดูไม่เหมือนตัดสินใจเขียนใหม่ด้วย Rust ตั้งแต่แรก แต่ใกล้เคียงกับ “Rust ดูเหมือนให้ทางแก้ปัญหา แต่ทำไม่ได้เพราะต้นทุนการเขียนใหม่สูง ทว่าพอลองพอร์ตด้วย LLM แล้วเห็นความเป็นไปได้ งั้นก็ไปทางการเขียนใหม่ด้วย LLM กัน” มากกว่า
ขอแสดงความยินดีกับ Jarred, ทีม Bun และ Anthropic ที่ทำสิ่งนี้สำเร็จ