前回は、C言語のサンプルコードを用いてSotaから直接センサ値を読み出すことを確認しました。
ただ、Sotaの主なプログラミング環境はJavaなので、これをSotaのプログラムに組み込む場合、例えばJavaのコードからシェル実行によりサンプルコードの実行ファイルを起動して、コンソール経由(標準出力)で数値を読み取る、というような面倒な手順が必要です。
この方法は回りくどいので、直接Javaのコードから取得できないか考えてみます。

Javaでネイティブライブラリを動かすには

前回、センサの制御にはlibusbというライブラリを用いていることがわかりました。前回はC言語からこのライブラリの機能を呼び出しましたが、同じことをJavaでどうやればよいでしょうか?

そもそもJavaは、平たく言えば仮想上のマシン(VM)でコードを実行し、ハードウェアの環境の差異に依存せずにアプリを利用できるシステムですが、一方libusbやUSB自体はハードウェアにかなり肉薄しているので、両者をつなぐ何らかの手段が必要になります。

このような環境依存(ネイティブ寄り)な処理をJavaから呼び出すための仕組みとして、Javaには「JNI(Java Native Interface)」という機能が準備されています。JNIは、他のプログラミング言語で作成された処理をJavaから呼び出すための仕組みです。主にC/C++のプログラムとの連携に用いられますが、C言語に限らず(インターフェースが準備されている限り)様々なプログラミング言語と連携させることができます。今回は元のサンプルコードに準じてC言語で実装します。

ちなみに、いくつかのオープンソースライブラリなどで、JNIを用いたJava用のラッパークラス(ライブラリ)が既に公開されているものがありますが、libusbについては有効なライブラリが見つからなかったため、今回は必要最低限な処理に絞って自作していきます。

なお、今回JNIの実装に当たり、下記のwebページを参照させていただきました。
https://www.ne.jp/asahi/hishidama/home/tech/java/jni.html

仕様の確認と必要なメソッドの準備

まずは、C言語のサンプルコードで具体的にUSBを制御している部分を確認し、ライブラリの仕様を決めていきます。サンプルプログラムのメイン処理はiws600cm.cですが、最終的にlibusbの関数(usb.hで宣言された関数)を呼び出しているのは hiddata.cのようなので、hiddata.cの内容をベースにJNIを作成するのがよさそうです。
センサの制御に必要最低限な処理を考えると、以下の4つの関数が必要と思われます。

  • usbhidOpenDevice関数…デバイスを開く
  • usbhidCloseDevice関数…デバイスを閉じる
  • usbhidGetReport関数…デバイスからバッファを読み取る
  • usbhidSetReport関数…デバイスにバッファを送る

それでは、この四つの処理を持つコードを、Java及びC言語でそれぞれ作成します。まずはJava側の実装を進めていきます。

Javaのコーディング

Javaのライブラリは、ソースを配置するディレクトリの構造に応じて「パッケージ」 呼ばれる単位で管理されます。まずは今回のライブラリのパッケージを表すディレクトリを 作成します。
Sota内の任意のディレクトリに、lib/usb/というディレクトリを作成してください。今回は「/home/root/lib/usb/」というディレクトリを作りました。作成したら、カレントディレクトリをそこに合わせてください。

$ mkdir lib
$ cd lib
$ mkdir usb
$ cd usb
lib/usbというディレクトリを作成する
このディレクトリにJavaのソースを作成していく

作成したディレクトリに、Hiddata.javaという新しいJavaのソースを作成し、コーディングを進めていきます。 なお、前回はテキストファイルの編集にviを使いましたが、操作性が決して良くないため、以降はあらかじめPC上で、Visual Studioやeclipse等汎用のIDE(統合開発環境)などのコーディングしやすい環境でソースを作成し、最後にSota本体に転送するといった方法を推奨します。

まずは、 前述のhiddata.cに含まれる4つの関数の宣言部のみを Hiddata.java にコピーしていきます。 コピーの際には、メソッドの宣言部分にJNIで呼ばれることを表す「native」を付け加えます。

package lib.usb;

public class Hiddata {

	public native int usbhidOpenDevice(usbDevice_t **device, int vendor, char *vendorName, int product, char *productName, char *serialNumber, int _usesReportIDs);
	public native void    usbhidCloseDevice(usbDevice_t *device);
	public native int usbhidSetReport(usbDevice_t *device, char *buffer, int len);
	public native int usbhidGetReport(usbDevice_t *device, int reportID, char *buffer, int *len);

}

次に、引数と戻り値を差し替えていきます。特に、JavaではC言語のポインタの扱いが難しいので、極力ポインタを必要としない形式に一部変更します。また、今回の用途に不要と思われる引数も省略していきます。


usbhidOpenDevice関数は、USBのベンダID・プロダクトIDだけを与えるようにします。また、デバイスハンドルを表す「usbDevice_t **dev」は、本来はダブルポインタを渡して、呼び出した関数内で引数に与えたポインタに取得したハンドルを代入させる仕組みですが、こちらを戻り値で取得するようにします。

JNIでポインタを受け取る場合、本来は処理系によって変動するアドレス長に対応するようbyte配列で受け渡すべきですが、今回は簡略化のためIntel Edisonのアドレス長(32bit)に合わせてint型とします。もし別のプラットフォームに応用する場合は、適時修正してください。

public native int usbhidOpenDevice(int vendor, int product);

usbhidCloseDevice関数は、usbhidOpenDeviceで得られたポインタを代入するので、Java側は引数をint型とします。

public native void usbhidCloseDevice(int device);

usbhidSetReport関数は、送信データはbyte配列とします。デバイスハンドルは他の関数に倣ってint型とします。

public native int usbhidSetReport(int device, byte[] buffer, int len);

usbhidGetReport関数は、受信データの格納バッファ(char *buffer)をbyte配列にし、int *lenはポインタ型ではなく整数型(int len)とします。Javaから実行する場合はbufferのサイズを格納します。

public native int usbhidGetReport(int device, int reportID,byte[] buffer,int len);

ここまででできたHiddata.javaのソース全体は下記の通りです。

package lib.usb;

public class Hiddata {

	public native int usbhidOpenDevice(int vendor, int product);
	public native void usbhidCloseDevice(int device);
	public native int usbhidSetReport(int device, byte[] buffer, int len);
	public native int usbhidGetReport(int device, int reportID,byte[] buffer,int len);

}

Javaのソースのコンパイル

ソースを作成したら、lib/usb/のルートディレクトリ(ここでは/home/root/)に戻り、Javaのソースをコンパイルします。コンパイルコマンドは「javac lib/usb/Hiddata.java」です。

$ cd ../..
$ javac lib/usb/Hiddata.java

正常にビルドできると、Hiddata.javaと同じディレクトリにHiddata.classと言うファイルができます。コンパイルエラーが出る場合は、ソースやディレクトリに間違いが無いか確認してください。

Hiddata.javaをコンパイル。
エラーが無ければ何も表示されずコンパイルが終わる

続いて、C言語側のヘッダファイルをjavahで生成します。コマンドは同じディレクトリ上で「javah lib.usb.Hiddata」です。コマンドに成功すると、カレントディレクトリに「lib_usb_Hiddata.h」というファイルが生成されます。

$ javah lib.usb.Hiddata
javahを使ってC言語用のヘッダファイルを自動生成

生成されたファイルの内容を見ると、以下のようにJavaのメソッドをベースにしたC言語のヘッダファイルになっています。

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

#ifndef _Included_lib_usb_Hiddata
#define _Included_lib_usb_Hiddata
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     lib_usb_Hiddata
 * Method:    usbhidOpenDevice
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_lib_usb_Hiddata_usbhidOpenDevice
  (JNIEnv *, jobject, jint, jint);
…

C言語のコーディング

次は、自動生成されたヘッダファイルに応じて、C言語のコードを実装します。 lib_usb_Hiddata.h と同じディレクトリ(ここでは/home/root/)に、「lib_usb_Hiddata.c」というC言語のソースファイルを作成し、冒頭に「#include “lib_usb_Hiddata.h”」と記述して生成したヘッダファイルを参照し、続いて、ヘッダファイルの関数宣言部分をコピーして行きます。

#include "lib_usb_Hiddata.h"

JNIEXPORT jint JNICALL Java_lib_usb_Hiddata_usbhidOpenDevice
  (JNIEnv *, jobject, jint, jint);

JNIEXPORT void JNICALL Java_lib_usb_Hiddata_usbhidCloseDevice
  (JNIEnv *, jobject, jint);

JNIEXPORT jint JNICALL Java_lib_usb_Hiddata_usbhidSetReport
  (JNIEnv *, jobject, jint, jbyteArray, jint);

JNIEXPORT jint JNICALL Java_lib_usb_Hiddata_usbhidGetReport
  (JNIEnv *, jobject, jint, jint, jbyteArray, jint);

コピーした宣言部分には各引数の型のみ記載されており、変数名が抜けているので、C言語ソース側に書き足します。第一引数・第二引数は、前述の参照webページに倣って「JNIEnv *env」「jobject thisj」とそれぞれ書き換えます。それ以外はJava側のソースで設定した引数の名前に従います。

JNIEXPORT jint JNICALL Java_lib_usb_Hiddata_usbhidOpenDevice
(JNIEnv *env, jobject thisj, jint vendor, jint product)

JNIEXPORT void JNICALL Java_lib_usb_Hiddata_usbhidCloseDevice
(JNIEnv *env, jobject thisj, jint device)

JNIEXPORT jint JNICALL Java_lib_usb_Hiddata_usbhidSetReport
(JNIEnv *env, jobject thisj, jint device, jbyteArray buffer, jint len)

JNIEXPORT jint JNICALL Java_lib_usb_Hiddata_usbhidGetReport
(JNIEnv *env, jobject thisj, jint device, jint reportID, jbyteArray buffer, jint len)

次に、関数の内容を実装します。センサに関する処理を実装する前に、まずはJavaからC言語のネイティブコードを実行できるかどうかを試してみたいと思います。
戻り値が設定されている関数は、ひとまず全て「return 0;」とします。また、関数が呼ばれたことがわかりやすいように、「printf(“called usbhidOpenDevice\n”);」のように関数名を表示させるようにします。

ここまでで出来た lib_usb_Hiddata.c の内容は下記の通りです。

#include "lib_usb_Hiddata.h"

JNIEXPORT jint JNICALL Java_lib_usb_Hiddata_usbhidOpenDevice
(JNIEnv *env, jobject thisj, jint vendor, jint product)
{
	printf("Called usbhidOpenDevice\n");
	return 0;
}

JNIEXPORT void JNICALL Java_lib_usb_Hiddata_usbhidCloseDevice
(JNIEnv *env, jobject thisj, jint device)
{
	printf("Called usbhidCloseDevice\n");
	return ;
}

JNIEXPORT jint JNICALL Java_lib_usb_Hiddata_usbhidSetReport
(JNIEnv *env, jobject thisj, jint device, jbyteArray buffer, jint len)
{
	printf("Called usbhidSetReport\n");
	return 0;
}

JNIEXPORT jint JNICALL Java_lib_usb_Hiddata_usbhidGetReport
(JNIEnv *env, jobject thisj, jint device, jint reportID, jbyteArray buffer, jint len)
{
	printf("Called usbhidGetReport\n");
	return 0;
}

lib_usb_Hiddata.cを作成したら、ビルドします。コンソールで以下のように入力します(長いですが1行分のコマンドです)。

$ gcc -shared -fPIC -I /home/vstone/java/jdk1.8.0_40/include/ -I /home/vstone/java/jdk1.8.0_40/include/linux/  lib_usb_Hiddata.c -o libHiddata.so

ビルドに成功すると、libHiddata.soという名前の共用ライブラリファイルが作成されます。ビルド時に何らかのエラーが表示された場合、誤字・脱字やファイルパスに問題が無いか確認してください。

lib_usb_Hiddata.c をビルドすると、 libHiddata.so というファイルができる
これは「 共用ライブラリファイル 」といい、Javaからも最終的にこのファイルが呼び出される

コンパイラのオプションのポイントについて簡単に説明すると、-shared -fPICで共用ライブラリとして出力指定します。-Iでjni.h、jni_md.hのファイルパスを指定します。-oで出力ファイル名を指定します。ファイル名は必ず冒頭に小文字で「lib」を付け、拡張子を「so」にします。

テストプログラムの作成

これでJavaからJNIでC言語の処理を呼び出す基本的な実装が完了しました。それでは簡単なテストプログラムを作って、実際に処理が呼び出されることを確認してみたいと思います。
テストプログラムは、先ほど作成したHiddata.javaの中に作成していきます。

Hiddata.javaのHiddataクラスにpublic static void main(String[] args) というメソッドを作成してください。

まず、生成された「libHiddata.so」を呼び出すことを宣言するため、メソッドの冒頭にSystem.loadLibrary(“Hiddata”);を入れてください。 loadLibrary()の引数に与える文字列は、ライブラリのファイル名から冒頭のlibと拡張子を取り除いたものです。

続いて、Hiddataクラスのインスタンスを新規に作成し、usbhidOpenDevice、usbhidCloseDeviceなどを呼び出します。引数は暫定なので0などを代入してください。

以上を踏まえた Hiddata.javaの内容は以下の通りです。

package lib.usb;

public class Hiddata {
	public native int usbhidOpenDevice(int vendor, int product);
	public native void usbhidCloseDevice(int device);
	public native int usbhidSetReport(int device, byte[] buffer, int len);
	public native int usbhidGetReport(int device, int reportID,byte[] buffer,int len);

	public static void main(String[] args) {
		System.loadLibrary("Hiddata");
		Hiddata hiddata = new Hiddata();
		hiddata.usbhidOpenDevice(0,0);
		hiddata.usbhidCloseDevice(0);
	}
}

ソースを編集したら、javacでコンパイルします。コンパイル方法は先ほどと同じです。 カレントディレクトリを lib/usb/のルートディレクトリ(ここでは/home/root/)に忘れずに合わせてからコンパイルしてください。

$ javac lib/usb/Hiddata.java

コンパイルが正常に完了したら、続いてプログラムを実行します。同じディレクトリ上で「java -Djava.library.path=./ lib.usb.Hiddata」と実行します。-Dオプションは、共用ライブラリの参照先のパスです。作成した共用ライブラリ(libHiddata.so)のパスを指定します。lib.usb.Hiddataは実行するJavaのクラスで、先ほどjavacでコンパイルしたHiddataです。

$ java -Djava.library.path=./ lib.usb.Hiddata
サンプルプログラムをコンパイルして実行
実行後、C言語ソースにprintfで書いた文言が表示されたら、正しく処理が呼び出されている

実行後、C言語で作成した関数のprintfが呼び出されていれば、JNIによるネイティブコードの実行に成功しています。実行時にエラーが出る場合は、出力した共用ファイルの名前が異なっているか、共用ファイルのファイルパスを間違えている可能性があるため、それぞれ確認してください。

今回は直接センサに関係する部分は少なめでしたが、次回は実際にJNIによってJavaからセンサを扱う部分を実装していきます。

今回のサンプルコードは以下のリポジトリに公開しています。ライセンスがGPL2なので、ご利用の際にはご注意ください。

https://github.com/vstoneofficial/HumanSensor_Sota/releases/tag/HumanSensor_2nd

このリポジトリは、本連載全体のソースコードです。今回のソースを取得する場合は、上記URLより「Source code (zip)」をクリックすれば、記事に合ったzip形式のソースをダウンロードできます。