28 12월 2024
우리의 프레임워크 CodePorting.Translator Cs2Cpp는 .NET 플랫폼용으로 개발된 라이브러리를 C++로 배포할 수 있게 합니다. 이 기사에서는 두 언어의 메모리 모델을 어떻게 조정했는지와 비관리 환경에서 번역된 코드가 제대로 작동하도록 보장한 방법에 대해 논의할 것입니다.
우리가 사용하는 스마트 포인터와 그것들을 위해 왜 독자적인 구현을 개발해야 했는지에 대해 배울 것입니다. 또한, 객체 수명 관리 관점에서 C# 코드를 포팅하기 위한 준비 과정, 우리가 직면한 일부 도전과제, 그리고 작업에서 사용해야 하는 특정 진단 방법에 대해 다룰 것입니다.
C# 코드는 관리되는 환경에서 가비지 컬렉션과 함께 실행됩니다. 번역의 목적을 위해, 이는 주로 C# 프로그래머가 C++ 개발자와 달리 사용되지 않는 힙 메모리를 시스템에 반환할 필요가 없음을 의미합니다. 이는 가비지 컬렉터(GC)에 의해 수행됩니다. GC는 프로그램에서 아직 사용 중인 객체를 주기적으로 확인하고 더 이상 활성 참조가 없는 객체를 정리하는 CLR 환경의 구성 요소입니다.
활성 참조는 다음과 같은 경우로 정의됩니다:
가비지 컬렉터 알고리즘은 일반적으로 스택과 정적 필드에 위치한 참조에서 시작하여 객체 그래프를 순회하고 이전 단계에서 도달하지 않은 모든 것을 삭제하는 것으로 설명됩니다. GC 작업 중 참조 그래프가 무효화되는 것을 방지하기 위해, 정지-세계(stop-the-world) 메커니즘이 구현됩니다. 이 설명은 간단할 수 있지만, 우리의 목적에는 충분합니다.
스마트 포인터와 달리, 가비지 컬렉션 접근 방식은 교차 또는 순환 참조 문제에서 자유롭습니다. 두 객체가 서로를 참조하는 경우, 여러 중간 객체를 통해서도, GC는 고립된 섬으로 알려진 그룹 전체가 더 이상 활성 참조를 가지지 않을 때 이를 삭제하는 것을 막지 않습니다. 따라서 C# 프로그래머는 객체를 언제든지 어떤 조합으로도 연결하는 것에 대한 편견이 없습니다.
C#의 데이터 유형은 참조형과 값형으로 나뉩니다. 값형의 인스턴스는 항상 스택, 정적 메모리, 또는 구조체 및 객체의 필드에 직접 위치합니다. 반면, 참조형의 인스턴스는 항상 힙에 생성되며, 스택, 정적 메모리 및 필드에는 참조(주소)만 저장됩니다. 값형에는 구조체, 기본 산술 값 및 참조가 포함됩니다. 참조형에는 클래스와 역사적으로 대리자가 포함됩니다. 이 규칙의 유일한 예외는 박싱(boxing)의 경우로, 구조체나 다른 값형이 특정 컨텍스트에서 사용되기 위해 힙에 복사되는 경우입니다.
객체 소멸의 순간은 정의되지 않았으며, 언어는 마지막 활성 참조가 제거되기 전에 발생하지 않는다는 것만 보장합니다. 변환된 코드에서 객체가 마지막 활성 참조가 제거된 직후에 소멸되는 경우, 이는 프로그램의 작동을 방해하지 않습니다.
다른 중요한 점은 C#의 제네릭 타입 및 메서드 지원과 관련이 있습니다. C#은 제네릭을 한 번 작성한 후 참조형 및 값형 매개변수 모두와 함께 사용할 수 있습니다. 나중에 보여줄 것처럼, 이 점은 중요함을 입증합니다.
C# 타입을 C++ 타입으로 매핑하는 방법에 대해 간단히 설명하겠습니다. CodePorting.Translator Cs2Cpp 프레임워크는 애플리케이션이 아닌 라이브러리를 포팅하기 위한 것이므로, 중요한 요구 사항 중 하나는 원래 .NET 프로젝트의 API를 가능한 한 정확하게 재현하는 것입니다. 따라서, 우리는 C# 클래스, 인터페이스 및 구조체를 대응되는 기본 타입을 상속하는 C++ 클래스로 변환합니다.
예를 들어, 다음 코드를 고려해보세요:
interface I1 {}
interface I2 {}
interface I3 : I2 {}
class A {}
class B : A, I1 {}
class C : B, I2 {}
class D : C, I3 {}
class Generic<T> { public T value; }
struct S {}
이는 다음과 같이 번역됩니다:
class I1 : public virtual System::Object {};
class I2 : public virtual System::Object {};
class I3 : public virtual I2 {};
class A : public virtual System::Object {};
class B : public A, public virtual I1 {};
class C : public B, public virtual I2 {};
class D : public C, public virtual I3 {};
template <typename T> class Generic { public: T value; };
class S : public System::Object {};
System::Object
클래스는 변환된 프로젝트 외부에 있는 번역 지원 라이브러리에서 선언된 시스템 클래스입니다. 클래스와 인터페이스는 다이아몬드 문제를 피하기 위해 가상으로 상속됩니다. 변환된 C++ 코드의 구조체는 System::Object
를 상속하며, C#에서는 이를 System.ValueType
을 통해 상속하지만, 최적화를 위해 이 추가 상속은 제거됩니다. 제네릭 타입과 메서드는 각각 템플릿 클래스와 메서드로 변환됩니다.
번역된 타입의 인스턴스는 C#에서 제공된 것과 동일한 보증을 따라야 합니다. 클래스 인스턴스는 힙에서 생성되어야 하며, 그 수명은 활성 참조의 수명에 의해 결정되어야 합니다. 구조체 인스턴스는 박싱의 경우를 제외하고는 스택에서 생성되어야 합니다. 델리게이트는 특수하고 비교적 간단한 경우로, 이 기사에서는 다루지 않습니다.
우리는 C#의 메모리 관리 모델이 C++로의 코드 변환 프로세스에 어떻게 영향을 미치는지 고려했습니다. 다음 기사에서는 힙에 할당된 객체의 수명을 관리하는 방법에 대한 결정 과정과 이 접근 방식을 어떻게 구현했는지에 대해 논의할 것입니다.