笔者最近在研究CEF的CMake工程,心血来潮想要对各种编译工具链以及构建系统做一个简单的总结,于是就有了本文。本文不会讲解任何关于C/C++语言方面的内容,主要C/C++的编译出发,介绍各种编译工具链与构建系统的关系。此外,由于笔者水平有限,无法从非常专业的角度剖析C/C++的语言特性与编译,仅做入门介绍,如有不正确的地方还请评论提出。

基本:编译流程

老生常谈,关于C/C++代码到最终可执行程序的流程大致如下:

C与CPP常见编译工具链与构建系统简介-LMLPHP

  1. 预处理(Preprocessing)
  2. 编译(Compilation)
  3. 汇编(Assemble)
  4. 链接(Linking)

当然,对于外部库的链接,又分为静态链接和动态链接,它们的区别如下:

C与CPP常见编译工具链与构建系统简介-LMLPHP

读者可以把自己源代码编译后的目标文件(像上图的main.cpp.obj文件)想象成一个块不完整的拼图,对于外部库文件想象成拼图剩下的部分。对于静态链接来说,在最后的链接过程,相当于把两块拼图组成完成的“图片”,这个图片就是可执行程序(像上图的my-app.exe);对于动态链接来说,这个过程不会将两块拼图完整的拼接在一起,而是给我们自己的“拼图”缺失的位置添加一个上下文信息(包括动态库的查找方式、内存地址等),程序运行的时候,会动态的加载这些库文件并执行这些外部动态库的程序代码等。

构建的基石:编译工具链

简单讲解了源代码到最终可执行文件的流程以后,让我们更进一步,聊一聊上述中的编译、链接环节。既然谈到了编译、链接的动作,那必然有程序来完成这个过程,而编译器与链接器就扮演了这个角色。

Windows MSVC与SDK

MSVC

在Windows上,我们经常会听到MSVC。这里需要解释的是,MSVC并不是特指某一个编译、链接器,它实际上是一套工具链合集,包含但不限于了如下的内容:

  • cl.exe
  • link.exe
  • 包含更多帮助开发调试的运行时、debugger等

在一开始看cl.exe的文档的时候,官方文档是这样描述的:

这说法让笔者一度以为cl是编译器compiler和链接器linker的两个单词首字母的缩写,基于这个错误的认识,笔者特别好奇既然cl.exe已经具备了编译和链接的功能,那为什么还有一个单独的link.exe链接器呢?直到后来,继续查阅文档:CL 调用链接器,发现了这样一段话:

原来,cl.exe依然履行的是编译环节的职责,只是在默认不使用/c选项的情况下,它附加了一个功能:帮你调用链接器。

Windows SDK

除了MSVC以外,在Windows上进行C/C++应用开发,我们还需要另一块很重要的东西:Windows SDK。正如文档所说:

Windows SDK主要提供了在编译链接过程中要使用到的C、C++标准库以及Windows平台的库文件、元数据等。如果把MSVC理解为工具的话,那么SDK更偏向于工具所需要的物料。

安装MSVC与Windows SDK

说了这么多,我们如何安装MSVC和Windows SDK呢?通过搜索,也许你会发现这个文档:

Microsoft Visual C++ 可再发行程序包最新支持的下载

然而,这里的VC++ Redist并不是上述的MSVC工具链与SDK,它们的关系可以这样理解:MSVC和Windows SDK组成了一套开发工具集+运行时,而VC++ Redist主要是运行时。

举个例子,开发者在一台Windows机器上,编写源代码并通过MSVC提供的编译工具加上Windows SDK提供的库文件等,构建成一个可运行的应用。理论上,这个应用在这台Windows机器上运行一般没有环境问题。然而,当把这个应用分发到客户Windows机器上的时候,如果客户机器没有对应运行环境,则大概率会有异常,但我们没有必要让客户机器安装MSVC工具链和SDK(他们又不需要开发),只需要一个最小的运行环境即可,而VC++Redist就承载了这个能力。

基于MSVC+SDK是一整套工具链+库文件,不同版本的MSVC与SDK就会包含不同版本编译器和标准库、Windows库等,自然不同版本构建出来的应用,需要有对应不同版本的的运行时。所以,我们经常在网络上会看到所谓的VC++Redist的All In One,封装了各种版本的VC++ Redist运行库,让客户端机器不管三七二十一全部装就行了。

回到一开始的问题,如何安装MSVC工具链和Windows SDK呢?一般来说,我们在通过宇宙第一IDE Visual Studio 的安装器(Visual Studio Installer)就能够安装不同版本的Windows SDK:

C与CPP常见编译工具链与构建系统简介-LMLPHP

当然,有的小伙伴会想,难道我只能通过Visual Studio的安装器来安装MSVC和SDK吗?为了在Windows上进行C、C++开发,我必须要安装VS吗?根据前面的讨论可以知道,我们想要在Windows上构建C、C++应用,核心是需要编译器工具链标准C、C++库文件以及Windows库文件,VS并非必须。好在微软也提供仅仅安装这些工具的途径:

进入VS的下载页面:下载页面,找到Visual Studio 20XX 生成工具

C与CPP常见编译工具链与构建系统简介-LMLPHP

当然,如果是作为初学者,笔者还是建议通过Visual Studio Installer安装。

Linux GCC

在Linux上想要编译构建C/C++应用,我们总是离不开讨论GCC。首先,GCC曾经是GUN C Compiler的缩写,也就是GUN的C语言编译器,然而随着不断的发展,GCC已经能够处理C++、Object-C、Go语言等语言了,社区对它的定位也更上了一层,所以它现在的全称是GNU Compiler Collection,即GNU编译器集。

GCC主要包含一下几部分:

  • gcc-core:即GCC编译器,用于完成预处理和编译过程,把C代码转换成汇编代码。
  • Binutils :除GCC编译器外的一系列小工具包括了链接器ld,汇编器as、目标文件格式查看器readelf等。
  • glibc:包含了主要的 C语言标准函数库,C语言中常常使用的打印函数printf、malloc函数就在glibc 库中。

到这里,读者应该很容易想到,GCC工具链和MSVC+Windows SDK这套算是平级的关系,也属于是工具链集,且有比较强的对应关系,比如gcc-corecl.exe都属于编译器角色,而ldlink.exe都属于链接器,glibc也与Windows SDK类似包含标准函数库等。

另外,在Linux中使用GCC的命令行工具叫gcc,你可以认为它是一个工具入口命令行。在默认的情况下将一份源代码编译为可执行程序只需要:

 gcc hello.cpp -o hello

格式就是:gcc 源文件名 -o 生成的执行文件名

这个过程其实自动包含了至少两步:1、调用内部的编译器cc,将源代码编译为目标文件;2、调用内部链接器ld将链接目标文件和标准函数库文件。同样的,你可以通过给gcc命令行参数来控制是否只进行编译而不链接,甚至直接调用GCC包含的cc命令和ld命令来分别手动对文件进行编译和对汇编代码进行链接操作。

安装GCC

在Linux中想要安装GCC,一般通过包管理工具进行安装即可。例如,在Debian系的Linux发行版(Debian、Ubuntu等),我们使用apt-get来安装:sudo apt-get install gcc;或是在RedHat系的Linux发行版(RedHat、CentOS等),使用yum进行安装:sudo yum install gcc

有的时候,当我们搜索在Linux中如何安装gcc的时候,我们一般会看到这样的一个关键词:build-essential,中文翻译过来就是构建基础集,它本身不是一个软件,而是一个工具集,包含了在Linux开发程序的一些比较必要的软件包,包括但不限于gcc、g++、make等。所有一般我们通过命令sudo apt-get install build-essential就把GCC、make等一键安装了。但是务必注意build-essential是Debian系Linux发行版提供的合集包,如果是RedHat系的Linux发行版(例如CentOS)是没有的,但是RedHat系提供了类似的软件开发合集包Development Tools,通过yum安装。

macOS clang/LLVM

在macOS上,我们一般使用clang/LLVM体系工具链来进行代码构建。clang/LLVM有太多优质的文档介绍它的架构体系了,读者可以自行搜索阅读。至于clang怎么使用?其实和GCC的命令几乎一样:clang test.c -o test。同样的,它也支持相关参数来控制仅编译还是仅链接等操作,这里不再赘述,请读者自行实践。不难理解,在macOS上,clang/LLVM相关工具链与上面的Windows下的MSVC+SDK和Linux下的GCC工具链的层次差不多。

安装clang/LLVM工具链

和上面Linux的GCC安装途径比较类似,在macOS我们一般不安装单个的clang相关工具链。取而代之的,我们都会安装Command Line Tools工具合集。这个tools就相当于上面的build-essential的定位,包含了开发常用工具。当我们安装Command Line Tools的时候,不仅仅会把clang/LLVM整个工具链安装,同时还会安装包含了诸如svn、git、make、perl等工具以及库文件等内容。

本节小结

至此,我们了解了不同平台的主流编译工具集以及开发工具包,这里我们做一个简单的总结来描述它们的关系。首先,每一个操作系统都有其底层核心的应用编译工具链:

在Windows上是MSVC+Windows SDK,其中MSVC主要作为工具提供编译能力,Windows SDK提供编译过程需要的库文件;

在Linux上是GCC编译工具链,其中包含的gcccc以及ld等命令工具提供编译构建的能力,glibc提供构建过程所需要的C/C++语言所需的标准库等。另外,在Linux编译跟Linux平台相关的应用需要单独安装Linux的开发库文件以及Linux头文件。

在macOS上是clang/LLVM编译工具链,它与Linux较为类似,通过内部的工具、命令行以及提供的标准库文件等完成构建应用的能力。另外,在macOS上编译跟macOS平台相关的应用是需要安装macOS平台特定的库文件的,不过在Command Line Tools安装的时候,就会帮助我们安装了。

更上一层楼:构建系统

前面我们简单介绍了主流操作系统上的编译工具链,我们会发现工具链整体提供的是比较底层的核心的编译与链接(后简称编译)等功能。对于较为简单的项目,你确实可以直接通过手写命令的方式来调用这些工具来对代码进行编译。比如,将一个main.cpp编译为可执行文件,一个简单的命令即可:

gcc hello.c -o hello

但是请注意,这个例子只是一个文件。随着项目工程越来越复杂,源代码文件越来越多,编译配置项根据场景的不同越来越复杂(例如,Debug模式和Release模式下编译参数不一样)的时候,依然通过直接调用这些命令的时候就会很复杂,我们需要编写大量复杂的命令行才能完成一个复杂项目的编译工作。基于这样的背景,我们诞生了构建系统(Build System)。

如何理解构建系统呢?如果把上一节介绍的编译工具链比作炒菜的铲子,把源代码、库文件比作食材,那么最原始的方式,就是人工使用铲子,先炒什么,再放什么调料,再炒什么,最终制作出一盘菜。而构建系统,可以理解为一个炒菜的机器人,它接收炒菜的图纸文件,只要启动以后,就会自己拿着锅铲摆弄食材来制作出一盘菜。当然,即使是炒菜机器人,底层依然用的锅铲和食材,只是炒菜的流程自动化、机器化了。也就是说,构建系统在底层依赖使用的是编译工具链,只是进行了一定的用户友好的抽象,并降低了项目编译的复杂度。

在不同的平台上,构建系统是不一样的。接下来我们就进一步介绍不同平台的构建系统。

Windows MSBuild与sln、vcxproj

在Windows上的构建系统,最主流的是MSBuild构建系统。作为编译工具链的上层,它可以调用系统中安装的MSVC。当然,它需要按照一定的规则逻辑来调用MSVC,而这个规则逻辑就如同上面比喻中的炒菜机器人的图纸文件一样。一般来说,“图纸”就是xxx.sln解决方案配置和xxx.vcxproj项目工程配置。这些配置文件通常会指明一些关于编译构建的信息,例如项目工程所包含的源文件有哪些;相关库的头文件查找路径、二进制库文件查找路径;不同场景(Debug或Release)下的代码编译方式(是否代码优化,是否移除符号等)。

这个时候,有的读者可能有疑问。我在Windows上开发的时候,一直用的Visual Studio打开的这些.sln.vcxproj,没有看到过MSBuild的参与呢。实际上,VS作为IDE,更大的作用是可视化展示你的项目工程以及集成更多便利的开发工具,当你在IDE中编写源码,配置编译选项,其实就在影响.sln.vsxproj配置文件。另外,当你按下了VS上项目运行/构建的按钮的时候,底层就是在调用msbuild.exe。

总结来说,Windows上的VS IDE、MSBuild、MSVC等关系和流程可以用下图简单描述:

C与CPP常见编译工具链与构建系统简介-LMLPHP

Linux make与Makefile

在Linux/Unix上的构建系统最最历史悠久的就是make工具,而与之配合的就是Makefile配置文件。与Windows上的MSBuild体系类似,make这个命令行工具可以认为与msbuild.exe是同一层次,而Makefiles配置文件则是与.sln.vcxproj文件是一个功能,指明了项目中具有哪些源代码、编译的规则逻辑等信息。当make执行的时候,读取Makefile配置文件,生成GCC相关的调用命令行,再调用GCC的相关命令行工具进行编译构建。于是,make、GCC的关系和流程就可以如下描述了:

C与CPP常见编译工具链与构建系统简介-LMLPHP

macOS xcodebuild与xcodeproj

据笔者了解,在macOS存在基于xcodebuild的构建系统,其核心的命令行工具就是xcodebuild。这个xcodebuild可以理解为与Windows下的msbuild异曲同工。流程也十分类似,也是读取项目配置文件,进行项目编译。只不过项目配置文件变为了.xcodeproj

C与CPP常见编译工具链与构建系统简介-LMLPHP

此外,macOS 由于其内核包含Unix BSD部分,所以在macOS操作系统的发展过程中,很多有关Unix的工具同样能被使用。在macOS上,我们同样可以使用make+Makefile的构建系统体系来调用底层的编译器进行项目构建。

生成构建系统的工具:CMake

前面我们介绍了构建系统的能力:允许用户以配置的方式来组织项目,并让构建系统代替用户完成底层编译工具链的调用。

然而随着软件工程的发展,有人在实践中发现,不同平台的构建系统,都有自己独有的构建工具和配置,它们并不通用。每当我们在新的一个平台进行项目构建,我们需要针对新平台编写一套属于该平台的构建配置。此外,还要保证某个平台的配置发生了修改,那其他的平台也需要有对应一致的修改,于是维护成本又逐渐上来了。

那么面对一致性与可维护性的问题,有人提出这样的解决思路,既然每个平台的构建系统比较成熟了, 那么暂时不考虑重新做一套的跨平台的构建系统,而是换一个角度,提供一个工具并约定一套几乎与平台无关的通用配置。通过工具加上特定的配置,就可以做到:

  • 如果用户希望在Windows上构建应用的时候,那么这个工具就基于配置生成一套msbuild能够加载的.sln.vcxproj工程配置。于是,我们可以直接使用msbuild构建或是用VS打开工程开发构建;
  • 如果用户希望在Linux上基于同样的代码构建Linux平台的应用,那么这个工具就利用同一份配置生成一套make能够加载的Makefile配置。于是,在Linux,我们就可以使用make命令来构建这个应用了;
  • macOS同理。

基于上述的设想,CMake面世。与CMake搭配的所谓的“通用配置”,就是我们经常见到的CMakeLists.txt文件,通过特定的DSL(领域特定语言),来描述项目结构以及编译规则。CMake工作流程就是根据CMakeLists.txt来生成平台构建系统特定的项目结构和配置。基于上述设想的结果,各个平台的构建系统配置的编写与维护的任务就沉淀到了CMake中,减轻用户的一部分开发负担。

于是,我们简单总结CMake与构建系统的关系:

C与CPP常见编译工具链与构建系统简介-LMLPHP

还有什么工具

ninja:跨平台高效构建系统

有的时候,我们会从一些项目中看到ninja(Ninja, a small build system with a focus on speed (ninja-build.org))这个东西,很多初学者经常会混淆ninja、make、CMake等工具之间的关系。请注意,ninja扮演的角色主要是构建系统,而不是生成构建系统的工具!它和msbuild、make、xcodebuild属于同一层次的工具,而ninja作为构建系统,同样需要配置,这个配置一般就是.ninja文件。同时,ninja也是跨平台的,也就意味着在前面提到的“那么暂时不考虑重新做一套的跨平台的构建系统”被ninja打破这一局面。

C与CPP常见编译工具链与构建系统简介-LMLPHP

既然ninjs也属于构建系统,不难想到,CMake也能指定参数生成ninja工程。

xmake:即可作为构建系统也可以生成构建系统

Xmake 是一个基于 Lua 的轻量级跨平台构建工具。在这里笔者不想介绍太多关于它的内容,因为xmake的官方文档写的很好(比起CMake来说简直是天壤之别),感兴趣的读者可以直接阅读官方文档:xmake

这里只是提一下,xmake既可以作为构建系统来直接调用编译工具链进行项目编译(默认的),同时,还可以作为CMake的角色来生成特定的构建项目配置,借用官方的文档来说:

也就是说,即可作为构建系统也可以生成构建系统。

写在最后

其实本文的内容比较浅,主要对一些常见的编译工具链和构建系统进行了介绍。另外,需要补充一点的是,在Windows中其实也能使用GCC工具链,至于方式就是有的小伙伴经常看到的MinGW和MSYS2了。由于篇幅的原因就不在本文一一介绍了,读者可以从本文的出发,自己去探究关于MinGW、MSYS2是什么。

本文同样会发布在本文所在博客:CompileMind,欢迎大家访问。

09-12 15:59