Java调用C语言程序(JNI技术)

Java为什么要调用C语言编写的程序

因为涉及操作系统底层用java实现不了时,需要Java调用底层的接口,比如c语言库或者c++语言库。

早年公司项目中是触摸屏项目就是用的java调用jni的方法实现的.
写这篇文章也是由于kubenetes中涉及到cgo包的调用,所以顺手也写了这个,多年以前也写过php调用c模块,利用phpize
https://blog.csdn.net/wf_moonlight/article/details/6318385 这里有个转载的,很多年了,当时还发布在php论坛上的.

使用Java如何去调用C语言的接口呢?这里我们来简单实现Java调用JNI,看看JNI技术。

实际我们如果看java jvm代码会发现大量使用了jni技术,jvm内部调用c函数库和c++对象等.

CentOS6环境下做的实验环境

[root@fpNet-WEB-10 java]# cat /etc/redhat-release 
CentOS release 6.4 (Final)

 [root@fpNet-WEB-10 java]# uname -r
 2.6.32-358.el6.x86_64

 [root@fpNet-WEB-10 java]# /usr/bin/gcc --version  //gcc的版本
gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-3)
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


简单说下:
//gcc源码地址: https://gitee.com/mirrors/gcc

gcc 是编译器,基本上 Linux 下所有的程序(包括内核)都是 gcc 编译的,libc 当然也是

glibc是gnu发布的libc库,也即c运行库。glibc是linux 系统中最底层的api(应用程序开发接口),几乎其它任何的运行库都会倚赖于glibc

jdk环境:  

[root@fpNet-WEB-10 java]# java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)

1.首先创建Java文件 aozhejinJni.java ,并创建native方法

public class aozhejinJni
{
    public native void aozhejinc();
    static
    {
        System.loadLibrary("aozhejinlib");
    }
    public static void main(String[] args)
    {
        new  aozhejinJni().aozhejinc();
    }
}

  备注:

#gcc -Wall -fPIC -c aozhejin.c -I ./ -I $JAVA_HOME/include/linux/ -I $JAVA_HOME/include/
#gcc -Wall -rdynamic -shared -o aozhejinlib.so aozhejinc.o

 编译java文件,变成class文件

[root@fpNet-WEB-10 java]# javac aozhejinJni.java
[root@fpNet-WEB-10 java]# ll
total 16-rw-r--r-- 1 root root 591 May  5 19:09 aozhejinJni.class
-rw-r--r-- 1 root root 321 May  5 19:09 aozhejinJni.java

javah aozhejinJni生成java头文件

[root@fpNet-WEB-10 java]# javah aozhejinJni
[root@fpNet-WEB-10 java]# ll
-rw-r--r-- 1 root root 591 May 5 19:09 aozhejinJni.class
-rw-r--r-- 1 root root 492 May 5 19:09 aozhejinJni.h
-rw-r--r-- 1 root root 321 May 5 19:09 aozhejinJni.java

查看头文件 aozhejinJni.h 

[root@fpNet-WEB-10 java]# cat aozhejinJni.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class aozhejinJni */

#ifndef _Included_aozhejinJni
#define _Included_aozhejinJni
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     aozhejinJni
 * Method:    aozhejinc
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_aozhejinJni_aozhejinc (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif


下面要做的就是具体来实现Java_aozhejinJni_aozhejinc函数原型
1.引入生成的.h头文件
2. 把这个方法实现

创建C语言文件,aozhejinc.c

复制代码
#include "jni.h"
#include "aozhejinJni.h"
#include <stdio.h>
#include <stdlib.h>
//这里要注意,要加参数名env和obj,否则会报parameter name omitted的错误,不小心就会犯错.
JNIEXPORT void JNICALL Java_aozhejinJni_aozhejinc (JNIEnv * env, jobject obj){
  printf("it's call me!\n");
}

生成动态链接库文件 libaozhejin.so(windows下为.dll文件)

[root@fpNet-WEB-10 java]#gcc -Wall -fPIC -c aozhejinc.c -I ./ -I $JAVA_HOME/include/linux/ -I $JAVA_HOME/include/
//.c生成.o文件
[root@fpNet-WEB-10 java]#gcc -Wall -rdynamic -shared -o aozhejinlib.so aozhejinc.o   
//这里可以优化下,改成gcc -Wall -rdynamic -shared -o libaozhejinlib.so aozhejinc.o
//.o生成.so文件

 [root@fpNet-WEB-10 java]#ll

-rw-r--r-- 1 root root  189 May  5 22:28 aozhejinc.c
-rw-r--r-- 1 root root 1584 May  5 22:28 aozhejinc.o
-rw-r--r-- 1 root root  462 May  5 23:15 aozhejinJni.class
-rw-r--r-- 1 root root  390 May  5 22:22 aozhejinJni.h
-rw-r--r-- 1 root root  344 May  5 23:15 aozhejinJni.java
-rwxr-xr-x 1 root root 5942 May  5 22:30 aozhejinlib.so  //这里需要改成libaozhejinlib.so
解释下参数:
-c表示只编译(compile)源文件但不链接,会把.c或.cc的c源程序编译成目标文件
  注意这里调用的共享库名遵循Linux对库文件的命名惯例,实际加载的库文件应为 "libaozhejinlib.so",在引用时遵循命名惯例,不带"lib"前缀和".so"的扩展名。对于没有按照上述惯例命名的Native库,在加载时仍需要写成完整的文件名。
  所以需要把aozhejinlib.so改成libaozhejinlib.so,否则会报异常....

[root@fpNet-WEB-10 java]# java aozhejinJni
Exception in thread "main" java.lang.UnsatisfiedLinkError: no aozhejinlib in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1886)
at java.lang.Runtime.loadLibrary0(Runtime.java:849)
at java.lang.System.loadLibrary(System.java:1088)
at aozhejinJni.<clinit>(aozhejinJni.java:7)

设置 LD_LIBRARY_PATH 目录,便于java 命令运行时可以找到该so链接库文件

[root@fpNet-WEB-10 java]#vi /etc/profile
.....
export JAVA_HOME=/home/jdk1.8.0_161
export PATH=$JAVA_HOME/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/src/java:$LD_LIBRARY_PATH
[root@fpNet-WEB-10 java]#source /etc/profile //重载环境配置文件

 执行结果:

[root@fpNet-WEB-10 java]# java aozhejinJni
it's call me!

  我们查看下文件的目录内容

-rw-r--r-- 1 root root  189 May  5 22:28 aozhejinc.c
-rw-r--r-- 1 root root 1584 May  5 22:28 aozhejinc.o
-rw-r--r-- 1 root root  462 May  5 23:15 aozhejinJni.class
-rw-r--r-- 1 root root  390 May  5 22:22 aozhejinJni.h
-rw-r--r-- 1 root root  344 May  5 23:15 aozhejinJni.java
-rwxr-xr-x 1 root root 5942 May  5 22:30 libaozhejinlib.so

 
gcc编译的时候注意的内容:

#gcc -Wall -fPIC -c aozhejin.c -I ./ -I $JAVA_HOME/include/linux/ -I $JAVA_HOME/include/
#gcc -Wall -rdynamic -shared -o aozhejinlib.so aozhejinc.so
编译选项的说明: 
http://www.gnu.org/software/gcc/
https://gcc.gnu.org/onlinedocs/gcc/#toc-GCC-Command-Options
https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html#Link-Options
https://gcc.gnu.org/onlinedocs/gcc/Preprocessor-Options.html#Preprocessor-Options
https://gcc.gnu.org/onlinedocs/gcc/Developer-Options.html#Developer-Options
-c    
只编译,不链接成可执行文件。gcc compiler(或其他编译器)只是由输入的 .c等源代码文件生成 .o为后缀的目标文件,
一般用于编译不包含主程序的子程序文件。
-o output_filename    
肯定输出文件的名称为output_filename。同时这个名称不能和源文件同名。若是不给出这个选项,gcc就给出默认的可执行文件 a.out-g
产生符号调试工具(GNU的 gdb)所必要的符号信息。想要对源代码进行调试,就必须加入这个选项。
-O(大写o)
对程序进行优化编译、连接。采用这个选项,整个源代码会在编译、连接过程当中进行优化处理,这样产生的可执行文件的执行效率能够提升.
-Idirname
将 dirname 所指出的目录加入到程序头文件目录列表中,是在预编译过程当中使用的参数。
-shared
生成一个共享的目标文件,它可以与其余的目标一块儿连接生成一个可执行的文件。

一般编译分几个阶段:

程序在编译时,GCC需要调用预处理程序,由它负责展开在源文件中定义的宏,并向其中插入“#include”语句所包含的内容。
接着GCC会调用ccl和as将处理后的源代码编译成目标代码。最后GCC会调用链接程序ld把生成的目标代码链接成一个可执行程序。
使用GCC编译程序时, 一般编译过程可细分为四个阶段:预处理阶段、编译、汇编、链接

1.gcc test.c -o test
 GCC编译一般使用指令gcc test.c -o test 可将test.c编译成名为test的可执行文件.
2.gcc -E test.c -o test.i
预处理阶段(Pre-Processing)
使用-E参数可以让GCC在预处理结束后停止编译过程
此时若查看test.i文件中的内容会发现stdio.h的内容却是插到文件中去了,而其他相应被预处理的宏定义也都做了相应的处理
在hello.i文件中最后仍可看到main函数,test.c中main函数部分的程序仍然在里面,只是将“stdio.h”的内容插入到 test.i中。
在 /usr/include 目录中可找到“stdio.h”的头文件。
3.gcc -S test.i -o test.s
编译阶段(Compiling)
代码编译阶段,在这个阶段中GCC首先要jia检查代码的规范性、是否由语法错误等,
以确定代码的实际要作的工作。在检查无误后GCC把代码翻译成汇编语言。
用户可以使用“-S”选项来进行查看,该选项只进行编译而不进行汇编,但是会生成汇编代码。
汇编语言是非常有用的,它为不同高级语言不同编译器提供了通用的语言
执行编译过程。生成汇编文件,打开生成的test.s文件

4.gcc -c test.s -o test.o
汇编阶段(Assembling)
汇编阶段是把编译阶段生成的“test.s”文件转成目标文件,可使用”-c“参数可看到汇编代码转化为".o"的二进制代码。
输入gcc -c test.s -o test.o指令生成 test.o文件

____________________________________________________________________________________________________
1.最基本的gcc编译格式,gcc test.c 输出可执行程序test.out

 [root@fpNet-WEB-10 test]# cat test.c
  #include<stdio.h>
  int main(void){
    printf("\n aozhejin\n");
  return 0;
 }
 [root@fpNet-WEB-10 test]# gcc test.c
 [root@fpNet-WEB-10 test]# ll
 total 12
 -rwxr-xr-x 1 root root 6464 May 6 10:59 a.out
 -rw-r--r-- 1 root root 74 May 6 10:59 test.c
 [root@fpNet-WEB-10 test]# chomd 777 a.out
 [root@fpNet-WEB-10 test]# ./a.out
  aozhejin
 2.使用-o选项
 [root@fpNet-WEB-10 test]# gcc test.c -o test
 -rwxr-xr-x 1 root root 6464 May 6 11:05 test
 -rw-r--r-- 1 root root 74 May 6 10:59 test.c
 [root@fpNet-WEB-10 test]# ./test
 aozhejin
  3.使用-c选项

 [root@fpNet-WEB-10 test]# gcc -c test.c
 [root@fpNet-WEB-10 test]# ll
 total 8
 -rw-r--r-- 1 root root 74 May 6 10:59 test.c
 -rw-r--r-- 1 root root 1496 May 6 11:07 test.o

# test.o是目标文件,不是可执行文件,因为这里用到了-c,
# 告诉gcc到汇编为止,不要进行链接。
# 链接就是将目标文件、启动代码、库文件链接成可执行文件的过程,
# 这个文件可被加载或拷贝到存储器执行。

[root@fpNet-WEB-10 test]# ./test.o
-bash: ./test.o: Permission denied
[root@fpNet-WEB-10 test]# chmod 777 test.o
[root@fpNet-WEB-10 test]# ./test.o
-bash: ./test.o: cannot execute binary file
[root@fpNet-WEB-10 test]# file test.o
test.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

再详细进行一次分步执行:

分四步执行,得到可执行文件.
一、预编译( 生成 hello.i 文件) 
预编译的处理规则:
    1. 将所有的 “#define” 删除,并展开所有的宏定义。
    2. 处理所有的条件预编译指令,比如:" #if #ifdef #elif #else #endif "3. 处理所有的 “#include” 预编译指令。
        4. 删除所有的注释 “//” 、 “/* */”。
        5. 添加行号和文件名标识,以便编译时产生的行号信息以及用于编译错误或警告时能够显示行号。
        6. 保留所有的 “#pragma” 编译器指令。
   命令: gcc -E test.c -o test.i

二、编译(生成汇编代码 hello.s) 
        编译过程是编译器gcc把预处理完的文件进行词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。
  编译过程可以使用如下命令
 gcc -S test.i -o test.s
三、汇编(生成hello.o文件,得到中间目标文件)
   汇编是 汇编器as 把汇编代码转变成中间目标文件。
   汇编过程可以使用如下命令:
   gcc -c test.s -o test.o  gcc连接器进行连接。
四、链接(生成可执行程序)
  链接器 ld:负责将程序的目标文件与所需的所有附加的目标文件连接起来,附加的目标文件包括静态连接库和动态连接库。
  链接是链接器ld把中间目标文件和相应的库一起链接成为可执行文件。
  衔接命令如下:
  gcc test.o -o test

具体执行过程:
[root@fpNet-WEB-10 test]#  gcc -E test.c -o test.i
[root@fpNet-WEB-10 test]# ll
total 24
-rw-r--r-- 1 root root    74 May  6 10:59 test.c
-rw-r--r-- 1 root root 16732 May  6 11:34 test.i
[root@fpNet-WEB-10 test]# gcc -S test.i -o test.s
[root@fpNet-WEB-10 test]# ll
total 28
-rw-r--r-- 1 root root    74 May  6 10:59 test.c
-rw-r--r-- 1 root root 16732 May  6 11:34 test.i
-rw-r--r-- 1 root root   441 May  6 11:35 test.s
[root@fpNet-WEB-10 test]# gcc -c test.s -o test.o
[root@fpNet-WEB-10 test]# ll
total 32
-rw-r--r-- 1 root root    74 May  6 10:59 test.c
-rw-r--r-- 1 root root 16732 May  6 11:34 test.i
-rw-r--r-- 1 root root  1496 May  6 11:35 test.o
-rw-r--r-- 1 root root   441 May  6 11:35 test.s
[root@fpNet-WEB-10 test]# ./test.o
-bash: ./test.o: Permission denied
[root@fpNet-WEB-10 test]# chmod 777 test.o
[root@fpNet-WEB-10 test]# ll
total 32
-rw-r--r-- 1 root root    74 May  6 10:59 test.c
-rw-r--r-- 1 root root 16732 May  6 11:34 test.i
-rwxrwxrwx 1 root root  1496 May  6 11:35 test.o
-rw-r--r-- 1 root root   441 May  6 11:35 test.s
[root@fpNet-WEB-10 test]# ./test.o
-bash: ./test.o: cannot execute binary file
[root@fpNet-WEB-10 test]#  gcc test.o -o test
[root@fpNet-WEB-10 test]# ll
total 40
-rwxr-xr-x 1 root root  6464 May  6 11:38 test
-rw-r--r-- 1 root root    74 May  6 10:59 test.c
-rw-r--r-- 1 root root 16732 May  6 11:34 test.i
-rwxrwxrwx 1 root root  1496 May  6 11:35 test.o
-rw-r--r-- 1 root root   441 May  6 11:35 test.s
[root@fpNet-WEB-10 test]# ./test
 aozhejin

关于gcc -fpic 和 -fPIC 参数
gcc 官网说明:https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#Code-Gen-Options

-fpic
Generate position-independent code (PIC) suitable for use in a shared library, if supported for the target machine. 
Such code accesses all constant addresses through a global offset table (GOT).
The dynamic loader resolves the GOT entries when the program starts (the dynamic loader is not part of GCC;
it is part of the operating system). If the GOT size for the linked executable exceeds a machine-specific maximum size,
you get an error message from the linker indicating that -fpic does not work; in that case, recompile with -fPIC instead.
(These maximums are 8k on the SPARC, 28k on AArch64 and 32k on the m68k and RS/6000. The x86 has no such limit.) Position-independent code requires special support, and therefore works only on certain machines. For the x86,
GCC supports PIC for System V but not for the Sun 386i. Code generated for the IBM RS/6000 is always position-independent. When this flag is set, the macros __pic__ and __PIC__ are defined to 1. -fPIC If supported for the target machine, emit position-independent code, suitable for dynamic linking and avoiding any limit
on the size of the global offset table. This option makes a difference on AArch64, m68k, PowerPC and SPARC. Position-independent code requires special support, and therefore works only on certain machines. When this flag is set, the macros __pic__ and __PIC__ are defined to 2.

 



 

posted @ 2022-05-06 00:15  jinzi  阅读(1633)  评论(0编辑  收藏  举报