• 使用 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.o 、 crtbegin_dynamic.o 、 crtend_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 属于 glibc , glibc 是 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)
      #include <crypt.h>
      #endif
      #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 Level 为 28 即可。

    • 交叉编译 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 上。

    参考

  • 相关阅读:
    左值与右值引用 详解
    MFC---导出 Excel 方法
    Linux怎么读? Linux读音考古一日游
    nginx url自动加斜杠问题
    FileBeats配置应用详解
    nginx配置选项try_files详解
    mongodb副本集集群构建
    平凡主丛上的Yang-Mills理论
    Kneser猜想与相关推广
    Lorenzini:Laplacian与图上的黎曼-罗赫定理
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13312541.html
Copyright © 2020-2023  润新知