เจาะลึกปัญหา Async Traits ใน Rust ทำไมยังไม่รองรับ Object-Safe พร้อมทางแก้ที่ใช้งานได้จริง
พบปัญหา error E0038 เมื่อใช้งาน async ใน trait ของภาษา Rust หรือไม่? บทความนี้พาไปดูสาเหตุเชิงโครงสร้างและ 3 วิธีแก้ปัญหาที่ใช้งานได้จริง

ภาพประกอบจากคลังภาพสต็อก ไม่ใช่ภาพจากเหตุการณ์จริง
- ฟังก์ชันแบบ async ใน trait ของ Rust ไม่สามารถทำ object safety ได้ ส่งผลให้เกิด error E0038 เมื่อนำไปเก็บใน Vec สมชาย
- สาเหตุหลักมาจากโครงสร้างการแปลง async fn ให้เป็น State Machine ที่มีขนาดแตกต่างกันในแต่ละผู้ใช้งาน (implementor) ซึ่งไม่ตรงกับข้อกำหนดของ vtable
- ทีมนักพัฒนาคอมไพล์ปะวะยืนยันว่าปัญหานี้จะไม่ได้รับการแก้ไขในเร็วๆ นี้ นักพัฒนาจึงต้องอาศัยทางเลือกอื่น
- มีแนวทางแก้ไขหลัก 3 รูปแบบ ขึ้นอยู่กับความเหมาะสมของชุดโค้ดและไลบรารีที่คุณกำลังพัฒนาอยู่
นักพัฒนาภาษา Rust หลายคนอาจเคยเจอประสบการณ์ชวนหงุดหงิด เมื่อเขียน trait ที่มีเมธอดแบบ asynchronous นำไปใช้งานกับประเภทข้อมูลที่หลากหลาย แล้วพยายามนำพวกมันไปใส่รวมกันไว้ใน dynamic dispatch collection อย่าง Vec<Box<dyn Notifier>> สิ่งที่ปรากฏบนหน้าจอคอมไพล์คือข้อความแสดงข้อผิดพลาดว่า error[E0038]: the trait Notifier cannot be made into an object ซึ่งสร้างความประหลาดใจไม่น้อย เพราะรูปแบบโค้ดที่หน้าตาคล้ายคลึงกันนี้กลับใช้งานกับ synchronous trait ได้อย่างไร้ปัญหา แต่ทันทีที่เติมคำว่า async เข้าไปเพียงหนึ่งเมธอด คุณสมบัติ object safety ก็พังทลายลงทันที
สาเหตุเบื้องหลังของข้อจำกัดนี้ไม่ได้เกิดจากการขาดฟีเจอร์ใดฟีเจอร์หนึ่ง แต่เป็นปัญหาเชิงโครงสร้างที่ฝังราลึก ตัวคอมไพล์เลอร์จะทำการแปลง async fn ให้กลายเป็น State Machine ที่ทำงานสอดคล้องกับเทรท Future ซึ่งประเภทข้อมูลแท้จริง (concrete type) ของ State Machine เหล่านี้จะเป็นแบบไร้ชื่อ (anonymous) และมีขนาดพื้นที่หน่วยความจำแตกต่างกันออกไปตามแต่ละผู้พัฒนาที่นำไปอิมพลีเมนต์ ในขณะที่ระบบ vtable ของไดนามิกดิสแพทช์จำเป็นต้องอาศัยขนาดของช่องข้อมูลในเมธอดที่ตายตัวและเท่ากันเสมอในทุกๆ ผู้ใช้งาน ข้อเท็จจริงทางสถาปัตยกรรมสองประการนี้จึงไม่สามารถนำมาประสานเข้าด้วยกันได้ และทีมงานผู้พัฒนาคอมไพล์เลอร์ Rust ก็เคยออกมาชี้ชัดเจนแล้วว่า ปัญหานี้ยังไม่มีแผนที่จะถูกปรับแก้ในอนาคตอันใกล้นี้
ในเชิงสถาปัตยกรรมคอมไพล์เลอร์ การทำ Dynamic Dispatch ในภาษาแบบ Static-typed อย่าง Rust จำเป็นต้องอาศัย vtable (Virtual Method Table) เพื่อชี้ไปยังตำแหน่งฟังก์ชันที่ถูกต้องในขณะรันไทม์ ซึ่ง vtable จำเป็นต้องทราบขนาดและประเภทข้อมูลล่วงหน้าอย่างแม่นยำ ทว่า Future จากฟังก์ชันอะซิงโครนัสกลับมีขนาดไม่คงที่ ขึ้นอยู่กับสถานะภายในและตัวแปรที่ถูกจับไว้ (captures) ในบล็อกโค้ดนั้นๆ ความขัดแย้งระหว่างความยืดหยุ่นของ Future กับความตายตัวของ vtable จึงเป็นโจทย์ยากที่ภาษาโปรแกรมมิ่งสมัยใหม่หลายภาษาต้องเผชิญ
เมื่อข้อจำกัดทางสถาปัตยกรรมทำให้ไม่สามารถใช้งานตามปกติได้ คำถามสำคัญคือเหล่านักพัฒนาควรรับมือกับปัญหานี้อย่างไร? แนวทางแก้ไขที่ใช้ได้ผลจริงในปัจจุบันถูกแบ่งออกเป็น 3 รูปแบบหลัก โดยการเลือกแนวทางที่เหมาะสมที่สุดจะขึ้นอยู่กับบริบทของชุดข้อมูล เช่น ตัวผู้ใช้งานเป็นเซ็ตปิดที่คุณเป็นผู้ควบคุมทั้งหมด เป็นเซ็ตเปิดจากโค้ดภายนอกของบุคคลที่สาม หรือคุณกำลังพัฒนาไลบรารีที่ต้นทุนเรื่องการใช้ Box อาจส่งผลกระทบต่อผู้ใช้งานปลายทาง

ภาพประกอบจากคลังภาพสต็อก ไม่ใช่ภาพจากเหตุการณ์จริง
ที่มา: Dev.to
พบข้อมูลผิดพลาดในบทความนี้? แจ้งปัญหาบทความนี้
ความคิดเห็น
แสดงความคิดเห็น