本文转自知乎,由超级码农陈文礼大神编写,已获原作者授权,原文链接: https://zhuanlan.zhihu.com/p/28598462
上一篇我们写了一个最基本的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 - 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:\Users\Tim.AzureAD\Source>svn help
usage: svn <subcommand> [options] [args]
Subversion command-line client.
Type 'svn help <subcommand>' 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:\Users\Tim.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:\Users\Tim.AzureAD>grep
Usage: grep [OPTION]... PATTERN [FILE]...
Try `grep --help' for more information.
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:\Users\Tim.AzureAD\Source>svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
Clang是作为LLVM的一个前端,即,把C/C++翻译为LLVM可以懂的byte code的工具。LLVM再把byte code翻译为具体的机器指令。执行下面的命令签出Clang的代码并放在LLVM妥当的位置:
C:\Users\Tim.AzureAD\Source>cd llvm\tools
C:\Users\Tim.AzureAD\Source\llvm\tools>svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
还记得我们前面编译的main.c吗?编译产生的输出,也就是中间文件main.obj,target文件main.exe都是和main.c在一个目录里的。
C:\Users\Tim.AzureAD\Source\Repos\GameEngineFromScratch>dir
驱动器 C 中的卷是 OS
卷的序列号是 38A2-CBDD
C:\Users\Tim.AzureAD\Source\Repos\GameEngineFromScratch 的目录
2017/08/18 09:31 <DIR> .
2017/08/18 09:31 <DIR> ..
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”
所以让我们从llvm\tools这个目录出去,然后建立一个build目录,专门用来保存编译过程当中生成的文件。
C:\Users\Tim.AzureAD\Source\llvm\tools>cd ..\..
C:\Users\Tim.AzureAD\Source>mkdir build
C:\Users\Tim.AzureAD\Source>cd build
C:\Users\Tim.AzureAD\Source\build>
因为我们现在电脑上还只有Visual Studio所提供的编译工具,所以我们需要使用CMake工具来生成Visual Studio所需的Solution文件和Project文件,以便使用Visual Studio来编译LLVM
C:\Users\Tim.AzureAD\Source\build>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:\Users\Tim.AzureAD\Source\Repos\GameEngineFromScratch>cmake --help
Usage
cmake [options] <path-to-source>
cmake [options] <path-to-existing-build>
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 <initial-cache> = Pre-load a script to populate the cache.
-D <var>[:<type>]=<value> = Create a cmake cache entry.
-U <globbing_expr> = Remove matching entries from CMake cache.
-G <generator-name> = Specify a build system generator.
-T <toolset-name> = Specify toolset name if supported by
generator.
-A <platform-name> = 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 <dir> = Build a CMake-generated project binary tree.
-N = View mode only.
-P <file> = 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=<file> = 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 [<f>] = Print version number and exit.
--help-full [<f>] = Print all help manuals and exit.
--help-manual <man> [<f>] = Print one help manual and exit.
--help-manual-list [<f>] = List help manuals available and exit.
--help-command <cmd> [<f>] = Print help for one command and exit.
--help-command-list [<f>] = List commands with help available and exit.
--help-commands [<f>] = Print cmake-commands manual and exit.
--help-module <mod> [<f>] = Print help for one module and exit.
--help-module-list [<f>] = List modules with help available and exit.
--help-modules [<f>] = Print cmake-modules manual and exit.
--help-policy <cmp> [<f>] = Print help for one policy and exit.
--help-policy-list [<f>] = List policies with help available and exit.
--help-policies [<f>] = Print cmake-policies manual and exit.
--help-property <prop> [<f>] = Print help for one property and exit.
--help-property-list [<f>] = List properties with help available and
exit.
--help-properties [<f>] = Print cmake-properties manual and exit.
--help-variable var [<f>] = Print help for one variable and exit.
--help-variable-list [<f>] = List variables with help available and exit.
--help-variables [<f>] = 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:\Users\Tim.AzureAD\Source\build>msbuild LLVM.sln
这个编译看机器性能。我在i7 8核的SSD机器上大概1个半小时。
(注意:采用上面的方法编译出的是x86的Debug版本。如果需要编译x64的Release版本,请如下使用CMake):
cmake -G "Visual Studio 15 2017 Win64" -DCMAKE_BUILD_TYPE=Release -Thost=x64 ..\llvm
msbuild ALL_BUILD.vcxproj /V:m /p:Platform=x64 /p:Configuration=Release /t:rebuild
编译完成之后,我们来测试我们编译出的clang是否有问题。首先我们需要将生成物的目录加入环境变量PATH,以便在命令行能够找到它:(目录请根据你的本地实际情况修改)
重启命令行,检查是否可以找到clang
C:\Users\Tim.AzureAD\Source>clang -v
clang version 6.0.0 (trunk 311143)
Target: i686-pc-windows-msvc
Thread model: posix
InstalledDir: C:\Users\Tim.AzureAD\Source\build\Debug\bin
Found CUDA installation: /Program Files/NVIDIA GPU Computing Toolkit/CUDA/v8.0, version 8.0
最后一行CUDA是我的环境里面别的事情安装的。与目前无关。没有安装的应该看不到这一行。
然后确保我们目前是处于LLVM的顶级目录,就是下面有llvm和build这两个目录的那一级目录,执行下面的命令:
C:\Users\Tim.AzureAD\Source>python.exe llvm\utils\lit\lit.py -sv --param=build_mode=Win32 --param=build_config=Debug --param=clang_site_config=build\tools\clang\test\lit.site.cfg llvm\tools\clang\test
我这里的环境是执行会失败,python抱怨找不到一些测试用的程序。需要修改build\tools\clang\test\lit.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:\Users\Tim.AzureAD\Source>python llvm\utils\lit\lit.py -sv --param=build_mode=Win32 --param=build_config=Debug --param=clang_site_config=build\tools\clang\test\lit.site.cfg llvm\tools\clang\test
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:\Users\Tim.AzureAD\Source\llvm\utils\lit\lit\discovery.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,比较新)导致,可以无视。
C:\Users\Tim.AzureAD\Source\Repos\GameEngineFromScratch>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。所以,我们需要尽早摆脱微软的安乐窝。(_)
(-- EOF -- )
本作品采用知识共享署名 4.0 国际许可协议进行许可。