JNI 之 HelloWorld
学了一段时间的Android后,有些地方需要用JNI调用C/C++程序,开始写JNI版的HelloWorld。
过程如下:
- native
- javac
- javah
- c/cpp
- dll/so
- java
详细过程
1. native
在文件夹com_pany下编写HelloWorld.java(com_pany是机构或你的公司名)
写个简单的HelloWorld而已,不需要用package的,不过用的时候要注意一些东西。
native关键字声明本地方法,只准申明不准实现。内容如下:
package com.pany;
public class HelloWorld
{
public native void showMessage();
static /* static块也可以放在最后需要调用的Main类里加载,放在这里表示用到HelloWorld类时加载,需要import */
{
System.loadLibrary("HelloWorld");
}
}
2. javac
产生com/pany/HelloWorld.class文件
javac com/pany/HelloWorld.java
或者
cd com/pany
javac HelloWorld.java
如果你的 .java 文件依赖第三方jar包,比如android.jar,则需要添加对应的classpath,点表示当前路径,多个用分号(Windows系统)或冒号(类Unix系统)隔开。比如用Android API level 14:
javac -cp ".;%ANDROID_SDK%\platforms\android-14\android.jar" com/pany/HelloWorld.java
3. javah
javah -jni com.pay.HelloWorld
生成 com_pany_HelloWorld.h 文件,与 com/pany 在同一级目录,-jni在这里标记暗色,表示可有可无,因为系统已默认。如果是Android开发,提供 -d ../jni 选项生成到jni目录里。
跟上面一样,如果依赖包名,则要写成 javah -cp ".;%ANDROID_SDK%\platforms\android-14\android.jar" com.pany.HelloWorld 的形式。
用 Eclipse 开发 Android 应用的话,在没有JNI 代码的时候编译,.class 文件生成在 bin/classes/ 目录。在工程根目录下:
javah -cp "bin/classes;%ANDROID_SDK%/platforms/android-21/android.jar" -d jni com.pany.HelloWorld
现在用的一般是 Android Studio 开发了,.class 文件生成在工程的 app/build/intermediates/classes/debug/ 目录
javah -cp "app/build/intermediates/classes/debug;%ANDROID_SDK%/platforms/android-21/android.jar" -d app/src/main/jni com.pany.HelloWorld
注意是 com.example.package.ClassName 形式,不是 com/example/package/ClassName 形式。
classpath(缩写为cp)的分隔符不同的操作系统不一样,参照第二步,否则会报错找不到com.pany.HelloWorld类文件。
-stubs 选项是让 javah 从Java类文件里产生C声明的,可惜被废弃了。
-stubs is a leftover from a long-obsolete first cut at JNI. It never generated anything useful for any version of Java used in the current century.
Just copy the header file into your .c file and add a function body to each function.
如果坚持使用 javah 的 -stubs 选项会给出如下信息
Error: JNI does not require stubs, please refer to the JNI documentation.
因为 HelloWorld.class 文件存在于包 com.pany 中,需要退到 com.pany 外执行,否则报错如下:
com_pany@trek:~/Desktop/HelloWorld$javah HelloWorld
error: cannot access HelloWorld
bad class file: ./HelloWorld.class
class file contains wrong class: com.pany.HelloWorld
Please remove or make sure it appears in the correct subdirectory of the classpath.
com.sun.tools.javac.util.Abort
at com.sun.tools.javac.comp.Check.completionError(Check.java:164)
at com.sun.tools.javadoc.DocEnv.loadClass(DocEnv.java:149)
at com.sun.tools.javadoc.RootDocImpl.<init>(RootDocImpl.java:77)
at com.sun.tools.javadoc.JavadocTool.getRootDocImpl(JavadocTool.java:159)
at com.sun.tools.javadoc.Start.parseAndExecute(Start.java:330)
at com.sun.tools.javadoc.Start.begin(Start.java:128)
at com.sun.tools.javadoc.Main.execute(Main.java:66)
at com.sun.tools.javah.Main.main(Main.java:147)
javadoc: error - fatal error
2 errors
成功后,生成的com_pany_HelloWorld.h内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_pany_HelloWorld */
#ifndef _Included_com_pany_HelloWorld
#define _Included_com_pany_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_pany_HelloWorld
* Method: showMessage
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_pany_HelloWorld_showMessage
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
正如代码中注释说的,不要动它就行了。看看里面写了什么。
在Java世界活得太久了,一看到#define等宏定义,自然有一种亲切感。
学习JNI,首先要了解 JAVA_HOME 下 include/x/jni.h 和 include/x/jni_md.h 文件中的内容(x stands for win32 or linux,or any other OS name),都是一些简单的宏定义。
关于上面代码一些陌生的东西的定义在Java安装路径里的 include 中,Windows系统上是win32文件夹;Linux系统上是linux文件夹。
在win32/jni_md.h文件里
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall
在linux/jni_md.h文件里
#define JNIEXPORT
#define JNIIMPORT
#define JNICALL
4. c/cpp
留意到 com_pany_HelloWorld.h 中的 extern "C",C++用户应该清楚。
这里可以写 com_pany_HelloWorld.c 或者 com_pany_HelloWorld.cpp。因为C++几乎全部兼容C,com_pany也喜欢C++,所以就写成 com_pany_HelloWorld.cpp 吧。
将 com_pany_HelloWorld.h 中native方法的实现(implementation)写入 com_pany_HelloWorld.cpp
第3步中的 -stubs 本来是要完成生成 com_pany_HelloWorld.c 文件使命的,后来被Java拿掉了。
于是自己写了个javacpp.exe 来生成 com_pany_HelloWorld.cpp stub,行为如同 javah 生成 com_pany_HelloWorld.h,方便以后使用。
com_pany_HelloWorld.cpp 内容如下。很简单,输出一句Hello World!。
注意JNI的函数签名,譬如 findClass("Ljava/lang/String;") 对应 jstring 类型,注意到末尾的分号。如果想偷懒,不想自己推导类型,可以使用 javap 反编译上面生成的类,用上 -s 选项,它将输出输出内部类型签名。
private static native android.graphics.PointF[][] nativeDetectFace(android.graphics.Bitmap, java.lang.String, java.lang.String);
descriptor: (Landroid/graphics/Bitmap;Ljava/lang/String;Ljava/lang/String;)[[Landroid/graphics/PointF;
可以看到,返回类型 PointF[][]二维数组对应字符串"[[Landroid/graphics/PointF;",其中 Lfully/qualified/class; 表示类名,这里再次强调末尾的分号一下,[表示数字,两个[表示二维数组。
#include <stdio.h>
#include "com_pany_HelloWorld.h"
JNIEXPORT void JNICALL Java_com_pany_HelloWorld_showMessage(JNIEnv *env, jobject obj)
{
printf("Hello World!\n");
}
5. dll/so
编译以上c/cpp文件成库文件,Windows下扩展名.dll (Dynamic Link Library),Linux下扩展名.so (Shared Object),所以看你在哪个OS上。生成的名字也有要求,Windows下是<filename>.dll,Linux下则是lib<filename>.so,交给JVM去加载库时,只认名字<filename>,其他不管,这样就跨平台了。
Linux下有好多扩展名,都分不清了。什么.c .C .cc .cxx .s .so .i .ii .m文件,够呛的。搞得像Windows把那些好听的名字.lib .dll .obj .exe等都用了,Linux只能捡其他的来用一样。这让我想起了新浪干的事。新浪微博网址是www.weibo.com,让其他门户的微博网站去取名t.xxx.com。
Windows有自己庞大的的IDE,一般都会在Visual Studio里完成各种工作。建立Win32 Dynamic-Link Libray Project ,添加__declspec(dllexport)声明等,大家应该比com_pany清楚,不清楚的可以查看相关资料。下面是用命令生成库文件。
I) 在linux下编译成so文件
我的环境变量 JAVA_HOME=/usr/lib/java/jdk1.6.0_21
gcc com_pany_HelloWorld.cpp -I$JAVA_HOME/include/ -I$JAVA_HOME/include/linux/ -fPIC -shared -o libHelloWorld.so
-I:是为了include两个文件jni.h和jni_md.h,用JNI必须关联的头文件。
-fPIC:表示编译为位置独立的(可重定向的)代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
-shared 该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件
-L.:表示要连接的库在当前目录中
-lHelloWorld:编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.so来确定库的名称
LD_LIBRARY_PATH:这个环境变量指示动态连接器可以装载动态库的路径。
当然如果有root权限的话,可以修改/etc/ld.so.conf文件,然后调用/sbin/ldconfig来达到同样的目的,不过如果没有root权限,那么只能采用输出LD_LIBRARY_PATH的方法了。
将你要用到该库的main.cpp文件与动态库libHelloWorld.so连接生成可执行文件main。(当然,如果感觉不自在,取名生成main.exe也行)
gcc main.cpp -L. -lHelloWorld -o main
测试是否动态连接,如果列出libHelloWorld.so,那么应该是连接正常了。
ldd命令是用来查看共享库的依赖关系的命令。
com_pany@trek:~/Desktop/HelloWorld$ ldd libHelloWorld.so
linux-gate.so.1 => (0x00b82000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00ce4000)
libm.so.6 => /lib/tls/i686/cmov/libm.so.6 (0x00bc0000)
libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x00b19000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00698000)
/lib/ld-linux.so.2 (0x003fe000)
II) 在Windows下编译成dll文件
我的环境变量JAVA_HOME=F:\Program Files (x86)\Java\jdk1.6.0_25
cl -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -LD com_pany_HelloWorld.cpp -FeHelloWorld.dll
cl是Microsoft C/C++ Optimizing Compiler,猜是Compile and Link的简写。没用过该命令的可在命令行提示符输入cl命令查看简介,输入cl /?查看命令选项。
命令中-是编译选项,命令的注释里给的是/。应该用/,觉得还是用-会好看些,毕竟对com_pany来讲,正斜杠/经常用在文件夹上,反斜杠\经常用在转义字符上。还有就是上面的目录与子目录分割符\,在WIN7下试了用/甚至\\也行。妈妈再也不用担心我在字符串里想表达\而没用\\了。
/I<dir> 添加<dir>到include搜索路径,注意上面的引号,因为我的JAVA_HOME路径里面有空格,没有的话报如下错误:
Command line warning D4024 : unrecognized source file type 'Files', object file assumed
Command line warning D4024 : unrecognized source file type '(x86)\Java\jdk1.6.0_25/include', object file assumed
Command line warning D4024 : unrecognized source file type 'Files', object file assumed
Command line warning D4024 : unrecognized source file type '(x86)\Java\jdk1.6.0_25/include/win32', object file assumed
HelloWorld.cpp
/MD 确保输出的.dll与Win32 multithreaded C library链接。(由于没有涉及到多线程,所以我没有加该选项,加上也行)
/LD 确保产生.dll文件而不是.exe文件
/Fe 这里不是铁元素哦,/Fe<file>表示命名生成的可执行文件为<file>
成功后的打印信息如下:
D:\HelloWorld>cl -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -LD com_pany_HelloWorld.cpp -FeHelloWorld.dll
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 12.00.8168 for 80x86
Copyright (C) Microsoft Corp 1984-1998. All rights reserved.
com_pany_HelloWorld.cpp
Microsoft (R) Incremental Linker Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.
/dll
/implib:HelloWorld.lib
/out:HelloWorld.dll
com_pany_HelloWorld.obj
Creating library HelloWorld.lib and object HelloWorld.exp
看到上面的输出日期,你应该想到了,对,我用的还是Visual Studio 6.0。
虽然安装了Visual Studio 2010,但是com_pany心里还是割舍不下经典的6.0啊。
命令cl所对应目录,我的是C:\Program Files (x86)\Microsoft Visual Studio\VC98\Bin\CL.EXE
在VS2010中的目录是C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\cl.exe
其他的VS版本,路径也都差不多啦。
6. java
要调用HelloWorld库的Main.java文件内容如下:
import com.pany.HelloWorld;
public class Main
{
/*
static // 如果有许多库,可以放在这里统一提前加载
{
System.loadLibrary("HelloWorld");
}
*/
public static void main(String[] args)
{
HelloWorld hello = new HelloWorld();
hello.showMessage();
}
}
接着终端或命令提示符下输入命令
javac Main.java
java Main
应该不会输出Hello World!,报错如下:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorld in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1734)
at java.lang.Runtime.loadLibrary0(Runtime.java:823)
at java.lang.System.loadLibrary(System.java:1028)
at HelloWorld.<clinit>(HelloWorld.java:6)
Could not find the main class: HelloWorld. Program will exit.
原来没有连接上库,用java的-D选项申明native lib 路径,.表示当前路径。
java -Djava.library.path=. Main
依旧报错如下:
com_pany@trek:~/Desktop/HelloWorld$ java -Djava.library.path=. HelloWorld
Exception in thread "main" java.lang.UnsatisfiedLinkError: /home/com_pany/Desktop/HelloWorld/libHelloWorld.so: /home/com_pany/Desktop/HelloWorld/libHelloWorld.so: undefined symbol: __gxx_personality_v0
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1803)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1728)
at java.lang.Runtime.loadLibrary0(Runtime.java:823)
at java.lang.System.loadLibrary(System.java:1028)
at HelloWorld.<clinit>(HelloWorld.java:6)
Could not find the main class: HelloWorld. Program will exit.
说明:
__gxx_personality_v0 is part of the G++ exception handling model defined by the new C++ ABI in GCC.? The symbol is provided by the libstdc++ library, which is added to the link line by g++.? If you have a C++ routine using exceptions and link with Java, libstdc++.so may not be referenced and the C++ support routines not found.?? You can inform the compiler that Java exceptions are to be used in a translation unit, irrespective of what it might think, by writing `#pragma GCC java_exceptions' at the head of the file.? This `#pragma' must appear before any functions that throw or catch exceptions, or run destructors when exceptions are thrown through them.
希望你能试着读懂上面的一段话。
用gcc编译链接C++文件时,不会自动链接C++标准库,改gcc为g++即可,或者用gcc时候加上参数-lstdc++也行,表示去连接C++标准库stdc++。gcc原是GNU C Compiler的缩写,后来发展壮大成是GNU Compiler Collection的缩写,而g++是GNU C++的编译器。
所以,上面第5步中的命令需要改成
g++ com_pany_HelloWorld.cpp -I$JAVA_HOME/include/ -I$JAVA_HOME/include/linux/ -fPIC -shared -o libHelloWorld.so
或(C++ 需要链接 stdc++ 库)
gcc com_pany_HelloWorld.cpp -I$JAVA_HOME/include/ -I$JAVA_HOME/include/linux/ -fPIC -lstdc++ -shared -o libHelloWorld.so
最后,终于看到输出Hello World!,长呼一口气~~~
其实一开始,如果用的是 com_pany_HelloWorld.c 而不是 com_pany_HelloWorld.cpp,就不会碰到上面的问题。。。