编者按
本文已于作者 @陈文礼 授权转载,原载于知乎,如需转载请务必联系原作者。
开始摆脱Visual Studio,建立独立的Toolchain
上一篇我们写了一个最基本的 Hello Engine,并用 Visual Studio 的命令行工具,cl.exe 进行了编译。
然而,Visual Studio 只能在Windows上面使用。而且Visual Studio对C/C++进行了很多非标准的扩展。因此基于Visual Studio写出来的程序,除非你写的时候就很清楚哪些可以用哪些不可以用,否则基本是不可以移植到别的平台的。因为Windows并不是一个POSIX (POSIX - Wikipedia)系统,也就是说是一个非常不“标准”的系统。基于这样的系统的API写出来的程序基本只能跑在这个系统上。
我打算让这个手打引擎跑在所有我可以接触到的平台上。目前我可以接触到的平台有:Windows/Linux/PS4/PSV/Android/IOS
所以我需要打造一个独立于特定平台的编译工具包,也就是Toolchain。
目前在开源领域用得比较多的Toolchain是GCC和Clang。GCC历史比较长,很多开源软件,包括Linux内核都是GCC编译的。但厚重的历史也使其很臃肿,里面包括很多已经死掉的东西。而Clang则较年轻,现在也比较流行。
另外,PS4的编译器就是基于Clang的。AMD的OpenGPU计划,以及Vulkan图形API等也是基于Clang的。苹果的最新开发平台一样是基于Clang的。所以,我选择Clang。
准备编译Clang的环境
Clang的项目页面在Clang - Getting Started
首先我们按照Clang项目页面的提示,在Windows上面安装Subversion,这个是获取Clang源代码用的。我推荐安装TortoiseSVN,这个相对比较好用。注意命令行工具缺省是不安装的,需要手工勾选安装。
然后是CMake。我们在Visual Studio里面建立工程的时候,会自动创建Solution和Project文件来进行代码的组织管理和编译选项的存储。然而,这些同样是只有Visual Studio才能使用的文件格式。在Linux等平台上一般是使用make,或者GNU版的make:gmake。make是依靠一个叫做Makefile的文件来存储项目文件清单和编译选项的。可以直接手写,但是文件多了一般我们就希望自动生成。况且,在不同平台上面,虽然都有C/C++编译器,能够编译C/C++代码,但是各种库的头文件、静态链接库、动态链接库的存储位置,甚至是名字都会有很微妙的差异。所以,如果直接手写Makefile,那么结果就是我们需要为每个平台单独写一个。有一些早期GNU软件就是这样的。这很不利于管理。比如我们添加了一个C++文件,那么我们就需要改所有不同版本的Makefile。
所以有一个工具叫Auto Tools,包括automake autoconf等一系列工具。这些工具可以根据一个叫做http://Makefile.am的模板(与Makefile的区别是里面基本只写项目里的文件,因为这些文件的位置是我们自己可以控制的)自动生成Makefile。这些工具可以为我们自动检测一些常见平台的差异,并在生成的Makefile里面消除这些差异。
然而这个Auto Tools本身也是足够复杂的,使用起来并不是很方便,况且不支持Windows平台。有兴趣的可以参考
CMake是近年兴起的新秀,支持包括Windows在内的诸多平台,使用也比Auto Tools要方便不少。只需要写个CMakelists.txt就可以了。CMake在这里下载。
安装的时候,同样需要注意,因为我们工作在命令行,需要让安装程序设置环境参数,如上图。否则在命令行会找不到cmake。
接下来是Python。注意Python 2和Python 3是不兼容的。Python 2很古老但是久经考验,Python 3比较新,但是还不是很成熟。我们这里需要的是Python 2.7(因为Clang的test case是2.7接口的)。话说Python近年随着阿尔法大红大紫,因为人工智能领域用 Python 用得很多。一般来说,越是偏应用方向的(比如人工智能算法研究),越是用高阶的语言(脚本),避免在本来关心的事物之外花费时间。
当然,我们这里安装Python是为了跑 Clang 的测试 case,确认我们自己编译出的 Clang 功能正常。这个步骤是十分重要的。因为如果是编译器的bug带来的问题,一般都可以轻易将码农坑在里面几个月出不来。比如一个变量明显代入了1,后面读出来偏偏变成了2...(CPU Cache控制问题)这种问题是最难查出来的问题之一。
最后是 GnuWin32 Tools,这是一组开源命令行工具。Linux 什么的都是自带或者可以很方便地安装的。Windows 上面就需要下载安装:
GetGnuWin32 - Maintaining a Gnuwin32 Package archive
这些工具数量众多,我们这里主要也是为了跑 Clang 的测试 Case,就不一一展开了。
需要注意的是,网页上能下载的东西只是装了个下载器,装完之后需要进入安装目标目录,执行 download.bat 和 install.bat 完成安装。之后需要更改环境变量 PATH,保证在我们的命令行里面可以找到这些工具。(具体路径请根据你安装的路径修改)
关于如何改Windows的环境变量,参考下面:
http://jingyan.baidu.com/article/8ebacdf02d3c2949f65cd5d0.html
好了。现在我们重新启动命令行,来使修改的环境变量生效。(命令行窗口会一直保持启动的时候的环境变量,所以改了环境变量之后需要重启命令行才能反映出我们的修改)
输入svn help,看到类似下面的输出,说明subversion安装OK了:
C:UsersTim.AzureADSource>svn help usage: svn[options] [args] Subversion command-line client. Type 'svn help ' for help on a specific subcommand. Type 'svn --version' to see the program version and RA modules or 'svn --version --quiet' to see just the version number. Most subcommands take file and/or directory arguments, recursing on the directories. If no arguments are supplied to such a command, it recurses on the current directory (inclusive) by default. Available subcommands: add ...
输入 python,看到类似下面的输出,则说明 python 安装 OK 了:
C:UsersTim.AzureAD>python Python 2.7.13 (v2.7.13:a06454b1afa1, Dec 17 2016, 20:53:40) [MSC v.1500 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>>
按 Ctrl+Z,回车,退出 python 交互模式。
输入 grep,看到类似下面的输出,则说明 GnuWin32 工具也安装成功了。
C:UsersTim.AzureAD>grep Usage: grep [OPTION]... PATTERN [FILE]... Try `grep --help' for more information.
开始编译Clang
Clang 是基于 LLVM 的。所谓 LLVM,就是一个小小的虚拟机。这个虚拟机抽象了不同的硬件平台,如 x86/arm/mips 等。最近还抽象了 GPU。有点像 Java 的VM,但是又和 Java 的 VM 很不同。Java 的 VM 是比较高层的,它的 byte code 包括很多硬件平台并不能直接支持的功能。而 LLVM 的 byte code 则是更加接近硬件(CPU/GPU)的实际功能,只不过它是独立于任何一个具体硬件存在的。非常简单粗糙地比喻的话,各种 CPU/GPU 就好比各个地方的人,说的是各个地方的方言;而 LLVM 的 byte code 则有些像普通话,与方言有着比较类似1对1的对应关系。(当然严格地来讲并不是这么回儿事情)
所以,首先需要签出 LLVM 的代码,如下操作:
C:UsersTim.AzureADSource>svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
Clang 是作为 LLVM 的一个前端,即,把 C/C++ 翻译为 LLVM 可以懂的 byte code 的工具。LLVM 再把 byte code 翻译为具体的机器指令。执行下面的命令签出 Clang 的代码并放在LLVM妥当的位置:
C:UsersTim.AzureADSource>cd llvmtools C:UsersTim.AzureADSourcellvmtools>svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
还记得我们前面编译的 main.c 吗?编译产生的输出,也就是中间文件 main.obj,target 文件 main.exe 都是和 main.c 在一个目录里的。
C:UsersTim.AzureADSourceReposGameEngineFromScratch>dir 驱动器 C 中的卷是 OS 卷的序列号是 38A2-CBDD C:UsersTim.AzureADSourceReposGameEngineFromScratch 的目录 2017/08/18 09:31. 2017/08/18 09:31 .. 2017/08/18 08:30 302 .gitignore 2017/08/18 08:30 1,088 LICENSE 2017/08/18 09:29 71 main.c 2017/08/18 09:31 97,280 main.exe 2017/08/18 09:31 1,285 main.obj 2017/08/18 08:30 103 README.md 6 个文件 100,129 字节 2 个目录 883,355,103,232 可用字节
对于简单的程序我们可以这样。对于大型软件来说,如果我们这样编译,成千上万的中间文件会把整个目录搞得混乱不堪,非常不利于管理。最为关键的是,如果我们的代码支持一些编译选项,可以从一套代码里编译出不同的版本(比如最常见的,Debug 版和 Release 版),那么不同编译选项编译所生成的中间文件就会相互覆盖,最后搞得编译器也弄不清楚哪些文件编译过,是怎么编译的(按照什么选项编译的)。在我们码农的日常当中,如果我们遇到了一个项目第一次编译得过,第二次开始就出错,有的时候 clean 了重新编译也没用,那么多半就是这个原因了。
这种编译方式老外叫做"build in the (source) tree",这是不良的习惯。我们应该改掉。推荐的是“build outside the (source) tree”
所以让我们从 llvmtools 这个目录出去,然后建立一个 build 目录,专门用来保存编译过程当中生成的文件。
C:UsersTim.AzureADSourcellvmtools>cd .... C:UsersTim.AzureADSource>mkdir build C:UsersTim.AzureADSource>cd build C:UsersTim.AzureADSourcebuild>
因为我们现在电脑上还只有 Visual Studio 所提供的编译工具,所以我们需要使用 CMake 工具来生成 Visual Studio 所需的 Solution 文件和 Project 文件,以便使用 Visual Studio 来编译 LLVM
C:UsersTim.AzureADSourcebuild>cmake -G "Visual Studio 15" ..llvm
-G "Visual Studio 15" 表示生成 Visual Studio 2017 用的项目文件。为什么叫"Visual Studio 15",这是因为在 Visual Studio 6 之后,微软改变了产品命名方式,Visual Studio 7叫Visual Studio .NET 了。后面的版本更是,一会儿差一年一会儿差两年的。但是实际上他们内部仍然继续着这个序号,证据就是你看 Windows 里面的注册表当中的信息,就知道这个序号仍然在继续。(Office 也是类似)
所以,从6开始数,Visual Studio 2017 正好是15,Visual Studio 2015 是 14,Visual Studio 2013 则是 12。(嗯?13呢?被吃掉了?估计是13这个数字风水不好。。。)
如果记不住,可以用 cmake --help 命令查看:
C:UsersTim.AzureADSourceReposGameEngineFromScratch>cmake --help Usage cmake [options]cmake [options] Specify a source directory to (re-)generate a build system for it in the current working directory. Specify an existing build directory to re-generate its build system. Options -C = Pre-load a script to populate the cache. -D [: ]= = Create a cmake cache entry. -U = Remove matching entries from CMake cache. -G = Specify a build system generator. -T = Specify toolset name if supported by generator. -A = Specify platform name if supported by generator. -Wdev = Enable developer warnings. -Wno-dev = Suppress developer warnings. -Werror=dev = Make developer warnings errors. -Wno-error=dev = Make developer warnings not errors. -Wdeprecated = Enable deprecation warnings. -Wno-deprecated = Suppress deprecation warnings. -Werror=deprecated = Make deprecated macro and function warnings errors. -Wno-error=deprecated = Make deprecated macro and function warnings not errors. -E = CMake command mode. -L[A][H] = List non-advanced cached variables. --build = Build a CMake-generated project binary tree. -N = View mode only. -P = Process script mode. --find-package = Run in pkg-config like mode. --graphviz=[file] = Generate graphviz of dependencies, see CMakeGraphVizOptions.cmake for more. --system-information [file] = Dump information about this system. --debug-trycompile = Do not delete the try_compile build tree. Only useful on one try_compile at a time. --debug-output = Put cmake in a debug mode. --trace = Put cmake in trace mode. --trace-expand = Put cmake in trace mode with variable expansion. --trace-source= = Trace only this CMake file/module. Multiple options allowed. --warn-uninitialized = Warn about uninitialized values. --warn-unused-vars = Warn about unused variables. --no-warn-unused-cli = Don't warn about command line options. --check-system-vars = Find problems with variable usage in system files. --help,-help,-usage,-h,-H,/? = Print usage information and exit. --version,-version,/V [ ] = Print version number and exit. --help-full [ ] = Print all help manuals and exit. --help-manual [ ] = Print one help manual and exit. --help-manual-list [ ] = List help manuals available and exit. --help-command [ ] = Print help for one command and exit. --help-command-list [ ] = List commands with help available and exit. --help-commands [ ] = Print cmake-commands manual and exit. --help-module [ ] = Print help for one module and exit. --help-module-list [ ] = List modules with help available and exit. --help-modules [ ] = Print cmake-modules manual and exit. --help-policy [ ] = Print help for one policy and exit. --help-policy-list [ ] = List policies with help available and exit. --help-policies [ ] = Print cmake-policies manual and exit. --help-property [ ] = Print help for one property and exit. --help-property-list [ ] = List properties with help available and exit. --help-properties [ ] = Print cmake-properties manual and exit. --help-variable var [ ] = Print help for one variable and exit. --help-variable-list [ ] = List variables with help available and exit. --help-variables [ ] = Print cmake-variables manual and exit. Generators The following generators are available on this platform: Visual Studio 15 2017 [arch] = Generates Visual Studio 2017 project files. Optional [arch] can be "Win64" or "ARM". Visual Studio 14 2015 [arch] = Generates Visual Studio 2015 project files. Optional [arch] can be "Win64" or "ARM". Visual Studio 12 2013 [arch] = Generates Visual Studio 2013 project files. Optional [arch] can be "Win64" or "ARM". Visual Studio 11 2012 [arch] = Generates Visual Studio 2012 project files. Optional [arch] can be "Win64" or "ARM". Visual Studio 10 2010 [arch] = Generates Visual Studio 2010 project files. Optional [arch] can be "Win64" or "IA64". Visual Studio 9 2008 [arch] = Generates Visual Studio 2008 project files. Optional [arch] can be "Win64" or "IA64". Visual Studio 8 2005 [arch] = Deprecated. Generates Visual Studio 2005 project files. Optional [arch] can be "Win64". Borland Makefiles = Generates Borland makefiles. NMake Makefiles = Generates NMake makefiles. NMake Makefiles JOM = Generates JOM makefiles. Green Hills MULTI = Generates Green Hills MULTI files (experimental, work-in-progress). MSYS Makefiles = Generates MSYS makefiles. MinGW Makefiles = Generates a make file for use with mingw32-make. Unix Makefiles = Generates standard UNIX makefiles. Ninja = Generates build.ninja files. Watcom WMake = Generates Watcom WMake makefiles. CodeBlocks - MinGW Makefiles = Generates CodeBlocks project files. CodeBlocks - NMake Makefiles = Generates CodeBlocks project files. CodeBlocks - NMake Makefiles JOM = Generates CodeBlocks project files. CodeBlocks - Ninja = Generates CodeBlocks project files. CodeBlocks - Unix Makefiles = Generates CodeBlocks project files. CodeLite - MinGW Makefiles = Generates CodeLite project files. CodeLite - NMake Makefiles = Generates CodeLite project files. CodeLite - Ninja = Generates CodeLite project files. CodeLite - Unix Makefiles = Generates CodeLite project files. Sublime Text 2 - MinGW Makefiles = Generates Sublime Text 2 project files. Sublime Text 2 - NMake Makefiles = Generates Sublime Text 2 project files. Sublime Text 2 - Ninja = Generates Sublime Text 2 project files. Sublime Text 2 - Unix Makefiles = Generates Sublime Text 2 project files. Kate - MinGW Makefiles = Generates Kate project files. Kate - NMake Makefiles = Generates Kate project files. Kate - Ninja = Generates Kate project files. Kate - Unix Makefiles = Generates Kate project files. Eclipse CDT4 - NMake Makefiles = Generates Eclipse CDT 4.0 project files. Eclipse CDT4 - MinGW Makefiles = Generates Eclipse CDT 4.0 project files. Eclipse CDT4 - Ninja = Generates Eclipse CDT 4.0 project files. Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.
好了,然后用下面的命令build生成的Solution。 (注意要在Visual Studio的命令行里面。也就是开始菜单里面的Developer Command Prompt)当然你也可以双击LLVM.sln打开Visual Studio的IDE进行编译。效果其实一样的。
C:UsersTim.AzureADSourcebuild>msbuild LLVM.sln
这个编译看机器性能。我在 i7 8 核的 SSD 机器上大概1个半小时。
编译完成之后,我们来测试我们编译出的clang是否有问题。首先我们需要将生成物的目录加入环境变量PATH,以便在命令行能够找到它:(目录请根据你的本地实际情况修改)
重启命令行,检查是否可以找到 clang
C:UsersTim.AzureADSource>clang -v clang version 6.0.0 (trunk 311143) Target: i686-pc-windows-msvc Thread model: posix InstalledDir: C:UsersTim.AzureADSourcebuildDebugbin Found CUDA installation: /Program Files/NVIDIA GPU Computing Toolkit/CUDA/v8.0, version 8.0
最后一行 CUDA 是我的环境里面别的事情安装的。与目前无关。没有安装的应该看不到这一行。
然后确保我们目前是处于 LLVM 的顶级目录,就是下面有 llvm 和 build 这两个目录的那一级目录,执行下面的命令:
C:UsersTim.AzureADSource>python.exe llvmutilslitlit.py -sv --param=build_mode=Win32 --param=build_config=Debug --param=clang_site_config=buildtoolsclangtestlit.site.cfg llvmtoolsclangtest
我这里的环境是执行会失败,python 抱怨找不到一些测试用的程序。需要修改 buildtoolsclangtestlit.site.cfg
原来的版本:
## Autogenerated from C:/Users/Tim.AzureAD/Source/llvm/tools/clang/test/lit.site.cfg.in ## Do not edit! import sys config.llvm_src_root = "C:/Users/Tim.AzureAD/Source/llvm" config.llvm_obj_root = "C:/Users/Tim.AzureAD/Source/build" config.llvm_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/bin" config.llvm_libs_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/lib" config.llvm_shlib_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/bin" config.llvm_plugin_ext = ".dll" config.lit_tools_dir = "" config.clang_obj_root = "C:/Users/Tim.AzureAD/Source/build/tools/clang" config.clang_src_dir = "C:/Users/Tim.AzureAD/Source/llvm/tools/clang" config.clang_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/bin" config.host_triple = "i686-pc-win32" config.target_triple = "i686-pc-win32" config.llvm_use_sanitizer = "" config.have_zlib = 0 config.clang_arcmt = 1 config.clang_default_cxx_stdlib = "" config.clang_staticanalyzer = 1 config.clang_staticanalyzer_z3 = "" config.clang_examples = 0 config.enable_shared = 0 config.enable_backtrace = 1 config.host_arch = "AMD64" config.enable_abi_breaking_checks = ""
改为
## Autogenerated from C:/Users/Tim.AzureAD/Source/llvm/tools/clang/test/lit.site.cfg.in ## Do not edit! import sys config.llvm_src_root = "C:/Users/Tim.AzureAD/Source/llvm" config.llvm_obj_root = "C:/Users/Tim.AzureAD/Source/build" config.llvm_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/bin" config.llvm_libs_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/lib" config.llvm_shlib_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/bin" config.llvm_plugin_ext = ".dll" config.lit_tools_dir = "" config.clang_obj_root = "C:/Users/Tim.AzureAD/Source/build/tools/clang" config.clang_src_dir = "C:/Users/Tim.AzureAD/Source/llvm/tools/clang" config.clang_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/bin" config.host_triple = "i686-pc-win32" config.target_triple = "i686-pc-win32" config.llvm_use_sanitizer = "" config.have_zlib = 0 config.clang_arcmt = 1 config.clang_default_cxx_stdlib = "" config.clang_staticanalyzer = 1 config.clang_staticanalyzer_z3 = "" config.clang_examples = 0 config.enable_shared = 0 config.enable_backtrace = 1 config.host_arch = "AMD64" config.enable_abi_breaking_checks = ""
就是把所有的%(build_mode)改为%(build_config)
如果是用 vim 修改,可以用“:%s/build_mode/build_config/g”这条命令一次修改完毕。
感觉上应该是不同的 Visual Studio 对于项目文件当中 Output 目录宏展开的方式不同导致的。
测试正常执行的样子是这样的:
C:UsersTim.AzureADSource>python llvmutilslitlit.py -sv --param=build_mode=Win32 --param=build_config=Debug --param=clang_site_config=buildtoolsclangtestlit.site.cfg llvmtoolsclangtest lit.py: C:/Users/Tim.AzureAD/Source/llvm/tools/clang/test/lit.cfg:200: note: using clang: 'C:/Users/Tim.AzureAD/Source/build/Debug/bin/clang.EXE' lit.py: C:UsersTim.AzureADSourcellvmutilslitlitdiscovery.py:190: warning: test suite 'Clang-Unit' contained no tests -- Testing: 9208 tests, 8 threads -- ******************** Testing: 0 .. 10.. 20.. 30.. 40.. 50.. 60.. 70.. 80.. 90.. Testing Time: 843.01s ******************** Failing Tests (2): Clang :: Driver/offloading-interoperability.c Clang :: Driver/openmp-offload-gpu.c Expected Passes : 9077 Expected Failures : 24 Unsupported Tests : 105 Unexpected Failures: 2 1 warning(s) in tests.
星号当中的是进度条。在 i7 8 核心的机器上大约需要10分钟左右。
我这里执行的过程当中出现一些 CUDA 相关的错误,应该是版本不匹配(我的是CUDA 8.0,比较新)导致,可以无视。
用新的 Toolchain 编译我们的 Hello Engine
C:UsersTim.AzureADSourceReposGameEngineFromScratch>clang main.c main.c:3:1: warning: return type of 'main' is not 'int' [-Wmain-return-type] void main() { ^ main.c:3:1: note: change return type to 'int' void main() { ^~~~ int 1 warning generated.
可以看到,在 Visual Studio 下面编译完全没有问题的代码,在 clang 下面出现了 warning。所以,我们需要尽早摆脱微软的安乐窝。(*^_^*)
暂无关于此文章的评论。