LinuxでJNIを使ってJavaからCを実行してみる

JNIを触る機会があったのだけれど、それはCからJavaというものでした。逆にJavaからCを呼ぶというのが多分普通の使い方なのだと思います。この逆パターンの使われ方としては、レガシーなアプリを置き換えることができない場合のようにどうしてもCを使わないといけなくて、でも一部機能は別のアプリで作ったJavaの機能をそのまま流用できるとかそんな時でしょうか。
JavaからCを呼ぶ場合はけっこうあるようで、Eclipseなんかでも使われていたりするらしく、プラットフォーム固有のことをやりたい場合はやっぱりCということになるのでしょう。
そう言えばまだJavaを勉強しだした学生の頃、友人に「JavaからCで作ったものを呼び出すってできるの?」と聞かれて、「それはできへんのじゃないかなぁ」と訳知り顏で話していたのを思い出しました。全然できましたね。ごめんなさい。
プラットフォーム固有の処理ということで、JavaのプロセスIDとスレッドIDを表示するプログラムを書いてみました。意外に、JavaにはプロセスIDとかスレッドIDを返すようなメソッドはない模様です。

public class ProcInfo {
    public static void main(String[] args) {
        System.loadLibrary("procinfo");

        ProcInfo pi = new ProcInfo();
        System.out.println("Process ID : " + pi.getProcId());
        System.out.println("Thread  ID : " + pi.getThreadId());
    }

    private native int getProcId();
    private native int getThreadId();
}

ライブラリ(後述)をロードしただけで、あとはnative修飾子をつけたメソッドを呼び出しています。この2つのメソッドをC側で書きます。
その前にこれをjavacでコンマパイルしたと、javahでC用のヘッダファイルを作成します。

$> javac ProcInfo.java
$> javah -jni ProcInfo

できたヘッダがこちら。

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

#ifndef _Included_ProcInfo
#define _Included_ProcInfo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     ProcInfo
 * Method:    getProcId
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_ProcInfo_getProcId
  (JNIEnv *, jobject);

/*
 * Class:     ProcInfo
 * Method:    getThreadId
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_ProcInfo_getThreadId
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

このヘッダをincludeして、Java_ProcInfo_getProcIdとJava_ProcInfo_getThreadIdの2つの関数を書きます。プロセスIDとスレッドIDを返すだけなのでgetpid、gettidを使いましょう。gettidは_syscall0とかっておまじないが必要なようです。(man参照)

#include "ProcInfo.h"

#include <sys/types.h>
#include <unistd.h>

#include <linux/unistd.h>
#include <errno.h>

_syscall0(pid_t, gettid);

JNIEXPORT jint JNICALL Java_ProcInfo_getProcId(JNIEnv* env, jobject obj)
{
  return getpid();
}

JNIEXPORT jint JNICALL Java_ProcInfo_getThreadId(JNIEnv* env, jobject obj)
{
  return gettid();
}

Java側で戻りをintにした場合、C側の戻り型はjint型です。これはtypedefしてるだけなので、特に気にせずいけます。Stringの戻りとかだと、もうちょっとおまじないが必要です。
これをコンパイルし、共有ライブラリにします。合わせて、ライブラリパスに作成した共有ライブラリのパスを追加しておきましょう。ここではカレントにあるものとします。

$> gcc -shared -I$JAVA_HOME/include -I$JAVA_HOME/include/linux procinfo.c -o libprocinfo.so
$> export LD_LIBRARY_PATH=.

ライブラリ名は必ずlibを頭につけましょう。このlibと拡張子.soをとったものをJavaプログラム側のloadLibraryで文字列として指定します。(最初のほうの話し)
これで完了、実行するとプロセスID、スレッドIDが出力されます。(ただし、マルチスレッドではないのでプロセスIDもスレッドIDも同じ値です。。。)

$> java ProcInfo
Process ID : 2672
Thread  ID : 2672

簡単ですねぇ。ちなみに逆にCからJavaを使う場合は、慣れてしまえばさらさらっと書けますが、JVMの起動にも色々と記述が必要だったり、Java側メソッドの呼び出しもお作法があって、最初はけっこう面倒です。