27 มีนาคม 2568

การพอร์ตโค้ด C# ไปยัง C++: การสร้าง SmartPtr

ตั้งแต่เริ่มต้น งานนี้เกี่ยวข้องกับการพอร์ตหลายโปรเจกต์ซึ่งมีโค้ดรวมกันหลายล้านบรรทัด โดยพื้นฐานแล้ว ข้อกำหนดทางเทคนิคสำหรับตัวแปลภาษาสรุปได้เป็นวลีที่ว่า “ตรวจสอบให้แน่ใจว่าโค้ดทั้งหมดนี้สามารถพอร์ตและทำงานได้อย่างถูกต้องใน C++” งานของผู้ที่รับผิดชอบในการปล่อยผลิตภัณฑ์ C++ เกี่ยวข้องกับการแปลโค้ด การรันเทสต์ การเตรียมแพ็คเกจสำหรับรีลีส และอื่นๆ ปัญหาที่พบบ่อยมักจะอยู่ในหมวดหมู่ใดหมวดหมู่หนึ่งต่อไปนี้:

  1. โค้ดไม่สามารถแปลเป็น C++ ได้ - ตัวแปลภาษายุติการทำงานพร้อมข้อผิดพลาด
  2. โค้ดแปลเป็น C++ ได้ แต่คอมไพล์ไม่ผ่าน
  3. โค้ดคอมไพล์ผ่าน แต่ลิงก์ไม่ผ่าน
  4. โค้ดลิงก์และรันได้ แต่เทสต์ล้มเหลว หรือเกิดข้อขัดข้องขณะทำงาน (runtime crashes)
  5. เทสต์ผ่าน แต่เกิดปัญหาระหว่างการทดสอบที่ไม่เกี่ยวข้องโดยตรงกับการทำงานของผลิตภัณฑ์ ตัวอย่างเช่น: หน่วยความจำรั่วไหล (memory leaks), ประสิทธิภาพต่ำ เป็นต้น

ลำดับความคืบหน้าในรายการนี้เป็นแบบจากบนลงล่าง — ตัวอย่างเช่น หากไม่แก้ไขปัญหาการคอมไพล์ในโค้ดที่แปลแล้ว ก็เป็นไปไม่ได้ที่จะตรวจสอบการทำงานและประสิทธิภาพของมัน ด้วยเหตุนี้ ปัญหาเรื้อรังหลายอย่างจึงเพิ่งถูกค้นพบในช่วงท้ายๆ ของการทำงานในโปรเจกต์ CodePorting.Translator Cs2Cpp

ในตอนแรก เมื่อทำการแก้ไขปัญหาหน่วยความจำรั่วไหลง่ายๆ ที่เกิดจากการพึ่งพากันแบบวงกลม (circular dependencies) ระหว่างอ็อบเจกต์ เราใช้แอตทริบิวต์ CppWeakPtr กับฟิลด์ ส่งผลให้ฟิลด์มีไทป์เป็น WeakPtr ตราบใดที่ WeakPtr สามารถแปลงเป็น SharedPtr ได้โดยการเรียกเมธอด lock() หรือโดยปริยาย (ซึ่งสะดวกกว่าในทางไวยากรณ์) สิ่งนี้ก็ไม่ได้ก่อให้เกิดปัญหา อย่างไรก็ตาม ต่อมาเรายังต้องทำให้การอ้างอิงที่อยู่ในคอนเทนเนอร์เป็นแบบ weak ด้วย โดยใช้ไวยากรณ์พิเศษสำหรับแอตทริบิวต์ CppWeakPtr และสิ่งนี้นำไปสู่เรื่องน่าประหลาดใจที่ไม่พึงประสงค์สองสามอย่าง

สัญญาณแรกของปัญหาเกี่ยวกับแนวทางที่เราใช้คือ จากมุมมองของ C++, MyContainer<SharedPtr<MyClass>> และ MyContainer<WeakPtr<MyClass>> เป็นไทป์ที่แตกต่างกันสองไทป์ ด้วยเหตุนี้จึงไม่สามารถเก็บไว้ในตัวแปรเดียวกัน ส่งผ่านไปยังเมธอดเดียวกัน ส่งคืนจากเมธอดนั้น และอื่นๆ ได้ แอตทริบิวต์ซึ่งเดิมทีมีจุดประสงค์เพื่อจัดการวิธีการจัดเก็บการอ้างอิงในฟิลด์ของอ็อบเจกต์เท่านั้น เริ่มปรากฏในบริบทที่แปลกประหลาดมากขึ้นเรื่อยๆ ส่งผลกระทบต่อค่าส่งคืน อาร์กิวเมนต์ ตัวแปรโลคัล ฯลฯ โค้ดของตัวแปลภาษาที่รับผิดชอบในการจัดการกับมันก็ซับซ้อนขึ้นทุกวัน

ปัญหาที่สองก็เป็นสิ่งที่เราไม่ได้คาดการณ์ไว้เช่นกัน สำหรับโปรแกรมเมอร์ C# กลายเป็นเรื่องปกติที่จะมีคอลเลกชันแบบจับคู่ (associative collection) เพียงหนึ่งรายการต่ออ็อบเจกต์ ซึ่งมีทั้งการอ้างอิงที่ไม่ซ้ำกัน (unique references) ไปยังอ็อบเจกต์ที่อ็อบเจกต์ปัจจุบันเป็นเจ้าของและไม่สามารถเข้าถึงได้ด้วยวิธีอื่น รวมถึงการอ้างอิงไปยังอ็อบเจกต์แม่ (parent objects) ด้วย การทำเช่นนี้เพื่อเพิ่มประสิทธิภาพการดำเนินการอ่านจากรูปแบบไฟล์บางอย่าง แต่สำหรับเรา มันหมายความว่าคอลเลกชันเดียวกันสามารถมีการอ้างอิงทั้งแบบ strong และ weak ได้ ไทป์ของพอยเตอร์จึงไม่ใช่ตัวกำหนดสุดท้ายของโหมดการทำงานของมันอีกต่อไป

ไทป์ของการอ้างอิงเป็นส่วนหนึ่งของสถานะพอยเตอร์

เห็นได้ชัดว่า ปัญหาสองข้อนี้ไม่สามารถแก้ไขได้ภายในกระบวนทัศน์ที่มีอยู่ และไทป์ของพอยเตอร์จึงได้รับการพิจารณาใหม่ ผลลัพธ์ของแนวทางที่ปรับปรุงใหม่นี้คือคลาส SmartPtr ซึ่งมีเมธอด set_Mode() ที่รับค่าหนึ่งในสองค่า: SmartPtrMode::Shared และ SmartPtrMode::Weak คอนสตรักเตอร์ทั้งหมดของ SmartPtr ก็รับค่าเดียวกันเหล่านี้ ด้วยเหตุนี้ อินสแตนซ์ของพอยเตอร์แต่ละตัวจึงสามารถอยู่ในสถานะใดสถานะหนึ่งจากสองสถานะ:

  1. การอ้างอิงแบบ Strong: ตัวนับการอ้างอิงถูกห่อหุ้มอยู่ภายในอ็อบเจกต์
  2. การอ้างอิงแบบ Weak: ตัวนับการอ้างอิงอยู่ภายนอกอ็อบเจกต์

การสลับระหว่างโหมดสามารถเกิดขึ้นได้ในขณะทำงาน (runtime) และ ณ เวลาใดก็ได้ ตัวนับการอ้างอิงแบบ weak จะไม่ถูกสร้างขึ้นจนกว่าจะมีการอ้างอิงแบบ weak อย่างน้อยหนึ่งรายการไปยังอ็อบเจกต์นั้น

รายการคุณสมบัติทั้งหมดที่พอยเตอร์ของเรารองรับมีดังนี้:

  1. การจัดเก็บการอ้างอิงแบบ Strong: การจัดการอายุขัยของอ็อบเจกต์ผ่านการนับจำนวนการอ้างอิง (reference counting)
  2. การจัดเก็บการอ้างอิงแบบ Weak สำหรับอ็อบเจกต์
  3. ความหมายแบบ intrusive_ptr: พอยเตอร์จำนวนเท่าใดก็ได้ที่สร้างขึ้นสำหรับอ็อบเจกต์เดียวกันจะใช้ตัวนับการอ้างอิงร่วมกันเพียงตัวเดียว
  4. การดีเรเฟอเรนซ์ (Dereferencing) และโอเปอเรเตอร์ลูกศร (->): สำหรับการเข้าถึงอ็อบเจกต์ที่พอยเตอร์ชี้ไป
  5. ชุดคอนสตรักเตอร์และโอเปอเรเตอร์กำหนดค่า (assignment operators) ที่สมบูรณ์
  6. การแยกอ็อบเจกต์ที่ถูกชี้และอ็อบเจกต์ที่นับการอ้างอิง (คอนสตรักเตอร์แบบนามแฝง - aliasing constructor): เนื่องจากไลบรารีของลูกค้าของเราทำงานกับเอกสาร บ่อยครั้งจึงจำเป็นที่พอยเตอร์ไปยังองค์ประกอบของเอกสารจะต้องทำให้เอกสารทั้งฉบับยังคงอยู่ (keep alive)
  7. ชุดการแปลงชนิดข้อมูล (casts) ที่สมบูรณ์
  8. ชุดการดำเนินการเปรียบเทียบที่สมบูรณ์
  9. การกำหนดค่าและการลบพอยเตอร์: ทำงานบนไทป์ที่ไม่สมบูรณ์ (incomplete types)
  10. ชุดเมธอดสำหรับตรวจสอบและเปลี่ยนสถานะของพอยเตอร์: โหมดนามแฝง (aliasing mode), โหมดการจัดเก็บการอ้างอิง, จำนวนการอ้างอิงของอ็อบเจกต์ ฯลฯ

คลาส SmartPtr เป็นเทมเพลตและไม่มีเมธอดเสมือน (virtual methods) มันผูกพันอย่างแน่นหนากับคลาส System::Object ซึ่งจัดการการจัดเก็บตัวนับการอ้างอิง และทำงานเฉพาะกับคลาสที่สืบทอดมาจาก System::Object เท่านั้น

มีการเบี่ยงเบนไปจากพฤติกรรมทั่วไปของพอยเตอร์:

  1. การย้าย (move constructor, move assignment operator) ไม่ได้เปลี่ยนสถานะทั้งหมด มันยังคงรักษารูปแบบการอ้างอิง (weak/strong) ไว้
  2. การเข้าถึงอ็อบเจกต์ผ่านการอ้างอิงแบบ weak ไม่จำเป็นต้องทำการล็อก (สร้างการอ้างอิงแบบ strong ชั่วคราว) เนื่องจากแนวทางที่โอเปอเรเตอร์ลูกศรส่งคืนอ็อบเจกต์ชั่วคราวจะทำให้ประสิทธิภาพลดลงอย่างรุนแรงสำหรับการอ้างอิงแบบ strong

เพื่อรักษาความเข้ากันได้กับโค้ดเก่า ไทป์ SharedPtr จึงกลายเป็นชื่อแฝง (alias) สำหรับ SmartPtr ส่วนคลาส WeakPtr ตอนนี้สืบทอดจาก SmartPtr โดยไม่เพิ่มฟิลด์ใดๆ และเพียงแค่ทำการโอเวอร์ไรด์คอนสตรักเตอร์เพื่อให้สร้างการอ้างอิงแบบ weak เสมอ

ตอนนี้คอนเทนเนอร์จะถูกพอร์ตด้วยความหมายแบบ MyContainer<SmartPtr<MyClass>> เสมอ และชนิดของการอ้างอิงที่จัดเก็บจะถูกเลือกในขณะทำงาน สำหรับคอนเทนเนอร์ที่เขียนขึ้นเองโดยใช้โครงสร้างข้อมูล STL (ส่วนใหญ่เป็นคอนเทนเนอร์จากเนมสเปซ System) ไทป์การอ้างอิงเริ่มต้นจะถูกตั้งค่าโดยใช้ตัวจัดสรรหน่วยความจำ (allocator) ที่กำหนดเอง ในขณะที่ยังคงอนุญาตให้เปลี่ยนโหมดสำหรับองค์ประกอบแต่ละรายการในคอนเทนเนอร์ได้ สำหรับคอนเทนเนอร์ที่แปลแล้ว โค้ดที่จำเป็นสำหรับการสลับโหมดการจัดเก็บการอ้างอิงจะถูกสร้างขึ้นโดยตัวแปลภาษา

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

การเตรียมโค้ดสำหรับการแปล

วิธีการพอร์ตของเราต้องการการวางแอตทริบิวต์ด้วยตนเองในซอร์สโค้ด C# เพื่อทำเครื่องหมายว่าตำแหน่งใดที่การอ้างอิงควรเป็นแบบ weak โค้ดที่ไม่ได้วางแอตทริบิวต์เหล่านี้อย่างถูกต้องจะทำให้เกิดหน่วยความจำรั่วไหล และในบางกรณี อาจเกิดข้อผิดพลาดอื่นๆ หลังจากการแปล โค้ดที่มีแอตทริบิวต์จะมีลักษณะดังนี้:

struct S {
    MyClass s; // การอ้างอิงแบบ Strong ไปยังอ็อบเจกต์

    [CppWeakPtr]
    MyClass w; // การอ้างอิงแบบ Weak ไปยังอ็อบเจกต์

    MyContainer<MyClass> s_s; // การอ้างอิงแบบ Strong ไปยังคอนเทนเนอร์ของการอ้างอิงแบบ Strong

    [CppWeakPtr]
    MyContainer<MyClass> w_s; // การอ้างอิงแบบ Weak ไปยังคอนเทนเนอร์ของการอ้างอิงแบบ Strong

    [CppWeakPtr(0)]
    MyContainer<MyClass> s_w; // การอ้างอิงแบบ Strong ไปยังคอนเทนเนอร์ของการอ้างอิงแบบ Weak

    [CppWeakPtr(1)]
    Dictionary<MyClass, MyClass> s_s_w; // การอ้างอิงแบบ Strong ไปยังคอนเทนเนอร์ที่คีย์ถูกจัดเก็บด้วยการอ้างอิงแบบ Strong และค่าด้วยการอ้างอิงแบบ Weak

    [CppWeakPtr, CppWeakPtr(0)]
    Dictionary<MyClass, MyClass> w_w_s; // การอ้างอิงแบบ Weak ไปยังคอนเทนเนอร์ที่คีย์ถูกจัดเก็บด้วยการอ้างอิงแบบ Weak และค่าด้วยการอ้างอิงแบบ Strong
}

ในบางกรณี จำเป็นต้องเรียกใช้คอนสตรักเตอร์แบบนามแฝง (aliasing constructor) ของคลาส SmartPtr หรือเมธอดที่ตั้งค่าไทป์การอ้างอิงที่จัดเก็บด้วยตนเอง เราพยายามหลีกเลี่ยงการแก้ไขโค้ดที่พอร์ตแล้ว เนื่องจากการเปลี่ยนแปลงดังกล่าวจะต้องทำซ้ำใหม่ทุกครั้งหลังจากการรันตัวแปลภาษา แต่เรามุ่งมั่นที่จะเก็บโค้ดดังกล่าวไว้ภายในซอร์สโค้ด C# เรามีสองวิธีในการทำเช่นนี้:

  1. เราสามารถประกาศเมธอดบริการ (service method) ในโค้ด C# ที่ไม่ได้ทำอะไรเลย และในระหว่างการแปล จะแทนที่ด้วยโค้ดที่เขียนขึ้นเองซึ่งทำหน้าที่ที่จำเป็น:
class Service {
    public static void SetWeak<T>(T arg) {}
}
class Service {
public:
    template <typename T> static void SetWeak(SmartPtr<T> &arg)
    {
        arg.set_Mode(SmartPtrMode::Weak);
    }
};
  1. เราสามารถใส่ความคิดเห็น (comments) ที่จัดรูปแบบพิเศษในโค้ด C# ซึ่งตัวแปลภาษาจะแปลงเป็นโค้ด C++:
class MyClass {
    private Dictionary<string, object> data;
    public void Add(string key, object value)
    {
        data.Add(key, value);
        //CPPCODE: if (key == u"Parent") data->data()[key].set_Mode(SmartPtrMode::Weak);
    }
}

ในที่นี้ เมธอด data() ใน System::Collections::Generic::Dictionary จะส่งคืนการอ้างอิงไปยัง std::unordered_map ที่อยู่เบื้องหลังของคอนเทนเนอร์นี้

ข่าวที่เกี่ยวข้อง

วิดีโอที่เกี่ยวข้อง

บทความที่เกี่ยวข้อง