148

干货|C与JAVA之间的神转换操作 - 恒生技术之眼 - 恒生研究院

 6 years ago
source link: http://rdc.hundsun.com/portal/article/824.html?from=KFZTT
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

【 Structs】

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

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

f_b5f24d8938db3aa9bc21b2111fd3e04a.png

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

f_94b13f34b3fd423535d92dcb0790243e.png

若仅仅是简单的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方法即可,正常情况下类都会带着包名。

f_f858f21ed69bfbd9a9814e9845af9bdf.png

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

f_dae83cc6fc91a44d8dbbd4265a8bdba3.png

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

f_fa237c25460b7784a1ec4d11fbba277e.png

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

f_80986c6aca1796a928b521e851e3ede6.png

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

f_a8d5813c598878b04e4b4938b2e381b7.png

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

f_278acc5e08e768f5ff7d24b2a9eb18df.png

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

f_8b472b7a0fe8bfdc75cc0169df7a3bb8.png

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

f_646c53a0114e374659147b95613574bb.png

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

f_912609ba64825b52f22338d4bef7d3d5.png

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

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

f_99a9eadb1fa501de7552be88104c534e.png

六、完整实现代码和命令
上述相关代码已全部放到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方法,后续会整理出更多的参数转换、方法调用的文章。
▪ 阁楼猫:http://blog.csdn.net/honjane/article/details/53959587
▪ 博客:https://www.zybuluo.com/cxm-2016/note/566623


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK