Showing posts with label Visual Studio. Show all posts
Showing posts with label Visual Studio. Show all posts

Thursday, June 5, 2008

user32.dll Functions in JNI

UPDATE NOTE:
Please read this post explaining a small issue with the code used in this article.


If you've never used JNI before, please read my previous post first.

In this post I'm going to go over how to use some windows functions with JNI. Specifically, two which have caused me frustration recently: GetForegroundWindow and GetWindowText.

Reference Pages:
GetForegroundWindow
GetWindowText
GetWindowTextLength
GetLastError
C++ JNI Types/Functions

To the code! This is similar to my initial code - there is a problem with it. Hopefully you don't see it, so as to keep my programming ego intact. Again, this code DOES NOT WORK - if you're just here for the code look down further.

[javanative.cpp]

#include "windows.h"
#include "winuser.h"
#include "jni.h"
#include "javaclass.h" // generated header file

JNIEXPORT jstring JNICALL Java_javaclass_getForegroundWindow(JNIEnv *env, jobject obj){
HWND hwnd = GetForegroundWindow();
if(hwnd == 0){
return NULL;
}
int length = GetWindowLength(hwnd);
LPTSTR buffer = "xxxxxxxxxxxxxxxxxxxx"; // this is how I found a few examples online
int textLength = GetWindowText(hwnd, buffer, 20);
return env->NewStringUTF(buffer); // this too - memory leak. see code below
}

Note that the functions (GetWindowText, etc.) are in user32.dll; in order to use them, you must include the user32.lib library during compilation. The method to do this depends on your compiler; with the Visual Studio command-line compiler, you just append the libraries to the end (as you'll see below).

Compilation:

javac javaclass.java
javah -jni javaclass
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 javanative.cpp -Fejavanative.dll user32.lib



Now then, it compiles - but it doesn't work. It returns the "xxxx..." string as the name every time. After some debugging, I found that GetWindowText was returning an error code of 1400 - and that code means that the window does not exist. Some more testing confirmed that the window DID exist - IsWindow returned true, etc. So, what's the problem?

As you probably guessed (the only commented line in the code):


LPTSTR buffer = "xxxxxxxxxxxxxxxxxxxx"; // this is how I found a few examples online


Apparently, having the buffer on the stack makes the call return 1400 - a completely wrong error code. The correct way to do it is to put the buffer on the heap.


int length = GetWindowTextLength(hwnd);
// make some heap space - lots of ways to do this.
LPTSTR buffer = (LPTSTR)malloc(length * sizeof(TCHAR));


It works!

Here's the full, working code:

#include "windows.h"
#include "winuser.h"
#include "jni.h"
#include "javaclass.h"

JNIEXPORT jstring JNICALL Java_nimbus_getForegroundWindow(JNIEnv *env, jobject obj){

HWND hwnd = GetForegroundWindow();
if(hwnd == 0){
return NULL;
}
int length = GetWindowTextLength(hwnd);
LPTSTR buffer = (LPTSTR)malloc(length * sizeof(TCHAR));
int textLength = GetWindowText(hwnd, buf, length);
DWORD lasterror = GetLastError();
jstring jstr = env->NewStringUTF(buffer);
free(buffer);
return jstr;
}
Note that even though we allocate heap space, we don't want to free it ourselves. This is not a memory leak, Java's GC will deallocate it when it's no longer in use.

And there you have it. Calling getForegroundWindow() in your java source now returns the caption of the foreground window.

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.