我正在寻找一个高级构建系统/工具,可以帮助组织我的嵌入式C项目成"模块"one_answers"组件"。请注意,这两个术语是非常主观的,所以我的定义如下。
- 一个模块是c文件和h文件的集合,但是只有一个公共的h文件对其他模块可见。
- 另一方面,组件(或层)是模块的集合(例如应用层、库层、驱动层、RTOS层等)。
构建系统/工具应该-
- 防止组件和模块之间的循环依赖(模块内部的循环依赖是可以的)
- 防止访问模块的私有屏障。如果其他模块试图包含某个模块私有的头文件,则构建系统必须抛出错误。但是,私有屏障内的文件必须能够包含该屏障内的其他文件。
- 支持在主机上自动构建和执行单元测试(TDD的快速反馈循环)
- 支持在目标模拟器上运行单元测试
- 支持代码静态分析
- 支持代码生成
- 支持代码重复检测(强制DRY原则)
- 支持代码美化
- 支持生成单元测试代码覆盖率度量
- 支持生成代码质量度量
- 是独立于平台的
我可以写我自己的构建工具,花很多时间在上面。然而,这不是我的专业领域,如果有人已经创建了这样的工具,我宁愿不要重新发明轮子。
实现这一目标的传统方法是将每个模块的源代码放在一个单独的目录中。每个目录都可以包含模块的所有源文件和头文件。
每个模块的公共头文件可以放在一个单独的通用头文件目录中。对于每个头文件,我可能会使用从公共目录到相关模块目录的符号链接。
编译规则简单地声明,除了公共目录中的头文件外,任何模块都不能包含来自其他模块的头文件。这样做的结果是,任何模块都不能包含来自另一个模块的头文件——除了公共头文件(因此强制了私有屏障)。
自动防止循环依赖关系不是微不足道的。问题是,您只能通过一次查看几个源文件来建立循环依赖关系,而编译器一次只查看一个。
考虑一对模块ModuleA和ModuleB,以及一个使用这两个模块的程序Program1。
base/include
ModuleA.h
ModuleB.h
base/ModuleA
ModuleA.h
ModuleA1.c
ModuleA2.c
base/ModuleB
ModuleB.h
ModuleB1.c
ModuleB2.c
base/Program1
Program1.c
在编译Program1.c时,如果它使用了两个模块的服务,那么它包含ModuleA.h和ModuleB.h是完全合法的。因此,如果ModuleB.h被包含在同一个翻译单元(TU)中,ModuleA.h不能报错,如果ModuleA.h被包含在同一个TU中,ModuleB.h也不能报错。
让我们假设ModuleA使用ModuleB的功能是合法的。因此,在编译ModuleA1.c或ModuleA2.c时,同时包含ModuleA.h和ModuleB.h是没有问题的。
但是,为了防止循环依赖,您必须能够禁止ModuleB1.c和ModuleB2.c中的代码使用ModuleA.h。
据我所知,做到这一点的唯一方法是一些技术,需要ModuleB的私有头,说"ModuleA已经包含",即使它没有,这是在ModuleA.h被包含之前包含的。
ModuleA.h的框架将是标准格式(ModuleB.h将是类似的):
#ifndef MODULEA_H_INCLUDED
#define MODULEA_H_INCLUDED
...contents of ModuleA.h...
#endif
现在,如果ModuleB1.c中的代码包含:
#define MODULEA_H_INCLUDED
#include "ModuleB.h"
...if ModuleA.h is also included, it will declare nothing...
...so anything that depends on its contents will fail to compile...
这远不是自动的。
您可以对包含的文件进行分析,并要求依赖项具有无循环的拓扑类型。UNIX系统上曾经有一个程序tsort
(和一个同伴程序lorder
),它们一起提供了所需的服务,以便创建一个静态(.a
)库,该库以不需要重新扫描归档文件的顺序包含目标文件。ranlib
程序,以及最终的ar
和ld
承担了管理重新扫描单个库的职责,从而使lorder
变得特别冗余。但是tsort
有更广泛的用途;它可以在某些系统上使用(例如MacOS X;
因此,使用GCC和tsort
的依赖跟踪,您应该能够检查模块之间是否存在循环。但这需要小心处理。
可能有一些IDE或其他工具集自动处理这些东西。但通常情况下,只要需求和模块间的依赖关系被仔细地记录下来,程序员就可以足够自律地避免问题。
对于一般解决方案,我完全推荐使用Jonathan Leffler的解决方案。然而,如果你绝对需要一个自动测试你的模块是否自包含和隔离,你可以试试Debian的构建系统。
将每个模块打包到Debian包中(当它已经自动配置时,这非常快),正确声明build - depends并在pbuilder环境中构建包。这确保了只有每个模块的公共头文件是可用的(因为只有那些在.deb包中,pbuilder安装这些包来构建其他包),并且有很好的工具来查看Debian包树并确保它们是无循环的。
然而,这可能是过度的。只是为了完整性和情况,你肯定需要一个自动化的解决方案。