干货|C与JAVA之间的神转换操作

曾经有人问了我一个问题,如何将C里面的Struts结构体,给转成Java里面的对象?从来没想过会遇到这个问题,但事后仔细想想,在实际的开发过程中,一定会有类似的场景出现,因此总结出来用Java去调用C开发好的方法,分享给大家。

【 Structs】

C语言中的结构体,根据网上通俗的讲法,就像是一个打包封装,把一些有共同特征(比如同属于某一类事务的属性,往往是某种业务相关属性的聚合)的变量封装在内部,通过一定方法访问修改内部变量。

我个人的理解,结构体就是一种构造数据类型,把不同的数据类型封装起来,变成一个自定义的数据类型。可以把这个东西类比为Java中的类,Java中的对象是类的实例,类中描述了对象共有的属性和行为。
比如下图中的一个学生类,java对其抽象出来的类的描述,就是下图代码所示:

而在结构体中来c来表述这一种结构,结构体和结构体变量就如下图:

若仅仅是简单的Java对象和结构体互转,可以实现方法有很多种,只要约定好一个两边都能解析的格式,将对象序列化后再反序列化解析即可,比如利用cjson库转成json数据,或者直接用byte字节去传输。但是实际的开发场景下,结构体并非这么简单的东西,里面还有指针,这才是最麻烦的地方,所以要在开发过程中尽量避免这些复杂类型转换操作。

【JNI是什么】

一、Java Native Interface
JNI的全称是Java Native Interface,Java本地化接口,我们可以通过它来调用系统提供的API。不管是什么类型的操作系统机器最终识别的,都是一堆二进制码,像C/C++编译链接出来的,都是这些机器可以直接失败的二进制码。

但是Java不一样,Java的编译器不会直接将代码编译成机器码,而是将Java的.java源文件编译成虚拟机可以运行的Java字节码的.class文件(相当于就是JVM的机器语言),通过JIT技术即时地编译成本地机器码,也正是因为这个特性,让Java号称可以跨平台运行,一次编译到处运行,因为JVM屏蔽了底层系统的差异,但是这一层也带来了性能上的损失,不过可以通过现在硬件堆积来弥补这些损失。

简单的说,JNI就是Java用来和C/C++世界交互的一座桥梁,它在两者之间定义了一些接口,双方调用这一层接口,来和对方进行交互

二、前世今生
JDK1.0包含了一个本地方法接口,它允许JAVA程序调用C/C++写的程序,许多第三方的程序和JAVA类库。如:java.lang,java.io,java.net等都依赖于本地方法来访问底层系统环境的特征。

但是在JDK1.0的本地方法中主要存在两个问题:
1、本地方法像访问C中的结构(structures)一样访问对象中的字段。尽管如此,JVM规范并没有定义对象怎么样在内存中实现。如果一个给定的JVM实现在布局对象时,和本地方法假设的不一样,那你就不得不重新编写本地方法库。

2、因为本地方法可以保持对JVM中对象的直接指针,所以,JDK1.0中的本地方法采用了一种保守的GC策略。

JNI的诞生就是为了解决这两个问题,它可以被所有平台下的JVM支持:
▪ 每一个JVM实现方案可以支持大量的本地代码
▪ 开发工具作者不必处理不同的本地方法接口
▪ 本地代码可以运行在不同的JVM上面。

JDK1.1中第一次支持JNI,但是,JDK1.1仍在使用老风格的本地代码来实现JAVA的API。这种情况在JDK1.2下被彻底改变成符合标准的写法。

三、使用场景
标准的Java类库可能不支持你的程序所需要的特性,亦或者你已经有了一个用其他语言开发好的库或者程序,你希望能够在Java中直接调用它们,而不是重新用Java语言实现一遍。
此外若是对性能有较高的要求时,由于Java语言在性能上天生的劣势(2.1中提到的原因),使用C/C++或者汇编实现的代码性能会更加优秀,当提升的性能足够弥补JNI层转换损耗的资源,就是JNI大展身手的时候了。

 Hello JNI】

网上也有很多的示例代码,但是运行的时候因为自己电脑环境或者是其他的一些问题,跑起来会有点麻烦,下面是我在自己电脑上运行的时候的步骤,希望可以提供一些帮助,一些坑网上没有提到。

一、Java代码
直接在文件中写一个native方法即可,正常情况下类都会带着包名。

二、命令行编译java
编译Java的时候需要注意,若是带package目录的话,会发现无法使用命令行运行,需要加上-d参数来指定路径,若直接用javac编译,之后运行的时候就会发现package里描述的和.class文件的实际路径不一样,提示找不到主类。

编译后会在当前.java文件的目录下生成带有包结构的.class文件。

不加-d .的情况下,命令行运行会报错。

三、生成.h文件
编译完成后,就可以使用javah命令,在当前的目录下生成头文件。

打开头文件,里面结构也很简单,对于我们来说需要关注的就是第15行定义的那个方法,自动生成的注释里也说明了这个定义来自哪个类的哪个方法。

四、实现C代码
找一个简单的带有编译功能的IDE就可以来实现生成的.h文件中定义的方法, 这边用的简单的DEV-C++。

把头文件拷过去,简单的实现一下。

编译的时候可能会报找不到jni.h的错误,这个东西在你的jdk的include目录下有,去拷出来放到当前的目录下。
接下来是报找不到jni_md.h文件的错误,这个也在jdk的includewin32目录下,拷过来。

这样就完成了简单的编译,生成了一个dll文件。

五、运行代码
写一个类,首先要load这个dll文件,然后new一个对象就可以调用里面的native方法了。
加载dll有两种方式,一种是将dll所在的路径加入到系统环境变量PATH中,使用loadLibrary的方式加载,只需要填入dll的名字即可,还有一种就是通过写完全的文件名的方式,使用System.load()方法加载,此方式需要指明路径。
我这边用的是load方法,因为不用去改环境变量。
对于Java代码来说,你只要没修过native方法和类名,里面想怎么改就怎么改。
调用方法,会发现已经将c里的print方法执行了。

六、完整实现代码和命令
上述相关代码已全部放到github上,取下来后直接用命令行或者导入到eclipse里运行即可。
https://github.com/zeewane/JNIDemo

七、开发流程整理
1、 编写Java native接口;
2、 Javac编译.java文件;
3、 利用javah命令生成.h头文件;
4、 实现.h头文件里的方法;
5、 将需要的头文件和实现的c/cpp一起打成一个dll;
6、 在java代码中引入dll后调用native方法即可;

八、 Tips
1、 使用的jdk位数和dll的位数要一致,否则无法调用;
2、 Java代码中加载dll有两种方式,若采用loadLibrary,只需要写dll的名字即可,不需要带文件类型名,比如JNIDemo.dll,只要写JNIDemo;
3、 Javah命令生成的.h头文件只和Java类里的native接口相关,在native接口未变的情况下,无须重新生成头文件;

 【总结】

▲JNI入门
上面的代码只是用Java去调用了C里面的方法,实际的使用场景中,Java和C之间还会有一个参数的传递过程,甚至是方法互相调用、回调等,那些才是最复杂的地方。通过JNI作为中间代理层,完成了Java和C的相互调用,

 推荐资料
以下是我自己学习的时候收藏的网站教学资源,上面整理的材料绝大部分也总结于这些大牛的博客和教程,非常感谢他们的同时,也将这些资源列出来和大家分享,具体的使用方式还要参照官网的文档和博客中的资源,以及靠在实践中的摸索,这篇文章只是讲了一个最简单的调用c里面的print方法,后续会整理出更多的参数转换、方法调用的文章。

转载请注明出处:https://stgod.com/3743/