将 zygisk-module-sample 迁移至 cmake
zygisk 的示例模块 zygisk-module-sample 由 ndk-build 进行构建。本文讲述如何使用 cmake 构建一个 zygisk 模块。
使用 ndk 构建
我们先看下官方的构建方法:
克隆项目
1 | git clone https://github.com/topjohnwu/zygisk-module-sample.git |
项目根目录中有 gradle 配置。但是不要用这个 gradle
构建模块,因为里面没有包含 native 的构建配置。 根据 README 中的 Building
提示,我们直接进入 module 文件夹,执行 ndk-build
即可。
1 | cd module |
和普通的 ndk-build 项目一样,编译产物在 module/libs
中。
接下来我们看下 ndk-build 项目的一些配置:
Application.mk:
1 | APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 |
Application.mk 中设置了
APP_ABI
、APP_CPPFLAGS
、APP_STL
和
APP_PLATFORM
。APP_ABI
和APP_PLATFORM
我们按需正常设置即可。比较关键的是
APP_CPPFLAGS
和 APP_STL
。
APP_CPPFLAGS
设置了若干编译参数。下文将叙述如何正确的设置这些参数。
关于 APP_STL
,README 中是这样说的:
- The
APP_STL
variable inApplication.mk
is set tonone
. DO NOT use any C++ STL included in NDK. - If you'd like to use C++ STL, you have to use the
libcxx
included as a git submodule in this repository. Zygisk modules' code are injected into Zygote, and the includedlibc++
is setup to be lightweight and fully self contained that prevents conflicts with the hosting program. - If you do not need STL, link to the system
libstdc++
so that you can at least call thenew
operator. - Both configurations are demonstrated in the example
Android.mk
.
个人理解,这里的 hosting program 应该指的是 zygote fork 的子进程。一般的 app 都不会使用系统的 libc++,而是使用 ndk 中的 libc++。如果编译 zygisk 模块时又引入了新的 libc++,可能就会引起冲突。
示例所说的两种配置在 Android.mk
文件中。根据实际的情况选择对应的配置,注释或删除掉另一种即可
1 | LOCAL_PATH := $(call my-dir) |
使用 cmake 构建
CMake 集成 Android NDK 环境配置
查阅 Google 的文档以及 CMake 的文档,我们会发现 Google 和 CMake 有两套不同的构建方式。Google 文档中是这样写的:
CMake 拥有自己的内置 NDK 支持。在 CMake 3.21 之前,此工作流程不受 Android 支持,并且经常会被新的 NDK 版本中断。从 CMake 3.21 开始,这些实现将被合并。CMake 的内置支持与 NDK 工具链文件具有类似的行为,但变量名称有所不同。从 Android NDK r23 开始,工具链文件将在使用 CMake 3.21 或更高版本时委托给 CMake 的内置支持。 如需了解详情,请参阅问题 463。请注意,Android Gradle 插件仍会自动使用 NDK 的工具链文件。
因此,强烈建议使用 CMake 3.21 或更高版本,并且使用 CMake 的内置 NDK 支持,不要用 ndk 中的 CMake 工具链文件。
而根据所使用的开发环境的不同,CMake
又有Visual Studio Generators
和
Makefile Generators/Ninja Generators
两种配置方法。本文仅介绍
Makefile Generators/Ninja Generators
的配置方法。
而对于 Makefile Generators/Ninja Generators
,又分为 NDK和独立工具连两种配置方式。本文仅介绍
NDK 的配置方法。(通常情况下,你应该使用 NDK
的配置方法。独立工具链只适用于 r18 或更低版本的ndk,
r19以上版本已弃用,详见独立工具链)。
根据 CMake 文档以及 zygisk 模板的
Application.mk
,我们可能会编写出如下CMakeLists.txt
:
1 | cmake_minimum_required(VERSION 3.26) |
然而这样并不能正确的生成。原因在于 CMake
执行后已经初始化好了环境信息,CMakeLists.txt
中设置的变量无效了。事实上,所有这些控制构建的变量都不应放到
CMakeLists.txt
中,应该写入一个工具链文件中,然后通过
CMAKE_TOOLCHIAN_FILE
指定工具链文件;或是直接在命令行参数中指定所有的参数。我个人建议使用后者。因为如果编写工具链文件,都需要通过命令行传递不同的架构参数,那还不如直接调用命令行传递所有参数。最后将所需要的命令行写在一个generator.sh
脚本中。
因此对于 Android 这部分的配置放到命令行参数中:
1 | -DCMAKE_SYSTEM_NAME=Android |
对于 APP_CPPFLAGS
,可以通过 CMake
内建的配置而不是直接修改 CMAKE_CXX_FLAGS
来完成,这样可以防止其余编译参数被覆盖
编译参数 -fno-exceptions
和
-fno-rtti
,CMake 提供了独立的配置选项
CMAKE_ANDROID_RTTI
和
CMAKE_ANDROID_EXCEPTIONS
。默认情况下,CMake 会关闭
exceptions 和 rtti。不过最好还是显式的指定一下。(ndk
中的工具链文件对于这两个参数的的行为就和cmake 集成的行为不同,如果使用
ndk 的工具链文件,如果不指定 CMAKE_ANDROID_RTTI=off
和
CMAKE_ANDROID_EXCEPTIONS=off
,编译时就不会添加
-fno-exceptions
和 -fno-rtti
编译参数)。CMAKE_ANDROID_RTTI
和
CMAKE_ANDROID_EXCEPTIONS
属于控制构建的变量
,因此应该放到命令行参数中:
1 | -DCMAKE_ANDROID_RTTI=off |
编译参数 -std=c++17
,可以通过设置
CMAKE_CXX_STANDARD
和 CMAKE_CXX_EXTENSIONS
来完成
1 | # 对应 -std=c++17 |
编译参数 -fvisibility=hidden
和
-fvisibility-inlines-hidden
,CMake 提供了对应的配置选项
CMAKE_VISIBILITY_INLINES_HIDDEN
和
CMAKE_CXX_VISIBILITY_PRESET
:
1 | set(CMAKE_VISIBILITY_INLINES_HIDDEN on) |
C++STL
如果不使用 C++STL,则只需链接一下系统的 stdc++ 即可。(注意需要注释掉
example.cpp 中的 #include <cstdlib>
这一行)。
1 | target_link_libraries(example log stdc++) |
如果需要使用 C++STL,则情况稍微复杂一些。首先 zygisk-module-samble 使用的 cxx 是经过作者修改过的 llvm-libcxx,转为 Android 设计,移除了对异常和RTTI的支持。编译方式也从 cmake 改为了 ndk-build。
使用预编译 libcxx
比较简单的一种方法是将该 libcxx 先用 ndk-build
编译成静态库,然后再链接到 zygisk 模块中。在原始的
zygisk-module-sample/module
文件夹中执行
ndk-build
后,在
zygisk-module-sample/module/obj/local/<arch>/
目下会生成 libcxx.a
。将其复制到某个 lib
目录(可以在项目中如 libs,也可以是外部目录),然后使用
link_libraries
制定链接目录,再链接 libcxx 。之后将
zygisk-module-sample/module/jni/libcxx/include
和
zygis-module-sample/module/jni/libcxx/src
目录复制到某个
include 目录(可以在项目中如 include,也可以是外部的全局目录),然后使用
include_directories
指定 include 目录。注意需要在
link_libraries
和 include_directories
之后创建
target 才能链接成功。
假设外部的 libcxx 目录结构如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/path/to/libcxx
├── include
│ ├── ...
│ └── wctype.h
├── src
│ ├── ...
│ └── verbose_abort.cpp
├── libs
│ ├── armeabi-v7a
│ │ └── libcxx.a
│ ├── arm64-v8a
│ │ └── libcxx.a
│ ├── x86
│ │ └── libcxx.a
│ └── x86_64
│ └── libcxx.a
└── Android.mk
则可以使用如下的 CMakeLists.txt 配置包含目录和链接目录:
1 | # ... |
使用 libcxx 源码
你也可以像 zygisk-module-sample 一样,直接将 libcxx 的源码放到项目中。这样做的好处是无需做额外的配置即可直接编译。但是这样做的缺点是编译时间会变长。
topjohnwu 的 libcxx 项目只配置了 ndk-build
的配置文件,并未适配 CMake
。项目中的
CMakeLists.txt
文件原自
llvm-project
(作为llvm-project
),不能直接用
add_subdirectory
添加进来,需要对
CMakeLists.txt
进行修改,更改一些配置。改原版 libcxx 的
CMakeLists.txt
难度较大,不如直接将 Android.mk
翻译成 CMakeLists.txt
。Android.mk
其实非常简单粗暴,直接设定源文件、头文件、编译参数、链接参数等,没有任何复杂的逻辑。因此翻译成
CMakeLists.txt
也比较简单,也是设定源文件、头文件、编译参数。翻译后的
CMakeLists.txt
如下,删除了几个 CMake
默认会添加的编译参数:
1 | cmake_minimum_required(VERSION 3.13.4) |
修改后,使用 add_subdirectory
添加,注意还需要使用
include_directories
设置 libcxx
的头文件目录。
1 | add_subdirectory(libcxx) |
总结
最终的 CMakeLists.txt
如下:
1 | cmake_minimum_required(VERSION 3.26) |
编译单一架构:
1 | cmake -G Ninja -B build -S . -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=21 -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a -DCMAKE_ANDROID_NDK="${ANDROID_NDK_HOME}" -DCMAKE_ANDROID_STL_TYPE=none -DCMAKE_ANDROID_RTTI=off -DCMAKE_ANDROID_EXCEPTIONS=off |
使用脚本编译多个架构:
1 | !/usr/bin/env sh |