商业支持:
JavaCPP 提供了对 Java 内部本机 C++ 的高效访问,这与某些 C/C++ 编译器与汇编语言交互的方式没有什么不同。无需发明新语言,例如 SWIG、SIP、C++/CLI、Cython 或 RPython。相反,与 cppyy 为 Python 所做的事情类似,它利用了 Java 和 C++ 之间的语法和语义相似性。在底层,它使用 JNI,因此除了 Android、Avian 和 RoboVM(指令)之外,它还适用于 Java SE 的所有实现。
更具体地说,与上述或其他地方的方法(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 等),它自然而有效地映射了 C++ 语言提供的许多常见功能,并且通常被认为是有问题的,包括重载运算符、类和函数模板、通过函数指针的回调、函数对象(又名函子) )、虚函数和成员函数指针、嵌套结构定义、可变长度参数、嵌套命名空间、包含任意循环的大型数据结构、虚拟和多重继承、按值/引用/字符串/向量传递/返回、匿名联合、位字段、异常、析构函数和共享或唯一指针(通过 try-with-resources 或垃圾收集)以及文档注释。显然,整齐地支持整个 C++ 需要更多的工作(尽管人们可能会争论 C++ 内在的整洁性),但我们在这里发布它作为概念证明。
举个例子,我们已经使用它生成了 OpenCV、FFmpeg、libdc1394、PGR FlyCapture、OpenKinect、videoInput、ARToolKitPlus、Leptonica、Tesseract、GSL、LLVM、HDF5、MKL、CUDA、Caffe、MXNet、TensorFlow 的完整接口、系统 API 等作为 JavaCPP Presets 子项目的一部分,也展示了早期解析功能C/C++ 头文件显示出有前途且有用的结果。
如果您在使用该软件时遇到任何问题,请随时在邮件列表或论坛上提问!我确信它远非完美......
包含 JAR 文件的存档可作为版本提供。
我们还可以使用以下命令自动下载和安装所有内容:
Maven(在pom.xml
文件内)
<依赖关系> <groupId>org.bytedeco</groupId> <artifactId>javacpp</artifactId> <版本>1.5.11</版本> </依赖>
Gradle(在build.gradle.kts
或build.gradle
文件内)
依赖项{ 实现(“org.bytedeco:javacpp:1.5.11”) }
Leiningen(在project.clj
文件内)
:依赖关系[ [org.bytedeco/javacpp“1.5.11”] ]
sbt(在build.sbt
文件内)
库依赖项 += "org.bytedeco" % "javacpp" % "1.5.11"
Gradle 用户可以使用的另一个选项是 Gradle JavaCPP,同样,Scala 用户可以使用 SBT-JavaCPP。
要使用JavaCPP,您需要下载并安装以下软件:
Java SE 7 或更高版本的实现:
OpenJDK http://openjdk.java.net/install/ 或
Oracle JDK http://www.oracle.com/technetwork/java/javase/downloads/ 或
IBM JDK http://www.ibm.com/developerworks/java/jdk/
一个 C++ 编译器,其中已经过测试:
https://docs.microsoft.com/en-us/cpp/build/walkthrough-compiling-a-native-cpp-program-on-the-command-line
对于 Windows x86 和 x64 http://mingw-w64.org/
GNU C/C++ 编译器(Linux 等)http://gcc.gnu.org/
LLVM Clang(Mac OS X 等)http://clang.llvm.org/
Microsoft C/C++ 编译器,Visual Studio 的一部分 https://visualstudio.microsoft.com/
要生成适用于 Android 4.0 或更高版本的二进制文件,您还必须安装:
Android NDK r7 或更高版本 http://developer.android.com/ndk/downloads/
与目标 iOS 类似,您需要安装:
Gluon VM http://gluonhq.com/products/mobile/vm/ 或
RoboVM 1.x 或更高版本 http://robovm.mobidevelop.com/downloads/
要修改源代码,请注意项目文件是为以下目的创建的:
Maven 3.x http://maven.apache.org/download.html
最后,由于我们处理的是本机代码,错误很容易使虚拟机崩溃。幸运的是,HotSpot VM 提供了一些工具来帮助我们在这些情况下进行调试:
使用 HotSpot VM 的 Java SE 故障排除指南
http://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-VM/html/
http://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/
要了解如何使用 JavaCPP,应该首先查看 C/C++ 库的映射方法,但也可以对基本架构进行高级概述,以了解更大的情况。 JavaCPP 预设的存储库进一步提供了我们可以用作模板的复杂示例,但它还包括一个关于如何创建新预设的 wiki 页面,该页面详细解释了它们的结构,以及一个小而完整的示例项目,人们可以从中开始试验和。
为了实现native
方法,JavaCPP 为 JNI 生成适当的代码,并将其传递给 C++ 编译器以构建本机库。我们永远不需要亲自使用 JNI、makefile 或其他本机工具。这里要认识到的重要一点是,虽然我们使用注释在 Java 语言中进行所有自定义,但与手动编码的 JNI 函数相比,JavaCPP 生成的代码开销为零(验证生成的 .cpp 文件以说服自己)。此外,在运行时, Loader.load()
方法会自动从 Java 资源加载本机库,这些资源由构建过程放置在正确的目录中。它们甚至可以存档在 JAR 文件中,这不会改变任何内容。用户根本不需要弄清楚如何让系统加载文件。这些特性使得 JavaCPP 适用于
访问本机 API,
使用复杂的 C++ 类型,
优化代码性能,或者
创建回调函数。
除了下面提供的几个示例之外,要了解有关如何使用此工具的功能的更多信息,请参阅 C/C++ 库的映射食谱以及 JavaCPP 预设的源代码示例。有关 API 本身的更多信息,可以参考 Javadoc 生成的文档。
当然,这一切也适用于 Scala 语言,但为了使过程更加顺利,添加对“本机属性”的支持应该不会太难,这样像@native var
这样的声明就可以生成本机 getter和设置方法...
最常见的用例涉及访问一些为 C++ 编写的本机库,例如,在包含此 C++ 类的名为NativeLibrary.h
的文件中:
#include <string>命名空间 NativeLibrary { class NativeClass { public: const std::string& get_property() { return property; } } void set_property(const std::string& property) { this->property = property; } std::string 属性; }; }
为了使用 JavaCPP 完成工作,我们可以轻松地定义一个像这样的 Java 类——尽管可以使用Parser
从头文件中生成它,如 JavaCPP Presets 子项目所示,遵循映射食谱中概述的原则对于 C/C++ 库:
导入 org.bytedeco.javacpp.*;导入 org.bytedeco.javacpp.annotation.*;@Platform(include="NativeLibrary.h")@Namespace("NativeLibrary")public class NativeLibrary { public static class NativeClass extends Pointer { static { 加载器.load(); } 公共 NativeClass() { allocate(); } 私有本机无效分配(); // 调用 getter 和 setter 函数 public native @StdString String get_property();公共本机无效set_property(字符串属性); // 直接访问成员变量 public native @StdString String property(); 公共本机无效属性(字符串属性); } public static void main(String[] args) { // 在 Java 中分配的指针对象一旦无法访问就会被释放, // 但 C++ 析构函数仍然可以通过 Pointer.deallocate() 及时调用 NativeClass l = new NativeClass (); l.set_property("你好世界!"); System.out.println(l.property()); } }
以通常的方式编译Java源代码后,我们还需要在执行之前使用JavaCPP进行构建,或者我们可以让它做所有事情,如下所示:
$ java -jar javacpp.jar NativeLibrary.java -exec 你好世界!
为了证明即使面对复杂的数据类型它也相对易于使用,假设我们有一个 C++ 函数,它接受一个vector<vector<void*> >
作为参数。为了支持这种类型,我们可以定义一个简单的类,如下所示:
导入 org.bytedeco.javacpp.*;导入 org.bytedeco.javacpp.annotation.*;@Platform(include="<vector>")public class VectorTest { @Name("std::vector<std::vector<void *> >") public static class PointerVectorVector extends Pointer { static { Loader.load(); } 公共 PointerVectorVector() { allocate(); } 公共 PointerVectorVector(long n) { 分配(n); } 公共 PointerVectorVector(指针 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); 公共本机@StdVector PointerPointer at(long n); 公共本机长尺寸(); 公共本机@Cast(“布尔”)布尔空(); 公共本机无效调整大小(长n); 公共本机@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, 指针 p); // (*this)[i][j] = p } public static void main(String[] args) { PointerVectorVector v = new PointerVectorVector(13); v.调整大小(0, 42); // v[0].resize(42) 指针 p = new Pointer() { { 地址 = 0xDEADBEEFL; } }; v.put(0, 0, p); // v[0][0] = p PointerVectorVector v2 = new PointerVectorVector().put(v); 指针 p2 = v2.get(0).get(); // p2 = *(&v[0][0]) System.out.println(v2.size() + " " + v2.size(0) + " " + p2); v2.at(42); } }
使用此命令执行该程序会产生以下输出:
$ java -jar javacpp.jar VectorTest.java -exec 13 42 org.bytedeco.javacpp.Pointer[地址=0xdeadbeef,位置=0,限制=0,容量=0,deallocator=null] 线程“main”中的异常 java.lang.RuntimeException: vector::_M_range_check: __n (即 42) >= this->size() (即 13) 在 VectorTest$PointerVectorVector.at(本机方法) 在 VectorTest.main(VectorTest.java:44)
其他时候,出于性能原因,我们可能希望使用 C++(包括 CUDA)进行编码。假设我们的分析器发现名为Processor.process()
方法占用了程序 90% 的执行时间:
public class Processor { public static void process(java.nio.Buffer buffer, int size) { System.out.println("Java 中的处理..."); // ... } public static void main(String[] args) { 进程(null, 0); } }
经过多天的努力和汗水,工程师们想出了一些窍门,成功地将这个比例降到了80%,但你知道,管理者们仍然不满意。因此,我们可以尝试用 C++(甚至通过内联汇编器使用汇编语言)重写它,并将生成的函数放入名为Processor.h
的文件中:
#include <iostream>static inline void process(void *buffer, int size) { std::cout << "用 C++ 处理..." << std::endl; // ...}
将Java源代码调整为如下所示:
导入 org.bytedeco.javacpp.*;导入 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); 公共静态无效主(字符串[] args){过程(null,0); } }
然后它会像这样编译和执行:
$ java -jar javacpp.jar Processor.java -exec 用 C++ 处理...
一些应用程序还需要一种从 C/C++ 回调 JVM 的方法,因此 JavaCPP 提供了一种简单的方法来定义自定义回调,无论是函数指针、函数对象还是虚拟函数。尽管存在可以将完整 Java API 映射到 C++ 的框架(例如 Jace、JunC++ion、JCC、jni.hpp 或 Scapix),但它们可能更难使用,因为从本机代码调用 Java 方法至少需要比相反的时间要长一个数量级,在我看来,像设计用于 Java 中使用的 API 那样导出并没有多大意义。尽管如此,假设我们想要在 Java 中执行一些操作,计划将其包装到一个名为foo()
的函数中,该函数调用类Foo
中的某个方法,我们可以在名为foo.cpp
的文件中编写以下代码,并注意初始化JVM(如有必要)可使用JavaCPP_init()
或任何其他方式:
#include <iostream>#include "jniFoo.h"int main() { JavaCPP_init(0, NULL); 尝试 { foo(6, 7); } catch (std::异常 &e) { std::cout << e.what() << std::endl; JavaCPP_uninit(); }
然后我们可以将该函数声明为FunctionPointer
中定义的call()
或apply()
方法,如下所示:
导入 org.bytedeco.javacpp.*;导入 org.bytedeco.javacpp.annotation.*;@Platform(include="<algorithm>")@Namespace("std")public class Foo { static { Loader.load(); } } public static class Callback extends FunctionPointer { // 仅在显式创建实例时才需要 Loader.load() 和 allocate() static { Loader.load(); } 受保护的回调() { 分配(); } 私有本机无效分配(); public @Name("foo") boolean call(int a, int b) throws Exception { 抛出新的异常(“栏”+ a * b); } // 我们还可以将 FunctionPointer 作为参数传递给其他函数(或从其他函数返回值) public static native void stable_sort(IntPointer first, IntPointer last, Callback Compare); // 要将其作为 C++ 函数对象传递(或获取),请使用 @ByVal 或 @ByRef 进行注释 public static native void sort(IntPointer first, IntPointer last, @ByVal Callback Compare); }
由于函数也有指针,因此我们可以相应地使用FunctionPointer
实例,其方式类似于 Haskell FFI 的FunPtr
类型,但抛出的任何java.lang.Throwable
对象都会转换为std::exception
。在 Linux x86_64 下使用这些命令构建并运行此示例代码会产生预期的输出:
$ 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.Exception:栏 42
在此示例中, FunctionPointer
对象是隐式创建的,但要调用本机函数指针,我们可以定义一个包含native call()/apply()
方法的对象,并显式创建一个实例。这样的类也可以在 Java 中扩展以创建回调,并且像任何其他普通Pointer
对象一样,必须使用native void allocate()
方法进行分配,因此请记住保留 Java 中的引用,因为这些引用将被垃圾收集。作为奖励, FunctionPointer.call()/apply()
实际上映射到 C++ 函数对象的重载运算符operator()
,我们可以通过使用@ByVal
或@ByRef
注释参数将其传递给其他函数,就像sort()
一样上面例子中的函数。
也可以对虚拟函数执行相同的操作,无论是否“纯”。考虑在名为Foo.h
的文件中定义的以下 C++ 类:
#include <stdio.h>class Foo {public: int n; Foo(int n) : n(n) { } virtual ~Foo() { } virtual void bar() { printf("C++ 中的回调 (n == %d)n", n); } };无效回调(Foo *foo) { foo->bar(); }
如果我们将同级类中的方法声明为本native
或abstract
并使用@Virtual
进行注释,则可以在 Java 中重写函数Foo::bar()
例如:
导入 org.bytedeco.javacpp.*;导入 org.bytedeco.javacpp.annotation.*;@Platform(include="Foo.h")public class VirtualFoo { static { Loader.load(); } public static class Foo extends Pointer { static { Loader.load(); } 公共 Foo(int n) { 分配(n); } 私有本机无效分配(int n); @NoOffset 公共本机 int n();公共本机 Foo n(int n); @Virtual public native void bar(); } 公共静态本机无效回调(Foo foo); 公共静态无效主(字符串[] args){ Foo foo = new Foo(13); Foo foo2 = new Foo(42) { public void bar() { System.out.println("Java 中的回调 (n == " + n() + ")"); } }; foo.bar(); foo2.bar(); 回调(foo); 回调(foo2); } }
它输出人们自然会假设的内容:
$ java -jar javacpp.jar VirtualFoo.java -exec C++ 中的回调 (n == 13) Java 中的回调(n == 42) C++ 中的回调 (n == 13) Java 中的回调(n == 42)
最容易使用的是使用 OpenJDK 类库编译的 Avian,所以让我们从它开始。如上所述创建并构建程序后,无需进一步修改,我们可以直接使用以下命令执行它:
$ /path/to/avian-dynamic -Xbootclasspath/a:javacpp.jar <MainClass>
然而,对于 Android 来说,我们还需要做更多的工作。对于基于Ant的命令行构建系统,在项目目录中:
将javacpp.jar
文件复制到libs/
子目录中,然后
运行此命令以在libs/armeabi/
中生成*.so
库文件:
$ 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>/
为了使一切自动化,我们还可以将该命令插入到build.xml
文件中。或者,为了与 Android Studio 集成,我们可以使用 Gradle JavaCPP。
对于 RoboVM 类似,假设编译的类位于classes
子目录中:
将javacpp.jar
文件复制到项目目录中,并
运行以下命令来生成本机二进制文件:
$ java -jar javacpp.jar -cp 类/ -properties <ios-arm|ios-x86> -o lib $ /path/to/robovm -arch <thumbv7|x86> -os ios -cp javacpp.jar:classes/ -libs classes/<ios-arm|ios-x86>/lib.o <MainClass>
在这种情况下,应该使用System.load("lib.o")
来加载库,而不是Loader.load()
,并且可能根本不需要。
项目负责人:Samuel Audet gmail.com at
samuel.audet
开发者网站:https://github.com/bytedeco/javacpp
讨论组:http://groups.google.com/group/javacpp-project