ScalaからJNAでCライブラリ(libpafe)にアクセス
常日頃名古屋鉄道(名鉄)で通勤しているが、JRに遅れることはや数年、名古屋市営地下鉄とともについにIC乗車券(通称manaca)が導入された。
そして前回やっと解像度がまともに表示できるようになったと、喜びの報告をしたVAIO YにはPaSoRi(ICカードリーダ)が内蔵されている。せっかく内蔵されているのだからこれを使わない手はないということで、早速IC乗車券を購入してきた。
LinuxからPaSoRiにアクセスするにはlibpafeが利用できる。当初Pythonのctypesからlibpafeにアクセスしていたが、せっかく(?)なのでScalaから制御してやろうと思い立った。
結論からいうと知識不足のためなかなか苦戦したがなんとかlibpafeを叩くことができた。
ScalaからCライブラリへのアクセスする際の参考にもなると思うので、備忘がてらここに記載しておく。
libpafeの準備
libpafeのサイトからDLして./configure;make;make install。
作者さんの環境がDebianらしくdebパッケージ対応されているので、Debian系OSであれば
$ dpkg-buildpackage -rfakeroot
これだけでdebパッケージができる。便利。
あとはREADMEに従ってudevの設定をしてやればlibpafeの準備は完了。
JNAでlibpafeにアクセス
Scalaの標準ライブラリにはCライブラリへのアクセスなどは提供されていないので、Javaの資産を活用する。
JavaからCライブラリへのアクセスというとJNIが有名(?)らしいけど、今回はより簡単にアクセスできると評判のJNAを使ってみた。
とりあえずREPLでいけるところまでやってみる。まずはライブラリのインスタンスを取得。
$ scala -classpath /usr/share/java/jna-3.2.4.jar scala> import com.sun.jna._ import com.sun.jna._ scala> val libpafe = NativeLibrary.getInstance("/usr/lib64/libpafe.so") libpafe: com.sun.jna.NativeLibrary = Native Library </usr/lib64/libpafe.so@140237998485072>
次にPaSoRiにアクセスして初期化処理をおこなう。
scala> val pasori_open = libpafe.getFunction("pasori_open") pasori_open: com.sun.jna.Function = native function pasori_open(/usr/lib64/libpafe)@0x7f8bbb0daba0 scala> val pasori = pasori_open.invokePointer(null) pasori: com.sun.jna.Pointer = native@0x26aed70 scala> val pasori_init = libpafe.getFunction("pasori_init") pasori_init: com.sun.jna.Function = native function pasori_init(/usr/lib64/libpafe)@0x7fe027dfdd50 scala> pasori_init.invokeInt(Array(pasori)) res0: Int = 0
libpafeの関数pasori_openを取得して実行している。引数は必要ないのでnullを渡している。invokePointerは返り値がPointerの関数実行。
Pasoriへのポインタを取得したのちpasori_initで初期化している。pasori_initの返り値0は正常終了。
ここまででPaSoRiを使う準備はできた。試しにPaSoRiのバージョンを取得してみる。
scala> import java.nio.IntBuffer import java.nio.IntBuffer scala> val v1 = IntBuffer.allocate(1) v1: java.nio.IntBuffer = java.nio.HeapIntBuffer[pos=0 lim=1 cap=1] scala> val v2 = IntBuffer.allocate(1) v2: java.nio.IntBuffer = java.nio.HeapIntBuffer[pos=0 lim=1 cap=1] scala> pasori_version.invokeInt(Array(pasori, v1, v2)) res11: Int = 0 scala> (v1.get, v2.get) res12: (Int, Int) = (1,30)
参照渡しする場合はそれ用のオブジェクトをつくってやる必要がある。pasori_versionはintを参照渡しするのでIntBufferを使った。
1.30というバージョンが取得できた。
最後にPaSoRiをcloseする。
scala> val pasori_close = libpafe.getFunction("pasori_close") pasori_close: com.sun.jna.Function = native function pasori_close(/usr/lib64/libpafe)@0x7fe027dfdb50 scala> pasori_close.invokeVoid(Array(pasori))
JNAeratorで構造体を自動生成
簡単な関数であればREPLでも実行できてしまうあたりはさすがJNA。(というかScalaもすごい)しかし構造体を渡したり受け取ったりとなるとさすがに簡単にはいかない。具体的にはcom.sun.jna.Structureを継承して必要な構造体を作る必要がある。
幸いJNAeratorというものがある。これはヘッダから、Structureを継承したJavaクラスやその他ライブラリへのアクセスを簡単にするクラスなどをひとまとめにしたJarを作成してくれるすぐれもの。
しかし残念ながら生成されたJarを使用するとJVMがSIGSEGVしてしまうという現象にみまわれあえなく断念。JarではなくJavaのソースファイルを吐かせて、それを修正して使うことにした。
Javaソースを吐かせるには-noComp -noJarを付けて実行してやれば良い。
$ java -jar ./jnaerator-0.9.5.jar -noComp -noJar -o ./ -runtime JNA /usr/include/libpafe/libpafe.h -package org.libpafe
上のコマンドラインだとJNAランタイムを使用する(JNAeratorランタイムを使用しない)、パッケージがorg.libpafeのJavaソースをカレントディレクトリに吐き出す。
たとえば
struct tag_felica { pasori *p; uint16 systemcode; uint8 IDm[8]; uint8 PMm[8]; uint16 area_num; felica_area area[256]; uint16 service_num; felica_area service[256]; struct tag_felica *next; };
こういう構造体だと、以下のようなソースを吐く。
public class tag_felica extends Structure { /// C type : pasori* public org.libpafe.tag_pasori.ByReference p; public short systemcode; /// C type : uint8[8] public byte[] IDm = new byte[(8)]; /// C type : uint8[8] public byte[] PMm = new byte[(8)]; public short area_num; /// C type : felica_area[256] public org.libpafe.felica_area[] area = new org.libpafe.felica_area[(256)]; public short service_num; /// C type : felica_area[256] public org.libpafe.felica_area[] service = new org.libpafe.felica_area[(256)]; /// C type : tag_felica* public tag_felica.ByReference next; public tag_felica() { super(); } // 以下略 }
ただし吐き出されたこのままを使用するとSIGSEGVになってしまう。それを回避するためにtag_pasori.ByReferenceやtag_felica.ByReferenceをcom.sun.jna.Pointerに書き換えて使っている。
ちなみにPointerから構造体へキャストするには、ポインタを引数にして構造体をnewしてやればよい。