最近项目需要,研究了一下怎么样跨平台部署现有的项目。由于大多数的项目都是用.net core写的。.net项目可以通过.net runtime的JIT(Just-in-Time)编译器,将IL(Intermediate Language)类库在不同的平台上编译成相对应的机器语言,所以没有cross compiler的要求,平台迁移也很方便。但是头痛的是,我们中间还有一部分的底层的类库是用C++,然后通过P/Invoke方式来进行调用。C++部分因为编译成机器语言,与平台是紧耦合的关系,所以无法进行平台迁移,所以就开始研究cross compile。

简介

现在的可运行的平台有很多,从本质上可以从三个维度进行划分。一个维度是操作系统,主流的操作系统有 Windows, Linux, MacOS等电脑操作系统,还有Android,iOS等手机操作系统。第二个维度是CPU的运算位数,是32位还是64位。最后一个维度是CPU的架构,Intel主导的x86架构, 还是ARM, MIPS这些在移动端架构。这三者的组合可以得到不同的运行平台。要让我们编写的C++程序,在每个平台上运行,是不是需要在每个运行平台上编译呢?当然不是,cross compile正是用来解决这类问题的。

Cross compile被翻译成交叉编译,这个名字真让人吐槽。在我看来,这里的cross取“跨越”的意思,表示跨过不同平台的界限。是不是有了cross compile,C++就可以在一个平台上编译所有平台的程序呢?想法很好,但是现实比较残酷。横亘在面前的是操作系统这个很难跨越的坎。有些语言通过编译成IL,再在目标系统上启动runtime,在runtime里跑这个程序。比如:Java和.net。有些语言是解释型的,在不同平台上有各自的解释器来实时翻译。比如:js和Python。但是C++直接编译成机器语言,可以在对应的平台上直接运行,不需要解释器来解释,也不需要runtime来进行二次编译。这就决定了C++不可能跨操作系统进行编译,因为在编译过程中不可避免地要和操作系统内核进行交互。当然你可以在一个操作系统中,装上另一个操作系统的微内核,然后通过与这个微内核交互来达到跨操作系统的编译(MinGW就是这样的微内核)。这种方式也不是全然没有用,但是编译出来的类库,无法保证与实际操作系统无缝衔接,可能会存在一些潜在问题。所以,安全起见,对于不同的操作系统上C++编译还是在相对应的操作系统上独立完成吧。

对于CPU位数和架构的跨越,倒是没有操作系统那么难。现有的编译器都能做到这一点。也就是说,我们完全可以在一台Windows的机器上,编译出IA32,x86-64,ARM64上Windows的程序。

因为没有Mac的机器来实验,所以后续的测试主要集中在Windows和Linux上。

准备工作

在Windows上,编译C++项目需要安装Visual Studio的C++编译器,其中就可以选装各种的cross compile的编译工具。
LolWh5OHHC.png
在Linux上,就需要自己安装各种的编译环境。不同的Linux有各自的包管理器,那些编译器的名字也可能不一样,这里就以最常用的Ubuntu为例。除了安装常规编译需要的工具包,例如build-essential,cmake,gcc等。进行跨CPU架构编译的时候,还需要下载不同的编译器。例如,如果要编译在树莓派(Raspberry Pi)上可以运行的C++程序,就必须安装gcc-arm-linux-gnueabihf包。下面罗列了我在apt中找到的跨平台的编译器。
mstsc_WtRSLPdpQF.png
mstsc_DLnwFafYQd.png

编译

Cross compile的初衷在于一份代码在不同的平台上编译运行。在不同的平台上,有不同的C++编译器,每个编译器都有各自不同的参数。如果我们为每个环境都要写一个编译脚本,这就与cross compile的初衷违背了。为此,需要有一个通用的编译脚本。将这个脚本在不同的平台上运行,就会自动按照你的要求来编译C++代码。CMake是这种通用的语言。很多的C++项目都用CMake来编译。
下面就是最简单的CMake的一个脚本

  1. cmake_minimum_required(VERSION 3.13) #注明CMake的最低支持版本。
  2. #这个很重要,有些功能低版本的CMake不支持
  3. set(CMAKE_CXX_STANDARD 14) #C++代码的标准。要支持最新的C++20,最低CMake version是3.12
  4. set(CMAKE_C_STANDARD 11) #C代码的标准。
  5. set(CMAKE_BUILD_TYPE RelWithDebInfo) #这个参数只是对那些Makefile生成器或者Ninja有用,告诉它们编译的配置。可选参数:Debug, Release, RelWithDebInfo, MinSizeRel,...
  6. project(<your project>) #设置project名字
  7. add_library(<target name> SHARED|STATIC|MODULE <.c file> <.h file>) #将.c, .h文件编译

但是这个脚本对于cross compile来说就不够了。

Windows

在Windows上,只要安装了相应的cross compile的编译工具,这个就可以非常简单来实现。

  1. cmake -G "Visual Studio 16 2019" -A Win32 #用VS2019 32-bit x86编译器
  2. cmake -G "Visual Studio 16 2019" -A x64 #用VS2019 64-bit x86编译器
  3. cmake -G "Visual Studio 16 2019" -A ARM #用VS2019 32-bit ARM编译器
  4. cmake -G "Visual Studio 16 2019" -A ARM64 #用VS2019 64-bit ARM编译器

然后在命令行中输入msbuild命令就可以编译成相应的类库了。

  1. msbuild <project>.sln -p:Configuration=Release|Debug -t:Rebuild

Linux

Linux上,没有Visual Studio这样的集成编译系统,所以我们只能通过构造CMakeLists.txt,通过配置一些参数来达到这个目的。

操作系统

不考虑MacOS的情况下,为了区分Linux和Windows,我们可以使用CMake的条件语句来实现。

  1. if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
  2. #在Linux上的特殊配置
  3. endif()

32位 vs. 64位

在Linux中,默认使用gcc来编译C++代码。在gcc中可以通过传参-m来实现编译出32位还是64位程序。用-m32表示编译成32位类库,-m64则表示编译成64位类库。理想的情况是,我们通过cmake的传参来确定gcc是编译成32位类库还是64位类库。如果缺省,就不进行设置。

在CMakeLists.txt中可以写成这样

  1. if (BUILD_BITS EQUAL "x64")
  2. add_compile_options(-m64)
  3. add_link_options(-m64)
  4. elseif(BUILD_BITS EQUAL "x86")
  5. add_compile_options(-m32)
  6. add_link_options(-m32)
  7. endif()

CMake可以这样传参

  1. cmake -DBUILD_BITS=x86 #编译成32位程序
  2. cmake -DBUILD_BITS=x64 #编译成64位程序

另外,在cmake中,是通过判断void指针在系统中的位数的来判断操作系统的位数。8位为64位操作系统,4位为32位操作系统。代码可以写成

  1. #CMAKE_SIZEOF_VOID_P这个环境变量是表示一个指针的长度。64位操作系统指针长度8位,32位操作系统指针长度4位
  2. if (CMAKE_SIZEOF_VOID_P EQUAL 8)
  3. #64位操作系统
  4. elseif(NOT BUILD_BITS)
  5. #32位操作系统
  6. endif()

x86 vs. ARM

在x86的机器上编译ARM的类库的时候,需要将gcc编译器指定到gcc-ARM上。CMake官方文档中给出了下面的配置方案

  1. set(CMAKE_SYSTEM_NAME Linux)
  2. set(CMAKE_SYSTEM_PROCESSOR arm)
  3. set(CMAKE_SYSROOT <path to system root>) #e.g. /home/devel/rasp-pi-rootfs (Optional)
  4. set(CMAKE_STAGING_PREFIX <path to stage folder>) #e.g. /home/devel/stage (Optional)
  5. set(tools <path to arm-linux-gnueabihf>) #e.g. /home/devel/gcc-4.7-linaro-rpi-gnueabihf
  6. set(CMAKE_C_COMPILER ${tools}/bin/arm-linux-gnueabihf-gcc)
  7. set(CMAKE_CXX_COMPILER ${tools}/bin/arm-linux-gnueabihf-g++)
  8. set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
  9. set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
  10. set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
  11. set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

其中,CMAKESYSROOT是CMake中find*命令(例如:find_package)搜寻的根目录,虽然是可选的,但是推荐还是要设置一下。

总结

如果一个C++项目想要进行cross compile,最好有不同操作系统的机器各一台,然后装好必要的编译器。在Windows上,装好Visual Studio的C++相关编译库;在Linux上,根据不同的版本,装好gcc相关的库。在CMakeLists.txt里做好cross compile的相关配置,例如:

  1. cmake_minimum_required(VERSION 3.13)
  2. set(CMAKE_CXX_STANDARD 14)
  3. set(CMAKE_C_STANDARD 11)
  4. set(CMAKE_BUILD_TYPE RelWithDebInfo)
  5. if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
  6. if (BUILD_BITS EQUAL "x64")
  7. add_compile_options(-m64)
  8. add_link_options(-m64)
  9. elseif(BUILD_BITS EQUAL "x86")
  10. add_compile_options(-m32)
  11. add_link_options(-m32)
  12. endif()
  13. if (${TARGET_PLATFORM} MATCHES "arm64")
  14. set(CMAKE_SYSTEM_NAME Linux)
  15. set(CMAKE_SYSTEM_PROCESSOR arm)
  16. set(CMAKE_SYSROOT <path to system root>) #e.g. /home/devel/rasp-pi-rootfs (Optional)
  17. set(CMAKE_STAGING_PREFIX <path to stage folder>) #e.g. /home/devel/stage (Optional)
  18. set(tools <path to arm-linux-gnueabihf>) #e.g. /home/devel/gcc-4.7-linaro-rpi-gnueabihf
  19. set(CMAKE_C_COMPILER ${tools}/bin/arm-linux-gnueabihf-gcc)
  20. set(CMAKE_CXX_COMPILER ${tools}/bin/arm-linux-gnueabihf-g++)
  21. set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
  22. set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
  23. set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
  24. set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
  25. endif(${TARGET_PLATFORM} MATCHES "arm64")
  26. endif(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
  27. project(<your project>)
  28. add_library(<target name> SHARED|STATIC|MODULE <.c file> <.h file>) #将.c, .h文件编译

运行cmake命令

  1. cmake -DBUILD_BITS=x64 -DTARGET_PLATFORM=arm64 #build for Linux-ARM64