从Java调用C函数

72

如何从Java调用C函数。 看起来C是编译器类型的。

我想在Windows中从Java调用C函数,以及从Java调用GCC函数。

有任何参考资料吗?


1
你可能想看一下JNI(Java Native Interface)。 - Shamim Hafiz - MSFT
34
要求在2011年5月11日11:11提问 :-) - andreee
10个回答

87

请查看Java Native Interface: Getting Started

2.1 概述

[...]编写一个简单的Java应用程序,调用C函数来打印“Hello World!”。该过程由以下步骤组成:

创建一个声明本地方法的类(HelloWorld.java)。使用javac编译HelloWorld源文件,生成类文件HelloWorld.class。Javac编译器随JDK或Java 2 SDK发行。使用javah -jni生成一个C头文件(HelloWorld.h),其中包含本地方法实现的函数原型。Javah工具提供了JDK或Java 2 SDK发行版。编写本地方法的C实现(HelloWorld.c)。使用主机环境上可用的C编译器和链接器将C实现编译为本地库,创建Hello-World.dlllibHello-World.so。使用Java运行时解释器运行HelloWorld程序。在运行时加载类文件(HelloWorld.class)和本地库(HelloWorld.dll或libHelloWorld.so)。本章的其余部分详细介绍了这些步骤。

2.2 声明本地方法

您首先需要使用Java编程语言编写以下程序。该程序定义了一个名为HelloWorld的类,其中包含一个本地方法print。

class HelloWorld {
    private native void print();

    public static void main(String[] args) {
        new HelloWorld().print();
    }

    static {
        System.loadLibrary("HelloWorld");
    }
}

HelloWorld类定义以print本地方法的声明开始。接下来是一个main方法,该方法实例化Hello-World类并调用此实例的print本地方法。 类定义的最后部分是一个静态初始化程序,它加载包含print本地方法实现的本地库。

在Java编程语言中,声明本机方法(例如print)与声明常规方法有两个不同之处。本机方法声明必须包含本机修饰符。本机修饰符表示该方法在另一种语言中实现。此外,本机方法声明以分号终止,即语句终止符号,因为类本身没有本机方法的实现。我们将在单独的C文件中实现print方法。

在调用print本地方法之前,必须加载实现print的本地库。在本例中,我们在HelloWorld类的静态初始化程序中加载本地库。Java虚拟机会在调用HelloWorld类中的任何方法之前自动运行静态初始化程序,从而确保在调用print本机方法之前加载本地库。

我们定义了一个main方法来运行HelloWorld类。 Hello-World.main以与调用常规方法相同的方式调用print本地方法。

System.loadLibrary接受一个库名称,定位与该名称对应的本地库,并将本地库加载到应用程序中。我们将在本书中讨论确切的加载过程。现在只需记住,为了使System.loadLibrary(“HelloWorld”)成功,我们需要在Win32上创建名为HelloWorld.dll的本地库,或者在Solaris上创建名为libHelloWorld.so的本地库。

2.3编译HelloWorld类

在定义了HelloWorld类之后,请将源代码保存在名为HelloWorld.java的文件中。然后使用随JDK或Java 2 SDK发行版一起提供的javac编译器编译源文件:

 javac HelloWorld.java

这条命令将在当前目录生成一个HelloWorld.class文件。

2.4 生成本地方法头文件

接下来,我们将使用javah工具生成一个JNI风格的头文件,在C语言中实现本地方法时非常有用。您可以按以下方式在Hello-World类上运行javah:

  javah -jni HelloWorld

头文件的名称是类名加上".h"。上面显示的命令会生成一个名为HelloWorld.h的文件。我们不会在此处列出完整的生成的头文件。头文件中最重要的部分是Java_HelloWorld_print函数原型,它是实现HelloWorld.print方法的C函数:

 JNIEXPORT void JNICALL   Java_HelloWorld_print (JNIEnv *, jobject);

暂时忽略JNIEXPORTJNICALL宏。您可能已经注意到原生方法的C实现接受了两个参数, 即使相应的原生方法声明没有接受任何参数。每个原生方法实现的第一个参数都是JNIEnv接口指针。 第二个参数是指向HelloWorld对象本身的引用(类似于C++中的"this"指针)。 我们将在本书后面讨论如何使用JNIEnv接口指针和jobject参数,但此简单示例忽略了这两个参数。

2.5编写本机方法实现

javah生成的JNI样式头文件帮助您为本地方法编写C或 C++实现。您编写的函数必须遵循生成的头文件中指定的原型。 您可以在C文件HelloWorld.c中实现Hello-World.print方法,如下所示:

#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h"   

JNIEXPORT void JNICALL   Java_HelloWorld_print(JNIEnv *env, jobject obj)  {
     printf("Hello World!\n");
     return;
}

这个本地方法的实现很简单。它使用printf函数显示字符串“Hello World!”,然后返回。正如之前提到的,两个参数,即JNIEnv指针和对象引用,都被忽略。

C程序包括三个头文件:

jni.h - 此头文件提供本地代码调用JNI函数所需的信息。编写本地方法时,您必须始终在C或C++源文件中包含此文件。 stdio.h - 以上代码片段还包括stdio.h,因为它使用printf函数。 HelloWorld.h - 使用javah生成的头文件。它包括Java_HelloWorld_print函数的C/C++原型。 2.6 编译C源文件并创建本地库

请记住,在HelloWorld.java文件中创建HelloWorld类时,您包括了一行代码,将本地库加载到程序中:

 System.loadLibrary("HelloWorld");   

现在所有必要的C代码已经编写完成,你需要编译Hello-World.c并构建这个本地库。

不同的操作系统支持不同的构建本地库的方法。在Solaris上,以下命令将构建一个名为libHello-World.so的共享库:

 cc -G -I/java/include -I/java/include/solaris HelloWorld.c -o libHelloWorld.so

-G选项指示C编译器生成共享库而不是常规的Solaris可执行文件。由于本书页面宽度的限制,我们将命令行分成两行。您需要在单行中键入命令,或将命令放置在脚本文件中。在Win32上,以下命令使用Microsoft Visual C++编译器构建动态链接库(DLL)HelloWorld.dll

 cl -Ic:\java\include -Ic:\java\include\win32 -MD -LD HelloWorld.c -FeHelloWorld.dll 

-MD选项确保HelloWorld.dll链接了Win32多线程C库。 -LD选项指示C编译器生成DLL而不是常规的Win32可执行文件。 当然,在Solaris和Win32上,您需要放入包含反映自己机器设置的路径。

2.7 运行程序

此时,你已经准备好运行程序的两个组件。 类文件(HelloWorld.class)调用本地方法, 而本地库(Hello-World.dll)实现了本地方法。

由于HelloWorld类包含自己的main方法,因此可以在Solaris或Win32上如下运行程序:

 java HelloWorld

您应该看到以下输出:

   Hello World! 

设置正确的本地库路径对于你的程序正常运行非常重要。本地库路径是Java虚拟机在加载本地库时搜索的目录列表。如果你没有正确设置本地库路径,那么你会看到类似以下错误:

 java.lang.UnsatisfiedLinkError: no HelloWorld in library path
         at java.lang.Runtime.loadLibrary(Runtime.java)
         at java.lang.System.loadLibrary(System.java)
         at HelloWorld.main(HelloWorld.java) 
确保本地库位于本地库路径中的一个目录中。如果您正在 Solaris 系统上运行,则使用 LD_LIBRARY_PATH 环境变量来定义本地库路径。确保它包括包含 libHelloWorld.so 文件的目录的名称。如果 libHelloWorld.so 文件在当前目录中,您可以在标准 shell (sh) 或 KornShell (ksh) 中发出以下两个命令来正确设置 LD_LIBRARY_PATH 环境变量:
 LD_LIBRARY_PATH=.
 export LD_LIBRARY_PATH

在C shell(csh或tcsh)中,等效的命令如下:

 setenv LD_LIBRARY_PATH .

如果您正在运行Windows 95或Windows NT计算机,请确保HelloWorld.dll在当前目录中,或者在路径环境变量中列出的目录中。

在Java 2 SDK 1.2版本中,您还可以通过以下系统属性在java命令行中指定本地库路径:

 java -Djava.library.path=. HelloWorld

命令行选项"-D"可设置Java平台系统属性。将java.library.path属性设置为"."会指示Java虚拟机在当前目录中搜索本地库文件。


Mac用户可以忽略Solaris命令。这篇博客帮助我完成了这个答案的开端:http://nerdposts.blogspot.com/2010/10/jni-mac-os-x-simple-sample.html - cloudsurfin
在阅读此内容时,如果您遇到“错误:找不到'YourFileName.class'的类文件”的javah工具错误并感到沮丧,那么这篇文章也会对您有所帮助。https://dev59.com/cmIk5IYBdhLWcg3wl_Ep - Martin Krajčírovič
4
对于Java >= 9,javah已被弃用,因此请使用javac -h . HelloWorld.java。这个问题已经得到了广泛的回答,但为了方便阅读,我在这里再次提及。 - Juicestus

12

简单来说,只需确保加载包含函数定义的相关库,加载符合JNI规范并将目标函数从第一个库封装的库,从您的Java类公开本机方法,这样就可以开始使用了。

我建议不要使用原始JNI,因为它包含大量样板代码,如果您开始封装一个大型的C库,您最终会感到非常恼怒。当然,在刚开始学习时可以尝试一下JNI,但真正做实际工作时可以使用像JNA这样的工具。


JNA和JNI的速度一样快吗? - Pacerier

5
您的选择包括: Java本地接口 参见:https://en.wikipedia.org/wiki/Java_Native_Interface 引用: JNI使程序员能够编写本机方法来处理无法完全使用Java编程语言编写的应用程序,例如当标准Java类库不支持特定于平台的功能或程序库时。 Java本地访问 参见:https://en.wikipedia.org/wiki/Java_Native_Access 引用: Java本地访问是一个由社区开发的库,它为Java程序提供了轻松访问本地共享库的方式,而无需使用Java本地接口。 JNR-FFI 参见:https://github.com/jnr/jnr-ffi 引用: jnr-ffi是一个Java库,可加载本地库,而无需手动编写JNI代码或使用诸如SWIG之类的工具。

那么JNR和JNA的区别是什么? - Pacerier

3
在“异类”类别中,可以看到NestedVM,它将C代码编译为Mips指令,并在JVM内部运行一个Mips虚拟机。 http://nestedvm.ibex.org/

1

1

@Jonas 给出了非常详细的答案,但我认为也值得查看这个网站,你会在那里找到所有必要的答案:

http://www.ntu.edu.sg/home/ehchua/programming/java/javanativeinterface.html

本文介绍如何使用JNI调用程序:

  • 适用于C语言、C++语言或两者的混合
  • 不涉及任何参数、原始类型、字符串或原始类型数组的JNI
  • 访问对象变量、回调实例方法等等。

1
网页已经无法访问,新链接为https://personal.ntu.edu.sg/ehchua/programming/java/javanativeinterface.html。 - hcnak

1
如果您正在使用Windows和MinGW gcc,如果在lib中的特定方法出现UnsatisfiedLinkError错误,则可能需要使用其他标志。
gcc -D_JNI_IMPLEMENTATION_ -Wl,--kill-at -I"%JAVA_HOME%"\include -I"%JAVA_HOME%"\include\win32 BestCode.c -shared -o BestCode.dll

0

为了制作64位兼容的dll,请从下面的语句中删除“-MD”选项

cl -Ic:\java\include -Ic:\java\include\win32 -MD -LD HelloWorld.c -FeHelloWorld.dll

0

首先,请确保通过设置属性java.library.path将本地库或.dll文件加载到类路径中。

然后使用System.loadLibrary()

Do not use .dll extension at the end.

0
我有一个解决方案来解决这个问题。你需要确保使用64位的C++编译器来编译代码,以便调用在64位JRE上运行的Java函数。此外,我们还需要将创建的dll文件路径保存在“环境变量”下的“路径”中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接