28 十二月 2024
我们的框架 CodePorting.Translator Cs2Cpp 使得开发的 .NET 平台库能够在 C++ 中发布。在本文中,我们将讨论如何调和这两种语言的内存模型,并确保翻译代码在非托管环境中的正确运行。
您将了解我们使用的智能指针以及为什么我们必须为它们开发自己的实现。此外,我们还将介绍从对象生命周期管理的角度准备 C# 代码进行移植的过程,我们遇到的一些挑战以及我们在工作中必须使用的特定诊断方法。
C# 代码在具有垃圾回收的托管环境中执行。对于翻译的目的,这主要意味着 C# 程序员,不同于 C++ 开发人员,免于需要将不再使用的已分配的堆内存归还系统。这由垃圾回收器(GC)完成,GC 是 CLR 环境的一个组件,它定期检查程序中哪些对象仍在使用,并清理那些不再有活动引用的对象。
活动引用被认为是:
垃圾回收器算法通常描述为遍历对象图,从堆栈和静态字段中的引用开始,然后删除前一个阶段未达到的所有内容。为防止在 GC 操作期间使引用图无效,实施了停止世界机制。这种描述可能是简化的,但对于我们的目的已经足够。
与智能指针不同,垃圾回收方法没有交叉引用或循环引用的问题。如果两个对象相互引用,可能通过一些中间对象,这不会阻止 GC 在整个组(称为隔离岛)不再有活动引用时删除它们。因此,C# 程序员在任何时候以任何组合方式链接对象没有任何偏见。
C# 中的数据类型分为引用类型和值类型。值类型的实例始终位于堆栈、静态内存或结构和对象的字段中。而引用类型的实例总是创建在堆中,而堆栈、静态内存和字段中只存储它们的引用(地址)。值类型包括结构、基本算术值和引用。引用类型包括类和历史上的委托。这一规则的唯一例外是装箱的情况,即结构或其他值类型被复制到堆中以在特定上下文中使用。
对象销毁的时刻没有定义——语言仅保证它不会在删除最后一个活动引用之前发生。如果在转换后的代码中对象在删除最后一个活动引用后立即销毁,这不会干扰程序的运行。
另一个重要点是与 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++ 的过程。在文章的下一部分中,我们将讨论如何决定堆分配对象的生命周期管理方法以及如何实现这种方法。