22 พฤศจิกายน 2567
การสร้างตัวแปลโค้ดที่มีประสิทธิภาพระหว่างภาษาเช่น C# และ C++ เป็นงานที่ซับซ้อน การพัฒนาเครื่องมือ CodePorting.Translator Cs2Cpp พบปัญหาหลายประการเนื่องจากความแตกต่างในไวยากรณ์ ความหมาย และแนวคิดการเขียนโปรแกรมของสองภาษานี้ บทความนี้จะกล่าวถึงความยากลำบากหลักที่เราพบและวิธีการที่เป็นไปได้ในการแก้ไข
ตัวอย่างเช่น using
และ yield
:
using (var resource = new Resource())
{
// การใช้ทรัพยากร
}
public IEnumerable<int> GetAllNumbers()
{
for (int i = 0; i < int.MaxValue; i++)
{
yield return i;
}
}
ในกรณีเช่นนี้ คุณต้องเขียนโค้ดที่ค่อนข้างซับซ้อนเพื่อเลียนแบบพฤติกรรมของโค้ดต้นฉบับทั้งในตัวแปลและในไลบรารี หรือในกรณีที่สองคือปฏิเสธการสนับสนุนโครงสร้างดังกล่าว
ตัวอย่างเช่น โค้ดต้นฉบับประกอบด้วยเมธอดเจนเนริกเสมือนหรือคอนสตรัคเตอร์ที่ใช้ฟังก์ชันเสมือน:
public class A
{
public virtual T GenericMethod<T>(T param)
{
return param;
}
}
public class A
{
public A()
{
VirtualMethod();
}
public virtual void VirtualMethod()
{
}
}
public class B : A
{
public override void VirtualMethod()
{
}
}
ในกรณีเช่นนี้ เราไม่มีทางเลือกอื่นนอกจากต้องเขียนโค้ดปัญหาดังกล่าวในแง่ที่สามารถแปลเป็น C# ได้ โชคดีที่กรณีเช่นนี้หายากและเกี่ยวข้องกับโค้ดขนาดเล็กเท่านั้น
ซึ่งรวมถึงทรัพยากร การสะท้อน การเชื่อมโยงแบบไดนามิกของแอสเซมบลี และการนำเข้าฟังก์ชัน:
static void Main()
{
var rm = new ResourceManager("MyApp.Resources", typeof(Program).Assembly);
var value = rm.GetString("MyResource");
}
static void Main()
{
var type = typeof(MyClass);
var method = type.GetMethod("MyMethod");
var result = method.Invoke(null, null);
Console.WriteLine(result);
}
public class MyClass
{
public static string MyMethod()
{
return "Hello, World!";
}
}
static void Main()
{
var assembly = Assembly.Load("MyDynamicAssembly");
var type = assembly.GetType("MyDynamicAssembly.MyClass");
var instance = Activator.CreateInstance(type);
var method = type.GetMethod("MyMethod");
method.Invoke(instance, null);
}
ในกรณีเช่นนี้ เราต้องเลียนแบบกลไกที่สอดคล้องกัน ซึ่งรวมถึงการสนับสนุนทรัพยากร (สร้างในแอสเซมบลีเป็นอาร์เรย์แบบสแตติกและอ่านผ่านการใช้งานสตรีมเฉพาะ) และการสะท้อน ที่ชัดเจนคือ เราไม่สามารถเชื่อมต่อ .NET แอสเซมบลีเข้ากับโค้ด C++ โดยตรงหรือเรียกฟังก์ชันจากไลบรารีไดนามิกของ Windows เมื่อทำงานบนแพลตฟอร์มอื่น ดังนั้นโค้ดดังกล่าวจึงต้องถูกตัดออกหรือเขียนใหม่
ในกรณีนี้ เราดำเนินการตามพฤติกรรมที่สอดคล้องกัน โดยปกติจะใช้การใช้งานจากไลบรารีของบุคคลที่สามที่ไม่ได้ห้ามการใช้ในผลิตภัณฑ์เชิงพาณิชย์
ในบางกรณี เหล่านี้เป็นข้อผิดพลาดในการใช้งานที่แก้ไขได้ง่าย ๆ แต่สิ่งที่แย่กว่านั้นคือตอนที่ความแตกต่างของพฤติกรรมอยู่ในระดับของระบบย่อยที่ใช้โดยโค้ดไลบรารี
ตัวอย่างเช่น ไลบรารีหลายแห่งของเราใช้คลาสจากไลบรารี System.Drawing
ที่สร้างขึ้นบน GDI+ อย่างกว้างขวาง เวอร์ชันของคลาสเหล่านี้ที่เราพัฒนาสำหรับ C++ ใช้ Skia เป็นเอนจินกราฟิก Skia มักจะมีพฤติกรรมแตกต่างจาก GDI+ โดยเฉพาะอย่างยิ่งบน Linux และเราต้องใช้ทรัพยากรอย่างมากเพื่อให้ได้ผลการแสดงผลเดียวกัน ในทำนองเดียวกัน libxml2 ซึ่งการใช้งาน System::Xml
ของเราใช้ ก็มีพฤติกรรมที่แตกต่างในกรณีอื่น ๆ และเราต้องแก้ไขหรือทำให้แรปเปอร์ซับซ้อนขึ้น
โปรแกรมเมอร์ C# จะปรับแต่งโค้ดให้เหมาะสมกับสภาวะที่โค้ดจะทำงาน อย่างไรก็ตาม โครงสร้างหลายอย่างจะเริ่มทำงานช้าลงในสภาพแวดล้อมที่ไม่คุ้นเคย
ตัวอย่างเช่น การสร้างวัตถุขนาดเล็กจำนวนมากใน C# โดยทั่วไปจะทำงานเร็วกว่าใน C++ เนื่องจากมีแผนการจัดการฮีปที่แตกต่างกัน (แม้จะพิจารณาการจัดเก็บขยะแล้วก็ตาม) การแคสติ้งไดนามิกใน C++ ก็ทำงานช้ากว่าเล็กน้อย การนับการอ้างอิงเมื่อคัดลอกพอยน์เตอร์ก็เป็นอีกแหล่งของโอเวอร์เฮดที่ไม่มีใน C# สุดท้าย การใช้แนวคิดที่แปลจาก C# (enumerators) แทนที่จะใช้ตัวที่สร้างขึ้นภายในและปรับแต่งอย่างเหมาะสมใน C++ (iterators) ก็ทำให้ประสิทธิภาพของโค้ดลดลงเช่นกัน
วิธีการกำจัดปัญหาคอขวดขึ้นอยู่กับสถานการณ์เป็นหลัก หากโค้ดไลบรารีสามารถปรับแต่งได้ค่อนข้างง่าย การรักษาพฤติกรรมของแนวคิดที่แปลแล้วในขณะที่ปรับแต่งประสิทธิภาพของมันในสภาพแวดล้อมที่ไม่คุ้นเคยนั้นอาจเป็นสิ่งที่ท้าทายอย่างยิ่ง
ตัวอย่างเช่น API สาธารณะอาจมีเมธอดที่รับ SharedPtr<Object>
คอนเทนเนอร์ไม่มี iterator และเมธอดที่จัดการกับสตรีมรับ System::IO::Stream
แทน istream
ostream
หรือ iostream
เป็นต้น
เราขยายตัวแปลและไลบรารีอย่างต่อเนื่องเพื่อทำให้โค้ดของเราสะดวกสำหรับโปรแกรมเมอร์ C++ ตัวอย่างเช่น ตัวแปลสามารถสร้างเมธอด begin
-end
และ overloads ที่ทำงานกับสตรีมมาตรฐานได้แล้ว
ไฟล์หัวของ C++ ประกอบด้วยชนิดและชื่อของฟิลด์ส่วนตัว รวมถึงโค้ดทั้งหมดของเมธอดแม่แบบ ข้อมูลนี้มักถูกทำให้คลุมเครือเมื่อปล่อยแอสเซมบลีของ .NET
เราพยายามที่จะยกเว้นข้อมูลที่ไม่จำเป็นโดยใช้เครื่องมือของบุคคลที่สามและโหมดพิเศษของตัวแปลเอง แต่ก็ไม่สามารถทำได้เสมอไป ตัวอย่างเช่น การลบฟิลด์สแตติกส่วนตัวและเมธอดที่ไม่ใช่เสมือนไม่ส่งผลต่อการทำงานของโค้ดลูกค้า อย่างไรก็ตาม การลบหรือเปลี่ยนชื่อเมธอดเสมือนไม่สามารถทำได้หากไม่สูญเสียฟังก์ชัน ฟิลด์สามารถเปลี่ยนชื่อได้ และชนิดของพวกมันสามารถถูกแทนที่ด้วย stubs ขนาดเดียวกัน ตราบใดที่คอนสตรัคเตอร์และดีสตรัคเตอร์ถูกเอ็กซ์พอร์ตจากโค้ดที่คอมไพล์ด้วยไฟล์หัวเต็ม ในขณะเดียวกันก็ไม่สามารถซ่อนโค้ดของเมธอดแม่แบบสาธารณะได้
การเปิดตัว ผลิตภัณฑ์ สำหรับภาษา C++ ที่สร้างโดยใช้เฟรมเวิร์กของเรา ได้รับการเปิดตัวอย่างประสบความสำเร็จมาหลายปีแล้ว ในตอนแรกเราเปิดตัวเวอร์ชั่นที่ลดขนาดลงของผลิตภัณฑ์ แต่ตอนนี้เราสามารถรักษาฟังก์ชันการทำงานที่สมบูรณ์ยิ่งขึ้นได้
ในขณะเดียวกัน ยังมีพื้นที่อีกมากสำหรับการปรับปรุงและการแก้ไข ซึ่งรวมถึงการสนับสนุนโครงสร้างไวยากรณ์และส่วนของไลบรารีที่เคยถูกละไว้ก่อนหน้านี้ รวมถึงการเพิ่มประสิทธิภาพในการใช้งานตัวแปล
นอกเหนือจากการแก้ไขปัญหาปัจจุบันและการปรับปรุงที่วางแผนไว้แล้ว เรายังทำงานในการย้ายตัวแปลไปยังตัววิเคราะห์ไวยากรณ์สมัยใหม่ Roslyn จนกระทั่งเมื่อไม่นานมานี้เราใช้ตัววิเคราะห์ NRefactory ซึ่งจำกัดการรองรับ C# เวอร์ชั่นจนถึง 5.0 การเปลี่ยนไปใช้ Roslyn จะช่วยให้เราสามารถรองรับโครงสร้างภาษา C# สมัยใหม่ได้ เช่น:
สุดท้าย เราวางแผนที่จะขยายจำนวนภาษาที่รองรับ ทั้งภาษาปลายทางและแหล่งที่มา การปรับโซลูชันที่ใช้ Roslyn เพื่ออ่านโค้ด VB จะค่อนข้างง่าย โดยเฉพาะอย่างยิ่งเมื่อพิจารณาว่ามีไลบรารีสำหรับ C++ และ Java พร้อมใช้งานแล้ว ในทางกลับกัน วิธีที่เราใช้เพื่อสนับสนุน Python ง่ายกว่ามาก และเช่นเดียวกัน ภาษาเขียนสคริปต์อื่น ๆ เช่น PHP ก็สามารถรองรับได้เช่นกัน