สิ่งที่ทำได้ดี
- การรีไรต์ทำเป็นขั้นเล็ก ๆ แบบค่อยเป็นค่อยไป (incremental, stop-and-go), ใช้งานได้ดี และโค้ดใหม่ก็อ่านและทำความเข้าใจได้ง่ายขึ้น
- การได้มองเห็นภาพรวมของโค้ดทั้งหมดทำให้พบโอกาสในการปรับแต่งประสิทธิภาพ
- ลบโค้ดที่ไม่ได้ใช้ออกไปได้ราว 1/3 ~ 1/2 ภาษาโปรแกรมสมัยใหม่อย่าง Rust หรือ Go ค้นหา dead code ได้ดีกว่าและแจ้งให้นักพัฒนาทราบ
- ไม่ต้องกังวลเรื่องการเข้าถึงนอกขอบเขตหรือ overflow/underflow
- เฟรมเวิร์กทดสอบที่มีมาในตัวมีประโยชน์มาก
- ดีใจที่สามารถลบไฟล์ CMake ออกได้
สิ่งที่ทำได้ไม่ดี
ยังต้องไล่ตาม undefined behavior อยู่ดี
- การรีไรต์จาก C/C++ มาเป็น Rust แบบค่อยเป็นค่อยไปทำให้ต้องใช้ raw pointer และบล็อก
unsafe{} จำนวนมาก
- กฎของ Rust ยังคงใช้ภายใน
unsafe แต่คอมไพเลอร์ไม่ตรวจสอบ จึงเกิด undefined behavior ได้ง่าย
- ภายใน
unsafe สามารถทำผิดกฎหลายตัวชี้แบบอ่านอย่างเดียว XOR หนึ่งตัวชี้แบบแก้ไขได้ง่าย
- Miri ทำหน้าที่เป็นผู้ช่วยสำคัญในการจับปัญหาเหล่านี้
Miri ไม่ได้ทำงานได้เสมอไป และยังต้องใช้ Valgrind
- หากใช้ไลบรารีที่มีบางส่วนเขียนด้วย C หรือแอสเซมบลี เช่น ไลบรารีเข้ารหัส Miri จะทำงานไม่ได้
- มีโค้ด
unsafe จำนวนมากที่ Miri ตรวจไม่ครอบคลุม
- บางการทดสอบต้องรันด้วย
valgrind
ยังต้องไล่ตาม memory leak อยู่ดี
- แพตเทิร์นทั่วไปของ C API คือจองหน่วยความจำใน
MYLIB_init() แล้วปล่อยใน MYLIB_release() ซึ่งลืมเรียก MYLIB_release ได้ง่าย
- นักพัฒนา Rust อยากสร้าง wrapper object แบบ RAII แต่ในการทดสอบที่ใช้ C API จะใช้ความสามารถนี้ไม่ได้
- ในลอจิกที่ซับซ้อน การเรียกฟังก์ชัน cleanup ให้ครบทุกครั้งเป็นเรื่องยาก ใน C ใช้
goto แก้ปัญหาได้ แต่ Rust ไม่รองรับ
- แก้ด้วย crate
defer แต่ borrow checker ไม่ค่อยชอบ
cross-compilation ไม่ได้ทำงานได้เสมอไป
- เช่นเดียวกับ Miri หากใช้ไลบรารีที่มีบางส่วนเขียนด้วย C หรือแอสเซมบลี
cargo build --target=... จะไม่ทำงานได้ทันที
Cbindgen ไม่ได้ทำงานได้เสมอไป
- Cbindgen ถูกใช้บ่อยในการสร้าง C header จากโค้ดเบส Rust แต่ก็มีข้อจำกัดหรือบั๊ก
ABI ที่ไม่เสถียร
- type ใน standard library ที่มีประโยชน์อย่าง
Option ไม่มี ABI ที่เสถียร จึงต้องทำสำเนาเองด้วย annotation repr(C)
ไม่มีการรองรับ custom memory allocator
- ไลบรารี C จำนวนมากเปิดให้ผู้ใช้ส่ง allocator เข้าไปตอนรันไทม์ได้ แต่ใน Rust เลือก global allocator ได้เฉพาะตอนคอมไพล์
- ปัญหาการจัดการทรัพยากรแก้ได้ด้วย arena allocator แต่ใน Rust วิธีนี้ไม่ใช่แนวปฏิบัติแบบ idiomatic และไม่ผสานกับ standard library
ความซับซ้อน
- ต้องใช้สิ่งอย่าง
UnsafeCell, RefCell, MaybeUninit, Pin เพื่อจัดการ FFI ทำให้ความซับซ้อนสูง
- แม้แต่ Rust ล้วน ๆ ก็ซับซ้อนอยู่แล้ว และพอเพิ่มชั้น FFI เข้าไปก็ยิ่งกลายเป็นสัตว์ประหลาด
- ถึงขั้นมีนักพัฒนาบางคนปฏิเสธที่จะทำงานกับโค้ดเบสนี้เพราะความซับซ้อนของ Rust
บทสรุป
- โดยรวมพอใจกับการรีไรต์เป็น Rust แต่ก็ผิดหวังในบางด้าน และต้องใช้ความพยายามมากกว่าที่คาดไว้มาก
- Rust ที่ต้องทำงานร่วมกับ C อย่างหนักให้ความรู้สึกเหมือนเป็นคนละภาษากับ Rust ล้วน ๆ โดยสิ้นเชิง มีแรงเสียดทานมากและกับดักเยอะ ปัญหาหลายอย่างของ C++ ที่ Rust อ้างว่าแก้ได้ จริง ๆ แล้วกลับไม่ได้ถูกแก้เลยในบริบทนี้
- รู้สึกขอบคุณอย่างยิ่งต่อนักพัฒนาของ Rust, Miri, cbindgen และอื่น ๆ พวกเขาทำงานได้ยอดเยี่ยมมาก ถึงอย่างนั้น ภาษาและเครื่องมือสำหรับงานที่ต้องทำ C FFI หนัก ๆ ก็ยังดูไม่สุกงอม และให้ความรู้สึกเหมือนยังไม่พ้นยุคก่อน v1.0
- หาก ergonomics ของ
unsafe, standard library, เอกสาร, เครื่องมือ, และ ABI ที่ยังไม่เสถียร ได้รับการปรับปรุงในอนาคต ประสบการณ์นี้ก็น่าจะสนุกขึ้นมาก
- ดูเหมือนว่า Microsoft และ Google ก็รับรู้ประเด็นเหล่านี้ทั้งหมดเช่นกัน จึงกำลังลงทุนเงินจริงในด้านนี้
- ถ้ายังไม่รู้จัก Rust โปรเจกต์แรกควรใช้ Rust ล้วน ๆ และอยู่ห่างจากประเด็น FFI ไว้ก่อนจะดีกว่า
- ตอนแรกเคยพิจารณาใช้ Zig หรือ Odin สำหรับการรีไรต์ครั้งนี้ แต่ไม่อยากใช้ภาษาที่ยังไม่ถึง v1.0 กับโค้ดเบสโปรดักชันขององค์กร ตอนนี้เลยเริ่มสงสัยว่าประสบการณ์นั้นจะแย่กว่า Rust จริงหรือไม่ อาจเป็นเพราะโมเดลของ Rust เข้ากันไม่ได้กับโมเดลของ C (หรือ C++) อย่างแท้จริง จึงเกิดแรงเสียดทานสูงมากเมื่อใช้ร่วมกัน
- หากในอนาคตต้องทำงานลักษณะนี้อีก จะพิจารณา Zig อย่างจริงจัง และทุกครั้งที่มีคนพูดว่า "ก็แค่รีไรต์เป็น Rust สิ" ก็ควรยื่นบทความนี้ให้เขาอ่านแล้วถามว่าความคิดเปลี่ยนไปหรือยัง
12 ความคิดเห็น
แม้ว่า Zig จะยังเป็น pre-v1 แต่ก็สามารถใช้ไลบรารี C จำนวนมากได้ จึงใช้งานได้ดีกว่าที่คิด ถ้าจะเสริมอะไรบางอย่างเข้าไปในโปรเจกต์ที่ทำงานอยู่ซึ่งมีพื้นฐานเป็น C บางที Zig อาจเหมาะกว่า Rust
ตอนที่เริ่มดู Rust แค่เห็นคีย์เวิร์ด
unsafeก็รู้สึกหวั่น ๆ ขึ้นมาทันที และ...ผมคิดว่า Rust ไม่สามารถแก้ปัญหาเรื้อรังที่ C++ มีอยู่ได้ ซึ่งเป็นมุมมองในเชิงปฏิบัติงานมากกว่าเชิงไวยากรณ์
เหตุผลคือ
ตอนนี้มีระบบ Production จำนวนมากที่ใช้ C/C++ อยู่แล้ว และมันก็ทำงานได้เสถียรดี ส่วนใหญ่ก็ไม่ได้คิดจะพอร์ตสิ่งนี้ไปเป็น Rust โดยเฉพาะ
ตั้งแต่แรก ฮาร์ดแวร์ก็ไม่ได้ถูกสร้างมาโดยตั้งสมมติฐานเรื่อง reference count กรณีที่ใช้ C/C++ จำนวนมากก็เพื่อควบคุมฮาร์ดแวร์, OS, ไดรเวอร์, และระดับไบนารีด้วยความเร็วสูง แต่หากจะรองรับ Rust นักพัฒนาระดับล่างก็สุดท้ายต้องใช้
unsafeเพื่อจัดการ lifecycle ของทรัพยากรด้วยตนเองอยู่ดี และนี่ก็เป็นต้นทุนก้อนใหญ่เช่นกันผมคิดว่าประสบการณ์ของผู้เขียนสำคัญยิ่งกว่าคุณค่าที่ภาษาอาจมีในเชิงศักยภาพหรือเรื่องเล่าในเชิงทฤษฎี
ในความเป็นจริง ระดับการจัดการทรัพยากรในงานที่ต้องการภาษาระดับ C/C++ ทำให้รู้สึกว่ามันเป็นอะไรที่กลืนไม่เข้าคายไม่ออกหากจะให้ Rust มาแทนที่
บทความนี้ก็ดูเหมือนจะเข้าใจ Rust ผิดแล้วพุ่งเข้าไปเหมือนกัน
ดูจากเนื้อหาแล้วน่าจะเป็นไลบรารีที่ต้องสื่อสารกับภายนอกของ Rust บ่อย ๆ ซึ่งพอถึงจุดนั้นก็เละได้อยู่แล้ว... แต่เดิมก็แทบไม่มีภาษาเนทีฟไหนที่ไม่เละอยู่แล้ว และสำหรับ Rust นั้นมันเป็นแค่การห่อสิ่งเหล่านั้นให้ปลอดภัยในระดับภาษา ดังนั้นยิ่งมีจุดเชื่อมต่อกับภายนอกภาษามากเท่าไร ข้อดีก็ยิ่งหายไปมากเท่านั้น
จะว่าถูกก็ถูกอยู่ในระดับหนึ่ง แต่ในสภาพแวดล้อมการพัฒนาตามต้นฉบับนั้น มันก็เลี่ยงไม่ได้ที่จะไม่ถูกแก้ และผมคิดว่าปัญหาคือการเข้าไปหา Rust ราวกับมันเป็นยาครอบจักรวาล
ฉันรู้สึกว่าความหมายคือ ในกระบวนการค่อย ๆ เปลี่ยนจาก C/C++ ไปเป็น Rust จำเป็นต้องใช้
unsafeอยู่แล้ว จึงไม่มีความหมายมากนักที่จะเปลี่ยนมาใช้ Rust และถ้าจะค่อย ๆ เปลี่ยนแทนที่จะใช้ Rust ก็จะเลือก Zig มากกว่า แต่ในเนื้อหาตรงไหนที่เขียนว่าเป็นไลบรารีที่ต้องสื่อสารกับภายนอกของ Rust บ่อย ๆ?การใช้ FFI ก็หมายถึงการสื่อสารกับภายนอกของ Rust นั่นเอง
และเมื่อดูจากเนื้อหาในบทความ ดูเหมือนว่าจะไม่ได้จบแค่การรับส่งสถานะบางอย่างหรือข้อมูลง่าย ๆ เท่านั้น แต่เป็นการที่ภายในและภายนอกโต้ตอบกันอย่างซับซ้อน
ถ้าจะค่อย ๆ เปลี่ยนไลบรารีที่เขียนด้วย C ไปเป็น Rust ก็คงเลี่ยง FFI ไม่ได้ใช่ไหม? คงต้องเปลี่ยนส่วนเล็ก ๆ ของโปรแกรมให้เป็น Rust แล้วจัดการส่วน C ที่เหลือด้วย FFI แบบนี้ สิ่งที่คุณเรียกว่าเป็นการสื่อสารกับภายนอก หมายถึงงานลักษณะนี้หรือเปล่า? ถ้าเป็นอย่างนั้น ผมคิดว่าเป็นเรื่องธรรมดาที่ผู้เขียนต้นฉบับจะเริ่มรู้สึกกังขาต่อ Rust เพราะตราบใดที่ยังไม่ได้เปลี่ยนโค้ดทั้งหมดในครั้งเดียว ก็แทบจะไม่ได้ประโยชน์จาก Rust เลย จึงแนะนำ Zig แทน
^-^
คาดว่าจะมีประโยชน์ เพราะส่วนที่เป็น
unsafeถูกระบุไว้อย่างชัดเจนในซอร์สโค้ด จึงทำให้สามารถระบุขอบเขตผลกระทบของ FFI ได้ทั้งหมดตั้งแต่จุดเริ่มต้นของโปรแกรม ตราบใดที่ไม่ได้ใช้บล็อกunsafeตั้งแต่แรก แต่ดูเหมือนว่าสำหรับผู้เขียนแล้วประเด็นนี้จะไม่ค่อยโดนใจนักจริง ๆ แล้วตั้งแต่วินาทีที่ใช้ FFI การออกแบบที่ปลอดภัยก็แทบหมดหวังไปแล้ว
ถูกต้องครับ
ก็ใช่น่ะสิ ทั้งที่เขียนไว้อย่างมั่นหน้ามากว่าแปะ
unsafeไว้เต็มไปหมด แต่กลับบอกว่ายังแก้ปัญหาไม่ได้...