跨平台:GN实践详解(ninja,编译,windowsmacandroid实战)
强烈推荐
展开
⽬录
⼀、概览
⼆、跨平台代码编辑器
三、GN⼊门
四、⽰范⼯程
五、关键细节
六、结语 [编译器选项]
其中前两部分是前缀部分,原本没有跨平台构建经验和知识的同学可以借助来帮助理解,后四部分则是讲述GN⼯程的基本结构、如何搭建⼀个GN构建的⼯程、以及关键的⼀些GN知识
⼀、概览
如何开始这个话题是我⽐较在意的,因为对于部分⼈⽽⾔,真正从思维和理解上切⼊这篇⽂章真正要阐述的点是有困难的。这在于跨平台编译和开发这块,如果没有⼀定的经历,可能会很难理解这些跨平台⼯具出现的真正意义,它们所要解决的问题是什么,所以这⾥我需要做⼀点前缀⼯作,如果对GN/GYP/CMake这类话题已经有上下⽂的同学可以直接跳过这部分赘述内容,同时也希望借助对这篇⽂章的理解,⼤家往后对于跨平台⼯具的理解不局限于GN,⽽是有⾃⼰的认知。在不断发展和变化的⼤潮中,⼯具始终是在演变,能够始终抓住紧要的精髓,见到陌⽣的⼯具时对它从根上有淡定从容的认知,以最⼩的精⼒消耗掌握它,最快发挥它的价值服务于我们,才是我们的取胜之道。
集成编译开发环境
以实际⼯作为例,我们在开发过程中需要接触到的有源代码⼯程管理、代码编辑、代码编译,⽽统⼀囊括了这⼏部分的开发环境就是集成开发环境,由于集成开发环境的存在,⼤部分⼈其实只需要重点关注代码编辑这块,⼯程管理和编译集成开发环境已经内置做了绝⼤部分⼯作,所以理所当然也就容易忽略它们,我们也有必要重新认识下这⼏部分⼯作。
苹果删减片段⼯程管理:源代码包含、模块输出设置(动态库/静态库/可执⾏⽂件、模块依赖关系配置、编译选项设置、链接选项设置、⽣成后执⾏事件……等⾮常多的设置。
代码编辑:⾃动补齐、格式对齐、定义跳转、关键字⾼亮、代码提⽰……等我们编写代码时⽤着很⼈性化的功能。
代码编译:语法语⾳分析、代码优化、错误提⽰、警告提⽰、最终⽣成⼆进制代码和符号表。
各操作系统应⽤开发的集成开发环境
Windows应⽤开发:Visual Studio,简称VS,是微软提供和⽀持的集成开发编译环境,⽤于开发Windows上应⽤。
Mac/iOS应⽤开发: Xcode,是苹果提供和⽀持的集成开发编译环境,⽤于开发Mac和iOS应⽤。
Android应⽤开发:Android Studio,简称AS,是⾕歌提供和⽀持的集成开发环境,⽤于开发Android应⽤。
Ubuntu等Linux应⽤开发:没有集成开发环境,⼯程管理使⽤CMake/GN/GYP…各类⼯具(也可以使⽤⼈脑)、代码编辑使⽤vim等⼯具,编译器使⽤GCC。
但实际上,这些集成开发环境都不约⽽同的将编译相关的细节很好的隐藏起来了,因为这块实在是太晦涩和繁琐,这⾥我们需要有⼀个很清晰的认知,那就是这些集成开发环境之下,隐藏的编译器基本上只
有:微软的msvc,GNU的gcc,苹果的clang+llvm这三⼤类,让我们再次展开这⼏个集成开发环境,⼀窥的它的全貌
其中蓝⾊部分就是跨平台⼯具的主战场,旨在脱离各平台开发环境的限制,统⼀进⾏⼯程管理和⼯程设置,最终由⼯具⾃动⽣成统⼀的各平台makefile⽂件,调⽤各平台编译器完成编译。
沿途的风景
在通往终点的路上,总有⼀些风景值得欣赏,如果有感兴趣的同学,可以品味下编译器的技术历史,以及gcc+llvm这种畸形产物的闲闻逸事:
1.msvc,对于微软⽽⾔,闭源是它⼀直以来的做派,如今有所改善,⽽编译器则是那时候的产物,所以理所当然,宇宙最强IDE中的编译器,只限于windows这个平台上
<,是GNU开发的编程语⾔编译器,以GPL及LGPL许可证所发⾏的⾃由软件,开放给所有⼈,是最通⽤和⼴泛的编译器
<+llvm,苹果早期使⽤的gcc,gcc⾮常通⽤,⽀持多种语⾔,但对苹果官⽅提出的Objective-C编译器优化的诉求爱答不理,导致苹果只能想办法⾃⼰来做,⼀般⽽⾔,按照保守的重构做法⼀部分⼀部分渐进式替换原有模块是⽐较理性的思路,苹果也是采⽤这种策略,将编译器分为前后两端,前端做语义
语法分析,后端做指令⽣成编译,⽽后端则是苹果实现⾃⼰愿望的第⼀步,由此llvm横空出世。
后端使⽤llvm,在编译期间针对性优化,例如以显卡特性为例,将指令转换为更为⾼效的GPU指令,提升运⾏性能。
前端使⽤gcc,在编译前期做语义语法分析等⾏为
4.clang+llvm,由于gcc语义语法分析上,对于Objective-C做得不够细致,gcc+llvm这种组合⽆法提供更为⾼级的语法提⽰、错误纠正等能⼒,所以苹果开始更近⼀步,开发实现了编译器前端clang,所以到⽬前为⽌xcode中缺省设置是clang+llvm,clang在语义和语法上更为先进和⼈性化,但缺点显⽽易见,Clang只⽀持C,C++和Objective-C,Swift这⼏种苹果体系内会⽤到的语⾔。
跨平台开发遇到的问题
⼀般⽽⾔,如果⼀个应⽤需要在各个操作系统上都有实现,基本的⽅式都是将⼤部分能复⽤的代码和逻辑封装到底层,减少冗余的开发和维护⼯作,像、QQ、⽀付宝等知名软件,都是这种⽅式。由于各个操作系统平台的集成开发环境不同,这部分底层⼯程需要在各个集成开发环境中搭建和维护⼯程、编译应⽤,这样就会产⽣⼤量重复⼯作,任何⼀个源码⽂件的增加/删除、模块调整都会涉及到多个集成开发环境的⼯程调整。这个时候,⼤多数⼈都会想,如果有这么⼀个⼯具,我们⽤它管理⼯程和编译,⼀份⼯程能够⾃动编译出各个平台的应⽤或底层库,那将是多么美好的⼀个事情。
幸运的是,已经有前⼈替我们感受过这个痛苦的过程,也有前⼈为我们种好了树,我们只需要好⼀个合适的树,背靠着它,调整好坐姿,怀着⽆⽐感恩的⼼情在它的树荫下乘凉。树有好多课,该选哪⼀颗,⼤家兴趣⾄上⾃⾏选择,这⾥我们主要介绍我个⼈倾向的GN这⼀棵。
原始跨平台:
编写makefile⽂件,使⽤各平台上的make(微软的MS nmake、GNU的make)来编译makefile⽂件,这种做法的缺点是各平台的make实现不同,导致这种原始的做法其实复⽤度并不⾼,需要针对各平台单独编写差异巨⼤的makefile⽂件,那为什么要介绍它呢,因为这是跨平台的根,所有跨平台⼯具,最终都是要依赖各平台应⽤的集成开发环境的编译器来执⾏编译,这是固定不变的,也就是说各平台的编译,最终还是需要各平台的makefile,这⼀点是⽆法逃避的,⽽怎么由⼈⼯转为⾃动化,才是跨平台编译的进阶之路。
进阶跨平台:
使⽤cmake,编写统⼀的makefile⽂件,最后由cmake⾃动⽣成各平台相关的makefile⽂件执⾏编译,这⼀点上,cmake已经是⽐较好的跨平台⼯具了,⼀般的跨平台⼯程基本已经满⾜需求了。
现代跨平台:
当⼯程规模增⼤到难以想象的量级时,编译速度和⼯程模块的划分变得尤为重要,其中chromium⼯程就遇到这两个问题,于是最初诞⽣了gyp,最后演化升级为gn,其旨在追求⼯程更加清晰的模块和结构呈现,以及更快的编译速度。前者通过语法层⾯实现,后者则依靠ninja来提升编译速度,因为⼤型⼯程的编译,很⼤⼀部分时间都花在了源⽂件编译依赖树的分析这块,⽽ninja更像是⼀个编译器的预处理,其主要⽬的是舍弃gcc、msvc、clang等编译器在编译过程中递归查依赖的⽅式,因为这⾥存在很多重复的依赖查,⽽ninja改进了这⼀过程,提前⽣成编译依赖树,编译期间按照编译依赖树的顺序依次编译,这样就⼤⼤减少了编译期间杂乱的编译顺序造成的重复依赖关系查。
关于⼀点说明:
本⽂主要针对底层库领域的跨平台构建,这也是⽐较常见和通⽤的跨平台应⽤⽅式,但不排除有例外,像Qt就是⼀套从底层开发到UI框架,再到代码编辑、⼯程管理的C++整体跨平台解决⽅案,它的涵盖⾯更⼤,只有应⽤层使⽤Qt的UI框架时才能发挥它的真正威⼒(有兴趣的同学可以研究它,⽬前我所在团队研发的CCtalk客户端Windows和Mac,以及即将外发的Chromebook版都是使⽤它,我也打算在⼀个合适的时机从我最擅长的UI框架领域来介绍Qt的实战应⽤),⽽这篇⽂章寻求的是⽬前为⽌更通⽤化的跨平台⽅式,也就是C++跨平台底层+原⽣UI应⽤层的组合⽅式,所以本⽂跨平台的切⼊重点也是针对底层库这⼀块,做得好的应⽤,⼀般会把⽹络、数据、业务上下⽂状态管理归属到底层由跨平台实现,这部分可以达到整个应⽤程序代码量的60~70%,完成这块的复⽤是⾮常值得的。
⼆、跨平台代码编辑器
gn解决了跨平台编译问题,但是各平台的代码编辑,并不属于底层C++跨平台构建⼯具的范畴(全框架C++流程的Qt例外)。⼀种做法是,通过gn⽣成各个平台的⼯程(xcode⼯程、vs⼯程)然后再进⾏代码编写,源⽂件和⼯程模块的修改需要另外同步到gn⼯程⽂件中;另⼀种做法是使⽤vscode。显⽽易见,使⽤vscode+gn是最佳选择,这样就相当于⼀个各平台统⼀的集成开发环境,这⾥我给⼤家预览下vscode和gn配合之下的预览图:
代码编写使⽤vscode,有各种插件提供了语法提⽰和检测、⾃动补齐、⾼亮等编辑器功能
代码编译使⽤gn,在vscode中直接有内置的命令⾏,直接运⾏编译脚本执⾏编译,编译脚本中是gn的编译命令的调⽤
在vscode+gn的使⽤过程中,我们会慢慢体会到代码编辑、编译的⽩盒流程,⽽不是以前的⿊盒,对编辑和编译流程的理解也将越来越深刻,这是软件开发越来越上层的当下,最难能可贵的地⽅,是抱有⼀颗求知之⼼的⼈最珍视的东西。*
鸟瞰⼀个GN⼯程
我们先从⼯程的全貌来看,这是我们demo⼯程的全局视图,GN组织⼀个跨平台⼯程分为3⼤块:
第1部分:整体⼯程⼊⼝,这部分常年都不⽤做修改
第2部分:GN通⽤⽂件,这部分常年都不⽤做修改
第3部分:GN源代码⼯程⽂件,这部分与平常我们在集成开发环境中类似,源⽂件的组织和管理就在这个部分
详解各GN⽂件职责
四、⽰范⼯程
⼀般⽽⾔,这⼀部分仔细阅读后,基本就可以依葫芦画瓢使⽤起GN了,更加细节的部分则需要靠⼤家阅读官⽅⽂档以及后续的第五部分内容
准备环境
1.安装vscode(主要是⽅便⼤家阅读⼯程结构,也可以跳过这⼀步,只是会增加⼯程的理解难度⽽已)
vscode的使⽤⾮常简单,指定打开某个⽂件夹,然后整个⽂件夹的⽬录结构就⾃动呈现在vscode的左侧了,同时插件安装异常⽅便,直接在vscode左侧最后⼀个tab中搜索关键字就会有插件的在线安装提⽰,根据提⽰安装好安装C++插件、gn插件等插件后,就可以作为代码编
辑器编写代码了。
2.安装python3
关于python的语法,简单了解就好,遇到看不懂的稍微查下语法,脚本语⾔总是易于理解和使⽤的,⼤家要有信⼼。
⼯程⽬录结构
各平台编译脚本执⾏
windows:在gn_project⽬录下,命令⾏中运⾏(vscode内置的更⽅便)命令build_for_win.bat debug
mac:gn_project⽬录下,命令⾏中运⾏(vscode内置的更⽅便)命令./build_for_mac.sh debug
android: gn_project⽬录下,命令⾏中运⾏(vscode内置的更⽅便)命令./build_for_android.sh debug arm
mac平台编译⼊⼝脚本
./build_for_mac.sh,关键脚本如下(只是截取部分关键代码,旨在让⼤家看到各平台下gn编译命令的调
⽤):
# ninja files
$gn gen $build_cache_path --args="is_debug=$debug_mode target_cpu=“x64"”
if [ $? != 0 ]; then
echo “generate ninja failed”
exit 1
fi
echo -
echo -
echo ---------------------------------------------------------------
echo 第4步:开始ninja编译…
echo ---------------------------------------------------------------
# build
$ninja -C $build_cache_path > $log_path
if [ $? != 0 ]; then
echo “build failed. log: $log_path”
exit 1
fi
windows平台编译⼊⼝脚本
./build_for_win.sh 和 ./build_for_win.py,关键脚本如下(只是截取部分关键代码,旨在让⼤家看到各平台下gn编译命令的调⽤):
# --------------------------------------------------------------------------------
# 3. build kernel
# --------------------------------------------------------------------------------
# 1) generate ninja file for build
# 2) build
# 3) print end log
# --------------------------------------------------------------------------------
print(“build kernel …”)
gn_cmd = ‘"{}" gen “{}” --args=“is_debug={} target_cpu=\“x86\” win_vc=\”{}\" win_vc_ver=\"{}\" win_xp={}"’.format(gn, build_cache_path, debug_mode, win_vc, vs_ver, support_xp)
proc = subprocess.run(gn_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=False)
stdout = sole_to_str(proc.stdout)
stderr = sole_to_str(proc.stderr)
if ‘Done.’ not in stdout:
print(stdout)
print(stderr)
with open(os.path.join(build_cache_path, ‘build.log’), ‘w’) as log:
log.write(stdout)
ninja_cmd = ‘"{}" -C “{}”’.format(ninja, build_cache_path)
proc = subprocess.run(ninja_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=False)
stdout = sole_to_str(proc.stdout)
stderr = sole_to_str(proc.stderr)
urncode != 0:
print(stdout)
print(stderr)
with open(os.path.join(build_cache_path, ‘build.log’), ‘w’) as log:
log.write(stdout)
with open(os.path.join(build_cache_path, ‘build.log’), ‘w’) as log:
log.write(stdout)
android平台编译⼊⼝脚本
./build_for_android.sh,关键脚本如下(只是截取部分关键代码,旨在让⼤家看到各平台下gn编译命令的调⽤):
echo -
echo -
echo ---------------------------------------------------------------
echo 第5步:使⽤gn⽣成ninja⽂件
echo ---------------------------------------------------------------
# ninja file
$gn gen $build_cache_path --args="
target_os = “android”
target_cpu = “$target_config”
use_qt_for_android = false
ndk = “$ndk”
qt_sdk = “$qt_sdk”"
if [ $? != 0 ]; then
echo “generate ninja failed”
exit 1
fi
echo -
echo -
echo ---------------------------------------------------------------
echo 第6步:使⽤ninja开始编译…
echo ---------------------------------------------------------------
# build
$ninja -C $build_cache_path > $log_path
if [ $? != 0 ]; then
exit 1
fi
整个⼯程的各个代码模块
⼯程总共有多少模块,需要在./⽂件中配置,⽽各个模块的⼯程则在各个模块中,关键代码如下(只是截取部分关键代码,旨在呈现整体⼯程各个模块):
具体代码模块
以整体⼯程⽂件./中各个模块的配置为例,"//system_wrappers:system_wrappers"表⽰在system_wrappers⽬录下,寻 ⽂件,在这个⽂件中定义了system_wrappers模块,以及该模块的源⽂件、编译选项设置等,具体代码如下:
i⽂件
这是⼀个GN模版⽂件,是我们⾃定义的⼀个GN⽂件,⽂件名可以根据⼤家的喜好⾃⾏修改,确保在要使⽤的地⽅import进来即可,每个代码模块的编译选项设置其实是有蛮多编译选项和⼯程定义需要设置,⽽这些编译选项和定义的设置其实是重复的,如果每个模块都写⼀遍,会⽐较冗余,所以我们抽取
了公共的模版,使得各个代码模块的⽂件可以只关注源代码包含、附件包含⽬录、附加包含库等⼯程相关的事项。
五、关键细节
这是GN⽐较进阶的⼀部分,需要参考官⽅⽂档中语法部分进⾏解读
template(模版)的作⽤是抽取公共gn代码,节省整体的gn代码量,使得⼯程⽂件更容易阅读,以i为例,有⼏个关键语法我们需要熟知:
模版中的变量和模版实例之间的数据传递,是通过invoker来承载,例如以sources的值为例,模版中可以通过sources = invoker.sources来传递值
模版实例的名称,则通过target_name来承载,例如,我们定义了模版libaray,那么library(“aysnevent”)的target_name值就是”aysnevent" template(“library”) {
assert(defined(invoker.sources), “Need sources in $target_name listing the source files.”)
type = “static_library”
if (is_win) {
type = “shared_library”
}
if (is_mac || is_android) {
type = “source_set”
}
target(type, target_name) {
configs += [
“//:qt_config”,
]
sources = invoker.sources
# output_prefix_override = true
output_dir = “$root_out_dir”
lib_dirs = [
rebase_path("$output_dir/libs"),
]
if (type == “static_library”) {
output_dir += “/libs”
}
if (defined(invoker.deps)) {
if (defined(deps)) {
deps += invoker.deps
} else {
deps = invoker.deps
}
}
if (defined(invoker.data_deps)) {
if (defined(data_deps)) {
data_deps += invoker.data_deps
} else {
data_deps = invoker.data_deps
}
}
if (defined(invoker.include_dirs)) {
if (defined(include_dirs)) {
include_dirs += invoker.include_dirs
} else {
include_dirs = invoker.include_dirs
}
}
if (figs)) {
if (defined(configs)) {
configs += figs
} else {
configs = figs
}
}
if (defined(invoker.public_configs)) {
if (defined(public_configs)) {
public_configs += invoker.public_configs
} else {
public_configs = invoker.public_configs
}
}
if (defined(invoker.cflags)) {
if (defined(cflags)) {
cflags += invoker.cflags
} else {
cflags = invoker.cflags
}
}
if (defined(invoker.cflags_cc)) {