Neulich musste ich eine DLL aus Java heraus ansprechen. Zur Wahl stehen JNI und JNA, wobei JNA “nur” ein Wrapper für JNI ist – und bei mir nicht funktioniert hat. In der Theorie ist JNI aber auch nicht schwer, allerdings legt Visual Studio einem mir Steine in den Weg. Ein Grund mehr, bei Java zu bleiben.
Der Java-Teil ist dann auch denkbar einfach:
1 2 3 4 5 6 7 8 9 10 |
public class DllBridge { static { // loads file MyDll.dll: System.loadLibrary("MyDll"); } // declare all functions you want to use as "native": public native String hello(String input); // ... } |
Diese DllBridge wird dann in eine Headerdatei übersetzt:
1 2 |
// erzeugt DllBridge.h; Achtung: ohne .java: $ javah DllBridge |
Und diese Header-Datei dann in “das” C/C++-Projekt kopiert. An “das” Projekt kommt man wie folgt:
- Visual Studio installieren; ich habe Visual Studio Community 2017 mit den Standardkomponenten genommen
- Neues C++/Win32-Projekt anlegen, im Dialog unter “Anwendungseinstellungen” den Anwendungstyp “DLL” wählen
- Ich habe das Projekt testweise MyDll genannt, die entstehende .dll heißt später genauso
Nachdem man die DllBridge.h in das Projekt kopiert und über Rechtsklick > Include In Project eingebunden hat, kann man ihre Methoden implementieren. O.g. Code erzeugt genau eine Methode Java_DllBridge_hello, siehe DllBridge.h. Deren Implementierung in der .c-Datei:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include "stdafx.h" #include <jni.h> #include "DllBridge.h" // ! JNIEXPORT jstring JNICALL Java_DllBridge_hello(JNIEnv *env, jobject thisObj, jstring input) { const char *name = (*env).GetStringUTFChars(input, NULL); char msg[60] = "Hello "; strcat(msg, name); (*env).ReleaseStringUTFChars(input, name); MessageBoxA(NULL, msg, "", MB_OK | MB_SYSTEMMODAL); return (*env).NewStringUTF(msg); } |
Alleine die Importe und die korrekte Version von MessageBox herauszufinden, hat sicher eine Stunde gedauert 😡 Oh, Stichwort “Importe”: jni.h wird vom JDK mitgebracht, die entsprechenden Ordner muss man in VS bekannt machen. Das funktioniert nicht über einen Symlink, sondern über die Projekteinstellungen, indem man unter Configuration Properties > C/C++ > General > Additional Include Directories diese drei Verzeichnisse hinzufügt (Rekursion wäre ja auch zu einfach.):
- C:\Program Files (x86)\Java\jdk1.8.0_121\include
- C:\Program Files (x86)\Java\jdk1.8.0_121\include\win32
- C:\Program Files (x86)\Java\jdk1.8.0_121\include\win32\bridge
PS Ja: Das ist ein 32-Bit-Java, weil ich letztlich eine 32Bit dll ansprechen will.
Das Ganze sollte jetzt bauen (Build > Build solution, oder STRG+Shift+B) und eine <Projektname>.dll erzeugen, siehe Konsolenausgabe. Der Name der .dll muss dem im initialen Java-Code entsprechen.
Ein Aufruf von
1 2 3 |
DllBridge dll = new DllBridge(); String s = dll.hello("moto"); System.out.println("dll says: " + s); |
öffnet dann die MessageBox:
Sowie die erwartete Ausgabe auf der Konsole. Für reine C-Anbindung könnte man hier aufhören.
Interessant wird es nun, wenn man langlebigere C++-Objekte von Java aus referenzieren möchte. Idee:
- Eine Klasse auf Java-Seite, die die dll lädt, sowie die nativen Methoden bereitstellt, und eine Klasse auf C++-Seite, die diese implementiert
- Die C++-Klasse wird initialisiert, ihren Pointer gibt man in Form einer long an Java zurück (bzw. jlong, vgl. hier), vermutlich ist das die Speicheradresse
- Über diese long kann Java dann das Objekt referenzieren
- Braucht man das C++-Objekt nicht mehr, wird es deleted.
(via, längeres Beispiel, Danke Nils!)
Hier sieht das so aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
@SuppressWarnings({"SameParameterValue", "unused", "WeakerAccess"}) public class DllBridge { static { System.loadLibrary("MyDll"); } private native long initNative(); private native void destroyNative(long ptr); private native void incrementCounter(long ptr); private long nativePtr; public DllBridge() { nativePtr = initNative(); } public void incrementCounter() { incrementCounter(nativePtr); } public void destroy() { destroyNative(nativePtr); nativePtr = 0L; } @Override protected void finalize() { destroy(); } } |
Bzw. in C++:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include "stdafx.h" #include <jni.h> #include "DllBridge.h" class DllBridge { private: int counter; public: DllBridge(); void incrementCounter(); }; DllBridge::DllBridge() { counter = 0; } void DllBridge::incrementCounter() { counter++; printf("increased counter to %d\n", counter); } JNIEXPORT jlong JNICALL Java_DllBridge_initNative(JNIEnv *env, jobject thisObj) { return (long) new DllBridge(); } JNIEXPORT void JNICALL Java_DllBridge_incrementCounter(JNIEnv *env, jobject thisObj, jlong ptr) { DllBridge* bridge = (DllBridge*) ptr; bridge->incrementCounter(); } JNIEXPORT void JNICALL Java_DllBridge_destroyNative(JNIEnv *env, jobject thisObj, jlong ptr) { // TODO? What about the DllBridge's counter? I want my garbage collector delete (DllBridge*) ptr; } |
Aufruf in Java dann:
1 2 3 4 5 |
DllBridge dll = new DllBridge(); dll.incrementCounter(); dll.incrementCounter(); dll.incrementCounter(); dll.destroy(); |
Last but not least die andere Richtung, also C++ nach Java. Die Methode
1 2 3 4 |
// in DllBridge.java: public void callback(String payload) { System.out.println("C++ says " + payload); } |
lässt sich von C++ aus aufrufen über
1 2 3 4 5 6 7 8 9 |
// might be env->GetObjectClass(thisObj) as well; use full qualified path if class has package: jclass javaClass = env->FindClass("DllBridge"); // syntax is "(parameter type)return type" (plus semicolon if not a primitive) // see https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html#type_signatures jmethodID callback = env->GetMethodID(javaClass, "callback", "(Ljava/lang/String;)V"); // use CallStaticVoidMethod and pass javaClass, if static method is called env->CallVoidMethod(thisObj, callback, env->NewStringUTF("hi")); |
Weiterführendes dazu hier.
hth