我们如何自动将产品从C#移植到C ++

哈Ha 在本文中,我将讨论我们如何组织C ++语言的库的每月发行,C ++的源代码是用C#开发的。这与托管C ++无关,甚至与在非托管C ++和CLR环境之间建立桥梁无关,而是与自动化C ++代码生成并重复原始C#代码的API和功能有关。

我们编写了必要的基础架构,用于在语言之间翻译代码并自己模拟.Net库的功能,从而解决了通常被认为是学术性的问题。这使我们也可以开始每月发布C ++语言的Donets之前版本的产品,并从相应版本的C#代码获取每个版本的代码。同时,涵盖原始代码的测试将与之一同移植,并允许您控制最终解决方案的性能以及用C ++编写的特殊测试。

在本文中,我将简要描述我们项目的历史以及所使用的技术。我只会一味地谈到经济合理性问题,因为技术方面对我来说更加有趣。在本系列的以下文章中,如果社区有相关问题,我计划将重点讨论代码生成和内存管理等主题,以及其他主题。

背景


最初,我们公司从事.Net平台库的发布。这些库主要提供用于处理某些文件格式(文档,表格,幻灯片,图形)和协议(电子邮件)的API,在此类解决方案的市场中占有一定的位置。所有开发都在C#中进行。

在2000年代末,该公司决定为自己进入一个新市场,开始发布类似的Java产品。从头开始开发显然需要投入与所有受影响产品的初始开发相当的资源投资。由于某些原因,也拒绝了将Donnet代码包装到将调用和数据从Java转换为.Net以及反之亦然的层的选项。相反,提出了一个问题,即是否有可能以任何方式将现有代码完全迁移到新平台。这更有意义,因为它不是一次性促销,而是每个产品的新版本的每月发行,在两种语言之间同步。

决定将决定分为两部分。第一种-所谓的Porter-将C#源代码语法转换为Java,同时用Java库中的.Net类型和方法替换.Net类型和方法。第二个库(库)将模仿.Net库中那些很难或不可能与Java建立直接对应关系的部分的工作,为此吸引了可用的第三方组件。

为了支持这一计划的主要可行性,以下发言:

  1. 从思想上讲,C#和Java语言非常相似-至少与类型结构和带有内存的工作组织有关;
  2. 这是关于移植库的;不需要移植GUI;
  3. , , - , System.Net System.Drawing;
  4. , .Net ( Framework, Standard Xamarin), .

我不再赘述,因为它们值得单独发表(而不是一篇)。我只能说,从开始开发到发布第一个Java产品花了大约两年的时间,从那时起,发布Java产品已成为公司的常规做法。在项目的开发过程中,搬运工已经从一个简单的实用程序转变为根据源代码的AST表示形式工作的复杂代码生成器,该简单的实用程序可以根据既定规则转换文本。该库也充满了代码。

Java方向的成功决定了该公司希望自己进一步扩展到新市场的渴望,并在2013年提出了在类似情况下发布C ++语言产品的问题。

问题的提法


为了确保发布正式版本的产品,有必要创建一个框架,该框架将允许您从任意C#代码中获取C ++代码,对其进行编译,检查并将其提供给客户端。它涉及的库容量从几十万行到几百万行(不包括依赖项)。

同时,考虑了Java搬运程序的使用经验:最初,当它只是转换语法的简单工具时,自然会出现手动完成移植代码的实践。在短期内,专注于产品的快速发布是很重要的,因为它可以加快开发过程,但是从长远来看,由于每次翻译时都需要纠正每个翻译错误,这大大增加了准备发布每个版本的成本。

当然,这种复杂性是可以控制的-至少通过仅将补丁传输到所得的Java代码中即可,这些补丁的计算方式为下两个C#代码修订版的porter输出之间的差。这种方法使每条移植线仅能校正一次,将来可以使用已经开发的代码进行任何修改。但是,在开发可靠的porter时,目标是摆脱固定移植代码的阶段,而不是固定框架本身。因此,每个任意罕见的翻译错误都将被纠正一次-在搬运工代码中,并且此修补程序将应用于所有已移植产品的所有将来发行版。

除了搬运工本身,还需要用C ++开发一个库,该库可以解决以下问题:

  1. 在需要移植的代码正常工作的范围内模拟.Net环境;
  2. 使移植的C#代码适应C ++的实际情况(类型结构,内存管理,其他服务代码);
  3. 平滑“重写的C#”和C ++本身之间的差异,以使不熟悉.Net范例的程序员更容易使用移植的代码。

出于明显的原因,没有尝试将.Net类型直接映射到标准库中的类型。取而代之的是,决定始终使用他库中的类型来替代Donnet类型。

许多读者会立即问他们为什么不使用Mono这样的现有实现这是有原因的。

  1. 通过吸引这样一个完成的库,将可能仅满足第一个要求,而不能满足第二个要求,而不能满足第三个要求。
  2. Mono C# , , , .
  3. (API, , , C++, ) , .
  4. , .Net, . , , .

从理论上讲,这样的库可以完全使用端口转换为C ++,但是,这在开发的一开始就需要一个功能齐全的porter,因为如果没有系统库,原则上不可能调试任何已移植的代码。此外,由于对系统库的调用往往会成为瓶颈,因此优化系统库的已翻译代码的问题甚至比移植产品的代码更为尖锐。

结果,决定将库开发为一组适配器,这些适配器提供对第三方库中已经实现的功能的访问,但可以通过类似于.Net的API(类似于Java)进行。这将减少工作量,并使用现成的,已经优化的C ++组件。

该框架的一项重要要求是,移植的代码必须能够作为用户应用程序的一部分工作(就库而言)。这意味着内存管理模型应该已经对C ++程序员明确了,因为我们不能强制任意客户端代码在垃圾回收环境中运行。选择使用智能指针作为折衷模型。关于我们如何确保这种过渡(特别是解决循环引用的问题),我将在另一篇文章中进行讨论。

另一个要求是不仅可以移植库,还可以对其进行测试。该公司具有很高的产品测试覆盖率文化,并且能够在C ++中运行与原始代码相同的测试,从而极大地简化了翻译后的问题查找。

其余要求(启动格式,测试范围,技术等)主要涉及与项目一起工作以及在项目上进行工作的方法。我不会细说。

故事


在继续之前,我必须先谈谈公司的结构。该公司是远程工作的,它的所有团队都是分布式的。某种产品的开发通常是团队的责任,由语言(几乎总是)和地理位置(主要是)团结在一起。

该项目的积极工作始于2013年秋天。由于公司的分布式结构,并且由于对开发的成功存在一些疑问,因此立即启动了三个版本的框架:其中两个版本分别为一个产品提供服务,第三个版本同时包含三个产品。据认为,这将停止开发效率较低的解决方案,并在必要时重新分配资源。

将来,将有四个团队加入“通用”框架的工作,其中两个后来重新考虑了他们的决定,并拒绝发布C ++产品。 2017年初,公司决定停止开发其中一种“个人”解决方案,并让相应的团队转而使用“通用”框架。停止的开发假设使用Boehm GC作为内存管理手段,并且包含了系统库某些部分的丰富实现,然后将其转移到“通用”解决方案中。

因此,最终发展有两个进展-即移植产品的发布-一个“个人”和一个“集体”。基于我们(“通用”)框架的第一版发行于2018年2月。随后,使用该解决方案的所有六个团队的发布成为每月一次,并且框架本身作为公司的单独产品发布。甚至有人提出将其开源的问题,但这种讨论尚未发展。

该团队继续在类似的框架上独立工作,该团队还在2018年发布了其首个C ++版本。

第一个发行版包含原始产品的截短版本,从而可以最大程度地延迟广播不重要的部分的工作。在随后的发行版中,已经(并且正在)部分地添加功能。

组织项目工作


由几个团队组成的项目联合工作组织进行了重大更改。最初,决定由一个大型的“中心”团队负责开发,支持和修复框架,而参与发布C ++最终产品的小型“产品”团队则主要负责尝试移植他们的产品。代码并提供反馈(有关移植,编译和执行错误的信息)。但是,由于中央团队因所有“产品”团队的要求而超负荷工作,并且最终解决了遇到的问题后,他们无法继续前进,因此这种方案最终没有效果。

由于很大程度上与特定开发状态无关的原因,因此决定解散“中心”团队,并将人员转移到“产品”团队,这些团队现在负责根据其需求确定框架。在这种情况下,每个团队自己都会决定是使用其共同的基础,还是生成自己的项目分支。这样的问题说明与Java框架有关,当时Java框架的代码是稳定的,但是需要进行合并以尽快填充C ++库,以便团队仍可以一起工作。

这种形式的工作也有其弊端,因此将来会进行另一项改革。恢复了“中央”团队,虽然组成较小,但职能不同:现在,它不负责项目的实际开发,而是负责组织联合工作。这包括对CI环境的支持,组织合并请求实践,与开发参与者定期举行会议,支持文档,涵盖测试,提供体系结构解决方案和故障排除帮助等。此外,该小组还开展了消除技术债务和其他资源密集型领域的工作。在这种模式下,发展一直持续到今天。

因此,该项目是由几名(约五名)开发人员的努力发起的,在最好的时候,人数约为二十人。近年来,大约有十到十五个人负责框架的开发和支持以及六种移植产品的发布。

这些专栏的作者于2016年中期加入公司,开始在一个使用“通用”解决方案广播其代码的团队中工作。同年冬天,当决定重新组建“中心”团队时,我搬到了她的团队负责人的位置。因此,我今天在该项目中的经验已超过三年半。

负责移植产品发布的团队的自治导致了这样一个事实,即在某些情况下,开发人员为移植程序添加操作模式要比在默认情况下妥协表现更容易。这说明了配置搬运程序时可用的选项数量,超出了您的预期。

技术领域


现在该讨论项目中使用的技术了。波特是一个用C#编写的控制台应用程序,因为以这种形式,它更容易嵌入到执行诸如“端口编译运行测试”之类的任务的脚本中。此外,还有一个GUI组件,使您可以通过单击按钮来实现相同的目标。

古老的NRefactory库负责解析代码和解析语义。不幸的是,在项目开始时,罗斯林还没有可用,尽管迁移到我们的计划中当然已经在计划中了。

搬运工使用AST木制人行道收集信息并生成C ++输出代码。生成C ++代码时,不会创建AST表示形式,并且所有代码都另存为纯文本。

在许多情况下,搬运工需要其他信息来进行微调。这些信息以选项和属性的形式发送给他。这些选项将立即应用于整个项目,并允许您设置例如代码分析中使用的类或C#预处理程序定义的导出宏成员的名称。属性挂在类型和实体上,并确定特定于它们的处理(例如,需要为类成员生成关键字“ const”或“ mutable”或将其排除在移植之外)。

C#类和结构被翻译成C ++类,它们的成员和可执行代码被翻译成最接近的等价物。通用类型和方法映射到C ++模板。 C#链接被转换为库中定义的智能指针(强或弱)。有关搬运工原理的更多详细信息将在另一篇文章中进行讨论。

因此,原始的C#程序集将转换为C ++项目,而不是.Net库取决于我们的共享库。如下图所示:



cmake用于构建库和移植的项目。当前支持VS 2017和2019(Windows),GCC和Clang(Linux)编译器。

如上所述,我们的大多数.Net实现都是第三方库的薄层,可完成大量工作。这包括:

  • Skia-用于处理图形;
  • Botan-支持加密功能;
  • ICU-用于处理字符串,编码和区域性;
  • libxml2-用于XML
  • PCRE2-用于正则表达式;
  • zlib-实现压缩功能;
  • 增强 -用于各种目的;
  • 其他几个库。

搬运工和库都经过大量测试。库测试使用gtest框架。波特测试主要用NUnit / xUnit编写,并分为几类,从而证明:

  • 这些输入文件上的搬运工输出与目标匹配;
  • 编译和启动后,移植程序的输出与目标一致;
  • 输入项目中的NUnit测试已成功转换为移植项目中的gtest测试并通过;
  • Ported Projects API可在C ++中成功运行;
  • 各个选项和属性对翻译过程的影响是预期的。

我们使用GitLab来存储源代码Jenkins被选为CI环境移植的产品以Nuget软件包和下载档案的形式提供。

问题


在进行该项目时,我们不得不面对很多问题。他们中的一些人是预期中的,而其他人已经在这一过程中出现了。我们简要列出主要的。

  1. .Net C++.
    , C++ Object, RTTI. .Net STL.
  2. .
    , , . , C# , C++ — .
  3. .
    — . , . , .
  4. .
    C++ , , .
  5. C#.
    C# , C++. , :

    • , ;
    • , (, yeild);
    • , (, , , C#);
    • , C++ (, C# foreground-).
  6. .
    , .Net , .
  7. .
    - , , «» , . , , , , using, -. . , .
  8. .
    , , , , , / - .
  9. .
    . , . , , , .
  10. 保护知识产权困难重重。
    如果盒装解决方案很容易混淆C#代码,那么在C ++中,您必须付出更多的努力,因为许多类成员都不能从头文件中删除而不会造成任何后果。将通用类和方法转换为模板也会通过公开算法来创建漏洞。

尽管如此,从技术角度来看,该项目还是非常有趣的。对其进行研究可以使您学到很多东西,也学到很多东西。任务的学术性质也为此做出了贡献。

摘要


作为该项目的一部分,我们能够实施一个能够解决其实际应用问题的有趣的学术问题的系统。我们每月组织一次公司图书馆,使用其初次使用的语言。事实证明,大多数问题都是可以完全解决的,并且最终的解决方案是可靠且实用的。

计划不久再发表两篇文章。其中之一将通过示例详细描述搬运工的工作方式以及C#结构在C ++中的显示方式。在另一篇演讲中,我们将讨论如何确保两种语言的内存模型兼容。

我将尝试回答评论中的问题。如果读者对我们开发的其他方面表现出兴趣,并且答案开始超出评论中的对应范围,我们将考虑发表新文章的可能性。

All Articles