开源第三方C/C++库移植Android系统

如何将第三方开源库移植到Android系统。
Views: 396
2 0
Read Time:6 Minute, 27 Second

最近由于项目需要,需要将第三方库移植到Android系统。在此之前我并没有开源库的移植经验,抱着一边工作一边学习的态度总算是完成了移植工作,而这篇文章将对近期的移植工作做一个总结,以供后期查备。

所谓移植,也就是让第三方库在Android平台上运行并能够被正常调用。由于很多第三方库都不是为Android系统开发的,为了能够让其正常运行,就涉及到交叉编译。

交叉编译简单来讲就是在A平台上编译出可以在B平台上运行的程序。对于Andoid开发者而言,就是在Windows/Linux/Mac系统上编译出可以在Android上运行的程序。

编译器

我们都知道C/C++的编译需要经过四个步骤:预处理、编译、汇编、链接。这些都需要编译器的支持,不同的平台、不同标准的支持程度(如C++11/C++17/C++21),编译器也需要做对应的选择。我们在交叉编译时第一点需要明确的就是编译工具链(编译器)的种类。

目前主流的编译器种类如下:

GCC:GNU Compiler,是目前应用最为广泛,最为出名的编译器,不仅支持C/C++(GCC/G++),也支持其他的语言。对各类编程语言的新标准、新特性支持力度是比较大的。

Clang/LLVM:这是另外一套著名的编译器组合,其中Clang作为前端,LLVM作为后端,可以为编译提供更好的组件化支持,如编译优化等,相比GCC支持也更多的语言。

Intel C++ Compiler:是Intel为Windows开发平台所开发维护的编译器,也是Microsoft Visual Studio所使用的编译器。

Keil C++ Compiler:ARM 出品的用于在Windows上开发arm嵌入式平台的编译器,很多单片机/MCU/STM32等开发则是使用该编译器。

除此之外还有一些编译器,他们的应用场景各自不同。而Android系统的编译器选择也经历过两个阶段,在Android7.1.1版本之前,使用GCC编译器;在这之后则使用Clang/LLVM作为编译器。故而在移植时我们就需要考虑到这一点,需要选择合适的编译器。

编译环境

编译环境包括编译器的选择,但除了编译器本身,还有一些基础的依赖库,以及系统所支持的一些标准库。由于我们移植的第三方库中可能会存在一些系统调用,或者是一些标准库依赖,这些都是与平台绑定的。如Linux平台的Glibc,Android平台的Bionic-c等。这些基础的依赖库是由编译环境所提供的。

在Android系统中,为开发者提供了统一的开发环境——NDK,也就是Native Developement Kit,在NDK中为我们提供了Android平台上可用的基础依赖库。我们移植C/C++库到Android系统中时,就需要基于NDK的编译环境去做配置。

NDK目前提供了Windows(64bit)、Linux(64bit)与Mac三个系统的安装包,我们可以根据自己的实际需要来选择合适的安装包,可通过该链接进行下载,同时也可以通过该链接查看每个NDK版本之间的差异与改动。

NDK版本会对应到Android版本以及Android API版本。以下为一个简单的对照表:

代号版本NDK版本API 级别
Android12L12.1NDK r25c  API 级别 32
Android1212NDK r25c API 级别 31
Android1111NDK r24bAPI 级别 30
Android1010NDK r23b API 级别 29
Pie9NDK r22bAPI 级别 28
Oreo8.1.0NDK r21eAPI 级别 27
Oreo8.0.0NDK r21eAPI 级别 26
Nougat7.1NDK r20b API 级别 25
Nougat7.0NDK r20b API 级别 24
Marshmallow6.0NDK r20b API 级别 23

更完整的列表可以参考这里

这里需要说明的是,NDK的版本会对应Android版本是指NDK的版本会随着Android版本的发布而一同发布,并不是指NDK只支持对应的Android版本,事实上同一个NDK版本是支持不同API级别的。而这些不同API级别可能跨越不同的Android版本。

移植时我们需要关注NDK版本,因为不同的NDK版本支持的API级别是不一样的,而这些API级别同样会影响基础依赖库的实现。如何查看API级别,我们可以随便从Android系统中找出某个系统库使用file命令进行查看:

trout_x86:/vendor/lib # 
trout_x86:/vendor/lib # file [email protected]                                                                                                                                        
[email protected]: ELF shared object, 32-bit LSB 386, for Android 32, BuildID=6efcc183a452ae5e68e3cb8d78eb83a6, stripped
trout_x86:/vendor/lib # 
trout_x86:/vendor/lib # 

如图,我们系统的API级别为32,需要使用支持API级别的NDK进行编译。

编译系统

相信有很大部分人认为编译系统与编译环境两者是同一类东西,但实际上两者是不一样的。同一套编译系统可以支持不同的编译环境。那么什么是编译系统呢,在我自己的理解里,编译系统是帮我们配置编译环境的。因为在编译时,我们可能需要某些条件判断,可能需要设定某些编译参数,可能需要解析某些编译配置。而这些,都是由编译系统所支持的。

这里举两个例子,比如Android系统编译时的soong编译系统,在编译时识别的是Android.mk或者Android.bp;而许多开源系统中比较常用的CMake编译系统,会根据顶层目录的CMakeList.txt以及*.cmake来构建。

编译系统很明显的一点就是,一般都提供了一层warp配置文件,来控制我们编译时的许多参数。但从实质上来讲,大部分的编译系统其实底层都依赖于GNU Make,编译系统就是帮我们生成各种makefile和编译参数。

在移植C/C++库至Android系统中时,编译环境是必须基于NDK的。但是编译系统我们是可以选择的。在Google官方文档中,目前支持的编译系统包括三类:

1.基于make的ndk-build编译系统

2.CMake编译系统

3.基于Autotools/Autoconf/libtool的编译系统

下面将会针对这三种编译系统进行展开,讲解如何基于这三种编译系统去进行交叉编译/移植。

NDK-Build

使用NDK-Build时,环境配置是非常简单的,基础环境需要GNU Make 4的版本。在NDK环境中已经包含。使用NDK-Build时,其内部原理其实就是用基础的make命令进行编译,不过需要通过-f参数指定基础的makefile,如下所示:

$GNUMAKE -f <ndk>/build/core/build-local.mk

具体使用方法:

首先我们需要位于我们待移植源码的顶层目录,使用如下命令:

${NDK_DIR}/ndk-build

一些支持的选项如下:

clean:清除编译缓存,作用相当于make clean

-B:强制执行完整的重新构建。

NDK_LOG:显示NDK Build过程中的日志消息。

NDK_HOST_32BIT:配置是否始终使用32位模式下的工具链。

NDK_APPLICATION_MK:配置所使用的Application.mk。

在正在开始编译之前,我们需要编写相应的Application.mk与Android.mk,这对于许多开发者人员是比较麻烦的。一般我们移植的第三方库都有成熟的编译系统支持,能够支持NDK的编译工具链,此时我们不推荐使用ndk-build这种方式去完成编译。但如果你移植的第三方库只有源代码,并没有成熟的编译系统做支持,那使用ndk-build是比较合理的。

CMake

CMake起源于2000年,截至到今天已经走过了23个年头。作为当今开源世界广泛采用的编译系统,其效率与灵活性都得到了开发人员们的认可。

NDK为Cmake编译系统提供了官方支持的工具链文件,CMake通过该工具链文件可以完成基础的设定,关于工具链文件的信息,可以参考Cmake官方网站的说明

这里先看一下使用Cmake编译系统的编译流程,我们进入到工程项目的顶层目录之后,会看到一个名为CMakeLists.txt的文件,这是CMake编译系统编译配置的入口,在其内部可能会include其他的.cmake编译配置。此时我们一般只需要经过以下几个步骤即可完成编译:

mkdir build && cd build //创建build文件夹,将用于存储编译过程中的中间产物
cmake .. 
make -j4 //进行编译
make install //将编译好的内容安装到指定位置

使Cmake调用NDK提供的工具链该如何使用命令行参数呢,在上述步骤不变的情况下,我们只需要在第二步通过CMake参数来完成设定,示例如下:

cmake \
    -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=$ABI \
    -DANDROID_PLATFORM=android-$MINSDKVERSION \
    $OTHER_ARGS

以下将对各个参数做出释义:

-DCMAKE_TOOLCHAIN_FILE:用于指定NDK提供的工具链文件,其中$NDK代表NDK的安装路径,我们可以手动设置该环节变量,也可以直接使用绝对路径作为替代,该参数是必须的。

-DANDROID_ABI:用于指定与Android运行环境所匹配的CPU/指令集组合。Android设备可以运行在多种CPU架构上,而不同的CPU支持不同的指令集。我们在交叉编译时就需要明确目标运行环境的CPU与指令集。目前支持的ABI选项包括:armeabi-v7aarm64-v8ax86x86_64.

-DANDROID_PLATFORM:用于指定底层依赖库所支持的最低API级别,该参数也可以通过其他别名来进行设定,如android-$API_LEVEL$API_LEVEL等。通常一个NDK版本会支持多个API级别。如何查看每个NDK版本所支持的API级别呢,我们可以下载NDK后解压进行查看,如下图所示:

FranzKafkaYu@:/opt/FranzKafkaYu/ndk/android-ndk-r21e/platforms$ ls -la
total 60
drwxr-xr-x 15 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 .
drwxr-xr-x 13 FranzKafkaYu FranzKafkaYu 4096 Mar 29 17:02 ..
drwxr-xr-x  4 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-16
drwxr-xr-x  4 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-17
drwxr-xr-x  4 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-18
drwxr-xr-x  4 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-19
drwxr-xr-x  6 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-21
drwxr-xr-x  6 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-22
drwxr-xr-x  6 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-23
drwxr-xr-x  6 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-24
drwxr-xr-x  6 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-26
drwxr-xr-x  6 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-27
drwxr-xr-x  6 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-28
drwxr-xr-x  6 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-29
drwxr-xr-x  6 FranzKafkaYu FranzKafkaYu 4096 Jan 11  2021 android-30
FranzKafkaYu@:/opt/FranzKafkaYu/ndk/android-ndk-r21e/platforms$ 

这意味着android-ndk-r21e版本所支持的Android API最低为16,最高为30。如果我们不进行指定,则默认会使用该NDK所支持的最低API级别。为了保证顺利移植,我们务必指定与我们运行环境所匹配的API版本。否则可能会在编译阶段报出很多错误。这里我们可以打开一个NDK提供的头文件(此处以android-ndk-r21e/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/pthread.h为例)进行查看:

#if __ANDROID_API__ >= 28
int pthread_attr_getinheritsched(const pthread_attr_t* __attr, int* __flag) __INTRODUCED_IN(28);
#endif /* __ANDROID_API__ >= 28 */

int pthread_attr_getschedparam(const pthread_attr_t* __attr, struct sched_param* __param);
int pthread_attr_getschedpolicy(const pthread_attr_t* __attr, int* __policy);
int pthread_attr_getscope(const pthread_attr_t* __attr, int* __scope);
int pthread_attr_getstack(const pthread_attr_t* __attr, void** __addr, size_t* __size);
int pthread_attr_getstacksize(const pthread_attr_t* __attr, size_t* __size);
int pthread_attr_init(pthread_attr_t* __attr);
int pthread_attr_setdetachstate(pthread_attr_t* __attr, int __state);
int pthread_attr_setguardsize(pthread_attr_t* __attr, size_t __size);

#if __ANDROID_API__ >= 28
int pthread_attr_setinheritsched(pthread_attr_t* __attr, int __flag) __INTRODUCED_IN(28);
#endif /* __ANDROID_API__ >= 28 */

int pthread_attr_setschedparam(pthread_attr_t* __attr, const struct sched_param* __param);
int pthread_attr_setschedpolicy(pthread_attr_t* __attr, int __policy);
int pthread_attr_setscope(pthread_attr_t* __attr, int __scope);
int pthread_attr_setstack(pthread_attr_t* __attr, void* __addr, size_t __size);
int pthread_attr_setstacksize(pthread_attr_t* __addr, size_t __size);

int pthread_condattr_destroy(pthread_condattr_t* __attr);

#if __ANDROID_API__ >= 21
int pthread_condattr_getclock(const pthread_condattr_t* __attr, clockid_t* __clock) __INTRODUCED_IN(21);
#endif /* __ANDROID_API__ >= 21 */

int pthread_condattr_getpshared(const pthread_condattr_t* __attr, int* __shared);
int pthread_condattr_init(pthread_condattr_t* __attr);

我们可以看到很多函数 定义都由__AND ROID_API__宏进行控制,如果API版本不匹配,可能会导致无法找到对应的函数定义。

除了上述比较重要的参数外,还支持如下参数,此处给出一个命令参数示例:

arguments :
-DCMAKE_FIND_ROOT_PATH=${HOME}/Dev/ello-jni/app/bug/prefab/armeabi-v7a/prefab
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_TOOLCHAIN_FILE=${HOME}/Android/Sdk/ndk/build/cmake/android.toolchain.cmake
-DANDROID_ABI=armeabi-v7a
-DANDROID_NDK=${HOME}/Android/Sdk/ndk/22.1.7171670
-DANDROID_PLATFORM=android-23
-DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a
-DCMAKE_ANDROID_NDK=${HOME}/Android/Sdk/ndk/22.1.7171670
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
-DANDROID_STL=c++_shared 
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=${HOME}/Dev/github-/universalDebug/obj/armeabi-v7a
-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=${HOME}/Dev/dk-samples/Debug/obj/armeabi-v7a
-DCMAKE_MAKE_PROGRAM=${HOME}/Android/Sdk/cmake/3.10.2.4988404/bin/ninja
-DCMAKE_SYSTEM_NAME=Android
-DCMAKE_SYSTEM_VERSION=23

Autotools

Autotools是另外一套编译系统,相比于CMake历史更为悠久,其致力于GNU标准下的编译配置,是很多Linux发行版和许多基础库所采用的编译系统。

基于Autotools的编译系统在编译时一般遵循如下步骤:

1./configure //顶层目录下执行
2.make  //编译
3.make install //安装

如果是采用Autotools编译系统的开源库如何移植呢。Google当然也想到了这一点,为我们提供了相应的工具来支持交叉编译。

在NDK环境中,提供了一个make_standalone_toolchain.py的Python脚本,在旧的NDK版本中是名为make-standalone-toolchain.sh的Shell脚本,我们可以通过这个脚本生成并安装自定义工具链。在Autotools的编译系统中,我们需要通过该脚本完成自定义工具链的设定,举例如下:

# Create an arm64 API 26 libc++ toolchain.
$NDK/build/tools/make_standalone_toolchain.py \
  --arch arm64 \
  --api 26 \
  --install-dir=my-toolchain

我们可以通过该脚本的help参数来获取它所支持的参数信息:

–arch:与Cmake中类似,用于定义ABI。需要与我们Android系统的运行环境相匹配,目前支持arm\arm64\x86\x86_64选项。

–api:用于设定Android API级别,该参数同样很重要。

–install-dir:用于设定自定义工具链的安装路径。

–stl:用于指定所使用的STL标准库。

在安装完自定义工具链后,我们还需要手动设置编译环境,如下所示:

# Add the standalone toolchain to the search path.
export PATH=$PATH:/path-to/my-toolchain/bin

# Tell configure what tools to use.
target_host=aarch64-linux-android
export AR=$target_host-ar
export AS=$target_host-clang
export CC=$target_host-clang
export CXX=$target_host-clang++
export LD=$target_host-ld
export STRIP=$target_host-strip

# Tell configure what flags Android requires.
export CFLAGS="-fPIE -fPIC"
export LDFLAGS="-pie"
export LDFLAGS="-llog"

之后我们在原来的步骤基础上稍作变化即可进行编译:

1./configure --host=$target_host//顶层目录下执行
2.make  //编译
3.make install //安装

以上即是开源库移植Android系统的常见方法,编译完成后我们可以将编译产物放入到Android运行环境中,如果是二进制binary,需要push到/system/bin目录下,如是so共享库,64位需要push到/system/lib64,而32位则push到/system/lib目录。

我们可以通过ldd命令查看其相应的依赖是否正常,如下为我将Google protobuf移植到Android系统后的示例:

trout_x86:/ # ldd /system/lib/libprotobuf.so                                                                                                                                                               
        linux-gate.so.1 => [vdso] (0xf30a0000)
        liblog.so => /system/lib/liblog.so (0xf2a09000)
        libz.so => /system/lib/libz.so (0xf29c9000)
        libc++_shared.so => /system/lib/libc++_shared.so (0xf2a40000)
        libm.so => /apex/com.android.runtime/lib/bionic/libm.so (0xf2b47000)
        libc.so => /apex/com.android.runtime/lib/bionic/libc.so (0xf2383000)
        libdl.so => /apex/com.android.runtime/lib/bionic/libdl.so (0xf2b9d000)
        libc++.so => /system/lib/libc++.so (0xf22c4000)
trout_x86:/ # 

如果还有其他问题,如undefined reference这类问题导致无法正常运行时,我们可以通过nm、readelf、objdump等命令来进行排查。

Happy
Happy
100 %
Sad
Sad
0 %
Excited
Excited
0 %
Sleepy
Sleepy
0 %
Angry
Angry
0 %
Surprise
Surprise
0 %
FranzKafka95
FranzKafka95

极客,文学爱好者。如果你也喜欢我,那你大可不必害羞。

文章: 85

留下评论

您的电子邮箱地址不会被公开。 必填项已用*标注

zh_CNCN