使用 NDK 移植 Linux C/C++ 程序到 Android 系统

区分基础概念:JNI 与 NDK

  • JNI(Java Native Interface)是一种 Java 语言特性

用于 Java 程序与 C、C++ 库间的互相调用。

  • NDK(Native Development Kit)是 Google 提供的使用 C/C++ 编写 Android 程序的开发工具包

它使用 JNI 实现 Java 程序调用 C/C++ 本地代码,允许 C/C++ 本地代码访问 Android API,不只是用来开发或移植 C/C++ 库,也可以是 C/C++ 程序。

引用自 Android NDK  |  Android NDK  |  Android Developers

Android NDK

The Android NDK is a toolset that lets you implement parts of your app in native code, using languages such as C and C++. For certain types of apps, this can help you reuse code libraries written in those languages.

NDK 提供一系列稳定的 C/C++ API,头文件在 sysroot/usr/include 下,主要包括 C 标准库、C++ 标准库、jni、math、pthread、zlib、OpenGL、Android 相关的库, NDK 支持的 API 也会随着需求的增加而日趋完善。

移植使用 GNU Autotools 的项目

NDK 提供了创建独立工具链的工具,对于移植使用 GNU Autotools 的项目到 Android 平台很有帮助,省去写 Android.mk

  • 创建独立工具链

    /opt/android-ndk/build/tools/make_standalone_toolchain.py \
        --arch arm64 --api 28 --stl=libc++ --install-dir /opt/android-toolchain
    
  • 环境变量配置

    export PATH=$PATH:/opt/android-toolchain/bin
    export CC=aarch64-linux-android-clang
    export CXX=aarch64-linux-android-clang++
    export LD=aarch64-linux-android-ld
    export AR=aarch64-linux-android-ar
    export STRIP=aarch64-linux-android-strip
    export RANLIB=aarch64-linux-android-ranlib
    export AS=aarch64-linux-android-clang
    export CFLAGS="-fPIE -fPIC"
    export CXXFLAGS="-fPIE -fPIC"
    export LDFLAGS="-pie"
    export SYSROOT=/opt/android-toolchain/sysroot
    export CROSS_COMPILE_HOST=aarch64-linux-android
    
  • 交叉编译

    ./configure --host=${CROSS_COMPILE_HOST} --prefix=/opt/local
    make
    make install
    

移植 Nginx 遇到的问题

  • 编译出错 cstddef:43:25: fatal error: stddef.h: No such file or directory

    看起来是 C++ 编译器找不到 C 头文件,是已知问题,在 Standalone 工具链中使用 gcc 就会出现,见

    stddef.h: No such file or directory · Issue #215 · android-ndk/ndk

    改为使用 clang 就好了,以后的 NDK 将彻底移除对 gcc 的支持。

  • 交叉编译 OpenSSL

    网上有大量的移植文档,基本上是基于 OpenSSL Android - OpenSSLWiki ,最大的问题是使用的 NDK 和 OpenSSL 版本都比较旧,最后主要参考

    couchbaselabs/couchbase-lite-libcrypto: Pre-built OpenSSL libcrypto static libraries

    移植成功。OpenSSL 的交叉编译需要完整的 NDK 包,最好新开一个 Shell 来编译 OpenSSL,避免为独立工具链设置的环境变量影响到 OpenSSL 的编译。

    编译过程中会报错 crtbegin_so.o: No such file: No such file or directory ,将它从工具链中拷到当前目录即可, crtend_so.ocrtbegin_dynamic.ocrtend_android.o 也进行相同处理,参考 gcc - crtbegin_so.o missing for android toolchain (custom build) - Stack Overflow

    # Download
    wget https://codeload.github.com/openssl/openssl/zip/OpenSSL_1_1_0-stable -O openssl-OpenSSL_1_1_0-stable.zip
    unzip openssl-OpenSSL_1_1_0-stable.zip
    wget https://raw.githubusercontent.com/couchbaselabs/couchbase-lite-libcrypto/master/build-android-setenv.sh -O openssl.setenv
    sed -i -e 's/^_ANDROID_NDK=/#_ANDROID_NDK=/g' openssl.setenv
    
    # Config
    export ANDROID_NDK_ROOT=/opt/android-ndk
    export _ANDROID_TARGET_SELECT=arch-arm64-v8a
    export _ANDROID_NDK="android-ndk"
    export ANDROID_EABI_PREFIX=aarch64-linux-android
    export _ANDROID_EABI="${ANDROID_EABI_PREFIX}-4.9"
    export _ANDROID_ARCH=arch-arm64
    export _ANDROID_API="android-28"
    source openssl.setenv
    
    # Build
    cd openssl-OpenSSL_1_1_0-stable
    ./Configure dist
    ./Configure no-ssl2 no-ssl3 no-comp no-hw no-engine --openssldir=/opt/local/ --prefix=/opt/local/ linux-generic64 -DB_ENDIAN -B$ANDROID_DEV \
                -I${ANDROID_NDK_ROOT}/sysroot/usr/include -I${ANDROID_NDK_ROOT}/sysroot/usr/include/${ANDROID_EABI_PREFIX} \
                -fPIE -pie -L${ANDROID_NDK_ROOT}/platforms/${_ANDROID_API}/${_ANDROID_ARCH}/usr/lib
    ln -s ${SYSROOT}/usr/lib/crtbegin_so.o
    ln -s ${SYSROOT}/usr/lib/crtend_so.o
    ln -s ${SYSROOT}/usr/lib/crtbegin_dynamic.o
    ln -s ${SYSROOT}/usr/lib/crtend_android.o
    make -j6
    make install
    
  • 交叉编译出的配置检测程序无法直接运行

    Nginx 没有使用 GNU Autotools,而是有自已的 Configure 脚本,遇到的第一个问题就是 Nginx 是通过使用编译器编译一个测试程序来获取配置信息,交叉编译出来的程序肯定是无法在编译机上运行的,一个可靠的办法就是修改 Configure 脚本,改为上传到目标设备上执行。

    下面是我写好的一个远程执行脚本 execute

    #!/bin/bash
    
    if [ $# != 2 ] || ([ $1 != "-c" ] && [ $1 != "-f" ]) ; then
        echo "usage: $0 -<c|f> <cmd|file>" >> /dev/stderr
        exit 22 #Invalid argument
    fi
    
    LOG_FILE=/dev/null
    
    if [[ "$CROSS_COMPILE_HOST" = *"android"* ]]; then
        adb connect ${CROSS_COMPILE_DEVICE_IP} >>$LOG_FILE 2>&1
        adb wait-for-device
        adb root >>$LOG_FILE 2>&1
        adb connect ${CROSS_COMPILE_DEVICE_IP} >>$LOG_FILE 2>&1
        adb wait-for-device
        adb remount >>$LOG_FILE 2>&1
        adb connect ${CROSS_COMPILE_DEVICE_IP} >>$LOG_FILE 2>&1
        adb wait-for-device
        if [ $1 == "-f" ]; then
            tempfile=`mktemp -u XXXXXXXX`
            tempdir=/tmp/cross-compile
            adb shell mkdir ${tempdir} >>$LOG_FILE 2>&1
            adb push $2 ${tempdir}/${tempfile} >>$LOG_FILE 2>&1
            adb shell "find ${tempdir} -ctime +1 -type f -exec rm {} \; >/dev/null 2>&1; cd ${tempdir} && chmod a+x $tempfile && ./$tempfile"
        else
            adb shell $2
        fi
    else
        if [ $1 == "-f" ]; then
            tempfile=`mktemp -u XXXXXXXX`
            tempdir=/tmp/cross-compile
            ssh -o StrictHostKeyChecking=no root@${CROSS_COMPILE_DEVICE_IP} mkdir ${tempdir} >>$LOG_FILE 2>&1
            scp -o StrictHostKeyChecking=no $2 root@${CROSS_COMPILE_DEVICE_IP}:${tempdir}/${tempfile} >>$LOG_FILE 2>&1
            ssh -o StrictHostKeyChecking=no root@${CROSS_COMPILE_DEVICE_IP} "find ${tempdir} -mmin +1 -type f -exec rm {} \; >/dev/null 2>&1; cd ${tempdir} && chmod a+x $tempfile && ./$tempfile"
        else
            ssh -o StrictHostKeyChecking=no root@${CROSS_COMPILE_DEVICE_IP} $2
        fi
    fi
    

    修改 nginx-1.14.0 的配置脚本,将本地运行测试程序的地方改为远程执行

    sed -i -e 's/\/bin\/sh -c $NGX_AUTOTEST/timeout 10 \/build\/execute -f $NGX_AUTOTEST/g' `find auto -type f`
    sed -i -e 's/$NGX_AUTOTEST >\/dev\/null/timeout 10 \/build\/execute -f $NGX_AUTOTEST >\/dev\/null/g' `find auto -type f`
    sed -i -e 's/`$NGX_AUTOTEST`/`timeout 10 \/build\/execute -f $NGX_AUTOTEST`/g' `find auto -type f`
    

    运行时需要预先设置环境变量 CROSS_COMPILE_DEVICE_IP 为目标设备 IP,对于嵌入式 Linux 设备,需要使用 ssh-copy-id 命令设置为本机免密码 ssh 登录,对于 Android 设备需要预先 adb 授权通过。

  • 编译时找不到 crypt.h

    crypt.h 属于 glibcglibc 是 Linux 下的 C 标准库实现,除了实现 ANSI C 标准库还有大量方便 Linux 开发的扩展功能。而 NDK 提供的 C 标准库并非 glibc 而是 Bionic libc ,这导致移植 Nginx 时由于缺少 crypt.h 头文件而编译不过。

    可以将 crypt 调用替换为 DES_crypt

    sed -i -e 's/#include <crypt.h>/#if (NGX_HAVE_CRYPT_H)\n#include <crypt.h>\n#endif\n#include <openssl\/des.h>/g' src/os/unix/ngx_linux_config.h
    sed -i -e 's/= crypt(/= DES_crypt(/g' src/os/unix/ngx_user.c
    
  • 链接时找不到 glob 函数

    NDK 工具链是有提供 glob.h 头文件的,但是链接时却找不到 glob 函数。网络上有很多单独提供 glob 下载的,但是都无法直接通过编译,因为里面有很多平台相关的特性。

    Undefined reference to glob and globfree in libc.so · Issue #718 · android-ndk/ndk 中说是已经在 r17b 版本解决,需安装 android-ndk-r17-beta2 同时设置 API Level28 即可。

  • 交叉编译 Nginx

    可使用独立工具链来交叉编译 Nginx,参考前面环境变量配置部分先设置好工具链的环境变量。

    CC_TEST_FLAGS="${CFLAGS} ${LDFLAGS}" ./configure --with-ld-opt="${LDFLAGS}" --prefix=/opt/local --crossbuild=`/build/execute -c 'uname -srm' | tr ' ' ':'` --user=root --group=root --with-select_module --with-poll_module --with-file-aio --with-http_ssl_module --without-mail_pop3_module --without-mail_imap_module  --without-mail_smtp_module
    make
    make install
    
  • 编译出的 Nginx 无法在低版本的 Android 上运行

    之前为了使用最新的 NDK 工具链包含的函数 glob ,而将 API Level 设置成 28 ,这就意味着编译出来的程序无法在低版本的 Android(<=8.1) 上运行,在 Android 7.1 上运行会报错 /system/bin/nginx: No such file or directory

    将 NDK 最新工具链带的 so 也拷到设备上,运行程序前设置一下 $LD_LIBRARY_PATH 优先使用最新的 so ,Nginx 运行后直接卡死或者立即崩溃。

    添加 -static -Wl,--dynamic-linker=/system/bin/linker 链接选项静态编译,运行时会报错 /system/bin/nginx: Accessing a corrupted shared library 的错误,需要使用 /system/bin/linker64

    由于 -static-pie 是互相冲突的选项,静态编译的程序运行时会报错 only position independent executables (PIE) are supported.

  • 编译在 Android 5 及以上版本运行的 Nginx

    参考以下项目,通过使用 docker 方便交叉编译 nginx-1.14.0

    tangxinfa/android-nginx: Cross compile nginx with android ndk

结论

一般的 C/C++ 库通常本身就会注重可移植性,不会生硬地依赖系统底层特性,使用 NDK 移植是可行的,即使是 ffmpeg 这种大型的库也可以成功移植到 Android。

而对于一些 Linux 下的程序,使用 NDK 直接移植会有很大的失败机率,因为他们可能使用了 NDK 不支持的特性(如 glibc )。

NDK 一直在改进,以前阻碍我们移植到 Android 的问题很可能会在新版本中解决,遗憾的是编译出的程序无法运行在旧版本 Android 上。