Kommerzielle Unterstützung:
JavaCPP bietet effizienten Zugriff auf natives C++ in Java, ähnlich wie einige C/C++-Compiler mit der Assemblersprache interagieren. Es besteht keine Notwendigkeit, neue Sprachen wie SWIG, SIP, C++/CLI, Cython oder RPython zu erfinden. Stattdessen nutzt es, ähnlich wie cppyy es für Python anstrebt, die syntaktischen und semantischen Ähnlichkeiten zwischen Java und C++. Unter der Haube nutzt es JNI, sodass es neben Android, Avian und RoboVM auch mit allen Implementierungen von Java SE funktioniert (Anleitung).
Genauer gesagt, im Vergleich zu den oben genannten oder anderen Ansätzen (CableSwig, JNIGeneratorApp, cxxwrap, JNIWrapper, Platform Invoke, GlueGen, LWJGL Generator, JNIDirect, ctypes, JNA, JNIEasy, JniMarshall, JNative, J/Invoke, HawtJNI, JNR, BridJ, CFFI, fficxx, CppSharp, cgo, pybind11, rust-bindgen, Panama Native usw.) bildet es auf natürliche und effiziente Weise viele gemeinsame Funktionen ab, die die C++-Sprache bietet und oft als problematisch angesehen wird, darunter überladene Operatoren, Klassen- und Funktionsvorlagen, Rückrufe durch Funktionszeiger, Funktionsobjekte (auch Funktoren genannt), virtuelle Funktionen und Mitgliedsfunktionszeiger, verschachtelte Strukturdefinitionen, Argumente variabler Länge, verschachtelte Namespaces, große Datenstrukturen mit beliebigen Zyklen, virtuelle und mehrfache Vererbung, Übergabe/Rückgabe nach Wert/Referenz/Zeichenfolge/Vektor, anonyme Unions, Bitfelder, Ausnahmen, Destruktoren und gemeinsame oder eindeutige Zeiger (entweder über Try-with-Resources oder Garbage Collection) und Dokumentationskommentare. Offensichtlich würde die ordentliche Unterstützung von C++ als Ganzes mehr Arbeit erfordern (obwohl man über die intrinsische Sauberkeit von C++ streiten könnte), aber wir veröffentlichen es hier als Proof of Concept.
Beispielsweise haben wir damit bereits vollständige Schnittstellen zu OpenCV, FFmpeg, libdc1394, PGR FlyCapture, OpenKinect, videoInput, ARToolKitPlus, Leptonica, Tesseract, GSL, LLVM, HDF5, MKL, CUDA, Caffe, MXNet, TensorFlow erstellt , System-APIs und andere als Teil des JavaCPP Presets-Unterprojekts, ebenfalls demonstriert Frühe Parsing-Funktionen von C/C++-Header-Dateien, die vielversprechende und nützliche Ergebnisse liefern.
Wenn Sie Probleme mit der Software haben, können Sie gerne Fragen über die Mailingliste oder das Diskussionsforum stellen! Ich bin mir sicher, dass es alles andere als perfekt ist ...
Archive mit JAR-Dateien sind als Releases verfügbar.
Wir können auch alles automatisch herunterladen und installieren lassen mit:
Maven (in der pom.xml
Datei)
<Abhängigkeit> <groupId>org.bytedeco</groupId> <artifactId>javacpp</artifactId> <version>1.5.11</version> </Abhängigkeit>
Gradle (in der Datei build.gradle.kts
oder build.gradle
)
Abhängigkeiten { Implementierung("org.bytedeco:javacpp:1.5.11") }
Leiningen (in der project.clj
Datei)
:Abhängigkeiten [ [org.bytedeco/javacpp "1.5.11"] ]
sbt (in der Datei build.sbt
)
Bibliotheksabhängigkeiten += "org.bytedeco" % "javacpp" % "1.5.11"
Eine weitere für Gradle-Benutzer verfügbare Option ist Gradle JavaCPP, und für Scala-Benutzer gibt es ebenfalls SBT-JavaCPP.
Um JavaCPP verwenden zu können, müssen Sie die folgende Software herunterladen und installieren:
Eine Implementierung von Java SE 7 oder neuer:
OpenJDK http://openjdk.java.net/install/ oder
Oracle JDK http://www.oracle.com/technetwork/java/javase/downloads/ oder
IBM JDK http://www.ibm.com/developerworks/java/jdk/
Ein C++-Compiler, von dem aus diese getestet wurden:
https://docs.microsoft.com/en-us/cpp/build/walkthrough-compiling-a-native-cpp-program-on-the-command-line
Für Windows x86 und x64 http://mingw-w64.org/
GNU C/C++ Compiler (Linux usw.) http://gcc.gnu.org/
LLVM Clang (Mac OS X usw.) http://clang.llvm.org/
Microsoft C/C++ Compiler, Teil von Visual Studio https://visualstudio.microsoft.com/
Um Binärdateien für Android 4.0 oder neuer zu erstellen, müssen Sie außerdem Folgendes installieren:
Android NDK r7 oder neuer http://developer.android.com/ndk/downloads/
Und ähnlich wie bei der Zielversion von iOS müssen Sie Folgendes installieren:
Gluon VM http://gluonhq.com/products/mobile/vm/ oder
RoboVM 1.x oder neuer http://robovm.mobidevelop.com/downloads/
Um den Quellcode zu ändern, beachten Sie bitte, dass die Projektdateien erstellt wurden für:
Maven 3.x http://maven.apache.org/download.html
Da es sich schließlich um nativen Code handelt, können Fehler leicht zum Absturz der virtuellen Maschine führen. Glücklicherweise bietet die HotSpot-VM einige Tools, die uns beim Debuggen unter solchen Umständen helfen:
Leitfaden zur Fehlerbehebung für Java SE mit HotSpot VM
http://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-VM/html/
http://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/
Um zu verstehen, wie JavaCPP verwendet werden soll, sollte man sich zunächst die Mapping-Rezepte für C/C++-Bibliotheken ansehen. Um das Gesamtbild zu verstehen, ist jedoch auch ein allgemeiner Überblick über die Grundarchitektur verfügbar. Das Repository der JavaCPP-Voreinstellungen bietet darüber hinaus komplexe Beispiele, die wir als Vorlagen verwenden können, aber es enthält auch eine Wiki-Seite zum Erstellen neuer Voreinstellungen, die ihre Struktur im Detail erklärt, sowie ein kleines, aber vollständiges Beispielprojekt, mit dem man mit dem Experimentieren beginnen kann mit.
Um native
Methoden zu implementieren, generiert JavaCPP geeigneten Code für JNI und übergibt ihn an den C++-Compiler, um eine native Bibliothek zu erstellen. Zu keinem Zeitpunkt müssen wir uns mit JNI, Makefiles oder anderen nativen Tools die Hände schmutzig machen. Hier ist es wichtig zu wissen, dass wir zwar alle Anpassungen innerhalb der Java-Sprache mithilfe von Annotationen vornehmen, JavaCPP jedoch Code erzeugt, der im Vergleich zu manuell codierten JNI-Funktionen keinen Overhead verursacht (überprüfen Sie die generierten CPP-Dateien, um sich selbst zu überzeugen). Darüber hinaus lädt die Methode Loader.load()
zur Laufzeit automatisch die nativen Bibliotheken aus Java-Ressourcen, die durch den Erstellungsprozess im richtigen Verzeichnis abgelegt wurden. Sie können sogar in einer JAR-Datei archiviert werden, es ändert sich nichts. Benutzer müssen einfach nicht herausfinden, wie sie das System dazu bringen, die Dateien zu laden. Aufgrund dieser Eigenschaften eignet sich JavaCPP für beides
Zugriff auf native APIs,
Verwendung komplexer C++-Typen,
Optimierung der Codeleistung, oder
Erstellen von Callback-Funktionen.
Um mehr über die Verwendung der Funktionen dieses Tools zu erfahren, lesen Sie neben den wenigen Beispielen unten auch die Mapping-Rezepte für C/C++-Bibliotheken sowie den Quellcode der JavaCPP-Voreinstellungen. Weitere Informationen zur API selbst finden Sie in der von Javadoc generierten Dokumentation.
Natürlich funktioniert das alles auch mit der Scala-Sprache, aber um den Prozess noch reibungsloser zu gestalten, sollte es nicht zu schwierig sein, Unterstützung für „native Eigenschaften“ hinzuzufügen, sodass Deklarationen wie @native var
native Getter generieren könnten und Setter-Methoden...
Der häufigste Anwendungsfall besteht darin, auf eine für C++ geschriebene native Bibliothek zuzugreifen, beispielsweise in einer Datei namens NativeLibrary.h
, die diese C++-Klasse enthält:
#include <string>namespace NativeLibrary { class NativeClass { public: const std::string& get_property() { return property; } void set_property(const std::string& property) { this->property = property; } std::string-Eigenschaft; }; }
Um die Arbeit mit JavaCPP zu erledigen, können wir ganz einfach eine Java-Klasse wie diese definieren – obwohl man den Parser
verwenden könnte, um sie aus der Header-Datei zu erstellen, wie das Unterprojekt „JavaCPP Presets“ zeigt, und dabei den in den Mapping-Rezepten beschriebenen Prinzipien folgt für C/C++-Bibliotheken:
import org.bytedeco.javacpp.*;import org.bytedeco.javacpp.annotation.*;@Platform(include="NativeLibrary.h")@Namespace("NativeLibrary")public class NativeLibrary { public static class NativeClass erweitert Pointer { static { Loader.load(); } public NativeClass() { allocate(); } private native void allocate(); // um die Getter- und Setter-Funktionen aufzurufen public native @StdString String get_property(); public native void set_property(String property); // um direkt auf die Mitgliedsvariable zuzugreifen public native @StdString String property(); öffentliche native void-Eigenschaft (String-Eigenschaft); } public static void main(String[] args) { // In Java zugewiesene Zeigerobjekte werden freigegeben, sobald sie nicht mehr erreichbar sind. // C++-Destruktoren können jedoch weiterhin rechtzeitig mit Pointer.deallocate() NativeClass l = new NativeClass aufgerufen werden (); l.set_property("Hallo Welt!"); System.out.println(l.property()); } }
Nachdem wir den Java-Quellcode auf die übliche Weise kompiliert haben, müssen wir ihn auch mit JavaCPP erstellen, bevor wir ihn ausführen, oder wir können ihm alles wie folgt überlassen:
$ java -jar javacpp.jar NativeLibrary.java -exec Hallo Welt!
Um die relative Benutzerfreundlichkeit selbst bei komplexen Datentypen zu demonstrieren, stellen Sie sich vor, wir hätten eine C++-Funktion, die einen vector<vector<void*> >
als Argument verwendet. Um diesen Typ zu unterstützen, könnten wir eine einfache Klasse wie diese definieren:
import org.bytedeco.javacpp.*;import org.bytedeco.javacpp.annotation.*;@Platform(include="<vector>")public class VectorTest { @Name("std::vector<std::vector<void *> >") öffentliche statische Klasse PointerVectorVector erweitert Pointer { static { Loader.load(); } public PointerVectorVector() { allocate(); } public PointerVectorVector(long n) { allocate(n); } public PointerVectorVector(Pointer p) { super(p); } // this = (vector<vector<void*> >*)p private native void allocate(); // this = new vector<vector<void*> >() private native void allocate(long n); // this = new vector<vector<void*> >(n) @Name("operator=") public native @ByRef PointerVectorVector put(@ByRef PointerVectorVector x); @Name("operator[]") public native @StdVector PointerPointer get(long n); public native @StdVector PointerPointer at(long n); öffentliche native lange Größe(); public native @Cast("bool") boolean empty(); public native void resize(long n); public native @Index long size(long i); // return (*this)[i].size() public native @Index @Cast("bool") boolean empty(long i); // return (*this)[i].empty() public native @Index void resize(long i, long n); // (*this)[i].resize(n) public native @Index Pointer get(long i, long j); // return (*this)[i][j] public native void put(long i, long j, Pointer p); // (*this)[i][j] = p } public static void main(String[] args) { PointerVectorVector v = new PointerVectorVector(13); v.resize(0, 42); // v[0].resize(42) Zeiger p = new Pointer() { { address = 0xDEADBEEFL; } }; v.put(0, 0, p); // v[0][0] = p PointerVectorVector v2 = new PointerVectorVector().put(v); Zeiger p2 = v2.get(0).get(); // p2 = *(&v[0][0]) System.out.println(v2.size() + " " + v2.size(0) + " " + p2); v2.at(42); } }
Das Ausführen dieses Programms mit diesem Befehl erzeugt die folgende Ausgabe:
$ java -jar javacpp.jar VectorTest.java -exec 13 42 org.bytedeco.javacpp.Pointer[address=0xdeadbeef,position=0,limit=0,capacity=0,deallocator=null] Ausnahme im Thread „main“ java.lang.RuntimeException: vector::_M_range_check: __n (das ist 42) >= this->size() (das ist 13) bei VectorTest$PointerVectorVector.at (Native Methode) bei VectorTest.main(VectorTest.java:44)
In anderen Fällen möchten wir aus Leistungsgründen möglicherweise in C++ (einschließlich CUDA) programmieren. Angenommen, unser Profiler hätte festgestellt, dass eine Methode namens Processor.process()
90 % der Ausführungszeit des Programms in Anspruch nahm:
public class Processor { public static void Process(java.nio.Buffer buffer, int size) { System.out.println("Processing in Java..."); // ... } public static void main(String[] args) { Process(null, 0); } }
Nach vielen Tagen harter Arbeit und Schweiß haben die Ingenieure einige Hacks ausgetüftelt und es geschafft, diese Quote auf 80 % zu senken, aber wissen Sie, die Manager waren immer noch nicht zufrieden. Wir könnten also versuchen, es in C++ (oder sogar in Assembler über den Inline-Assembler) neu zu schreiben und die resultierende Funktion in einer Datei namens Processor.h
abzulegen:
#include <iostream>statischer Inline-Void-Prozess(void *buffer, int size) { std::cout << "Verarbeitung in C++..." << std::endl; // ...}
Nachdem ich den Java-Quellcode so angepasst habe:
import org.bytedeco.javacpp.*;import org.bytedeco.javacpp.annotation.*;@Platform(include="Processor.h")public class Processor { static { Loader.load(); } public static native void Process(java.nio.Buffer buffer, int size); public static void main(String[] args) { Process(null, 0); } }
Es würde dann wie folgt kompiliert und ausgeführt:
$ java -jar javacpp.jar Processor.java -exec Verarbeitung in C++...
Einige Anwendungen erfordern auch eine Möglichkeit, von C/C++ aus die JVM zurückzurufen. Daher bietet JavaCPP eine einfache Möglichkeit, benutzerdefinierte Rückrufe zu definieren, entweder als Funktionszeiger, Funktionsobjekte oder virtuelle Funktionen. Obwohl es Frameworks gibt, die wohl schwieriger zu verwenden sind, wie Jace, JunC++ion, JCC, jni.hpp oder Scapix, die vollständige Java-APIs in C++ abbilden können, da das Aufrufen einer Java-Methode aus nativem Code mindestens einen Zeitaufwand erfordert Da es um Größenordnungen mehr Zeit kostet als umgekehrt, macht es meiner Meinung nach wenig Sinn, eine API zu exportieren, die für die Verwendung in Java entwickelt wurde. Angenommen, wir möchten dennoch einige Operationen in Java ausführen und planen, diese in eine Funktion namens foo()
zu packen, die eine Methode innerhalb der Klasse Foo
aufruft. Wir können den folgenden Code in eine Datei namens foo.cpp
schreiben und dabei darauf achten, diese zu initialisieren JVM bei Bedarf entweder mit JavaCPP_init()
oder auf andere Weise:
#include <iostream>#include "jniFoo.h"int main() { JavaCPP_init(0, NULL); versuche es mit {foo(6, 7); } Catch (std::Exception &e) { std::cout << e.what() << std::endl; } JavaCPP_uninit(); }
Wir können diese Funktion dann wie folgt für eine call()
oder apply()
Methode deklarieren, die in einem FunctionPointer
definiert ist:
import org.bytedeco.javacpp.*;import org.bytedeco.javacpp.annotation.*;@Platform(include="<algorithm>")@Namespace("std")public class Foo { static { Loader.load(); } public static class Callback erweitert FunctionPointer { // Loader.load() und allocate() sind nur erforderlich, wenn explizit eine Instanz erstellt wird static { Loader.load(); } protected Callback() { allocate(); } private native void allocate(); public @Name("foo") boolean call(int a, int b) löst eine Ausnahme aus { throw new Exception("bar " + a * b); } } // Wir können auch einen FunctionPointer als Argument an andere Funktionen übergeben (oder abrufen) (oder einen Wert von diesen zurückgeben). public static native void steady_sort(IntPointer first, IntPointer last, Callback Compare); // Und um es als C++-Funktionsobjekt zu übergeben (oder abzurufen), mit @ByVal oder @ByRef annotieren public static native void sort(IntPointer first, IntPointer last, @ByVal Callback Compare); }
Da Funktionen auch Zeiger haben, können wir FunctionPointer
Instanzen entsprechend verwenden, ähnlich wie der FunPtr
Typ von Haskell FFI, wobei jedoch jedes geworfene java.lang.Throwable
Objekt in std::exception
übersetzt wird. Das Erstellen und Ausführen dieses Beispielcodes mit diesen Befehlen unter Linux x86_64 erzeugt die erwartete Ausgabe:
$ java -jar javacpp.jar Foo.java -header $ g++ -I/usr/lib/jvm/java/include/ -I/usr/lib/jvm/java/include/linux/ foo.cpp linux-x86_64/libjniFoo.so -o foo $ ./foo java.lang.Ausnahme: Takt 42
In diesem Beispiel wird das FunctionPointer
Objekt implizit erstellt, aber um einen nativen Funktionszeiger aufzurufen, könnten wir einen definieren, der stattdessen eine native call()/apply()
Methode enthält, und explizit eine Instanz erstellen. Eine solche Klasse kann in Java auch erweitert werden, um Rückrufe zu erstellen, und muss wie jedes andere normale Pointer
Objekt mit einer native void allocate()
Methode zugewiesen werden. Denken Sie also bitte daran, Referenzen in Java beizubehalten , da diese im Müll gesammelt werden . Als Bonus FunctionPointer.call()/apply()
tatsächlich einem überladenen operator()
eines C++-Funktionsobjekts zugeordnet, das wir an andere Funktionen übergeben können, indem wir Parameter mit @ByVal
oder @ByRef
annotieren, wie bei sort()
Funktion im obigen Beispiel.
Das Gleiche ist auch mit virtuellen Funktionen möglich, egal ob „rein“ oder nicht. Betrachten Sie die folgende C++-Klasse, die in einer Datei namens Foo.h
definiert ist:
#include <stdio.h>class Foo {public: int n; Foo(int n) : n(n) { } virtual ~Foo() { } virtual void bar() { printf("Callback in C++ (n == %d)n", n); } };void callback(Foo *foo) { foo->bar(); }
Die Funktion Foo::bar()
kann in Java überschrieben werden, wenn wir die Methode in der Peer-Klasse entweder als native
oder abstract
deklarieren und sie mit @Virtual
annotieren, zum Beispiel:
import org.bytedeco.javacpp.*;import org.bytedeco.javacpp.annotation.*;@Platform(include="Foo.h")public class VirtualFoo { static { Loader.load(); } öffentliche statische Klasse Foo erweitert Zeiger { static { Loader.load(); } public Foo(int n) { allocate(n); } private native void allocate(int n); @NoOffset public native int n(); public native Foo n(int n); @Virtual public native void bar(); } public static native void callback(Foo foo); public static void main(String[] args) { Foo foo = new Foo(13); Foo foo2 = new Foo(42) { public void bar() { System.out.println("Callback in Java (n == " + n() + ")"); } }; foo.bar(); foo2.bar(); Rückruf(foo); Rückruf(foo2); } }
Welches gibt aus, was man natürlich annehmen würde:
$ java -jar javacpp.jar VirtualFoo.java -exec Rückruf in C++ (n == 13) Rückruf in Java (n == 42) Rückruf in C++ (n == 13) Rückruf in Java (n == 42)
Am einfachsten lässt sich Avian mit OpenJDK-Klassenbibliotheken zum Laufen bringen, also fangen wir damit an. Nachdem wir wie oben beschrieben ein Programm erstellt und aufgebaut haben, können wir es ohne weitere Änderungen direkt mit diesem Befehl ausführen:
$ /path/to/avian-dynamic -Xbootclasspath/a:javacpp.jar <MainClass>
Im Fall von Android müssen wir jedoch noch etwas mehr Arbeit leisten. Für das auf Ant basierende Befehlszeilen-Build-System im Verzeichnis des Projekts:
Kopieren Sie die Datei javacpp.jar
in das Unterverzeichnis libs/
und
Führen Sie diesen Befehl aus, um die *.so
Bibliotheksdateien in libs/armeabi/
zu erstellen:
$ java -jar libs/javacpp.jar -classpath bin/ -classpath bin/classes/ > -properties <android-arm|android-x86> -Dplatform.root=/path/to/android-ndk/ > -Dplatform .compiler=/path/to/<arm-linux-androideabi-g++|i686-linux-android-g++> -d libs/<armeabi|x86>/
Um alles automatisch zu machen, können wir diesen Befehl auch in die Datei build.xml
einfügen. Alternativ können wir für die Integration mit Android Studio Gradle JavaCPP verwenden.
Ähnliches gilt für RoboVM, vorausgesetzt, dass sich die kompilierten Klassen im Unterverzeichnis classes
befinden:
Kopieren Sie die Datei javacpp.jar
in das Projektverzeichnis und
Führen Sie die folgenden Befehle aus, um die native Binärdatei zu erstellen:
$ java -jar javacpp.jar -cpclasses/ -properties <ios-arm|ios-x86> -o lib $ /path/to/robovm -arch <thumbv7|x86> -os ios -cp javacpp.jar:classes/ -libsclasses/<ios-arm|ios-x86>/lib.o <MainClass>
Und anstelle von Loader.load()
sollte die Bibliothek in diesem Fall mit System.load("lib.o")
geladen werden und ist möglicherweise überhaupt nicht erforderlich.
Projektleitung: Samuel Audet samuel.audet at
gmail.com
Entwicklerseite: https://github.com/bytedeco/javacpp
Diskussionsgruppe: http://groups.google.com/group/javacpp-project