将 300 个 C/C++项目移植到 Buck Build 的经验教训

将 300 个 C/C++项目移植到 Buck Build 的经验教训

原文:https://medium.com/hackernoon/lessons-learned-from-porting-300-projects-to-buck-build-ff6463b65142

通过 Buckaroo ,我们正在将 C/C++项目的庞大生态系统转变为一系列易于组装的构建模块。我们无法独自完成这项工作,但为了启动社区,我们主动将 300 个项目移植到 Buck build 系统

我们选择这些库是基于它们在 GitHub、StackOverflow 上的受欢迎程度以及来自我们邮件列表的请求。它们的范围从极小的只有头文件的库整体的 C++项目古老但关键的 C 库

对于每一个库,我们都试图做一个完整的移植。有一些情况下,这并不奏效;有时项目结构非常复杂,以至于我们认为包装现有的构建系统更加实际。我们可以稍后再来看这些项目,但是大多数情况下,移植工作是成功的。

在研究了这么多项目之后,我们认为为构建干净的 C/C++库编制一份清单是有好处的。

不要:串联。cpp 文件转换成一个翻译单元

在许多 C/C++项目中发现的一种反模式是将多个 C/C++文件组合成一个翻译单元进行编译。理论上来说,这样可以缩短编译时间,因为在整个编译过程中,只需要执行一次预处理器和解析步骤。这对于单个构建来说是正确的,但是它破坏了您进行增量构建的能力。使用单个翻译单元,每当任何项目文件改变时,编译器必须从头开始执行整个构建!

做:明确你的依赖关系

使用现有的图书馆对每个人都有好处。库代码得到了更好的测试,这对社区来说很好,重复的工作更少了,这对您来说很好!然而,缺乏一个占主导地位的 C/C++包管理器意味着对此使用了多种方法——有些是次优的。

编写库时,确保清楚地记录它的依赖关系。以下是一些合理的方法:

  • Git 子模块
  • 一列apt-getbrew install命令——尽管如果你的库的用户不得不修改他们的系统来使用它,他们可能会很恼火!
  • 一个好的包管理器

不要:除非万不得已,否则不要使用 include_next

#include_next是预处理程序的一个晦涩的特性,允许用户将一个文件包含在它自身中。其目的是允许对系统集管进行修补; GCC 文件解释如下:

有时有必要调整系统提供的头文件的内容,而不直接编辑它。例如,GCC 的 fixincludes 操作就是这样做的。一种方法是创建一个同名的新头文件,并将其插入到搜索路径中的原始头文件之前。只要你愿意完全替换旧的头,那就很好。但是如果你想从新的头中引用旧的头呢?

实际上,这个特性会导致非常混乱的结果;许多程序员不熟悉它;它的行为很难追踪。除非万不得已,否则不要使用这个特性。

DO:将私有头和导出头分开

为您的库维护一个定义良好的 API 确实是一个好主意。它允许您在不破坏下游消费者的情况下进行调整,并且它为单元测试提供了一个清晰的表面。

理想情况下,您应该将私有和导出的头分别放在各自的文件夹中。我很喜欢这个惯例:

  • include —对库用户可用的标题
  • private —编译库所需的标头,但不应提供给库的使用者

不要:包含。cpp 文件

C/C++预处理器非常灵活,允许你做一些你可能不应该做的事情。一个例子是包含.cpp文件。这是非常混乱的,因为文件扩展名不再匹配的意图。这个文件是一个标题吗?然后给它一个.h.hpp的延期。这个文件需要编译吗?那就不要收录。

DO:在你的例子中,按照预期的方式使用库

很多时候,我发现一些例子被写成好像它们在库的源目录中:

#include “../../things.h”

例子应该是你的图书馆的预期用途的展示案例。这意味着要包括在下游使用的集管:

#include <my-library/things.h>

当然,这要求您正确设置包含路径,但是一个好的构建系统应该会让这变得简单。

不要:将依赖项复制到项目中

如果您没有使用包管理器,那么将您所依赖的项目复制到源代码控制中会很有诱惑力。这样做的好处是每个人都在构建相同的代码,但是对于库的消费者来说,这可能是一场噩梦。

例如,假设你是库A的作者,这个库依赖于库Bv1。但是,你的一个用户正在写库C,依赖ABv2。现在,当他们试图构建您的项目时,他们会遇到符号冲突,必须降级到Bv1或在A提交 PR!一个更好的方法是使用一个包管理器,它可以将B解析为一个适用于项目中所有依赖项的版本。至少,使用 Git 子模块可以使这种升级更易于管理。

这个规则的一个例外是包含了用于开发任务的微小的只有头文件的库,比如 Catch 测试框架

DO:命名您的头文件

你应该为你的库选择一个唯一的名字——如果你遇到困难,使用生成器——然后将你导出的所有文件头放入一个同名的文件夹中。这使得与其他库发生名称冲突的可能性大大降低。

不要:过度使用预处理器

C/C++预处理器本身就是一种编程语言,支持各种编译时魔法和代码生成。问题是预处理器很难调试,因为工具不如 C/C++的工具好。

那么为什么要使用预处理器呢?在过去,逻辑认为预处理器保证在编译时执行,因此为了获得最佳性能,应该尽可能在那里实现逻辑。从那时起,编译器已经有了很大的改进,现在将为你做很多工作。因此,使用现代工具,最好避免在预处理器中实现逻辑,因为它可以在 C/C++中实现。相信你的编译器!

如果你必须使用预处理器,比如实现无栈协同程序,那么就尽量减少它的使用。希望将来这些案例会被整合到 C++语言中,就像过去的模板一样。

DO:使用文件抽象平台差异

在你的源代码中加入几个#ifdef __MACOS__ #endif命令可能很诱人,但事实是这使得阅读代码变得非常困难。如果您将特定于平台的实现分成单独的文件,那么您的构建系统可以包含、编译和链接适当的文件。这使得新读者更容易维护和理解代码。

不要:依赖编译器特定的特性(除非你真的必须这样做)

三大 C++编译器(Clang、VC++、GCC)都有各自的怪癖,有可能编写的代码只能在其中一个编译器中编译,而不能在其他编译器中编译。这通常可以通过坚持语言标准(或者是 VC++的子集)来避免。

我明白了——这些特定于供应商的特性中的一些可能很方便(#pragma once)——但是不使你的库可移植,你就减少了你的工作的影响。你失去了对 C++社区产生巨大影响的满足感,社区也失去了一个伟大的、可移植的库。

如果你需要特定的特性,比如__builtin_popcount,在内置版本和可移植版本上写一个抽象。然后,将实现分成单独的文件,并让您的构建系统知道您的意图。更好的是,重用已经编写好的抽象。

DO:使用文件夹来划分文件类别

手动列出项目文件非常繁琐。常见的解决方案是使用 glob 命令。Glob 是一个非常强大的工具,但是你可以通过以一种 glob 友好的方式布局你的文件来使事情变得容易得多。这意味着根据目的将文件划分到逻辑文件夹中。

安排不当的项目

.
├── common.cpp
├── foo.cpp
├── pthread.cpp
└── win_thread.cpp

安排有序的项目

.
├── common
│   ├── common.cpp
│   └── foo.cpp
├── linux
│   └── pthread.cpp
└── windows
    └── win_thread.cpp

就是这样!🙌

Buckaroo 软件包现在已经准备好使用了,我们正在努力工作,移植更多的软件。如果你需要一个特定的图书馆,在愿望清单上创建一个问题。或者(甚至更好)如果你愿意贡献自己的力量, PRs 永远受欢迎

黑客中午是黑客如何开始他们的下午。我们是 @AMI 家庭的一员。我们现在接受投稿并乐意讨论广告&赞助机会。

如果你喜欢这个故事,我们推荐你阅读我们的最新科技故事趋势科技故事。直到下一次,不要把世界的现实想当然!


本站为非盈利网站,作品由网友提供上传,如无意中有侵犯您的版权,请联系删除