27 3월 2025
초기부터 이 작업은 최대 수백만 줄의 코드를 포함하는 여러 프로젝트를 포팅하는 것을 포함했습니다. 본질적으로 번역기에 대한 기술 사양은 "이 모든 것이 C에서 올바르게 포팅되고 실행되도록 보장하라"는 문구로 요약되었습니다. C 제품 출시 담당자의 작업에는 코드 번역, 테스트 실행, 릴리스 패키지 준비 등이 포함됩니다. 일반적으로 발생하는 문제는 여러 범주 중 하나로 분류됩니다:
이 목록의 진행은 위에서 아래로 이루어집니다. 예를 들어, 번역된 코드의 컴파일 문제를 해결하지 않고는 기능과 성능을 검증할 수 없습니다. 결과적으로, CodePorting.Translator Cs2Cpp 프로젝트 작업의 후반 단계에서야 오랫동안 존재했던 많은 문제가 발견되었습니다.
처음에는 객체 간의 순환 종속성으로 인해 발생하는 간단한 메모리 누수를 수정할 때 필드에 CppWeakPtr
애트리뷰트를 적용하여 WeakPtr
타입의 필드를 만들었습니다. WeakPtr
가 lock()
메서드를 호출하거나 암시적으로(구문상 더 편리함) SharedPtr
로 변환될 수 있는 한, 이는 문제를 일으키지 않았습니다. 그러나 나중에는 CppWeakPtr
애트리뷰트에 대한 특수 구문을 사용하여 컨테이너 내에 포함된 참조도 약하게 만들어야 했고, 이로 인해 몇 가지 불쾌한 문제가 발생했습니다.
우리가 채택한 접근 방식의 첫 번째 문제 징후는 C++ 관점에서 MyContainer<SharedPtr<MyClass>>
와 MyContainer<WeakPtr<MyClass>>
가 두 개의 다른 타입이라는 것이었습니다. 결과적으로 이들은 동일한 변수에 저장하거나, 동일한 메서드에 전달하거나, 반환하는 등의 작업을 할 수 없습니다. 원래 객체 필드에 참조를 저장하는 방법을 관리하기 위한 목적으로만 사용되었던 애트리뷰트가 반환 값, 인수, 지역 변수 등에 영향을 미치며 점점 더 이상한 컨텍스트에 나타나기 시작했습니다. 이를 처리하는 번역기 코드는 날이 갈수록 복잡해졌습니다.
두 번째 문제 역시 예상하지 못했던 것이었습니다. C# 프로그래머에게는 객체당 하나의 연관 컬렉션을 가지는 것이 자연스러운 것으로 판명되었습니다. 이 컬렉션에는 현재 객체가 소유하고 다른 방법으로는 접근할 수 없는 객체에 대한 고유 참조와 부모 객체에 대한 참조가 모두 포함될 수 있었습니다. 이는 특정 파일 형식에서의 읽기 작업을 최적화하기 위해 수행되었지만, 우리에게는 동일한 컬렉션에 강한 참조와 약한 참조가 모두 포함될 수 있음을 의미했습니다. 포인터 타입은 더 이상 작동 모드의 최종 결정 요인이 아니게 되었습니다.
분명히 이 두 가지 문제는 기존 패러다임 내에서 해결할 수 없었고, 포인터 타입이 재고되었습니다. 이 수정된 접근 방식의 결과는 SmartPtr
클래스였으며, 이 클래스는 SmartPtrMode::Shared
와 SmartPtrMode::Weak
두 값 중 하나를 받는 set_Mode()
메서드를 특징으로 합니다. 모든 SmartPtr
생성자는 동일한 값을 받습니다. 결과적으로 각 포인터 인스턴스는 다음 두 상태 중 하나로 존재할 수 있습니다:
모드 간 전환은 런타임 중 언제든지 발생할 수 있습니다. 약한 참조 카운터는 객체에 대한 약한 참조가 하나 이상 존재할 때까지 생성되지 않습니다.
우리 포인터가 지원하는 전체 기능 목록은 다음과 같습니다:
intrusive_ptr
시맨틱: 동일한 객체에 대해 생성된 임의 개수의 포인터가 단일 참조 카운터를 공유합니다.->
): 가리키는 객체에 접근하기 위함.SmartPtr
클래스는 템플릿화되어 있으며 가상 메서드를 포함하지 않습니다. 참조 카운터 저장을 처리하는 System::Object
클래스와 밀접하게 결합되어 있으며, 해당 파생 클래스에서만 작동합니다.
일반적인 포인터 동작과의 차이점은 다음과 같습니다:
이전 코드와의 호환성을 유지하기 위해 SharedPtr
타입은 SmartPtr
의 별칭이 되었습니다. WeakPtr
클래스는 이제 SmartPtr
를 상속하며, 필드를 추가하지 않고 생성자를 재정의하여 항상 약한 참조를 생성하도록 합니다.
컨테이너는 이제 항상 MyContainer<SmartPtr<MyClass>>
시맨틱으로 포팅되며, 저장된 참조의 타입은 런타임에 선택됩니다. STL 데이터 구조를 기반으로 수동으로 작성된 컨테이너(주로 System
네임스페이스의 컨테이너)의 경우, 기본 참조 타입은 사용자 지정 할당자를 사용하여 설정되지만, 여전히 개별 컨테이너 요소에 대해 모드를 변경할 수 있습니다. 번역된 컨테이너의 경우, 참조 저장 모드를 전환하는 데 필요한 코드가 번역기에 의해 생성됩니다.
이 솔루션의 단점은 주로 포인터 생성, 복사 및 삭제 작업 중 성능 저하를 포함합니다. 이는 일반적인 참조 카운팅에 참조 타입 확인이 필수로 추가되기 때문입니다. 구체적인 수치는 테스트 구조에 크게 의존합니다. 현재 포인터 타입이 변경되지 않음이 보장되는 곳에서 더 최적화된 코드를 생성하는 것에 대한 논의가 진행 중입니다.
우리의 포팅 방법은 참조가 약해야 하는 위치를 표시하기 위해 소스 C# 코드에 수동으로 애트리뷰트를 배치해야 합니다. 이러한 애트리뷰트가 올바르게 배치되지 않은 코드는 번역 후 메모리 누수 및 경우에 따라 다른 오류를 유발합니다. 애트리뷰트가 있는 코드는 다음과 같습니다:
struct S {
MyClass s; // 객체에 대한 강한 참조
[CppWeakPtr]
MyClass w; // 객체에 대한 약한 참조
MyContainer<MyClass> s_s; // 강한 참조 컨테이너에 대한 강한 참조
[CppWeakPtr]
MyContainer<MyClass> w_s; // 강한 참조 컨테이너에 대한 약한 참조
[CppWeakPtr(0)]
MyContainer<MyClass> s_w; // 약한 참조 컨테이너에 대한 강한 참조
[CppWeakPtr(1)]
Dictionary<MyClass, MyClass> s_s_w; // 키는 강한 참조로, 값은 약한 참조로 저장되는 컨테이너에 대한 강한 참조
[CppWeakPtr, CppWeakPtr(0)]
Dictionary<MyClass, MyClass> w_w_s; // 키는 약한 참조로, 값은 강한 참조로 저장되는 컨테이너에 대한 약한 참조
}
경우에 따라 SmartPtr
클래스의 에일리어싱 생성자나 저장된 참조 타입을 설정하는 메서드를 수동으로 호출해야 합니다. 우리는 포팅된 코드를 편집하는 것을 피하려고 노력합니다. 이러한 변경 사항은 번역기 실행 후 매번 다시 적용해야 하기 때문입니다. 대신, 이러한 코드를 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);
}
};
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);
}
}
여기서 System::Collections::Generic::Dictionary
의 data()
메서드는 이 컨테이너의 기본 std::unordered_map
에 대한 참조를 반환합니다. ```