18 คะแนน โดย xguru 2024-11-04 | 12 ความคิดเห็น | แชร์ทาง WhatsApp

สิ่งที่ทำได้ดี

  • การรีไรต์ทำเป็นขั้นเล็ก ๆ แบบค่อยเป็นค่อยไป (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 ความคิดเห็น

 
bus710 2024-11-05

แม้ว่า Zig จะยังเป็น pre-v1 แต่ก็สามารถใช้ไลบรารี C จำนวนมากได้ จึงใช้งานได้ดีกว่าที่คิด ถ้าจะเสริมอะไรบางอย่างเข้าไปในโปรเจกต์ที่ทำงานอยู่ซึ่งมีพื้นฐานเป็น C บางที Zig อาจเหมาะกว่า Rust

 
ahwjdekf 2024-11-04

ตอนที่เริ่มดู Rust แค่เห็นคีย์เวิร์ด unsafe ก็รู้สึกหวั่น ๆ ขึ้นมาทันที และ...

 
jkliop890 2024-11-04

ผมคิดว่า Rust ไม่สามารถแก้ปัญหาเรื้อรังที่ C++ มีอยู่ได้ ซึ่งเป็นมุมมองในเชิงปฏิบัติงานมากกว่าเชิงไวยากรณ์

เหตุผลคือ

  1. ตอนนี้มีระบบ Production จำนวนมากที่ใช้ C/C++ อยู่แล้ว และมันก็ทำงานได้เสถียรดี ส่วนใหญ่ก็ไม่ได้คิดจะพอร์ตสิ่งนี้ไปเป็น Rust โดยเฉพาะ

  2. ตั้งแต่แรก ฮาร์ดแวร์ก็ไม่ได้ถูกสร้างมาโดยตั้งสมมติฐานเรื่อง reference count กรณีที่ใช้ C/C++ จำนวนมากก็เพื่อควบคุมฮาร์ดแวร์, OS, ไดรเวอร์, และระดับไบนารีด้วยความเร็วสูง แต่หากจะรองรับ Rust นักพัฒนาระดับล่างก็สุดท้ายต้องใช้ unsafe เพื่อจัดการ lifecycle ของทรัพยากรด้วยตนเองอยู่ดี และนี่ก็เป็นต้นทุนก้อนใหญ่เช่นกัน

ผมคิดว่าประสบการณ์ของผู้เขียนสำคัญยิ่งกว่าคุณค่าที่ภาษาอาจมีในเชิงศักยภาพหรือเรื่องเล่าในเชิงทฤษฎี
ในความเป็นจริง ระดับการจัดการทรัพยากรในงานที่ต้องการภาษาระดับ C/C++ ทำให้รู้สึกว่ามันเป็นอะไรที่กลืนไม่เข้าคายไม่ออกหากจะให้ Rust มาแทนที่

 
cosine20 2024-11-04

บทความนี้ก็ดูเหมือนจะเข้าใจ Rust ผิดแล้วพุ่งเข้าไปเหมือนกัน
ดูจากเนื้อหาแล้วน่าจะเป็นไลบรารีที่ต้องสื่อสารกับภายนอกของ Rust บ่อย ๆ ซึ่งพอถึงจุดนั้นก็เละได้อยู่แล้ว... แต่เดิมก็แทบไม่มีภาษาเนทีฟไหนที่ไม่เละอยู่แล้ว และสำหรับ Rust นั้นมันเป็นแค่การห่อสิ่งเหล่านั้นให้ปลอดภัยในระดับภาษา ดังนั้นยิ่งมีจุดเชื่อมต่อกับภายนอกภาษามากเท่าไร ข้อดีก็ยิ่งหายไปมากเท่านั้น

ปัญหาหลายอย่างของ C++ ที่ Rust อ้างว่าแก้ได้ แท้จริงแล้วกลับไม่ได้ถูกแก้เลย

จะว่าถูกก็ถูกอยู่ในระดับหนึ่ง แต่ในสภาพแวดล้อมการพัฒนาตามต้นฉบับนั้น มันก็เลี่ยงไม่ได้ที่จะไม่ถูกแก้ และผมคิดว่าปัญหาคือการเข้าไปหา Rust ราวกับมันเป็นยาครอบจักรวาล

 
kotlinc 2024-11-04

ฉันรู้สึกว่าความหมายคือ ในกระบวนการค่อย ๆ เปลี่ยนจาก C/C++ ไปเป็น Rust จำเป็นต้องใช้ unsafe อยู่แล้ว จึงไม่มีความหมายมากนักที่จะเปลี่ยนมาใช้ Rust และถ้าจะค่อย ๆ เปลี่ยนแทนที่จะใช้ Rust ก็จะเลือก Zig มากกว่า แต่ในเนื้อหาตรงไหนที่เขียนว่าเป็นไลบรารีที่ต้องสื่อสารกับภายนอกของ Rust บ่อย ๆ?

 
cosine20 2024-11-04

การใช้ FFI ก็หมายถึงการสื่อสารกับภายนอกของ Rust นั่นเอง
และเมื่อดูจากเนื้อหาในบทความ ดูเหมือนว่าจะไม่ได้จบแค่การรับส่งสถานะบางอย่างหรือข้อมูลง่าย ๆ เท่านั้น แต่เป็นการที่ภายในและภายนอกโต้ตอบกันอย่างซับซ้อน

 
kotlinc 2024-11-05

ถ้าจะค่อย ๆ เปลี่ยนไลบรารีที่เขียนด้วย C ไปเป็น Rust ก็คงเลี่ยง FFI ไม่ได้ใช่ไหม? คงต้องเปลี่ยนส่วนเล็ก ๆ ของโปรแกรมให้เป็น Rust แล้วจัดการส่วน C ที่เหลือด้วย FFI แบบนี้ สิ่งที่คุณเรียกว่าเป็นการสื่อสารกับภายนอก หมายถึงงานลักษณะนี้หรือเปล่า? ถ้าเป็นอย่างนั้น ผมคิดว่าเป็นเรื่องธรรมดาที่ผู้เขียนต้นฉบับจะเริ่มรู้สึกกังขาต่อ Rust เพราะตราบใดที่ยังไม่ได้เปลี่ยนโค้ดทั้งหมดในครั้งเดียว ก็แทบจะไม่ได้ประโยชน์จาก Rust เลย จึงแนะนำ Zig แทน

 
cosine20 2024-11-05

^-^

 
savvykang 2024-11-04

คาดว่าจะมีประโยชน์ เพราะส่วนที่เป็น unsafe ถูกระบุไว้อย่างชัดเจนในซอร์สโค้ด จึงทำให้สามารถระบุขอบเขตผลกระทบของ FFI ได้ทั้งหมดตั้งแต่จุดเริ่มต้นของโปรแกรม ตราบใดที่ไม่ได้ใช้บล็อก unsafe ตั้งแต่แรก แต่ดูเหมือนว่าสำหรับผู้เขียนแล้วประเด็นนี้จะไม่ค่อยโดนใจนัก

 
carnoxen 2024-11-04

จริง ๆ แล้วตั้งแต่วินาทีที่ใช้ FFI การออกแบบที่ปลอดภัยก็แทบหมดหวังไปแล้ว

 
cosine20 2024-11-04

ถูกต้องครับ

 
kohs100 2024-11-04

ก็ใช่น่ะสิ ทั้งที่เขียนไว้อย่างมั่นหน้ามากว่าแปะ unsafe ไว้เต็มไปหมด แต่กลับบอกว่ายังแก้ปัญหาไม่ได้...