Thursday, June 5, 2008

Introduction to JNI

As my first technical post, I'm going to overview how to use JNI with C++.

My platform:
Windows Vista Ultimate
AMD Athlon64 X2 6000+
5GB DDR2
Java 1.6.0_06 64-bit
Microsoft Visual Studio 2008

Straight to the code:

[HelloWorld.java]

class HelloWorld {
public static native void HelloNative(String s);
static {
System.loadLibrary("HelloDLL");
}
public static void main(String[] Args){
HelloNative("Hello, world!");
}
}

Things to note:
- The method we're going to create in the DLL is declared as "native" but not defined.
- To load the methods from the DLL, we call System.loadLibrary("library");
- Do not put the extension in the call - our DLL will be HelloDLL.dll, so we call with "HelloDLL"
- After that, we can use the function just as normal.

[HelloWorld.cpp (here's where it gets messy)]

#include "HelloWorld.h"
JNIEXPORT void JNICALL Java_HelloWorld_HelloNative(JNIEnv *env, jobject obj, jstring javaString){
const char *input = env->GetStringUTFChars(javaString, 0);
printf("%s\n", input);
env->ReleaseStringUTFChars(javaString, input);
}


Things to note:
-The header "HelloWorld.h" will be generated by javah as we'll see in a bit.
-The function name must be Java_ClassName_MethodName
-The function parameters must start with a JNIEnv* and a jobject, after that come your own.
-To use the jstring you passed in as a character array, you have to call GetStringUTFChars
-You MUST call ReleaseStringUTFChars after calling GetStringUTFChars once you're done

Putting it all together:

javac HelloWorld.java
javah -jni HelloWorld

[Here you can use a C++ compiler of your choice, as long as you know how to use it to create a DLL. This example is with Visual Studio 2008]

call "C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat" amd64

cl -I"c:\program files\java\jdk1.6.0_06\include" -I"c:\program files\java\jdk1.6.0_06\include\win32" -I"." -MT -LD HelloWorld.cpp -FeHelloDLL.dll


All done!
Let's go over the compilation.

You'd better know what javac does already; javah creates the header file for the class, which we already included in our cpp file (HelloWorld.h).

The first Visual Studio command is a batch file, with an architecture parameter. I'm compiling to amd64. The batch file sets the paths and settings necessary for compilation, makes things easy. I don't believe the standard install of Visual Studio comes with the different architectures - you have to enable it during install (you can go to reinstall and add it without reinstalling the whole thing). Visual Studio Express may not have it - you'll have to install the Windows SDK to get them. There's a help page on the MSDN somewhere about that.

cl is the command-line compiler for Visual Studio. I'm adding include paths to Java's includes directories, as well as the current directory. Some extra switches for good measure, the source file, and the output file.

Viola! You now have your DLL.


Important Note:

With Visual Studio, you can either compile with -MT or -MD. I believe default is MD. the difference is that MT adds the library code directly (statically) to your DLL, while MD links it dynamically. If you use MD, your program is now dependent on msvcr90.dll (or whatever version of Visual Studio you're using). If it fails to run, you need to grab this DLL. I chose to just put it in with my own DLL, as not everyone is going to have it. If you don't, you can find it easily online, or if you have Visual Studio it should be at:

C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\redist\somewheredownthere

I prefer to use MT, so that I don't have to worry about distributing extra static libraries. There are up and downsides to using both. Static will make your DLL a little larger, will MD will make your DLL rely on other libraries. You also may have to embed the manifest file into your DLL if you use MD.

Also note that there are different versions of msvcr90.dll for different architectures, if you choose to use dynamic linking. You can't use the wrong one. Don't worry, it should tell you if you have the wrong one.

Anyway...
That's the end of my first post. Hope you enjoyed.

14 comments:

Orlanso Fernando said...

Hello I Am facing some problem withJNI i guess u can help

I am suffering from this problem does anyone can help me i am using JNI Tutorials Provided By java
in which one program is for Accessing arrays of objects i am using VC++ 6.0
everything is okey while compiling
but at the running time it is showing a error

Exception in thread "main" java.lang.UnsatisfiedLinkError: ObjectArrayTest.init[nt2DArray(I)[[I
at ObjectArrayTest.initInt2DArray(Native Method)
at ObjectArrayTest.main(ObjectArrayTest.java:4)

The Java Code Is
========================================================================

class ObjectArrayTest {
private static native int[][] initInt2DArray(int size);
public static void main(String[] args) {
int[][] i2arr = initInt2DArray(3);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
System.out.print(" " + i2arr[i][j]);
}
System.out.println();
}
}
static {
System.out.println("java.library.path="+System.getProperty("java.library.path"));
System.loadLibrary("ObjectArrayTest");
}
}

========================================================================

And VC++ 6.0 Code Is

========================================================================
#includejni.h
#includestdio.h

JNIEXPORT jobjectArray JNICALL
Java_ObjectArrayTest_initInt2DArray(JNIEnv *env,
jclass cls,
int size)
{
jobjectArray result;
int i;
jclass intArrCls = env ->FindClass( "[I");
if (intArrCls == NULL) {
return NULL; /* exception thrown */
}
result = env->NewObjectArray( size, intArrCls,
NULL);
if (result == NULL) {
return NULL; /* out of memory error thrown */
}
for (i = 0; i < size; i++) {
jint tmp[256]; /* make sure it is large enough! */
int j;
jintArray iarr = env->NewIntArray( size);
if (iarr == NULL) {
return NULL; /* out of memory error thrown */
}
for (j = 0; j < size; j++) {
tmp[j] = i + j;
}
env->SetIntArrayRegion(iarr, 0, size, tmp);
env->SetObjectArrayElement( result, i, iarr);
env->DeleteLocalRef( iarr);
}
return result;
}

========================================================================

after compiling i have built the .dll file and everything i had done
the dll also is in system32 folder of Windows

========================================================================
One more thing i am using Windows XP SP2
can u help me

Solnus said...

I believe your error is here:

jclass intArrCls = env ->FindClass( "[I");

Unknown said...

Solnus,

I've been reading your JNI posts. Great stuff! (and good info).
I'm facing a problem with JNI and large arrays that u may address easily or at least u could give me a hint from where I could start looking.
The thing is I'm compiling a dll just the way u did, the program seems running ok, (in linux though) but in windows (in certain machines) when pass a large arrays to the C++side the java programs ends abruptly. Is there any configuration that i have to set to run this java program that i made to change the maximun memory allocation?
And also is there any hint that i must be aware of memory deallocation? because i think the garbage collector is doing nothing after a throw some deletes over the arrays and the classes that i created.

This line:
jdouble* imagesPtr = env->GetDoubleArrayElements(images,0);

makes my program die when I pass a large array (200 millons of doubles)

Thanks
Marco

Solnus said...

It's difficult to say what the problem is without more information. Is there an exception thrown when it terminates? Is there a log of the error somewhere? My guess is that it's running out of memory.

GetDoubleArrayElements is allowed to return to you a copy of the array and not the actual data, which could be a problem when you have 200 million elements. The windows and linux versions of the JVM may be different in this respect - some testing would be necessary to confirm.

Also, do you have a matched ReleaseDoubleArrayElements for every call to GetDoubleArrayElements? If you don't let the JVM know that you're done with the elements, it can't free the object / copy.

Ross said...

Solnus,
Thank you for an excellent post. I have copied your code and compiled it the way that you said and am still getting:
Exception in thread "main" java.lang.UnsatisfiedLinkError: HelloWorld.HelloNative(Ljava/lang/String;)V
at HelloWorld.HelloNative(NativeMethod)
at HelloWorld.main(HelloWorld.java:7)

I am using VS 8, XP SP2, and Java jdk1.6.0_11 on an IA32 processor. The goal is to get a NetBeans 6.5 jar file working with a VS 8 DLL. I have been finding the DLL in all of my test cases, but not finding the method in the DLL (see above). TIA.

Ross

Ross said...

Solnus,
I looked at the HelloDLL.dll using dumpbin and found one entry point, ?Java_HelloWorld_HelloNative@@YGXPAUJNIEnv_@@PAV_jobject@@PAV_jstring@@@Z
This, of course, looks nothing like the signature from the .h file,
* Class: HelloWorld
* Method: HelloNative
* Signature: (Ljava/lang/String;)V
or the exception,
Exception in thread "main" java.lang.UnsatisfiedLinkError: HelloWorld.HelloNative(Ljava/lang/String;)V
The entry point in the DLL looks like what I would expect from a mangled CPP function call. Does these two really find each other? Thanks again.

Ross

Ross said...
This comment has been removed by the author.
Solnus said...

Ross,

I doubt the issue is the DLL; what you see with dumpbin is the native signature:

Java_HelloWorld_HelloNative(JNIEnv *env, jobject obj, jstring javaString)

When Java makes the call to the exported function, it fills in the first two parameters automatically and passes through the rest (which would be, in this case, javaString) - so you only see one parameter from the Java side.

I believe that it's just not finding the DLL correctly in your situation. Sometimes there are funny issues with the library path. Something you could try to be sure it's finding the correct file is System.load instead of System.loadLibrary. System.load takes an absolute path to the file (including the .dll extension) whereas System.loadLibrary tries to find the file and guess the extension.

System.load("c:/path/to/dll/HelloDLL.dll");

If that works, check out your java.library.path to make sure it includes where the DLL is located (usually it has current directory in the path). If it doesn't, then you could try sending me your loading code and header file or checking the Sun forums for solutions. Link errors are a big issue on there.

Solnus

Ross said...
This comment has been removed by the author.
Ross said...

I disagree that the problem is not the DLL. From an early post above you will see that the exception that I am getting is not that the program fails to find the DLL, but rather that it fails to find the entry point, HelloWorld.HelloNative(Ljava/lang/String;)V, in the DLL. I have gotten this whole thing to work if the DLL comes from a C program (obvious not your code) but not if the DLL comes from a CPP program. Is there some setting in VS 8 that will unmangle the entry points in the DLL? The header file is:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorld
* Method: HelloNative
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_HelloWorld_HelloNative
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

I removed the angle brackets around jni.h because the blogger thought that it was HTML

The CPP code is:
#include "HelloWorld.h"
JNIEXPORT void JNICALL Java_HelloWorld_HelloNative(JNIEnv *env, jobject obj, jstring javaString){
const char *input = env->GetStringUTFChars(javaString, 0);
printf("%s\n", input);
env->ReleaseStringUTFChars(javaString, input);
}
The compile batch file is:
call "C:\Program Files\Microsoft Visual Studio 8\VC\vcvarsall.bat"

cl -I"c:\program files\java\jdk1.6.0_11\include" -I"c:\program files\java\jdk1.6.0_11\include\win32" -I"." -MT -LD HelloWorld.cpp -FeHelloDLL.dll

The results of the compile are:
C:\Documents and Settings\s805814\My Documents\Java\Docs\JNI\HelloWorld>call "C:
\Program Files\Microsoft Visual Studio 8\VC\vcvarsall.bat"
Setting environment for using Microsoft Visual Studio 2005 x86 tools.
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 14.00.50727.762 for 80x86

Copyright (C) Microsoft Corporation. All rights reserved.

HelloWorld.cpp
Microsoft (R) Incremental Linker Version 8.00.50727.762
Copyright (C) Microsoft Corporation. All rights reserved.

/dll
/implib:HelloDLL.lib
/out:HelloDLL.dll
HelloWorld.obj
Creating library HelloDLL.lib and object HelloDLL.exp

The following is the exception:
Exception in thread "main" java.lang.UnsatisfiedLinkError: HelloWorld.HelloNativ
e(Ljava/lang/String;)V
at HelloWorld.HelloNative(Native Method)
at HelloWorld.main(HelloWorld.java:7)

Other than the above, I am not sure what you mean by loading code.
Thanks again.

Ross

Solnus said...

Ross,

I don't see anything wrong with your C++ code. As for name mangling, notice the

extern "C"

in the header. This makes everything happy when linking. What I want to see is the code that you use to load the DLL - the System.load or System.loadLibrary calls that you're making and the surrounding lines.


Solnus

Ross said...

Solnus,
There is the java code:
class HelloWorld {
public static native void HelloNative(String s);
static {
System.loadLibrary("HelloDLL");
//System.load("c:\\HelloDLL.dll");
}
public static void main(String[] Args){
HelloNative("Hello, world!");
}
}

The directory where the DLL resides is in my path. However, since I know where you are going with this, I tried System.load with the same exception error.

I have tried mis-spelling the name of the DLL. The exception I get then is:
Exception in thread "main" java.lang.UnsatisfiedLinkError: Can't load library: c
:\HelloDLL1.dll
at java.lang.ClassLoader.loadLibrary(Unknown Source)
at java.lang.Runtime.load0(Unknown Source)
at java.lang.System.load(Unknown Source)
at HelloWorld.clinit(HelloWorld.java:5)
Could not find the main class: HelloWorld. Program will exit.

This exception is different that the one that I am getting, so I believe that the java code is finding the DLL just not the entry point. Also, I agree that the extern "C" should be handling the mangling, but then should not the dumpbin show a C-style entry point (no argument list) rather than a C++-style entry point?

Ross

Solnus said...

Here's the problem:

Header file signature:
JNIEXPORT void JNICALL Java_HelloWorld_HelloNative(JNIEnv *, jclass, jstring);

Source file signature:
JNIEXPORT void JNICALL Java_HelloWorld_HelloNative(JNIEnv, jobject, jstring)


Those need to match. Make the source file match the generated file.

Solnus

Ross said...

Solnus,
You are a wonder. I can't tell you how many times I have looked at this code and did not see that. Thanks again!!

Ross