- โปรเจกต์ tmux-rs คือการพอร์ตโค้ดทั้งหมดของ tmux ที่เขียนด้วย C ไปเป็น Rust ตลอดระยะเวลาประมาณ 6 เดือน
- เริ่มแรกพยายามแปลงอัตโนมัติด้วย เครื่องมือ C2Rust แต่ผลลัพธ์ดูแลรักษาได้ยาก จึงลงท้ายด้วยการแปลงด้วยมือ
- ระหว่างกระบวนการ build และการเชื่อมต่อระหว่าง Rust-C ได้พบ บั๊กและปัญหาเชิงโครงสร้าง หลายอย่าง
- มีประเด็นและวิธีแก้เฉพาะในการย้ายแพตเทิร์นแบบ C ไปเป็น Rust เช่น คำสั่ง goto, โครงสร้างข้อมูลแบบแมโคร, parser yacc
- โปรเจกต์ยังคงอยู่บนพื้นฐานของ unsafe Rust แต่มีเป้าหมายสู่การย้ายทั้งหมดไปเป็น Rust อย่างสมบูรณ์ผ่านการคอมไพล์ล่วงหน้าและการรันจริง
ภาพรวมโปรเจกต์
- tmux-rs คือโปรเจกต์ที่พอร์ต โค้ดเบสทั้งหมดของ tmux (โค้ด C ราว 67,000 บรรทัด) ไปเป็น Rust (ราว 81,000 บรรทัด ไม่นับคอมเมนต์และบรรทัดว่าง)
- ผู้พัฒนาทำงานนี้เป็น โปรเจกต์งานอดิเรก และได้ผ่านการลองผิดลองถูกกับการเรียนรู้จำนวนมากในกระบวนการย้ายจาก C ไป Rust
การใช้ C2Rust และข้อจำกัด
- เดิมทีตั้งใจใช้เครื่องมือแปลงอัตโนมัติชื่อ C2Rust เพื่อย้ายโค้ด C ของ tmux ไปเป็น Rust
- โค้ดที่แปลงอัตโนมัตินั้น อ่านยาก มีขนาดใหญ่กว่าต้นฉบับ C มากกว่า 3 เท่า และด้วยการแปลงชนิดที่ไม่จำเป็นหลายจุด การสูญเสียชื่อค่าคงที่ ฯลฯ ทำให้ ความสามารถในการดูแลรักษาลดลงอย่างมาก
- ระหว่างการรีแฟกเตอร์ด้วยมือ ในที่สุดก็ ทิ้งผลลัพธ์จาก C2Rust และเปลี่ยนมาใช้วิธีอ้างอิงโค้ด C แล้วเขียนย้ายเป็น Rust โดยตรง
- อย่างไรก็ตาม การที่ C2Rust ทำให้สามารถ build และรันได้ตั้งแต่ช่วงแรกนั้น ช่วยอย่างมากในการยืนยันความเหมาะสมและความเป็นไปได้ของโปรเจกต์
การออกแบบกระบวนการ build
- tmux ใช้ระบบ build แบบ autotools และเชื่อมโค้ด Rust กับโค้ด C เดิมผ่าน static library
- ช่วงแรกเป็นการลิงก์ไลบรารี Rust เข้ากับไบนารี C แต่หลังจากที่โค้ดส่วนใหญ่ถูกย้ายเป็น Rust แล้ว จึงเปลี่ยนโครงสร้างเป็นให้ไบนารี Rust ลิงก์กับไลบรารี C แทน (ใช้
cc crate)
- เพื่อทำให้การ build เป็นอัตโนมัติ ได้เขียนทั้งสคริปต์ build.sh และ
build.rs เพื่อให้ออกแบบมาให้ตรวจสอบการ build แบบค่อยเป็นค่อยไประหว่างการแปลย้ายได้
- ปัญหาที่เกิดขึ้นบ่อยระหว่างการ build เช่น การประกาศ header ที่ขาดหาย, function signature ไม่ตรงกัน ถูกแก้แบบค่อยเป็นค่อยไปในระดับฟังก์ชัน
ตัวอย่างบั๊กที่พบระหว่างการแปลย้าย
บั๊ก 1: การประกาศแบบ implicit และการคืนค่า pointer
- ในฟังก์ชันที่แปลงเป็น Rust มีปัญหาที่ ชนิดคืนค่าเป็น pointer ถูก ประกาศแบบ implicit ในฝั่ง C ทำให้ 4 ไบต์บนของค่าที่ return ถูกตัดทิ้งและส่งต่อผิดพลาด
- วิธีแก้คือเพิ่ม function prototype ที่ถูกต้อง ในฝั่ง C เพื่อให้คอมไพเลอร์ทำงานได้อย่างถูกต้อง
บั๊ก 2: ชนิดของฟิลด์ใน struct ไม่ตรงกัน
- ใน struct
client การแปลชนิดฟิลด์ผิด (pointer vs integer) ทำให้ การคำนวณ memory offset คลาดเคลื่อน เกิดการตีความข้อมูลผิดและ segfault
- แก้ไขโดยทำให้คำจำกัดความของ struct ตรงกันอย่างแม่นยำทั้งใน C และ Rust
การย้ายแพตเทิร์นเฉพาะของ C ไปเป็น Rust
การใช้ Raw pointer
- การแมป C pointer ไปเป็น Rust reference โดยตรงอาจ ขัดกับกฎความปลอดภัยของ Rust เช่น การยอมให้เป็น null หรือการรับประกันการ initialize
- ดังนั้นโครงสร้าง pointer ส่วนใหญ่จึงถูกย้ายไปเป็น raw pointer (
*mut, *const) และใช้งานเฉพาะในขอบเขตที่ไม่ปลอดภัยเท่านั้น
การจัดการคำสั่ง goto
- ใน C2Rust มีการแปลง การควบคุมโฟลว์ของ goto ด้วยอัลกอริทึม แต่ในหลายกรณี Rust สามารถทำได้เพียงพอด้วย labeled block + break และ labeled loop + continue
การย้ายโครงสร้างข้อมูลแบบแมโคร
- tmux ใช้ intrusive red-black tree และ linked list ที่เขียนเป็นแมโคร C
- ใน Rust ได้ใช้งาน Generic trait และ custom iterator เพื่อสร้างอินเทอร์เฟซที่ใกล้เคียงกัน (ปัญหาการ implement trait เดียวซ้ำซ้อนแก้ด้วย dummy type)
การแปลง parser yacc
- tmux ใช้ yacc (lex) สำหรับ parser ของไฟล์คอนฟิก
- ใน Rust ใช้ lalrpop crate ที่มีโครงสร้างคล้ายกัน เพื่อพอร์ตทั้งไวยากรณ์และแอ็กชันมาเกือบตรงตัว พร้อมทั้งเขียน adapter สำหรับเชื่อมกับ C lexer ด้วย
- ระหว่างทางยังได้เจอกับข้อจำกัดของการรองรับ raw pointer ใน lalrpop (เช่น การใช้
NonNull<T>) ด้วย
สภาพแวดล้อมการพัฒนาและเครื่องมือ
- ใช้ neovim เป็นหลัก พร้อมแมโครที่ปรับแต่งเองเพื่อทำงานแปลงซ้ำ ๆ
- ตัวอย่าง:
ptr == NULL → ptr.is_null() / ptr->field → (*ptr).field เป็นต้น
- แม้จะลองใช้เครื่องมืออัตโนมัติอย่าง Cursor ด้วย แต่มีโค้ดที่หายหรือผิดพลาดอยู่มาก ทำให้ ภาระในการรีวิวโค้ด สูงขึ้น
- ช่วยลดความล้าของนิ้วได้บ้าง แต่ในแง่ productivity ยังมีข้อจำกัด
บทสรุปและทิศทางต่อไป
- โค้ดทั้งหมดถูกย้ายมาเป็น Rust เสร็จสมบูรณ์แล้ว และเปิดเผยเวอร์ชัน 0.0.1
- เมื่อเทียบกับ C2Rust โค้ดที่ย้ายด้วยมือบางส่วนดีกว่า แต่ก็ยังคงอยู่บนพื้นฐานของ unsafe Rust และยังมีบั๊กอีกจำนวนมาก
- เป้าหมายสุดท้ายคือการเปลี่ยนไปเป็น safe Rust code และทำให้การย้ายฟังก์ชันทั้งหมดของ tmux ไปเป็น Rust เสร็จสมบูรณ์
- ผู้พัฒนาหวังว่าจะได้ร่วมงานและรับ feedback จากนักพัฒนาที่สนใจ Rust และ tmux ผ่าน GitHub Discussions
4 ความคิดเห็น
โอ้.. แต่ Rust เบากว่าจริงเหรอ?
โอ้... ฟังดูดีนะ?
ในบรรดาปลั๊กอิน tmux ผมก็ถอด
resurectออกไปเหมือนกัน เพราะมันแอบกินหน่วยความจำค่อนข้างมากและมีอาการทำงานแปลก ๆ อยู่บ้าง เลยสงสัยว่าถ้าใช้คู่กับ tmux-rs แล้วจะดีกว่าหรือเปล่าแนะนำ tmux-rs
https://rosettalens.com/s/ko/tmux-rs-intro
ความเห็นจาก Hacker News
อยากถ่ายทอดความรู้สึกว่าประทับใจกับประสบการณ์การได้อ่านบันทึกของโปรเจ็กต์ที่ยอดเยี่ยมจริง ๆ และอยากแสดงความเคารพอย่างมากต่อความสม่ำเสมอและความมุ่งมั่นไม่ยอมแพ้ของผู้เขียน ประโยคที่ว่า "เหมือนทำสวน แต่มี segfault มากกว่า" ชวนให้รู้สึกร่วมอย่างลึกซึ้งมาก ประสบการณ์การเรียนรู้มักเกิดขึ้นมากที่สุดจากโปรเจ็กต์งานอดิเรกที่จริงจังแบบนี้เอง
ประสบการณ์เกี่ยวกับ c2rust น่าสนใจเป็นพิเศษ ฉันเคยเห็นการเปลี่ยนแปลงคล้ายกันจากตัวแปลงโค้ดอัตโนมัติระหว่างภาษามาก่อน เครื่องมือแบบนี้มีประโยชน์มากสำหรับเริ่มโปรเจ็กต์อย่างรวดเร็วและพิสูจน์ความเป็นไปได้ แต่สุดท้ายก็มักได้โค้ดที่ดูว่างเปล่าและไม่เป็นธรรมชาติสำหรับภาษาเป้าหมาย ฉันคิดว่าการตัดสินใจเปลี่ยนไปใช้การพอร์ตด้วยมือนั้นถูกต้องจริง ๆ แม้จะเจ็บปวดก็ตาม การแปลเจตนาของโค้ด C ให้เป็นโค้ดที่ปลอดภัยและเป็น Rust แบบแท้ ๆ นั้นระบบอัตโนมัติยังมีข้อจำกัด
ตอนอ่านหัวข้อ "บั๊กที่น่าสนใจ" แล้วเจอเรื่อง struct layout mismatch ข้อ #2 ก็ทำให้นึกถึงฝันร้ายสมัยทำ external function interface (FFI) ขึ้นมาทันที ครั้งหนึ่งฉันเคยเสียเวลาไปทั้งสัปดาห์กับการไล่หาความเสียหายของข้อมูลแบบละเอียดอ่อน เพราะ struct packing ระหว่าง C++ กับ C# ไม่ตรงกัน มันเป็นบั๊กที่ชวนให้สงสัยสติของตัวเองจริง ๆ เพราะพังในระดับความหมาย ต้องมีความอดทนในการดีบักสูงมากถึงจะเจอ ขอปรบมือให้ผู้เขียน
โดยรวมแล้วฉันคิดว่าโปรเจ็กต์นี้เป็นกรณีศึกษาที่แสดงให้เห็นความยากและกระบวนการจริงในการทำโค้ดโครงสร้างพื้นฐานหลักให้ทันสมัย เป้าหมายใหญ่ถัดไปคือการย้ายจาก unsafe ไปเป็น safe Rust ซึ่งทำให้ฉันอยากรู้มากว่าจะใช้กลยุทธ์แบบไหน
ฉันคิดว่าการแก้ raw pointer, goto และ control flow ที่ซับซ้อนทั้งหมดให้กลายเป็น Rust ที่ปลอดภัยและ idiomatic โดยไม่ทำให้ทั้งโค้ดพัง อาจยากยิ่งกว่าการพอร์ตครั้งแรกเสียอีก อยากรู้ว่ามีแผนจะค่อย ๆ นำ lifetime และ borrow checker เข้ามาเป็นรายโมดูลหรือไม่ และจะจัดการกับโครงสร้างข้อมูลแบบ intrusive อย่างไร ถ้าแทนที่ด้วยของอย่าง BTreeMap ใน standard library ก็อาจมีผลต่อประสิทธิภาพ และฉันก็สงสัยว่าเดิมทีการออกแบบแบบ intrusive อาจตั้งใจไว้เพื่อสิ่งนั้นหรือเปล่า
ยังไงก็ตาม นี่เป็นงานที่น่าทึ่งมาก ขอบคุณที่แชร์กระบวนการอย่างละเอียดแบบนี้ ฉันจะติดตามโปรเจ็กต์นี้ต่อไปบน GitHub
ข่าวนี้ดึงดูดฉันมากจริง ๆ
ฉันกำลังพัฒนา rmuxinator ซึ่งเป็นตัวจัดการ session ของ tmux ที่เขียนด้วย Rust มาหลายปีแล้ว ให้ความรู้สึกเหมือนเป็น tmuxinator clone ส่วนใหญ่ใช้งานได้ดี แต่ชีวิตยุ่งเลยทำให้ความคืบหน้าช้า และช่วงนี้ก็กำลังกลับมาจัดการเรื่องแก้บั๊กเป็นหลัก ฟีเจอร์ที่เพิ่งเพิ่มล่าสุดคือทำให้ rmuxinator ใช้เป็นไลบรารีได้ด้วย ฉันอยากลองทดสอบว่าการ fork tmux-rs แล้วใส่ rmuxinator เป็น dependency จากนั้นเริ่ม session ด้วยไฟล์ตั้งค่ารายโปรเจ็กต์จะใช้ได้จริงไหม ไม่ได้จะบอกว่าควรเอา rmuxinator เข้า upstream หรอก แต่ก็คิดว่าถ้ามีฟีเจอร์เทมเพลต session แบบนี้ฝังอยู่ใน terminal multiplexer เองเลยก็คงมีประโยชน์มาก
อีกมุมหนึ่งก็คิดว่า rmuxinator อาจจะดีกว่าถ้าใช้ tmux-rs เป็นไลบรารี แล้วจัดการ session ทั้งหมดโดยไม่ต้องสร้าง shell command เลยก็ได้ (แน่นอนว่าตอนนี้ยังไม่รู้ว่า tmux-rs รองรับแบบนั้นหรือเปล่า)
พอปิดงานแก้บั๊กที่กำลังทำอยู่ตอนนี้ได้ ฉันตั้งใจว่าจะลองอย่างน้อยหนึ่งในไอเดียข้างบนแน่นอน
ยังไงก็ต้องยอมรับว่า richardscollin ทำงานได้ยอดเยี่ยมมาก
ท่าทีแบบ "ไม่มีเหตุผลดีเป็นพิเศษหรอกที่เขียน tmux ใหม่ด้วย Rust มันก็แค่โปรเจ็กต์งานอดิเรก เหมือนทำสวน แต่มี segfault มากกว่า" นี่ดีมากจริง ๆ
เวลาเราสร้างอะไรใหม่ ไม่จำเป็นต้องมีแต่เหตุผลใหญ่โตหรือประโยชน์ใช้สอยเสมอไป บางครั้งโปรเจ็กต์งานอดิเรกก็นำไปสู่การค้นพบที่ไม่คาดคิดได้ ทึ่งกับบทความที่ผู้เขียนเล่าไว้อย่างละเอียดมาก
สำหรับสวนของฉัน segfault มีอยู่เต็มไปหมด การเขียนโค้ดโปรเจ็กต์ใหม่ในลานบ้านฉันยังดูปลอดภัยกว่าเสียอีก
เห็นด้วยทั้งหมด ไม่ใช่ว่าทุกโปรเจ็กต์จะต้องมีไว้เพื่อเปลี่ยนโลก
ไม่นานมานี้ฉันเพิ่งเขียน fzf ขึ้นใหม่ด้วย Rust rs-fzf-clone
ไม่ได้มีเหตุผลพิเศษอะไร เพราะ fzf เดิมก็ทำงานได้ดีมากอยู่แล้ว เป้าหมายหลักคืออยากสัมผัส channels ของ Rust และอัลกอริทึม fuzzy search ด้วยตัวเอง มันเป็นกระบวนการเรียนรู้ที่สนุกมาก และถึงแม้ fzf ตัวเดิมจะดีกว่า นั่นก็ไม่ใช่ประเด็นสำคัญ จุดประสงค์คือการได้ลองสิ่งใหม่และได้ทดลองเอง
"การทำสวนคือข้ออ้างที่ดีที่สุดในการเป็นนักปรัชญา"
ถ้ามีใครส่งนัยว่า Rust เหนือกว่า C แบบอัตโนมัติ ฉันจะตอบสนองด้วยความประชดประชันทันทีเป็นสัญชาตญาณ แต่ฉันก็มักลืมไปเรื่อย ๆ ว่าคนทำโปรเจ็กต์แบบนี้กันเพื่อความสนุก
ประโยคที่ว่า "เราไม่จำเป็นต้องมีเหตุผลเสมอไป เพียงเพื่อจะสร้างสิ่งใหม่" ฟังดูโดนใจมาก
แต่ tmux ก็ไม่ใช่ของใหม่อยู่แล้วนี่
มันเลยทำให้ฉันคิดว่าการเขียนซอฟต์แวร์เดิมขึ้นใหม่ในอีกภาษาหนึ่ง จำเป็นต้องมีเหตุผลหรือเปล่า
ประโยค "เหมือนทำสวน แต่มี segfault มากกว่า" ตลกดี ฉันยังไม่คุ้นกับ Rust มากนัก เลยสงสัยว่าในสถานการณ์แบบไหนถึงต้องใช้ unsafe
ฉันประทับใจกับท่าทีของโปรเจ็กต์นี้มาก รวมถึงบรรยากาศเชิงบวกของคอมเมนต์ส่วนใหญ่ด้วย
มักมีคนบอกว่าการเขียนแอปพลิเคชันที่โตเต็มที่แล้วขึ้นใหม่ในอีกภาษาไม่ใช่เรื่องดีเสมอไป แต่ความจริงคือการได้ลองทำจริงทำให้เกิดการเรียนรู้มากมาย กระบวนการสำคัญกว่าผลลัพธ์จริง ๆ
เมื่อดูจากความสนใจที่ได้รับตรงนี้ และแนวโน้มการพัฒนา AI ฉันคิดว่านี่อาจเติบโตเป็นโปรเจ็กต์งานอดิเรกที่น่าดึงดูดมากสำหรับผู้เริ่มต้น Rust เริ่มจากแก้บั๊กง่าย ๆ แล้วค่อยเพิ่มฟีเจอร์ใหม่หรือทำ optimization ก็น่าจะได้ประสบการณ์มาก
มีไอเดียหนึ่งที่อยากเสนอ คือทำให้ Gemini CLI (หรือ LLM ที่คุณชอบ) เป็นเหมือน scratch buffer ที่โต้ตอบกับหน้าต่าง/พาเนลต่าง ๆ ใน session ของ tmux ได้
สำหรับฉัน เวลารันคำสั่งบนหลายเซิร์ฟเวอร์ในพาเนลที่ซิงก์กันอยู่ ฉันต้องคอยจัดการความล้มเหลวและเรื่องต่าง ๆ ด้วยมือ ถ้าให้ AI เป็นคนรันคำสั่ง วิเคราะห์ผลลัพธ์แบบเรียลไทม์ แล้วสร้างคำสั่งใหม่แบบปรับตัวได้ ก็คงให้ความรู้สึกเหมือน shell script แบบสั่งทำเฉพาะที่ถูกสร้างขึ้นแบบไดนามิก
อย่างเช่นฉันใช้ gvim ทุกวัน แต่ถ้าจะทำเอดิเตอร์ ฉันคงไม่อยากทำให้มันเหมือน gvim เป๊ะ ๆ แต่อยากสร้างสิ่งใหม่ที่มีเฉพาะฟีเจอร์ที่ฉันต้องการมากกว่า ถ้าจะลงทุนเวลาขนาดนี้ ฉันคิดว่าการลองทำอะไรที่สร้างสรรค์และแปลกใหม่กว่าน่าจะมีความหมายกว่า
เมื่อกี้ฉันเพิ่งพอร์ต tmux ไปเป็น Fil-C ในเวลาไม่ถึง 1 ชั่วโมง (รวมทั้งพอร์ต libevent และรันเทสต์ผ่านแล้ว) มันทำงานได้ดีมาก และให้ประสบการณ์ความปลอดภัยของหน่วยความจำแบบสมบูรณ์
ฉันชอบโปรเจ็กต์แบบนี้มาก มันทำให้ฉันอยากอินกับ rust บ้าง
เผื่อใครสนใจ อยากแนะนำ zellij (terminal multiplexer ที่เขียนด้วย Rust)
ฉันเป็นแค่ผู้ใช้คนหนึ่ง และก็สนุกกับการคอยตามหาและย้ายไปใช้โซลูชันที่เขียนด้วย Rust อยู่เรื่อย ๆ
บังเอิญว่าฉันเพิ่งดูวิดีโอนี้ "Oxidise Your Command Line" อยู่พอดี
https://www.youtube.com/watch?v=rWMQ-g2QDsI
บางส่วนอาจไม่จำเป็นถ้าคุณไม่ใช่นักพัฒนา Rust แต่ก็มีเคล็ดลับที่มีประโยชน์มากพอสมควรสำหรับทุกคนที่คุ้นกับสภาพแวดล้อมแบบ command line
ฉันคิดว่าน่าจะปรับปรุง c2rust ให้ลดการสูญเสียข้อมูลที่ผู้เขียนพูดถึงได้มากกว่านี้ เช่น การคงชื่อคงที่ต่าง ๆ เอาไว้ เพราะภาระของการแปลงครั้งแรกมันหนักมาก
ถ้าวันหนึ่งถึงยุคที่ใช้ large language model แปลงโค้ด C ทั้งชุดให้เป็น Safe Rust ได้อย่างถูกต้องภายในหนึ่งชั่วโมง โปรเจ็กต์แบบนี้ก็คงกลายเป็นตัวอย่างชั้นดีที่ดูล้ำอนาคตมาก
แต่ผู้เขียนเองก็ลองใช้ Cursor ในขั้นตอนสุดท้ายแล้วเหมือนกัน (ณ กลางปี 2025) และบอกว่าประสิทธิภาพในการแปลงลดลงอย่างชัดเจน ดังนั้นฉันคิดว่าความสามารถในโลกจริงยังอีกไกล
ที่ codemod.com และที่อื่น ๆ ก็กำลังทำสิ่งนี้อยู่แล้วภายใต้แนวคิดที่เรียกว่า "codemods"
codemods ใช้ AST (abstract syntax tree) เพื่อให้สามารถแปลงโค้ดและรีแฟกเตอร์จำนวนมากได้อย่างรวดเร็ว
แนะนำการรีแฟกเตอร์ API ด้วย codemods
ส่วนที่ว่า "ใช้ large language model แปลง C ที่ซับซ้อนให้เป็น Safe Rust ได้อย่างสมบูรณ์ภายใน 1 ชั่วโมง" ฟังดูเฉพาะเจาะจงดี เลยยิ่งน่าสนใจ
หวังว่าโค้ดจะสะอาดขึ้นเรื่อย ๆ ในอนาคต ฉันลอง zellij มาหลายครั้งแล้ว แต่แม้พัฒนามาหลายปี มันก็ยังขาดฟีเจอร์บางอย่างที่ tmux มีให้อย่างสะดวก
โดยเฉพาะการซ่อน/แสดง status bar ไม่ได้ นี่รบกวนใจที่สุด
ดู zellij-org/zellij issue #694
คีย์ไบน์ที่ฉันใช้บ่อยดันไปชนกับคีย์ไบน์เริ่มต้นของปลั๊กอิน session manager เลยทำให้ฟีเจอร์สำคัญอย่างการเลือกไดเรกทอรีใช้งานไม่ได้
สุดท้ายโครงสร้างเลยกลายเป็นว่าต้องสร้าง session จาก command line โดยตรงแทนที่จะผ่านปลั๊กอิน