22 十一月 2024

C# 到 C++ 转换的挑战及我们计划如何改进代码转换器

在 C# 和 C++ 等语言之间创建有效的代码翻译器是一项复杂的任务。CodePorting.Translator Cs2Cpp 工具的开发由于这两种语言的语法、语义和编程范式的差异而遇到了许多问题。本文将讨论我们遇到的主要困难及其可能的解决方法。

代码翻译的问题及解决方法

  1. C# 语法在 C++ 中没有直接对应物。

例如,这涉及 usingyield 操作符:

using (var resource = new Resource())
{
    // 使用资源
}
public IEnumerable<int> GetAllNumbers()
{
    for (int i = 0; i < int.MaxValue; i++)
    {
        yield return i;
    }
}

在这种情况下,你要么必须编写相当复杂的代码来模拟源代码的行为(在翻译器和库中),要么在第二种情况下拒绝支持此类构造。

  1. C# 构造在我们接受的翻译规则下不能转译为 C++。

例如,源代码包含使用虚函数的虚泛型方法或构造函数:

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# 的术语重写这些问题代码。幸运的是,这种情况很少见,通常涉及小片段代码。

  1. C# 代码依赖于 .NET 特定的环境运行。

这包括资源、反射、动态程序集链接和函数导入:

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 动态库导入函数,因此必须削减或重写此类代码。

  1. 代码依赖于我们库中不支持的 .NET 类和方法。

在这种情况下,我们通常使用第三方库的实现来实现相应的行为,这些库的许可证不禁止在商业产品中使用。

  1. 库代码的行为与原始 .NET 类不同。

在某些情况下,这涉及简单的实现错误,通常很容易修复。然而,更糟糕的是,当行为差异在于库代码使用的子系统级别时。

例如,我们的许多库广泛使用基于 GDI+ 构建的 System.Drawing 库的类。我们为 C++ 开发的这些类的版本使用 Skia 作为图形引擎。Skia 的行为往往与 GDI+ 不同,尤其是在 Linux 上,为实现一致的渲染效果,我们必须投入大量资源。同样,我们的 System::Xml 实现所基于的 libxml2,在某些情况下表现也不同,我们必须对其进行修补或复杂化我们的包装器。

  1. 翻译后的代码有时比原始代码运行得更慢。

C# 程序员会针对代码执行的条件对代码进行优化。然而,许多结构在不熟悉的环境中开始运行得更慢。

例如,由于不同的堆管理方案(即使考虑到垃圾回收),在 C# 中创建大量的小对象通常比在 C++ 中快。C++ 中的动态类型转换也稍慢一些。复制指针时的引用计数是 C# 中不存在的另一个开销源。最后,使用从 C# 翻译的概念(如枚举器)而不是内置的、优化的 C++ 概念(如迭代器)也会降低代码性能。

消除瓶颈的方法在很大程度上取决于具体情况。如果库代码可以相对容易地优化,那么在不熟悉的环境中保留翻译概念的行为并优化其性能可能是相当具有挑战性的。

  1. 翻译后的代码不符合 C++ 的精神。

例如,公共 API 可能具有接受 SharedPtr<Object> 的方法,容器缺少迭代器,流处理方法接受 System::IO::Stream 而不是 istreamostreamiostream 等等。

我们不断扩展翻译器和库,使我们的代码对 C++ 程序员来说更加方便。例如,翻译器已经可以生成 begin-end 方法和适用于标准流的重载。

  1. 翻译后的代码暴露了我们的算法。

C++ 头文件包含私有字段的类型和名称,以及模板方法的完整代码。发布 .NET 程序集时通常会对这些信息进行混淆。

我们努力使用第三方工具和翻译器本身的特殊模式来排除不必要的信息,但这并不总是可能的。例如,删除私有静态字段和非虚方法不会影响客户端代码的操作;但是,不删除或重命名虚方法而不丧失功能是不可能的。字段可以重命名,其类型可以用相同大小的存根替换,前提是构造函数和析构函数从用完整头文件编译的代码中导出。同时,不可能隐藏公共模板方法的代码。

项目开发计划

多年来,使用我们的框架创建的 C++ 语言产品发布已经成功推出。最初,我们发布了精简版本的产品,但现在我们能够保持更为全面的功能。

同时,仍有大量改进和修正的空间。这包括支持以前省略的语法结构和库部分,以及增强翻译器的易用性。

除了解决当前问题和计划中的改进外,我们还致力于将翻译器迁移到现代 Roslyn 语法分析器。直到最近,我们一直使用 NRefactory 分析器,该分析器仅支持到 C# 5.0 版本。转向 Roslyn 将使我们能够支持现代的 C# 语言结构,例如:

  • 模式匹配
  • 表达式主体成员
  • 可空引用类型
  • 以及更多

最后,我们计划扩展支持的语言数量,包括目标语言和源语言。适应基于 Roslyn 的解决方案以读取 VB 代码将相对容易,特别是考虑到用于 C++ 和 Java 的库已经准备就绪。另一方面,我们用于支持 Python 的方法要简单得多,类似地,还可以支持其他脚本语言如 PHP。

相关新闻

相关视频

相关文章