[Java] 用C编写你自己的native方法


前言


看JDK代码的时候经常会发现native关键字,笔者就时常抱有下面的疑问

  • native关键字到底的作用是什么呢?
  • native方法的实现源码又放在什么地方呢?
  • 如何实现一个自己的native方法?

为了解决这些疑问,笔者对native做了一点小小的调查,并将在本文对native相关知识做一个简单的介绍。


native关键字


当一个方法被native修饰的时候,表明该方法是被平台相关代码(platform-dependent code)也就是被native code实现的,例如直接由C、C++等(甚至汇编语言)编译而来的程序实现。

8.4.3.4 native Methods
A method that is native is implemented in platform-dependent code, typically written in another programming language such as C. The body of a native method is given as a semicolon only, indicating that the implementation is omitted, instead of a block (§8.4.7).

需要注意的是native关键字仅可用于修饰方法。

附1:native code (机器码)


jls里提到的platform-dependent code笔者认为指的是native code。那什么是native code呢?
native code是指能直接被CPU执行的指令。也就是常说的机器码。机器码之所以能被CPU直接执行是因为其指令来自CPU提供的指令集。不同的CPU的架构、指令集不尽相同,所以A平台机器码到B平台无法执行。


附2:编译型语言与解释型语言


笔者在本节仅简单阐述一下下列概念。

编译型语言
由人能阅读的源代码(纯文本)直接编译到机器码保存到可执行文件(如:Win的PE格式、Mac的Mach-O格式、Linux系的ELF格式)。较为典型的有C、C++等,特点是执行速度较快、有编译检查,但无法跨平台。

解释型语言
不需要编译,而是在运行的时候,通过解释器在运行时“翻译“源码到机器码。典型的就是Shell Command Language与Shell (如bash、zsh、csh就是shell command lanugage的解释器),特点是其代码与平台无关,在运行时才会被转换成机器码。执行速度较编译型语言慢很多。

混合型语言:
折中方案,像微软的.Net家族(C#.NET、VB.NET、C++\CLR)、Java、Node.js等都是采用了中间语言+多平台虚拟机的方式。即

Step1:源代码 => 由编译器编译 => 中间代码
Step2:中间代码 => 由虚拟机解释 => 机器码 ,或下
Step2:中间代码 => 有虚拟机Just-In-Time Compiler编译 => 机器码

笔者不会在本文解释Just-In-Time Compiler,仅希望读者知道有这一个东西存在,如部分JVM提供参数可控制JIT Compiler的开闭,关闭JIT Compiler会导致程序程序执行速度大大降低。

各个编程语言的虚拟机的名字不尽相同,有兴趣的读者可以自行查询。这样的编程语言通过提供多个不同平台的虚拟机的方式,把应用开发者的工作给大大简化。但相应这部分的开发管理压力换由虚拟机提供商承担。


native方法的实现原理


JVM,能利用JNI技术实现与Native-Code的互连。其对Native-Code的加载,就像WIndows的动态链接库一样,在运行时加载到内存并链接。jvms为了已有的Linking概念做区分,用绑定(Binding)来称呼链接Native-Code的过程。
指定加载哪一个Native-Library这一步通常在Coding阶段由开发者完成,通过调用如System.loadLobrary(String)方法,更多可以参考JNI Functions - Oracle

5.6 Binding Native Method Implementations
Binding is the process by which a function written in a language other than the Java programming language and implementing a native method is integrated into the Java Virtual Machine so that it can be executed. Although this process is traditionally referred to as linking, the term binding is used in the specification to avoid confusion with linking of classes or interfaces by the Java Virtual Machine.

简单来说native方法的执行有下面五个注意点。

  1. Java开发者:Coding阶段指定native库名。
  2. Java编译器:在编译Java到JVM Bytecode的时候,native或非native方法的调用会不做区分地用invoke*(如invokevirtual)等jvm指令。(可通过jdk的javap工具反编译来检证。
  3. Native库开发者:用C、C++等语言编写native库并按JNI指定的命名规则对library做命名,必须和1相对应。
  4. JVM:JVM负责搜索并加载指定的native库到内存并负责链接(或绑定)。
  5. JVM:JVM还负责编译/解释 invoke*等jms指令到机器码,在这一步会加入对调用的方法是否是native方法的判断,调用native方法会导致native frame被追加到native method stack内存区,非native方法则会被追加到java method stack内存区。

笔者在接下来的博文里将介绍JVM的内存区。


实现native方法: JNI Hello world


知道native的作用和其原理之后,我们需要通过实践来加深对刚学的知识的理解。
笔者将在本节用实际例子来演示如何用C编一个native方法来输出”Hello world, JNI.“。


1. 创建JNIHelloWorld.java


由于JNI的实现,是动态链接的,也就是在程序运行的时候实现,所以在Coding阶段并不需要关心Native方法的实现。

package info.systemengineer.examples;

public class JNIHelloWorld {
	
	private static final String JNI_LIB_NAME = "myfirstjnilibrary";
	static {
		/*
		 * 这句话表明我们在编译的时候并不会去管native方法如何实现,就像调用接口一样。
		 * 在这里 
		 * 	1. Linux/Unix/Max 上,会去加载 lib${JNI_LIB_NAME}.jnilib
		 *  2. Solaris 上,会去加载 lib${JNI_LIB_NAME}.so
		 *  3. Win 上,则是 ${JNI_LIB_NAME}.dll, 并没有lib前缀。
		 */
		System.loadLibrary(JNI_LIB_NAME);
	}
	
	/**
	 * Native方法,该方法将输出”Hello World,JNI“ <br/>
	 * 到标准输出(Standard Output)
	 */
	public native void sayHelloWorld();

	public static void main(String[] args) {
		(new JNIHelloWorld()).sayHelloWorld();
	}
}

2. 编译JNIHelloWorld.java


你可以通过IDE编译,也可以通过命令行编译。

$ javac JNIHelloWorld.java
$ ls
JNIHelloWorld.class	JNIHelloWorld.java

3. 用javah生成 .h文件


javah是jdk里自带的工具,不过不是所有jdk都有此工具。例如笔者MAC上的openJDK11就不包含此工具,因为切换到了OracleJDK8。

  • jni option:默认输出,可以省略。
  • info.systemengineer.examples.JNIHelloWorld:这么长是因为前面是package名,如果不带包名会出现”Error: Could not find class file for ‘JNIHelloWorld’“的错误。
$ javah -jni info.systemengineer.examples.JNIHelloWorld
$ ls
info
info_systemengineer_examples_JNIHelloWorld.h

info_systemengineer_examples_JNIHelloWorld.h的内容如下,因为是生成的代码,第一句注释就是提升不要修改此文件。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class info_systemengineer_examples_JNIHelloWorld */

#ifndef _Included_info_systemengineer_examples_JNIHelloWorld
#define _Included_info_systemengineer_examples_JNIHelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     info_systemengineer_examples_JNIHelloWorld
 * Method:    sayHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_info_systemengineer_examples_JNIHelloWorld_sayHelloWorld
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

4. 创建源文件


创建与上述头文件相对应的 info_systemengineer_examples_JNIHelloWorld.c 源文件。

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

JNIEXPORT void JNICALL Java_info_systemengineer_examples_JNIHelloWorld_sayHelloWorld
  (JNIEnv *env, jobject object) {
    printf("Hello World,JNI\n");
}

5. 编译C源文件


笔者电脑是MAC,在本节将用gcc来实现对C的编译,输出文件的命名按照第一节的注释命名为
lib${指定的lib名}.jnilib,对象为shared library。

$ gcc -I"${JAVA_HOME}/include/" -I"${JAVA_HOME}/include/darwin/" -o libmyfirstjnilibrary.jnilib -shared info_systemengineer_examples_JNIHelloWorld.c
$ ls
info info_systemengineer_examples_JNIHelloWorld.c
info_systemengineer_examples_JNIHelloWorld.h	libmyfirstjnilibrary.jnilib

6. 执行


执行,然后恭喜,你成功实现了你的第一个native方法。

$ java -Djava.library.path=. info.systemengineer.examples.JNIHelloWorld
Hello World,JNI
$

总结


  • Java语言的编译成果物是JVM Bytecode,不能被CPU直接执行。
  • C等语言编译成果物是机器码,能被直接CPU直接执行。
  • JVM是一个进程,你的Java程序并不是进程。你所写的Java程序其实只是类似于动态链接库的东西,被JVM加载并管理着。
  • Oracle只负责制定JVM标准式样(jvms),JVM可以被多种语言实现,比如C、C++,而JVM的实现者可以选择他们偏好的语言。Oracle JVM被认为是用C++写的,所以实际上你的Java程序的”外包装“是C++。
  • JVM使用的中间语言叫JVM Bytecode,就像.NET CLR的LR (Intermediate Language)。
  • JDK和JVM并不是同一个东西,虽然他们总是成对出现。
  • 当使用Native方法,你需要对其Native实现负责。并失去跨平台性,如果想恢复跨平台性,你需要提供Native实现在不同平台的Shared Library。如lib*.jnilib、*.dll、*.so。

希望能对你理解Java的运行机制有所帮助,我是虎猫~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值