作者:庄晓立 (liigo)
日期:2011.6.15
原创链接:http://blog.csdn.net/liigo/archive/2011/06/14/6544649.aspx
转载请注明出处:http://blog.csdn.net/liigo
我(liigo)以前的围棋SGF棋谱解析器是使用C语言开发的,为了省事和加快移植,就不打算用Java语言重写了,计划通过 JNI (Java Native Interface) 在JAVA中调用C语言的代码,自然的,Android NDK 就派上用场了。我此前只是对NDK稍有耳闻,从来没用过。上周末抽了大概一天的时间实战试用 Android NDK 初见成效,收获不小。略加记录,以备后用。
一开始当然是先下载NDK(发现android.com已在墙内了),阅读其官方使用文档。文档中提到NDK在Windows下需要依赖Cygwin(却没有指导性操作提示),去cygwin.com下载一setup.exe为在线下载安装程序。由于NDK没有相关说明,我在安装cygwin问题上费了一些周折,经过网络搜索,在安装程序配置界面指定安装devel类别,设定为Install(见下图),以便确保下载安装得到其中的gcc和make程序。下载过程可能比较漫长,耐心等待吧。Devel里可能有许多不是NDK所必需的内容(导致cygwin安装目录大幅膨胀),多了就多了吧,总比缺胳膊少腿好。安装完成后,尝试在cygwin控制台运行gcc和make命令,有恰当输出反馈说明安装成功了(可以满足NDK需要),如果有SB的“command not found”之类错误提示(这里借用一网友语气),说明你还得考虑重新下载安装cygwin。
NDK和Cygwin都有了,接下来怎么办,我也糊涂了,NDK文档似乎没有说怎么把两者结合起来使用。因为发现NDK中的ndk-build是shell脚本,需要linux环境才能运行,初步想到去cygwin控制台命令行执行 ndk-build 命令,结果进去一看,全是 /home, /usr, /dev 之类Linux目录,我的NDK在windows系统中的D盘呀,怎么访问到?又是借助于网络搜索,知道了在cygwin内 /cygdrive 就可以访问当前Windows系统下的磁盘各分区,如 /cygdrive/c 表示C盘,/cygdrive/d 表示D盘。OK,按照NDK文档,执行命令:
cd $PROJECT $NDK/ndk-build
$PROJECT、$NDK 都是环境变量,以后再定义吧,先用绝对完整路径吧,写成这样:
cd /cygdrive/d/eclipse/workspaces/GoSgfViewer /cygdrive/d/android/ndk/ndk-build
每次都写这么长的路径谁受得了,得了,定义成环境变量吧,再次搜索网络,修改 <cygwin>/home/liigo/.bash_profile 这个文件(其中liigo目录应该是cygwin安装时根据当前windows用户生成的),在最后加入以下几行:
NDK=/cygdrive/d/Android/ndk export NDK EWS=/cygdrive/d/eclipse/workspaces export EWS SGF=/cygdrive/d/eclipse/workspaces/GoSgfViewer export SGF
以后编译命令行就简化了:
cd $SGF $NDK/ndk-build
这中间还出现一个插曲,我是用Windows系统自带的“写字板”程序(wordpad.exe)编辑修改 .bash_profile 文件的,结果进入cygwin控制台时它提示无法解析配置文件。分析发现写字板保存的文本文件行尾是"/r/n",而cygwin要求配置文件行尾是"/n",我一时没有趁手达到此要求的文本编辑器,于是我写了几行易语言代码(这里下载源码)解决了这个问题:
还是各有各的办法,你要是有EditPlus或者EmEditor或者UltraEdit,一个另存为就能搞定的事。我现在在想,当时为什么没想到用字节集替换的方法,还可以少写好几行代码,失误失误。
接下来算是真正进入正题。不过对我(liigo)来说好像有点轻车熟路。前些年使用JNI的经历,现在还有些印象,所以不觉得困难。先编写含有native方法的Java Class,用java编译器编译之;然后用javah处理刚才编译生成的class,生成用于实现对应native方法的C/C++头文件,包含相应的函数原型声明;自己创建对应的.c/.cpp文件,#include刚才生成的头文件,定义并实现其中的函数(代码见下文)。我是在记事本中完成这些函数编码的,这不是装B,实在是暂时没想到有其他更好的办法,Eclipse IDE似乎也没提供什么支持,且不说NDK其实跟ADT甚至没什么直接关联。好在这个过程是比较顺利的,中间自然少不了随时查看JNI官方文档中的接口函数说明。
代码写完之后,参照NDK文档,在Eclipse项目目录jni子目录内创建一个Android.mk文本文件,其内容就从NDK文档中抄下来稍作修改(修改了第三行和第四行):
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := liigo-sgf-parser LOCAL_SRC_FILES := sgf.c com_liigo_go_SgfParser.cpp include $(BUILD_SHARED_LIBRARY)
然后进cygwin,执行编译命令:
cd $SGF $NDK/ndk-build
如果遇到错误则根据提示做相应修改,重新编译。成功之后,NDK会在eclipse项目目录内生成一些文件(例如libs目录内的动态库.so)。此时回到Eclipse IDE,重新编译Android项目,生成的.apk就是包含有调用C/C++代码的应用软件了。我在模拟器中部署此apk时貌似还出现了一些部署性错误,导致apk无法正常启动,后来又莫名其妙好了,似乎需要重启eclipse IDE并重新编译整个项目?存疑。
现在总结一下NDK的作用,它提供一些库文件和头文件,提供交叉编译的手段(调用gcc,make),编译生成必要的文件到项目目录(供ADT打包.apk)。至于在Java中调用C函数(通过native方法),和在C/C++中调用Java方法或读写对象成员(fields),主要是Java本身的JNI的功劳。
在ndk-build编译过程中,我主要遇到了以下两种编译错误,C/C++语法错误,导致了一大批编译错误。第一个是调用JNI函数时,我参照其官方文档,想当然的编写代码为类似如下:
GetMethodID(env, ...)
其实在C++(.cpp文件)中正确的写法应该是:
env->GetMethodID(...)
C语言(.c文件)中正确的写法应该是:
(*env)->GetMethodID(env, ...)
遇到的另一个错误是结构体(struct)的预声明语法,我原来的写法在VC编译器中是可以编译的:
typedef struct _tagSGFParseContext SGFParseContext; //... typedef struct _tagSGFParseContext { //... } SGFParseContext;
但是在gcc(g++)编译器中不认可这种写法,说是类型重定义,修改为以下代码后编译通过:
typedef struct _tagSGFParseContext SGFParseContext; //... struct _tagSGFParseContext { //... };
后来经过测试,各种功能正常调用成功,说明NDK正确的发挥了它的作用。下面贴一些代码,仅供参考。
首先是 SgfParser.java:
package com.liigo.go; import java.util.ArrayList; import java.util.List; public class SgfParser { static { System.loadLibrary("liigo-sgf-parser"); } private int mContext; //used for native public native void nativeInitSGFParseContext(); public native void nativeCleanupSGFParseContext(); public native int nativeParseSGFText(String sgfText); public native int nativeParseSGFFile(String sgfFile); public void parseSGFFile(String sgfFile) { nativeInitSGFParseContext(); nativeParseSGFFile(sgfFile); nativeCleanupSGFParseContext(); } public void parseSGFText(String sgfText) { nativeInitSGFParseContext(); nativeParseSGFText(sgfText); nativeCleanupSGFParseContext(); } interface Listener { public void onSgfTree(String treeHeader, int treeIndex); public void onSgfTreeEnd(int treeIndex); public void onSgfNode(String nodeHeader); public void onSgfNodeEnd(); public void onSgfProperty(String id, String value); } private List<SgfParser.Listener> mListeners = new ArrayList<SgfParser.Listener>(); public void addListener(SgfParser.Listener listener) { mListeners.add(listener); } public void removeListener(SgfParser.Listener listener) { mListeners.remove(listener); } private void onTree(String treeHeader, int treeIndex) { for(Listener l : mListeners) { l.onSgfTree(treeHeader, treeIndex); } } private void onTreeEnd(int treeIndex) { for(Listener l : mListeners) { l.onSgfTreeEnd(treeIndex); } } private void onNode(String nodeHeader) { for(Listener l : mListeners) { l.onSgfNode(nodeHeader); } } private void onNodeEnd() { for(Listener l : mListeners) { l.onSgfNodeEnd(); } } private void onProperty(String id, String value) { for(Listener l : mListeners) { l.onSgfProperty(id, value); } } }
然后是com_liigo_go_SgfParser.cpp:
#include "com_liigo_go_SgfParser.h" #include "sgf.h" #include <stdlib.h> #include <stdio.h> static jclass mThisClass = 0; static jclass GetThisClass(JNIEnv* env) { if(mThisClass == 0) { mThisClass = env->FindClass("com/liigo/go/SgfParser"); } return mThisClass; } static jfieldID mContext_fieldID = 0; static jfieldID GetContextFiledID(JNIEnv* env) { if(mContext_fieldID == 0) mContext_fieldID = env->GetFieldID(GetThisClass(env), "mContext", "I"); return mContext_fieldID; } static jmethodID GetContextMethodID(SGFParseContext* pContext, const char* name, const char* sig) { JNIEnv* env = (JNIEnv*) pContext->pUserData1; return env->GetMethodID(GetThisClass(env), name, sig); } static void onProperty(SGFParseContext* pContext, const char* szID, const char* szValue) { jmethodID methodID = GetContextMethodID(pContext, "onProperty", "(Ljava/lang/String;Ljava/lang/String;)V"); JNIEnv* env = (JNIEnv*) pContext->pUserData1; jobject obj = (jobject) pContext->pUserData2; env->CallVoidMethod(obj, methodID, env->NewStringUTF(szID), env->NewStringUTF(szValue)); } static void onNode(SGFParseContext* pContext, const char* szNodeHeader) { jmethodID methodID = GetContextMethodID(pContext, "onNode", "(Ljava/lang/String;)V"); JNIEnv* env = (JNIEnv*) pContext->pUserData1; jobject obj = (jobject) pContext->pUserData2; env->CallVoidMethod(obj, methodID, env->NewStringUTF(szNodeHeader)); } static void onNodeEnd(SGFParseContext* pContext) { jmethodID methodID = GetContextMethodID(pContext, "onNodeEnd", "()V"); JNIEnv* env = (JNIEnv*) pContext->pUserData1; jobject obj = (jobject) pContext->pUserData2; env->CallVoidMethod(obj, methodID); } static void onTree(SGFParseContext* pContext, const char* szTreeHeader, int treeIndex) { jmethodID methodID = GetContextMethodID(pContext, "onTree", "(Ljava/lang/String;I)V"); JNIEnv* env = (JNIEnv*) pContext->pUserData1; jobject obj = (jobject) pContext->pUserData2; env->CallVoidMethod(obj, methodID, env->NewStringUTF(szTreeHeader), treeIndex); } static void onTreeEnd(SGFParseContext* pContext, int treeIndex) { jmethodID methodID = GetContextMethodID(pContext, "onTreeEnd", "(I)V"); JNIEnv* env = (JNIEnv*) pContext->pUserData1; jobject obj = (jobject) pContext->pUserData2; env->CallVoidMethod(obj, methodID, treeIndex); } /* * Class: com_liigo_go_SgfParser * Method: nativeInitSGFParseContext * Signature: ()V */ JNIEXPORT void JNICALL Java_com_liigo_go_SgfParser_nativeInitSGFParseContext (JNIEnv * env, jobject obj) { SGFParseContext* pContext = (SGFParseContext*) malloc(sizeof(SGFParseContext)); initSGFParseContext(pContext, onTree, onTreeEnd, onNode, onNodeEnd, onProperty, env, obj); //store pContext to mContext field env->SetIntField(obj, GetContextFiledID(env), (int)pContext); } static SGFParseContext* GetSGFParseContext(JNIEnv * env, jobject obj) { return (SGFParseContext*) env->GetIntField(obj, GetContextFiledID(env)); } /* * Class: com_liigo_go_SgfParser * Method: nativeCleanupSGFParseContext * Signature: ()V */ JNIEXPORT void JNICALL Java_com_liigo_go_SgfParser_nativeCleanupSGFParseContext (JNIEnv * env, jobject obj) { SGFParseContext* pContext = GetSGFParseContext(env, obj); cleanupSGFParseContext(pContext); free(pContext); env->SetIntField(obj, GetContextFiledID(env), (int)NULL); } /* * Class: com_liigo_go_SgfParser * Method: nativeParseSGFText * Signature: (Ljava/lang/String;)I */ JNIEXPORT jint JNICALL Java_com_liigo_go_SgfParser_nativeParseSGFText (JNIEnv * env, jobject obj, jstring sgftext) { SGFParseContext* pContext = GetSGFParseContext(env, obj); const char* data = env->GetStringUTFChars(sgftext, NULL); parseSGF(pContext, data, 0); env->ReleaseStringUTFChars(sgftext, data); } /* * Class: com_liigo_go_SgfParser * Method: nativeParseSGFFile * Signature: (Ljava/lang/String;)I */ JNIEXPORT jint JNICALL Java_com_liigo_go_SgfParser_nativeParseSGFFile (JNIEnv * env, jobject obj, jstring sgffile) { const char* filename = env->GetStringUTFChars(sgffile, NULL); int len = 0; char* filedata = NULL; FILE* pfile = fopen((const char*) filename, "r"); printf("/n/n--- test parse sgf file: %s ---/n", filename); if(pfile) { fseek(pfile, 0, SEEK_END); len = ftell(pfile); //assert(len > 0); fseek(pfile, 0, SEEK_SET); filedata = (char*) malloc(len + 1); //assert(filedata); fread(filedata, 1, len, pfile); filedata[len] = '/0'; //ensure end with '/0' const char* sgfdata = filedata; //ignore utf-8 BOM bytes if(len>=3 && filedata[0]==(char)0xEF && filedata[1]==(char)0xBB && filedata[2]==(char)0xBF) sgfdata += 3; SGFParseContext* pContext = GetSGFParseContext(env, obj); parseSGF(pContext, (const char*)sgfdata, 0); fclose(pfile); pfile = NULL; free(filedata); } else { printf("/n/n--- open sgf file error: %s ---/n/n", filename); } env->ReleaseStringUTFChars(sgffile, filename); }