Java C 程序调用

2015-12-23 language java c/cpp

简单介绍下 Java 和 C 程序的相互调用。

Java 调用 C

Java 可以通过 JNI 调用 C 程序,这里通过一个 HelloWorld 的 Java 程序调用 helloFromC 函数,一个保存在 ctest 的共享库中函数。

/* HelloWorld.java */
public class HelloWorld {
    native void helloFromC();        /* (1) */
    static {
        System.loadLibrary("ctest"); /* (2) */
    }
    static public void main(String argv[]) {
        HelloWorld helloWorld = new HelloWorld();
        helloWorld.helloFromC();     /* (3) */
    }
}

简单介绍下上述的步骤:

  1. 通知 JVM 存在 helloFromC() 函数;
  2. 加载 ctest 动态库,该库中定义了上述的函数;
  3. 直接调用上述的定义的函数。

即是现在还没有实现动态库,实际上仍可以编译 Java 程序,因为默认会在加载时可以找到相应的函数;只是在真正执行函数时会报错。

$ javac HelloWorld.java
$ java HelloWorld
Exception in thread "main" java.lang.UnsatisfiedLinkError: no ctest in java.library.path
        at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
        at java.lang.Runtime.loadLibrary0(Runtime.java:870)
        at java.lang.System.loadLibrary(System.java:1122)
        at HelloWorld.<clinit>(HelloWorld.java:5)

接下来,就需要通过 C 创建一个 ctest 库。首先,通过上面生成的 .class 文件创建相应的 C 语言头文件。

$ javah HelloWorld

该命令会生成 HelloWorld.h 头文件,包含了如下内容:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorld
 * Method:    helloFromC
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_helloFromC
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

直接从上述的文件中复制函数的声明,并生成如下的文件。

/* ctest.c */
#include <jni.h>
#include <stdio.h>

JNIEXPORT void JNICALL Java_HelloWorld_helloFromC
  (JNIEnv * env, jobject jobj)
{
    printf("Hello from C!\n");
}

接着就是生成动态库了,不同的平台对应 jni.h 文件位置有所区别,例如 CentOS 中可以通过如下命令查看头文件所在路径,并编译。

$ rpm -ql `rpm -qa | grep "java.*-openjdk-devel"` | grep 'jni.h'
$ gcc -o libctest.so -shared -I/path/to/jdk/headers ctest.c -lc

其中在 Mac 中要将 .so 文件替换为 .dylib ,在 Windows 中替换为 .dll 文件,编译完成后,就可以通过如下方式运行:

$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD; java HelloWorld

C 调用 Java

书写 C 文件,模拟 JDK 中自带的 Java 命令。

#include <jni.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char*argv[])
{
    JavaVM *jvm;
    JNIEnv *env;
    JavaVMInitArgs vm_args;
    JavaVMOption options[1];

    jobjectArray applicationArgs;
    jstring appArg;

    /* Setting VM arguments */
    vm_args.version = JNI_VERSION_1_2;
    vm_args.ignoreUnrecognized = JNI_TRUE;
    vm_args.nOptions = 0;

     /* Setting classpath */
    char classpath[1024] = "-Djava.class.path=";
    char *env_classpath = getenv("CLASSPATH");

    int mainclass_index = 1;
    if (argc >= 3 && !strcmp("-classpath", argv[1])) {
        options[0].optionString = strcat(classpath, argv[2]);
        vm_args.nOptions++;
        mainclass_index += 2;
    } else if (env_classpath) {
        options[0].optionString = strcat(classpath, env_classpath);
        vm_args.nOptions++;
    }

    if (vm_args.nOptions > 0) {
        vm_args.options = options;
    }

    if (mainclass_index >= argc) {
        printf("Main class not found, please specify it\n");
        return 0;
    }

    jint res = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
    if (res < 0) {
        printf("Create VM error, code = %d\n", res);
        return -1;
    }

    jclass cls = (*env)->FindClass(env, argv[mainclass_index]);
    if (!cls) {
        printf("Class %s not found\n", argv[mainclass_index]);
        return -1;
    }

    jmethodID mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");

    if (!mid) {
        printf("Method %s of Class %s not found\n", "main", argv[mainclass_index]);
        return -1;
    }
    applicationArgs = (*env)->NewObjectArray(env, argc - mainclass_index - 1,
                              (*env)->FindClass(env, "java/lang/String"),
                              NULL);

    int i = 0;
    for (i = mainclass_index + 1; i < argc; i ++) {
        appArg = (*env)->NewStringUTF(env, argv[i]);
        (*env)->SetObjectArrayElement(env, applicationArgs, i - mainclass_index - 1, appArg);
    }

    (*env)->CallStaticVoidMethod(env, cls, mid, applicationArgs);

    printf("before destroy\n");

    /*
     * Destroy the JVM.
     * This is necessary, otherwise if the called method exits,
     * this program will return immediately.
     */
    (*jvm)->DestroyJavaVM(jvm);

    printf("after destroy\n");

    return 0;
}

接着需要编写 Makefile 文件,注意要链接 JDK 中所自带的 libjvm.so 库文件,在 CentOS 中可以通过 java-N.N.N-openjdk-headless 包安装。

设置环境变量 LD_LIBRARY_PATH,也即 libjvm.so 所在的路径。

export LD_LIBRARY_PATH=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.131-3.b12.el7_3.x86_64/jre/lib/amd64/server/

然后运行命令 ./jvm -classpath . HelloWorld