[JNI]使用jni实现简单的Java调用本地C语言代码

news2024/11/27 17:56:53

[JNI]使用jni实现简单的Java调用本地C语言代码

JNI的解释

Java Native Interface,即Java本地接口。

在Java官方描述中为:

The JNI is a native programming interface. It allows Java code that runs inside a Java Virtual Machine (VM) to interoperate with applications and libraries written in other programming languages, such as C, C++, and assembly.

JNI是一个本地编程接口。它允许运行在Java虚拟机(JVM)内部的Java代码与其他编程语言(如C、C++和汇编)编写的应用程序和库进行互操作。

The most important benefit of the JNI is that it imposes no restrictions on the implementation of the underlying Java VM. Therefore, Java VM vendors can add support for the JNI without affecting other parts of the VM. Programmers can write one version of a native application or library and expect it to work with all Java VMs supporting the JNI.

JNI最重要的好处是它对底层Java虚拟机的实现不施加任何限制。因此,Java虚拟机的供应商可以添加对JNI的支持而不影响VM的其他部分。程序员可以编写一个版本的本地应用程序或库,并期望它能与所有支持JNI的Java虚拟机兼容。

简而言之,jvm提供了Java和C/C++/汇编代码的桥梁,可以对Java进行扩展,这个桥梁就是JNI

介绍表情包

我们知道,Java的主要优势之一是其可移植性——这意味着一旦我们编写并编译代码,这个过程的结果就是平台独立的字节码。

简单来说,这可以在任何能够运行Java虚拟机的机器或设备上运行,而且它的工作会像我们所期望的那样无缝进行。

然而,有时我们确实需要使用为特定架构原生编译的代码。

需要使用原生代码可能有一些原因:

  • 需要处理某些硬件
  • 对非常苛刻的过程进行性能提升
  • 我们希望复用而不是用Java重写的现有库。

为了实现这一点,JDK引入了一个桥梁,连接了在我们的JVM中运行的字节码和原生代码(通常用C或C++编写)。

这玩意儿咋工作的表情包

本地方法Native Methods:Java虚拟机与编译代码的交互

java 提供了 native关键字,用于指示该方法的实现将由本地代码提供,因此你是没办法在Java代码里看到这个方法的具体实现的。

比如我在研究ArrayList如何addAll数据时:

在这里插入图片描述

发现其中执行了一段代码:System.arraycopy(a, 0, elementData, size, numNew);

这段代码很显然是为了实现数组的复制,因此我想看看到底是怎么copy的,点进该方法一看:

在这里插入图片描述

好好好,native修饰的,Java代码里看不了,这说明System.arraycopy是用的本地C/C++代码实现的

通常来说,在C语言或C++语言中创建本地可执行程序时,我们可以选择编译为静态链接库动态链接库或者可执行文件,可执行文件是经历了链接之后的最终产物,不说了,我们:

静态链接库

所有库的二进制文件将在链接过程中作为我们可执行文件的一部分被包含进去。因此,我们不再需要这些库,但这会增加我们可执行文件的大小。在编译时链接,程序包含库的副本,运行时不需要库文件,但可能导致文件较大和内存使用效率低。

  1. 链接时机:静态链接库在编译时被链接到程序中。当编译器编译一个使用静态库的程序时,它会将程序中实际用到的库中的代码和数据复制到最终的可执行文件中。
  2. 文件大小:由于静态库的代码和数据被复制到每个使用它的程序中,这可能导致最终的可执行文件较大,尤其是当多个程序使用相同的静态库时。
  3. 运行时依赖:静态链接的程序在运行时不需要库文件,因为所有需要的代码和数据都已经包含在可执行文件中。
  4. 更新和维护:如果静态库更新了,所有使用它的程序都需要重新编译以包含新版本的库。
  5. 内存使用:由于每个程序都有自己的库副本,这可能导致内存使用效率较低,因为相同的库代码可能在多个程序中重复加载。

动态链接库

最终的可执行文件只包含对库的引用,而不是代码本身。它要求我们运行可执行文件的环境能够访问我们程序使用的所有库文件。 在运行时链接,程序不包含库的副本,运行时需要库文件,但可以减小文件大小和提高内存使用效率。

  1. 链接时机:动态链接库在程序运行时被链接。编译器在编译时只记录程序使用了哪些动态库,而不将库的代码和数据复制到可执行文件中。
  2. 文件大小:动态链接的程序通常比静态链接的程序小,因为它们不包含库的代码和数据。
  3. 运行时依赖:动态链接的程序在运行时需要库文件。如果库文件不存在或版本不匹配,程序可能无法运行。
  4. 更新和维护:动态库的更新不需要重新编译使用它的程序。只需替换库文件,所有使用该库的程序都可以使用新版本的库。
  5. 内存使用:动态库在内存中只有一份副本,多个程序可以共享,这提高了内存使用效率。

后面这种动态链接的做法对JNI来说才靠谱,因为我们不能把字节码和本地编译的代码混在一个二进制文件里。

所以,我们的动态链接库会把本地代码单独编译在.so、.dll或者.dylib文件里,而不是混在类文件里。

文件类型文件描述对应系统
.so共享对象(Shared Object)Unix 和 类Unix操作系统(如 Linux)
.dll动态链接库(Dynamic Link Library)Windows 操作系统
.dylib动态库(Dynamic Library)较早的macOS 操作系统

正如前面所看到的那样,native关键字修饰的方法我们称之为本地方法,由非Java代码实现

public native void theNativeMethod();

这个代码和 public void theMethod();的主要不同点在于:

这个方法不是由另一个Java类来实现的,而是由一个独立的本地共享库来实现,为了让我们能够从Java代码中调用这些本地方法,Java虚拟机会创建一个包含指向这些方法实现内存地址的指针表

重要组成部分表情包

下面简单说说几个重要的部分,我们得注意一下。

  • Java代码 – 就是我们的Java类。这些类里至少会有一个标记为“本地”的方法。就像是我们写的Java程序,里面会有一些特殊的方法,我们称它们为“本地方法”,这些方法不是用Java写的,而是用其他语言写的。

  • 本地代码 – 这是我们本地方法的实际执行逻辑,通常是用C语言或C++语言编写的。 这些是用C语言或C++语言写的代码,它们是那些“本地方法”的实现部分。

  • JNI头文件 – 这是一个给C/C++用的头文件(在JDK目录下的include/jni.h),里面包含了我们在本地程序中可能用到的所有JNI元素的定义。 这是一个特殊的文件,里面有很多关于如何让Java代码和本地代码相互交流的规则。

  • C/C++编译器 – 我们可以选择GCC、Clang、Visual Studio,或者任何我们喜欢的编译器,只要它能帮我们生成一个适用于我们平台的本地共享库就行。

JNI代码里的重要元素(JAVA和C/C++)

Java元素

  • “native”关键词:我们之前说过,任何标记为native的方法都必须在一个本地共享库中实现。
  • System.loadLibrary(String libname):一个静态方法,它可以从文件系统中加载一个共享库到内存,并让其导出的函数能够被我们的Java代码使用。

C/C++元素(很多都在jni.h中定义):

  • JNIEXPORT:用来标记共享库中的函数为可导出的,这样它就会被包含在函数表中,JNI就能找到它了。
  • JNICALL:和JNIEXPORT一起使用,确保我们的方法可以被JNI框架使用。
  • JNIEnv:一个结构体,包含了一些方法,让我们的本地代码能够访问Java元素。
  • JavaVM:一个结构体,允许我们操纵正在运行的JVM(甚至可以启动一个新的),增加线程,销毁等…

初次使用JNI表情包

接下来,我们要学习一下如何使用jni

根据前面的内容我们知道,肯定是需要有个编译器,编译C/C++的

我这里选择的windows平台的 MinGW进行编译(MinGW安装自行百度)

搭建好编译环境后:

新建一个Java类文件

public class TestJNI{
	
	// 静态代码块
	// 为了在运行时加载本地动态链接库,windows是 TestJNI.dll,linux是 TestJNI.so
	// 这个动态链接库中包含有sayHello方法的实现
	static{
		System.loadLibrary("TestJNI");
	}
	
	// 在Java类中声明一个实例本地方法sayHello()
	private native void sayHello();
	
	// main方法,测试,用来创建类的实例并调用本地方法
	public static void main(String[] args){
		new TestJNI().sayHello();
	}
}

代码的解释:

静态初始化器会在类加载的时候调用System.loadLibrary()来加载名为"TestJNI"的本地库(这个库包含一个叫做sayHello()的本地方法)。在Windows系统中,它对应的是"hello.dll";在Unix或Mac OS X系统中,对应的是"libTestJNI.so"。这个库必须被包括在Java的库路径里(这个路径保存在Java系统变量java.library.path中)。你可以通过虚拟机参数-Djava.library.path=链接库路径来把库加入Java的库路径。如果运行时找不到这个库,程序会抛出一个UnsatisfiedLinkError。(这个系统变量java.library.path会在后续代码中进行描述)

接下来,我们通过关键词native声明sayHello()方法为一个本地实例方法,这意味着这个方法是用另一种语言实现的。一个本地方法是没有方法体的。sayHello()方法应该在我们已经加载的本地库中被找到。

main()方法会创建一个TestJNI的实例,并调用本地方法sayHello()。

编译Java文件并且生成对应的C/C++头文件

进入命令行到 TestJNI.java所在文件目录下

通过命令生成对应的JNI规范.h头文件

// java8及以后
javac -h . TestJNI.java

// Java8以前
javac TestJNI.java
javah TestJNI

生成的文件如图:

在这里插入图片描述

我们现在点进TestJNI.h中看看里面是个啥?

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>  // 这个头文件中包含了JNI所有必要的类型定义和函数声明
/* Header for class TestJNI */

// 如果宏_Included_TestJNI没有在其他地方定义过,就将下面的内容进行定义。
//(这个宏包含的范围一直到最后一个#endif)
#ifndef _Included_TestJNI
#define _Included_TestJNI


// 告诉编译器下面的代码应该要以C语言的链接方式进行处理
// 这是为了解决c++的函数重载导致符号名称变化的问题,从而使得Java可以正确的调用本地方法
#ifdef __cplusplus
extern "C" {
#endif

// 本地方法的函数声明
/*
 * Class:     TestJNI    类名为 TestJNI
 * Method:    sayHello   方法名为 sayHello
 * Signature: ()V        方法签名是()V 表示无参且返回值为void的方法
 */
JNIEXPORT void JNICALL Java_TestJNI_sayHello
  (JNIEnv *, jobject);

    
#ifdef __cplusplus
}
#endif
// 这里结束了extern "C"块,即告诉C++编译器,后续的代码将继续使用C++的链接方式。
#endif
// 结束 _Included_TestJNI 的宏定义

上述代码解释:

头文件里声明了一个C语言函数,名为Java_TestJNI_sayHello,来源于固定的命名规则,规则为Java_{包名}_{类名}_{方法名}(JNI参数),其中包名的点会被下划线代替

参数中包含有:

  • JNIEnv*:指向JNI环境的引用,让你能够访问所有的JNI函数。
  • jobject:指向"this" Java对象的引用。

我们暂时先忽略掉 JNIEXPORTJNICALL 宏 定义,在零汇编的平台中,这些定义均为空,在x86或者sparc平台中会根据不同的定义可能会有实现。

#ifdef __cplusplus时会被c++编译器识别,也就是说extern "C"只会被c++编译器识别出来,它告诉c++编译器下面的函数需要按照C语言的函数命名规则来进行编译,不是以C++的函数命名规则

C++编译器对于函数名有特定的处理方式,即名称修饰或者称为名称变形,或者说不以C++的函数签名的方式进行编译

这是一种在编译阶段发生的处理,用于支持C++的特性,比如函数重载。由于C++允许多个函数共享相同的名字,只要它们的参数类型不同,编译器需要一种方法来区分这些函数的符号名,因此会对函数名进行修饰,加入额外的信息,如函数的参数类型和数量。

怎么理解呢?

这就像是你有一本做饼干的食谱,但是食谱上的指令是给两种不同厨房设备(C和C++)使用的。现在,你想用C++设备来做饼干,但是这个设备有自己的一套复杂的指令(比如可以做多种口味的饼干),所以你需要告诉它:“虽然你很牛逼,但得按照普通的C设备(只做一种饼干)的方式来做这个饼干。” 这样它就不会搞错了。

#include <jni.h>这个存在于何处呢?

以Windows Java8 为例,这个头文件在你的JAVA_HOME/include里面可以找得到或者在其于当前系统相关的子目录中

比如我的Java环境是:C:\Program Files\Java\jdk1.8.0_261,在其子目录include里面就能看到

在这里插入图片描述

对于Linux中,我的JAVA_HOME是/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.302.b08-0.el8_4.x86_64 在这个文件夹下有include文件目录

在这里插入图片描述

编写C实现代码

在同一个目录下,创建一个文件名和刚刚生成的头文件名称一样的文件 “TestJNI.c”

在这里插入图片描述

下面编写文件中的代码

#include<stdio.h>    // C语言的基本输入输出
#include<jni.h>      // jni的头文件,这个是JDK里面包含的
#include "TestJNI.h" // 前面通过命令生成的头文件

// java sayHello方法的本地实现
JNIEXPORT void JNICALL Java_TestJNI_sayHello(JNIEnv *env, jobject obj)
{
    printf("Hello,This is my First JNI Code!!");
    return;
}

编译C程序

我这里是windows,就以windows为例

JAVA_HOME为C:\Program Files\Java\jdk1.8.0_261

使用指令

gcc -I "C:\Program Files\Java\jdk1.8.0_261\include" -I "C:\Program Files\Java\jdk1.8.0_261\include\win32" -shared -o TestJNI.dll TestJNI.c

注意你的JAVA_HOME和我的可能不同,需要自行修改

编译完成后,当前文件夹下会出现动态链接库文件 TestJNI.dll

在这里插入图片描述

运行Java文件

我直接执行命令:java TestJNI一定会报错
在这里插入图片描述

所以需要将编译后的动态链接库传递给虚拟机执行

使用命令

java -Djava.library.path=. TestJNI

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1668750.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

智慧文旅赋能旅游服务升级:以科技创新驱动行业变革,打造智慧化、个性化、高效化的旅游新体验,满足游客日益增长的多元化需求

目录 一、引言 二、智慧文旅的概念与内涵 三、智慧文旅在旅游服务升级中的应用 1、智慧旅游服务平台建设 2、智慧景区管理 3、智慧旅游营销 四、智慧文旅推动旅游行业变革的案例分析 案例一&#xff1a;某智慧旅游城市建设项目 案例二&#xff1a;某景区智慧化改造项目…

命令重装Linux系统,无需登录控制面板

命令重装Linux系统&#xff0c;无需登录控制面板 部分无法登录控制面板使用这个脚本 自动安装安装脚本 wget https://lyvba.com/auto.sh bash auto.sh -d 12 -v 64 -a -p $passwd \--mirror https://mirrors.ustc.edu.cn/debian/安装命令参考 # 自动安装 Debian 10 buster …

二叉搜索数使用,底层原理及代码实现

1:二叉搜索树的定义 二叉搜索树的底层是一个二叉链表 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树 &#xff0c;或者是具有以下性质的二叉树 : 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值 若它的右子树不为空&#xff0c;则右子树上所…

kilimall非洲电商培训,基础版+进阶版+高阶版 从0-1个人可入驻的平台(12节)

亲爱的朋友们&#xff0c;你们知道吗&#xff1f;有一个神奇的电商平台——kilimall&#xff0c;它可以帮助你实现创业梦想&#xff0c;让你走上财富之路&#xff01; 首先&#xff0c;让我给大家介绍kilimall的基础版。基础版针对的是0经验的小白&#xff0c;提供了详细的教程…

vscode打开esp-idf工程,找不到头文件,有波浪线

就像这样 多半是因为原始的工程不是用vscode的插件新建的&#xff0c;因此没有相关的路径。需要在工程文件夹下的.vscode文件夹中的c_cpp_properties.json文件中增加路径&#xff0c;可以参考插件自动新建的工程里面的写法 {"configurations": [{"name":…

TeXlive TeXstudio安装指南

TeXlive & TeXstudio安装指南 记上次安装Visual Studio Code (Vscode)配置LaTeX后&#xff0c;由于Overleaf页数太多&#xff0c;项目超过了免费计划的编译时限&#xff08;这两天突然出现这个问题&#xff09;。加上毕设和PPT都是在Overleaf上编译&#xff0c;这两天突然…

第十二届蓝桥杯省赛真题 Java A 组【原卷】

文章目录 发现宝藏【考生须知】试题 A: 相乘试题 B: 直线试题 C : \mathrm{C}: C: 货物摆放试题 D: 路径试题 E: 回路计数试题 F : \mathrm{F}: F: 最少砝码试题 G: 左孩子右兄弟试题 H : \mathrm{H}: H: 异或数列试题 I \mathbf{I} I 双向排序试题 J : \mathrm{J}: J: 分…

electron进程间通信

Electron 应用程序的结构非常相似。 作为应用开发者&#xff0c;你将控制两种类型的进程&#xff1a;主进程 和 渲染器进程。 这类似于上文所述的 Chrome 的浏览器和渲染器进程。 主进程 每个 Electron 应用都有一个单一的主进程&#xff0c;作为应用程序的入口点。 主进程在 N…

STM32快速入门(总线协议之I2C一主多从(软件实现 硬件实现))

STM32快速入门&#xff08;总线协议之I2C一主多从&#xff08;软件实现 & 硬件实现&#xff09;&#xff09; 前言 支持一对多&#xff08;一主多从&#xff09;、多对多传输&#xff08;多主多从&#xff09;&#xff0c;只支持半双工&#xff0c;一般有两根数据线&…

蓝桥之链表

最近真的特别焦虑&#xff0c;体测、比赛和考试一个接一个&#xff0c;让人喘不过气来QAQ 甚至考试和比赛还有冲突&#xff0c;sad 最近因为看了牙&#xff0c;打了药的缘故&#xff0c;一直在吃素QAQ 本来今天还想写个知识点总结的&#xff0c;但是太晚了&#xff0c;现在已…

吃透前端文件上传与文件相关操作

最近在学文件上传的操作,所以想把学习到东西写成一文章 这片文章是我以小白视角 慢慢学习并熟悉前端文件相关操作的流程总结出来的 前端文件上传 我首先想到是 <input type"file">选择文件</input>如果我们想限制上传文件的格式,大小或进行裁剪分片上传…

2022——蓝桥杯十三届2022国赛大学B组真题

问题分析 看到这个问题的同学很容易想到用十层循环暴力计算&#xff0c;反正是道填空题&#xff0c;一直算总能算得出来的&#xff0c;还有些同学可能觉得十层循环太恐怖了&#xff0c;写成回溯更简洁一点。像下面这样 #include <bits/stdc.h> using namespace std; in…

树莓派4B-搭建一个本地车牌识别服务器

实现目标&#xff1a; 一、设备自启后能够获得服务的ip与端口号&#xff0c;用于计算机连接设备&#xff1b; 二、计算机可以通过服务ip与端口访问设备服务&#xff1b; 三、上传需要处理的数据&#xff0c;返回结果反馈给用户&#xff1b; 四、上传到服务器的数据不会导致设备…

Study--Oracle-02-单实例部署Oracle19C

一、CentOS 7 环境准备 1、软件准备 操作系统&#xff1a;CentOS 7 数据库版本: Oracle19C 2、操作系统环境配置 关闭selinux &#xff0c;编辑 /etc/selinux/config文件&#xff0c;设置SELINUX enforcing 为SELINUXdisabled [rootoracle ~]# grep SELINUX /etc/seli…

手游掘金最新玩法,单条视频变现1w+,一部手机即可操作,保姆级教程

如果你也想通过手机赚钱&#xff0c;在这里有一个非常好的项目&#xff0c;它可以让你轻松赚到额外的收入。 这个手游掘金最新玩法&#xff0c;是一个非常受欢迎的项目&#xff0c;它可以让你通过制作单条视频来获得高额收益。不同于传统的游戏赚钱方式&#xff0c;这个方法不…

HTML表单创建学习

文章目录 1、创建HTML框架2.body标签CSS3.表单创建3.1、添加fieldset与label标签3.2、为label标签添加css样式3.3、添加input标签3.4、添加提交按钮3.5、在input标签中添加required3.6、添加minlength属性3.7、pattern属性3.8、设置表单单选按钮无法同时选中3.9、添加链接3.10、…

SpringSecurity的核心原理使用总结

1. SpringSecurity的核心原理 对于最原始Servlet请求处理的层次结构 客户端->过滤器链->Servlet 对于在SpringMVC中处理请求的层次结构 如何让Filter与Spring建立连接呢? 因此它增加了一个DelegatingFilterProxy 它是SpringMVC提供的的Filter,它内部代理了一个原生的F…

HC-06 蓝牙串口从机 AT 命令详解

HC-06 蓝牙串口从机 AT 命令详解 要使用 AT 命令&#xff0c;首先要知道 HC-06 的波特率&#xff0c;然后要进入 AT 命令模式。 使用串口一定要知道三要素&#xff0c;一是波特率&#xff0c;二是串口号&#xff0c;三是数据格式, HC-06只支持一种数据格式: 数据位8 位&#…

测试平台开发:Django开发实战之注册界面实现(上)

实现注册功能&#xff0c;大概包括以下几个步骤 1、设计ui ##字段 通过看数据库里面的user表里面的字段&#xff0c;可以大概知道需要几个字段&#xff1a; emailusernamepasswordpassword_confirm 生成简单的ui界面&#xff0c;复制这个html代码 然后在项目路径下面创建一…

transformer与beter

transformer与beter 解码和编码器含义tokizer标记器和one-hot独热编码编码解码--语义较好的维度空间矩阵相乘--空间变换编码理解如何构造降维的嵌入矩阵--实现到达潜空间上面是基础&#xff0c;下面是transformer正文自注意力机制注意力分数--上下文修正系数为什么需要KQ两个矩…