Android JNI Parsec3.0 移植


最近一直在弄Android JNI相关的东西,为的就是能够毕业课题中需要在安卓的环境下调用parsec3.0的测试集,比如streamcluster,但是今天看了一下他的MakeFile,执行起来如下所示,因为依赖了很多库,之前想用Android JNI解决这个问题没有想象中那么简单。

/usr/bin/g++ -O3 -g -funroll-loops -fprefetch-loop-arrays -fpermissive -fno-exceptions -static-libgcc -Wl,--hash-style=both,--as-needed -DPARSEC_VERSION=3.0-beta-20150206 -DENABLE_THREADS -pthread -c parsec_barrier.cpp
/usr/bin/g++ -O3 -g -funroll-loops -fprefetch-loop-arrays -fpermissive -fno-exceptions -static-libgcc -Wl,--hash-style=both,--as-needed -DPARSEC_VERSION=3.0-beta-20150206 -DENABLE_THREADS -pthread -L/usr/lib64 -L/usr/lib -static streamcluster.o parsec_barrier.o  -o streamcluster

下面是Android studio用来生成JNI API接口,记录一下。

javah -d jni -classpath C:\Android\sdk\platforms\android-23\android.jar;C:\Android\sdk\extras\android\support\v4\android-support-v4.jar;C:\Android\sd
k\extras\android\support\v7\appcompat\libs\android-support-v7-appcompat.jar;..\..\build\intermediates\classes\debug com.example.seu_hgx.hello_world.MainActivity

streamcluster程序调用的方法为./streamcluster 10 20 64 8192 8192 1000 none none 32,参数的解释请看下面:

    fprintf(stderr,"  k1:          Min. number of centers allowed\n");
    fprintf(stderr,"  k2:          Max. number of centers allowed\n");
    fprintf(stderr,"  d:           Dimension of each data point\n");
    fprintf(stderr,"  n:           Number of data points\n");
    fprintf(stderr,"  chunksize:   Number of data points to handle per step\n");
    fprintf(stderr,"  clustersize: Maximum number of intermediate centers\n");
    fprintf(stderr,"  infile:      Input file (if n<=0)\n");
    fprintf(stderr,"  outfile:     Output file\n");
    fprintf(stderr,"  nproc:       Number of threads to use\n");

google了一把还有什么方法可以调用C写的代码,搜了一下,还真有,看了一下,安卓下有个api叫Runtime.getRuntime()貌似符合我的要求,链接在这里

备注:滋生一个想法,编译streamcluster的时候加上-static命令,实现静态链接,这样生成的可执行程序就可以不用担心依赖库的问题了(JNI 方法很有可能在这边就会出问题)然后将生成的可执行文件放到/system/bin/下,然后到Android App下调用。不知道行不行,先把想法记录一下吧。

Runtime.exec() 方法使用

Runtime.exec() 有四种调用方法: 1. public Process exec(String command); 2. public Process exec(String [] cmdArray); 3. public Process exec(String command, String [] envp); 4. public Process exec(String [] cmdArray, String [] envp);

网友是怎么用这个API的:

Process process = Runtime.getRuntime().exec("/system/bin/ping");

Linux下调用系统命令并弹出终端窗口就要改成下面的格式

String [] cmd={"/bin/sh","-c","xterm -e ln -s exe1 exe2"};
Process proc =Runtime.getRuntime().exec(cmd);

还有要设置调用程序的工作目录就要

Process proc =Runtime.getRuntime().exec("exeflie",null, new File("workpath"));

在当前目录执行dir命令,并将结果保存到c:\dir.txt文本文件中:

Runtime r = Runtime.getRuntime();
try{
  Process proc = r.exec("cmd /c dir > %dest%", new String[]{"dest=c:\\dir.txt", new File("d:\\test")});
  int exitVal = proc.waitFor(); // 阻塞当前线程,并等待外部程序中止后获取结果码
  System.out.println(exitVal == 0 ? "成功" : "失败");
}
catch(Exception e){
  e.printStackTrace();
}

使用NDK生成native C/C++的可执行程序

这一节摘自网友的blog。 众所周知, NDK可以生成lib,让java程序通过jni来调用,其实,NDK也可以生成C/C++的可执行程序.不过这个程序要被执行的话还有要求.

  1. 可执行文件的名字必须是lib*.so. 否则apk安装时不会安装上去,因为目前apk的安装只支持安装lib文件,即lib*.so文件,如果不是此文件格式的,安装时不会拷到lib目录里.也可以考虑把可执行文件放assets里,java程序运行后把它拷贝到其它目录或系统目录.
  2. 这个文件的执行必须由java程序通过Runtime.getRuntime().exec()来执行.

下面是我看到的几个Android.mk的相关内容记录一下:

其一:

 Android.mk中的内如如下:
 LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 LDFALGS += -Bdynamic -Wl,-dynamic-linker,/system/bin/linker
 LOCAL_LDLIBS += -llog
 LOCAL_MODULE    := test
 LOCAL_SRC_FILES := test.cpp
 include $(BUILD_EXECUTABLE)

其二:

 Android.mk:
 LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 LOCAL_SRC_FILES :=  main.c
 LOCAL_MODULE := main.out
 LOCAL_LDLIBS := -L$(LOCAL_PATH)/lib -llibeffecttest -llibeffectperformancetest -llibtestkittest -llibgraph -llibmediaplayer -llibkernel -llibmediaplayer -llibjsext -llibshell -llibdtv -llibdtvmx
 LOCAL_LDLIBS += -lz -llog -ldl 
 include $(BUILD_EXECUTABLE)

其三:

 CFLAGS="-march=corei7-avx -O2 -pipe -fomit-frame-pointer -fstack-check"
 CXXFLAGS="${CFLAGS} -fno-enforce-eh-specs -fno-optional-diags"
 LDFLAGS="${LDFLAGS} -Wl,--hash-style=gnu -Wl,--as-needed -Wl,-O1"

模块描述变量

下面的变量用于向编译系统描述你的模块。你应该定义在'include $(CLEAR_VARS)'和'include $(BUILD_XXXXX)'之间。正如前面描写的那样,$(CLEAR_VARS)是一个脚本,清除所有这些变量。 - LOCAL_PATH: 这个变量用于给出当前文件的路径。必须在 Android.mk 的开头定义,可以这样使用:LOCAL_PATH := $(call my-dir) 这个变量不会被$(CLEAR_VARS)清除,因此每个 Android.mk 只需要定义一次(即使在一个文件中定义了几个模块的情况下)。 - LOCAL_MODULE: 这是模块的名字,它必须是唯一的,而且不能包含空格。必须在包含任一的$(BUILD_XXXX)脚本之前定义它。模块的名字决定了生成文件的名字。例如,如果一个一个共享库模块的名字是,那么生成文件的名字就是 lib.so。但是,在的 NDK 生成文 件中(或者 Android.mk 或者 Application.mk),应该只涉及(引用)有正常名字的其他模块。 - LOCAL_SRC_FILES: 这是要编译的源代码文件列表。只要列出要传递给编译器的文件,因为编译系统自动计算依赖。注意源代码文件名称都是相对于 LOCAL_PATH的,你可以使用路径部分,例如:

LOCAL_SRC_FILES := foo.c toto/bar.c\
Hello.c

文件之间可以用空格或Tab键进行分割,换行请用"\".如果是追加源代码文件的话,请用LOCAL_SRC_FILES += 注意:在生成文件中都要使用UNIX风格的斜杠(/).windows风格的反斜杠不会被正确的处理。 注意:可以LOCAL_SRC_FILES := $(call all-subdir-java-files)这种形式来包含local_path目录下的所有java文件。

  • LOCAL_CPP_EXTENSION: 这是一个可选变量, 用来指定C++代码文件的扩展名,默认是'.cpp',但是可以改变它,比如: LOCAL_CPP_EXTENSION := .cxx

  • LOCAL_C_INCLUDES: 可选变量,表示头文件的搜索路径。默认的头文件的搜索路径是LOCAL_PATH目录。 示例:LOCAL_C_INCLUDES := sources/foo或LOCAL_C_INCLUDES := $(LOCAL_PATH)/../foo LOCAL_C_INCLUDES需要在任何包含LOCAL_CFLAGS/LOCAL_CPPFLAGS标志之前进行设置。

  • LOCAL_CFLAGS: 可选的编译器选项,在编译 C 代码文件的时候使用。这可能是有用的,指定一个附加的包含路径(相对于NDK的顶层目录),宏定义,或者编译选项。

注意:不要在 Android.mk 中改变 optimization/debugging 级别,只要在 Application.mk中指定合适的信息,就会自动地为你处理这个问题,在调试期间,会让NDK自动生成有用的数据文件。

  • LOCAL_CXXFLAGS: 与 LOCAL_CFLAGS同理,针对 C++源文件。
  • LOCAL_CPPFLAGS: 与 LOCAL_CFLAGS同理,但是对 C 和 C++ source files都适用。
  • LOCAL_STATIC_LIBRARIES: 表示该模块需要使用哪些静态库,以便在编译时进行链接。
  • LOCAL_SHARED_LIBRARIES:表示模块在运行时要依赖的共享库(动态库),在链接时就需要,以便在生成文件时嵌入其相应的信息。注意:它不会附加列出的模块到编译图,也就是仍然需要在Application.mk 中把它们添加到程序要求的模块中。

  • LOCAL_LDLIBS: 编译模块时要使用的附加的链接器选项。这对于使用‘-l’前缀传递指定库的名字是有用的。 例如,LOCAL_LDLIBS := -lz表示告诉链接器生成的模块要在加载时刻链接到/system/lib/libz.so。可查看 docs/STABLE-APIS.TXT 获取使用 NDK发行版能链接到的开放的系统库列表。

  • LOCAL_ALLOW_UNDEFINED_SYMBOLS:默认情况下,在试图编译一个共享库时,任何未定义的引用将导致一个“未定义的符号”错误。这对于在源代码文件中捕捉错误会有很大的帮助。然而,如果因为某些原因,需要不启动这项检查,可把这个变量设为‘true’。注意相应的共享库可能在运行时加载失败。(这个一般尽量不要去设为 true)。

  • LOCAL_ARM_MODE: 默认情况下, arm目标二进制会以 thumb 的形式生成(16 位),你可以通过设置这个变量为 arm如果你希望你的 module 是以 32 位指令的形式。 'arm' (32-bit instructions) mode. E.g.: LOCAL_ARM_MODE := arm 注意:可以在编译的时候告诉系统针对某个源码文件进行特定的类型的编译.比如,LOCAL_SRC_FILES := foo.c bar.c.arm 这样就告诉系统总是将 bar.c 以arm的模式编译。

  • LOCAL_MODULE_PATH 和 LOCAL_UNSTRIPPED_PATH 在 Android.mk 文件中, 还可以用LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH指定最后的目标安装路径.不同的文件系统路径用以下的宏进行选择:

TARGET_ROOT_OUT:表示根文件系统。
TARGET_OUT:表示 system文件系统。
TARGET_OUT_DATA:表示 data文件系统。

用法如:LOCAL_MODULE_PATH :=$(TARGET_ROOT_OUT) 至于LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH的区别,暂时还不清楚。

streamcluster MakeFile各个参数含义

首先还是抄下上面提到的这个streamcluster的编译配置情况

/usr/bin/g++ -O3 -g -funroll-loops -fprefetch-loop-arrays -fpermissive -fno-exceptions -static-libgcc -Wl,--hash-style=both,--as-needed -DPARSEC_VERSION=3.0-beta-20150206 -DENABLE_THREADS -pthread -L/usr/lib64 -L/usr/lib -static streamcluster.o parsec_barrier.o  -o streamcluster

下面一个一个来分析:

  • g: 如果希望可以使用gdb调试程序信息,需要在g++后添加-g参数。

  • O3:-O选项告诉GCC对源代码进行基本优化。这些优化在大多数情况下都会是程序执行的更快。-O2选项告诉GCC产生尽可能小和尽可能快的代码。如-O2,-O3,-On(n常为0-3);

    1. -O 主要进行跳转和延迟退栈两种优化
    2. -O2 除了完成-O1的优化之外,还进行一些额外的调整工作,如指令调整等。
    3. -O3 则包括循环展开和其他一些与处理特性相关的优化工作
  • funroll-loops:使用编译器的 -funroll-loops 选项 完全展开循环结构。原理: -funroll-loops编译选项使得程序中的循环步骤完全展开,这样会增加汇编代码的长度。

  • fprefetch-loop-arrays:生成数组预读取指令,对于使用巨大数组的程序可以加快代码执行速度,适合数据库相关的大型软件等。具体效果如何取决于代码。

  • fpermissive:在VS2010下编译通过的程序,移植到ARM平台时,通过ARM-GCC交叉编译时出现-fpermissive问题,问题描述是taking address of temporary [-fpermissive]查了一些资料,可能是不同编译器或者新旧编译器对于c++标准的不同解释的结果,在GCC下对于模板继承的规定与VS不同,有一个简单粗暴的解决办法,就是在交叉编译指令里面加入-fpermissive这一条命令,让模板代码由出错降为警告,从而编译通过。

  • fno-exceptions:禁用异常机制,一般只有对程序运行效率及资源占用比较看重的场合才会使用。

  • static-libgcc:静态链接 gcc 库,这里和半静态链接方式编译,这里和-L/usr/lib64以及-L/usr/lib一起说吧,这两个参数-Ldir指定库搜索路径。下面先介绍下三种标准库链接方式的选项和区别吧,更详细的说明在这里

标准库连接方式 示例连接选项 优点 缺点
全静态 -static -pthread -lrt -ldl 不会发生应用程序在 不同 Linux 版本下的标准库不兼容问题 生成的文件比较大,应用程序功能受限,不能调用动态库
全动态 -pthread -lrt -ldl 生成文件是三者中最小的 比较容易发生应用程序在不同Linux 版本下标准库依赖不兼容问题
半静态 -static-libgcc -L. -pthread -lrt -ldl 能够针对不同的标准库采取不同的链接策略,从而避免不兼容问题发生 比较难识别哪些库容易发生不兼容问题

备注:想用Android JNI还是用全静态靠谱

  • Wl,--hash-style=both,--as-needed : Wl,option把选项option传递给连接器。如果option中含有逗号, 就在逗号处分割成多个选项。这个关系不大好像,默认加上。

  • DPARSEC_VERSION=3.0-beta-20150206 -DENABLE_THREADS:这两选项跟streamcluster.cpp里自定义的编译编译选项有关系,请看代码,下面是跟PARSEC_VERTION相关:

#ifdef PARSEC_VERSION
#define __PARSEC_STRING(x) #x
#define __PARSEC_XSTRING(x) __PARSEC_STRING(x)
        fprintf(stderr,"PARSEC Benchmark Suite Version "__PARSEC_XSTRING(PARSEC_VERSION)"\n");
    fflush(NULL);
#else
        fprintf(stderr,"PARSEC Benchmark Suite\n");
    fflush(NULL);
#endif //PARSEC_VERSION
#ifdef ENABLE_PARSEC_HOOKS
  __parsec_bench_begin(__parsec_streamcluster);
#endif

再看跟ENABLE_THREADS相关的编译选项:

#ifdef ENABLE_THREADS
#include <pthread.h>
#include "parsec_barrier.hpp"
#endif

备注:两个选项应该也要加进去的

顺便看一下./streamcluster/inst/amd64-linux.gcc/build-info文件的CFLAGS

CFLAGS:  -O3 -g -funroll-loops -fprefetch-loop-arrays -static-libgcc -Wl,--hash-style=both,--as-needed -DPARSEC_VERSION=3.0-beta-20150206
LDFLAGS: -L/usr/lib64 -L/usr/lib -static
  • pthread: -pthread或者-pthreads的编译选项是用于在编译时增加多线程的支持

移植streamcluster前准备

上一节我们提到了要编译streamcluster需要依赖一些第三方库,现在简单调研一下,看下在android NDK中是否能继续应用 - android ndk中使用Pthread,在android中使用POSIX线程

  1. 在Android.mk中LOCAL_C_INCLUDES += system/core/include/cutils 线程库的头文件在这里。
  2. 在Android.mk中LOCAL_SHARED_LIBRARIES := libcutil
  3. 程序中加入include thread.h

关于这块还可以看下这里链接

  • -L/usr/lib在android.mk中的写法,链接在这里
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog

iostream:No such file or directory

已经在application.mk中添加了APP_STL := stlport_static这么一行,可是还是不行,最后google,找到答案:

ndk{
    moudleName "Jnitest"
    stl "stlport_static"
}

JNI C调用C++,报错undefined reference to start_function

本来回去睡觉了,临走前试了一下,因为我的jni目录下有很多文件,有的是Cpp,有的是c,而我是用C调用Cpp里的函数,怪不得提示找不到function,果断所有文件全部改成cpp,试了以下,编译通过,明天看下,打包出一个apk,放到gem5中跑一下,看看是否和linux跑的效果一样,一致性缺失会很多。

JNI中C部分实现log的API

按照网友的说法,分下面几步解决: 1.在对应的mk文件中加入:LOCAL_LDLIBS := -llog 2.在JNI的实现代码文件(.c或者.cpp)中加入包含LOG头文件的如下代码:

#include <android/log.h>
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "keymatch", __VA_ARGS__)

3.这样就可以使用了:LOGD("我要看到的调试信息^^"); 这样,在logcat端看到的输出是: 我要看到的调试信息^^。

#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, "ProjectName", __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG , "ProjectName", __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , "ProjectName", __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN , "ProjectName", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , "ProjectName", __VA_ARGS__)

使用方法如下图所示: JNI_log

按照上面做了之后,Android studio中JNI-NDK开发打印LOG出现 undefined reference to '__android_log_print',解决办法: 修改build.gradle配置工程中共有两个build.gradle配置文件,我们要修改的是在\app\build.gradle这个文件。 defaultConfig里面写入:

ndk{
        ldLibs "log", "z", "m"
                     }

我的设置是这样的:

defaultConfig {
        applicationId "com.example.seu_hgx.hello_world"
        minSdkVersion 8
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        ndk{
            moduleName "AddJni"
            stl "stlport_static"
            ldLibs "log","z","m"
        }
    }

这样设置完之后,问题解决。

解决JNI中C文件某行出错

一句一句debug,定位到出问题的地方,代码如下所示:

void outcenterIDs( Points* centers, long* centerIDs, char* outfile ) {
    LOGI("------------ outcenterIDs here  1 ------------------");
    FILE* fp = fopen(outfile, "a+");
    if( fp==NULL ) {
         LOGI("------------ outcenterIDs here  2 ------------------");
         fprintf(stderr, "error opening %s\n",outfile);
         exit(1);
     }
    int* is_a_median = (int*)calloc( sizeof(int), centers->num );
    LOGI("------------ outcenterIDs here  3 ------------------");
    for( int i =0 ; i< centers->num; i++ ) {
        is_a_median[centers->p[i].assign] = 1;
    }
    LOGI("------------ outcenterIDs here  4 ------------------");
    for( int i = 0; i < centers->num; i++ ) {
        if( is_a_median[i] ) {
            fprintf(fp, "%u\n", centerIDs[i]);
            fprintf(fp, "%lf\n", centers->p[i].weight);
            for( int k = 0; k < centers->dim; k++ ) {
                fprintf(fp, "%lf ", centers->p[i].coord[k]);
            }
            fprintf(fp,"\n\n");
        }
    }
  LOGI("------------ outcenterIDs here  5 ------------------");
    fclose(fp);
}

到了这里发现,总是发现他打印outcenterIDs here 1outcenterIDs here 2,而不打印3,4,5。很明显是因为文件打开失败,百度了一把,原来是要在AndroidManifest.xml下加读写权限,但是我找这样做了,还是不行,又检查了一下源码,发现,移植前文件outfile被定义为output.txt,这默认不在sdcard中,而上面设置的权限仅对于sdcard,所以,还需要将outfile改为/sdcard/output.txt。重新编译,搞定。

下面备注一下容易忘的创建文件方式:

"w" 写入方式打开,将文件指针指向文件头并将文件大小截为零。如果文件不存在则尝试创建之。 
"w+" 读写方式打开,将文件指针指向文件头并将文件大小截为零。如果文件不存在则尝试创建之。 
"a" 写入方式打开,将文件指针指向文件末尾。如果文件不存在则尝试创建之。 
"a+" 读写方式打开,将文件指针指向文件末尾。如果文件不存在则尝试创建之。

下面是我的Manifest文件:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.seu_hgx.hello_world" >
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

再看下工程中Android.mk的写法

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

#LOCAL_STATIC_LIBRARIES +=  libstlport

#LOCAL_C_INCLUDES += external/stlport/stlport
#LOCAL_C_INCLUDES += bionic

LOCAL_MODULE    := AddJni
LOCAL_SRC_FILES := jni.cpp
LOCAL_SRC_FILES += add.cpp
LOCAL_SRC_FILES += parsec_barrier.cpp
LOCAL_SRC_FILES += streamcluster.cpp

LOCAL_CFLAGS := -O3 -g -funroll-loops -fprefetch-loop-arrays -fpermissive -fno-exceptions -static-libgcc
LOCAL_CFLAGS += -pthread -static -static-libgcc
lOCAL_LDFLAGS :=  -Wl,--hash-style=both,--as-needed
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -L$(SYSROOT)/usr/lib64
LOCAL_LDLIBS += -llog
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include

include $(BUILD_SHARED_LIBRARY)

至此,Parsec3.0 streamcluster移植到Android JNI成功,剩余测试集的就有经验了,课题最担心的事情也算终于解决了。

附录A:

gcc命令的常用选项

-ansi               只支持 ANSI 标准的 C 语法。这一选项将禁止 GNU C 的某些特色,例如 asm 或 typeof 关键词。

-c                 只编译并生成目标文件。

-DMACRO                以字符串“1”定义 MACRO 宏。

-DMACRO=DEFN        以字符串“DEFN”定义 MACRO 宏。

-E                                只运行 C 预编译器。

-g                          生成调试信息。GNU 调试器可利用该信息。

-IDIRECTORY              指定额外的头文件搜索路径DIRECTORY。

-LDIRECTORY             指定额外的函数库搜索路径DIRECTORY。

-lLIBRARY                 连接时搜索指定的函数库LIBRARY。

-m486                      针对 486 进行代码优化。

-o FILE                     生成指定的输出文件。用在生成可执行文件时。

-O0                        不进行优化处理。

-O 或 -O1                  优化生成代码。

-O2                        进一步优化。

-O3                        比 -O2 更进一步优化,包括 inline 函数。

-shared                      生成共享目标文件。通常用在建立共享库时。

-static                        禁止使用共享连接。

-UMACRO                取消对 MACRO 宏的定义。

-w                         不生成任何警告信息。

-Wall                       生成所有警告信息。


Comments