22 11월 2024
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 동적 라이브러리에서 함수를 가져오는 것은 불가능하므로, 이러한 코드는 잘라내거나 다시 작성해야 합니다.
이 경우, 상용 제품에서 사용을 금지하지 않는 제3자 라이브러리의 구현을 사용하여 해당 동작을 구현합니다.
어떤 경우에는, 이는 일반적으로 수정하기 쉬운 간단한 구현 오류입니다. 더 나쁜 것은 라이브러리 코드에서 사용되는 하위 시스템 수준에서 동작 차이가 발생할 때입니다.
예를 들어, 많은 라이브러리가 GDI+를 기반으로 구축된 System.Drawing
라이브러리의 클래스를 광범위하게 사용합니다. C++용으로 개발한 이러한 클래스의 버전은 그래픽 엔진으로 Skia를 사용합니다. Skia의 동작은 특히 Linux에서 GDI+와 다르며, 일관된 렌더링을 달성하기 위해 상당한 자원을 소비해야 합니다. 마찬가지로, libxml2를 기반으로 한 System::Xml
의 구현도 다른 경우에 다르게 동작하며, 이를 패치하거나 래퍼를 복잡하게 만들어야 합니다.
C# 프로그래머들은 코드가 실행되는 조건에 맞게 최적화합니다. 그러나 많은 구조가 익숙하지 않은 환경에서 실행 속도가 느려지기 시작합니다.
예를 들어, C#에서 많은 수의 작은 객체를 생성하는 것은 일반적으로 다른 힙 관리 방식(가비지 컬렉션을 고려하더라도) 때문에 C보다 더 빠르게 작동합니다. C에서의 동적 타입 캐스팅도 약간 느립니다. 포인터를 복사할 때 참조 카운팅은 C#에서는 없는 또 다른 오버헤드 소스입니다. 마지막으로, C#에서 번역된 개념(열거자)을 사용하는 것은 내장된 최적화된 C++ 개념(반복자) 대신에 코드 성능을 저하시킵니다.
병목 현상을 제거하는 방법은 상황에 따라 크게 다릅니다. 라이브러리 코드를 비교적 쉽게 최적화할 수 있다면, 익숙하지 않은 환경에서 번역된 개념의 동작을 유지하면서 그 성능을 최적화하는 것은 상당히 도전적입니다.
예를 들어, 공개 API에 SharedPtr<Object>
을 수락하는 메서드가 있거나, 컨테이너에 반복자가 없으며, 스트림 처리 메서드가 istream
, ostream
, 또는 iostream
대신에 System::IO::Stream
을 수락하는 등의 예시가 있습니다.
우리는 C++ 프로그래머들이 코드를 편리하게 사용할 수 있도록 번역기와 라이브러리를 지속적으로 확장하고 있습니다. 예를 들어, 번역기는 이미 표준 스트림과 작동하는 begin
-end
메서드와 오버로드를 생성할 수 있습니다.
C++ 헤더 파일에는 프라이빗 필드의 유형 및 이름뿐만 아니라 템플릿 메서드의 전체 코드가 포함되어 있습니다. 이 정보는 일반적으로 .NET 어셈블리를 릴리스할 때 난독화됩니다.
불필요한 정보를 배제하기 위해 타사 도구와 번역기 자체의 특수 모드를 사용하려고 노력하지만, 항상 가능한 것은 아닙니다. 예를 들어, 프라이빗 정적 필드 및 비가상 메서드를 제거해도 클라이언트 코드 실행에는 영향을 미치지 않습니다. 그러나 가상 메서드를 제거하거나 이름을 변경하는 것은 기능을 잃지 않고는 불가능합니다. 필드는 이름을 변경할 수 있으며, 그 유형은 동일한 크기의 스텁으로 대체할 수 있습니다. 완전한 헤더 파일로 컴파일된 코드에서 생성자와 소멸자를 내보내는 경우입니다. 동시에, 공개 템플릿 메서드의 코드를 숨기는 것은 불가능합니다.
우리의 프레임워크를 사용하여 생성된 C++ 언어용 제품의 릴리스는 수년간 성공적으로 출시되었습니다. 초기에는 축소된 버전의 제품을 출시했지만, 이제는 훨씬 더 완전한 기능을 유지할 수 있게 되었습니다.
동시에, 개선 및 수정의 여지는 여전히 많이 남아 있습니다. 여기에는 이전에 생략된 구문 구조와 라이브러리 부분을 지원하는 것뿐만 아니라 번역기의 사용 용이성을 향상시키는 것이 포함됩니다.
현재 문제 해결 및 계획된 개선 사항 외에도, 번역기를 최신 Roslyn 구문 분석기로 마이그레이션하는 작업을 진행하고 있습니다. 최근까지 우리는 NRefactory 분석기를 사용했으며, 이는 C# 버전 5.0까지 지원에 제한되었습니다. Roslyn으로의 전환을 통해, 다음과 같은 최신 C# 언어 구조를 지원할 수 있습니다:
마지막으로, 지원하는 언어의 수를 확장할 계획입니다. 이는 대상 언어와 소스 언어 모두를 포함합니다. Roslyn 기반 솔루션을 사용하여 VB 코드를 읽는 데 적응하는 것은 상대적으로 쉬울 것입니다. 특히 C++ 및 Java 용 라이브러리가 이미 준비되어 있는 점을 고려하면 더욱 그렇습니다. 한편, Python 지원에 사용한 접근 방식은 훨씬 간단하며, 유사하게 PHP와 같은 다른 스크립트 언어도 지원할 수 있습니다.