将 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
2
cd module
ndk-build -j$(nproc) # windows 平台不要用 -j 参数

和普通的 ndk-build 项目一样,编译产物在 module/libs 中。

接下来我们看下 ndk-build 项目的一些配置:

Application.mk:

1
2
3
4
APP_ABI      := armeabi-v7a arm64-v8a x86 x86_64
APP_CPPFLAGS := -std=c++17 -fno-exceptions -fno-rtti -fvisibility=hidden -fvisibility-inlines-hidden
APP_STL := none
APP_PLATFORM := android-21

Application.mk 中设置了 APP_ABIAPP_CPPFLAGSAPP_STLAPP_PLATFORMAPP_ABIAPP_PLATFORM我们按需正常设置即可。比较关键的是 APP_CPPFLAGSAPP_STL

APP_CPPFLAGS 设置了若干编译参数。下文将叙述如何正确的设置这些参数。

关于 APP_STL,README 中是这样说的:

  • The APP_STL variable in Application.mk is set to none. 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 included libc++ 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 the new operator.
  • Both configurations are demonstrated in the example Android.mk.

个人理解,这里的 hosting program 应该指的是 zygote fork 的子进程。一般的 app 都不会使用系统的 libc++,而是使用 ndk 中的 libc++。如果编译 zygisk 模块时又引入了新的 libc++,可能就会引起冲突。

示例所说的两种配置在 Android.mk 文件中。根据实际的情况选择对应的配置,注释或删除掉另一种即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := example
LOCAL_SRC_FILES := example.cpp
LOCAL_STATIC_LIBRARIES := libcxx
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)

include jni/libcxx/Android.mk

# If you do not want to use libc++, link to system stdc++
# so that you can at least call the new operator in your code

# include $(CLEAR_VARS)
# LOCAL_MODULE := example
# LOCAL_SRC_FILES := example.cpp
# LOCAL_LDLIBS := -llog -lstdc++
# include $(BUILD_SHARED_LIBRARY)

使用 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 GeneratorsMakefile Generators/Ninja Generators 两种配置方法。本文仅介绍 Makefile Generators/Ninja Generators 的配置方法。

而对于 Makefile Generators/Ninja Generators,又分为 NDK独立工具连两种配置方式。本文仅介绍 NDK 的配置方法。(通常情况下,你应该使用 NDK 的配置方法。独立工具链只适用于 r18 或更低版本的ndk, r19以上版本已弃用,详见独立工具链)。

根据 CMake 文档以及 zygisk 模板的 Application.mk,我们可能会编写出如下CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
cmake_minimum_required(VERSION 3.26)
project(zygisk_module_sample_cmake)

set(CMAKE_CXX_STANDARD 17)

set(CMAKE_SYSTEM_NAME Android)
set(CMAKE_ANDROID_NDK /path/to/ndk)
set(CMAKE_SYSTEM_VERSION 21)
set(CMAKE_ANDROID_ARCH_ABI arm64-v8a)
set(CMAKE_ANDROID_STL_TYPE none)
set(CMAKE_CXX_FLAGS "-std=c++17 -fno-exceptions -fno-rtti -fvisibility=hidden -fvisibility-inlines-hidden")
# ...

然而这样并不能正确的生成。原因在于 CMake 执行后已经初始化好了环境信息,CMakeLists.txt 中设置的变量无效了。事实上,所有这些控制构建的变量都不应放到 CMakeLists.txt 中,应该写入一个工具链文件中,然后通过 CMAKE_TOOLCHIAN_FILE 指定工具链文件;或是直接在命令行参数中指定所有的参数。我个人建议使用后者。因为如果编写工具链文件,都需要通过命令行传递不同的架构参数,那还不如直接调用命令行传递所有参数。最后将所需要的命令行写在一个generator.sh脚本中。

因此对于 Android 这部分的配置放到命令行参数中:

1
2
3
4
5
-DCMAKE_SYSTEM_NAME=Android
-DCMAKE_SYSTEM_VERSION=21
-DCMAKE_ANDROID_ARCH_ABI=arm64-v8a
-DCMAKE_ANDROID_NDK=/path/to/android-ndk
-DCMAKE_ANDROID_STL_TYPE=none

对于 APP_CPPFLAGS,可以通过 CMake 内建的配置而不是直接修改 CMAKE_CXX_FLAGS 来完成,这样可以防止其余编译参数被覆盖

编译参数 -fno-exceptions-fno-rtti,CMake 提供了独立的配置选项 CMAKE_ANDROID_RTTICMAKE_ANDROID_EXCEPTIONS。默认情况下,CMake 会关闭 exceptions 和 rtti。不过最好还是显式的指定一下。(ndk 中的工具链文件对于这两个参数的的行为就和cmake 集成的行为不同,如果使用 ndk 的工具链文件,如果不指定 CMAKE_ANDROID_RTTI=offCMAKE_ANDROID_EXCEPTIONS=off,编译时就不会添加 -fno-exceptions-fno-rtti 编译参数)。CMAKE_ANDROID_RTTICMAKE_ANDROID_EXCEPTIONS 属于控制构建的变量,因此应该放到命令行参数中:

1
2
-DCMAKE_ANDROID_RTTI=off
-DCMAKE_ANDROID_EXCEPTIONS=off

编译参数 -std=c++17,可以通过设置 CMAKE_CXX_STANDARDCMAKE_CXX_EXTENSIONS 来完成

1
2
3
4
# 对应 -std=c++17
set(CMAKE_CXX_STANDARD 17)
# 如果 CMAKE_CXX_EXTENSIONS 为ON,则编译参数为 -std=gnu++17
set(CMAKE_CXX_EXTENSIONS off)

编译参数 -fvisibility=hidden-fvisibility-inlines-hidden,CMake 提供了对应的配置选项 CMAKE_VISIBILITY_INLINES_HIDDENCMAKE_CXX_VISIBILITY_PRESET:

1
2
set(CMAKE_VISIBILITY_INLINES_HIDDEN on)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)

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/includezygis-module-sample/module/jni/libcxx/src 目录复制到某个 include 目录(可以在项目中如 include,也可以是外部的全局目录),然后使用 include_directories 指定 include 目录。注意需要在 link_librariesinclude_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
2
3
4
5
6
# ...
# use prebuilt libcxx.a
set(LIBCXX_PATH "/path/to/libcxx")
include_directories(${LIBCXX_PATH}/include)
include_directories(${LIBCXX_PATH}/src)
link_directories(${LIBCXX_PATH}/libs/${CMAKE_ANDROID_ARCH_ABI})

使用 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.txtAndroid.mk 其实非常简单粗暴,直接设定源文件、头文件、编译参数、链接参数等,没有任何复杂的逻辑。因此翻译成 CMakeLists.txt 也比较简单,也是设定源文件、头文件、编译参数。翻译后的 CMakeLists.txt 如下,删除了几个 CMake 默认会添加的编译参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
cmake_minimum_required(VERSION 3.13.4)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_EXTENSIONS off)
set(CMAKE_VISIBILITY_INLINES_HIDDEN on)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)

set(libcxx_includes
include
src
)

set(libcxx_sources
src/algorithm.cpp
src/any.cpp
src/atomic.cpp
src/barrier.cpp
src/bind.cpp
src/charconv.cpp
src/chrono.cpp
src/condition_variable.cpp
src/condition_variable_destructor.cpp
src/debug.cpp
src/exception.cpp
src/filesystem/directory_iterator.cpp
src/filesystem/int128_builtins.cpp
src/filesystem/operations.cpp
src/functional.cpp
src/future.cpp
src/hash.cpp
src/ios.cpp
src/ios.instantiations.cpp
src/iostream.cpp
src/legacy_debug_handler.cpp
src/legacy_pointer_safety.cpp
src/locale.cpp
src/memory.cpp
src/mutex.cpp
src/mutex_destructor.cpp
src/new.cpp
src/optional.cpp
src/random_shuffle.cpp
src/random.cpp
src/regex.cpp
src/shared_mutex.cpp
src/stdexcept.cpp
src/string.cpp
src/strstream.cpp
src/system_error.cpp
src/thread.cpp
src/typeinfo.cpp
src/utility.cpp
src/valarray.cpp
src/variant.cpp
src/vector.cpp
src/verbose_abort.cpp
)

set(libcxx_cxxflags "-fvisibility-global-new-delete-hidden -DLIBCXX_BUILDING_LIBCXXABI -D_LIBCPP_NO_EXCEPTIONS -D_LIBCPP_NO_RTTI -D_LIBCPP_BUILDING_LIBRARY -D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS -D__STDC_FORMAT_MACROS")

set(libcxxabi_src_files
src/abi/abort_message.cpp
src/abi/cxa_aux_runtime.cpp
src/abi/cxa_default_handlers.cpp
src/abi/cxa_exception_storage.cpp
src/abi/cxa_guard.cpp
src/abi/cxa_handlers.cpp
src/abi/cxa_noexception.cpp
src/abi/cxa_thread_atexit.cpp
src/abi/cxa_vector.cpp
src/abi/cxa_virtual.cpp
src/abi/stdlib_exception.cpp
src/abi/stdlib_new_delete.cpp
src/abi/stdlib_stdexcept.cpp
src/abi/stdlib_typeinfo.cpp
)

set(libcxxabi_includes
include
include/abi
)

set(libcxxabi_cflags
-D__STDC_FORMAT_MACROS
)

set(libcxxabi_cppflags "-D_LIBCXXABI_NO_EXCEPTIONS -Wno-macro-redefined -Wno-unknown-attributes -DHAS_THREAD_LOCAL"
)

include_directories(${libcxx_includes})
include_directories(${libcxxabi_includes})
set(CMAKE_CXX_FLAGS "${libcxx_cxxflags} ${libcxxabi_cppflags} ${CMAKE_CXX_FLAGS}")

add_library(cxx STATIC ${libcxx_sources} ${libcxxabi_src_files})

修改后,使用 add_subdirectory 添加,注意还需要使用 include_directories 设置 libcxx 的头文件目录。

1
2
3
add_subdirectory(libcxx)
include_directories(libcxx/include)
include_directories(libcxx/include/src)

总结

最终的 CMakeLists.txt 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cmake_minimum_required(VERSION 3.26)
project(zygisk_module_sample_cmake)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_EXTENSIONS off)
set(CMAKE_VISIBILITY_INLINES_HIDDEN on)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)

# use prebuilt libcxx.a
#set(LIBCXX_PATH "/path/to/libcxx")
#include_directories(${LIBCXX_PATH}/include)
#include_directories(${LIBCXX_PATH}/src)
#link_directories(${LIBCXX_PATH}/libs/${CMAKE_ANDROID_ARCH_ABI})

# build cxx source
add_subdirectory(libcxx)
include_directories(libcxx/include)
include_directories(libcxx/include/src)

add_library(example SHARED example.cpp)

target_link_libraries(example log cxx)

编译单一架构:

1
2
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
cmake --build build -j $(nproc)

使用脚本编译多个架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env sh
TARGETS="armeabi-v7a arm64-v8a x86 x86_64"

for TARGET in ${TARGETS}
do
echo "build/${TARGET}"
# create one build dir per target architecture
mkdir -p "build/${TARGET}"

cmake -G Ninja -B "build/${TARGET}" -S . -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=21 -DCMAKE_ANDROID_ARCH_ABI="${TARGET}" -DCMAKE_ANDROID_NDK="${ANDROID_NDK_HOME}" -DCMAKE_ANDROID_STL_TYPE=none -DCMAKE_ANDROID_RTTI=off -DCMAKE_ANDROID_EXCEPTIONS=off -DCMAKE_BUILD_TYPE=Release

cmake --build "build/${TARGET}" --config Release -j $(nproc)
done