概要
どうしてもJavaからNimのpegs(PEGs)を使いたかったので、JNI経由のNim関数呼び出し方法を調べてみました。
※追記※
驚愕の事実が判明しました・・・
jnimを使うと、Nimのファイルだけで共有ライブラリを作成できるみたいです。
明日以降で、もう少し手順を整理します。
import jnim
import dynlib
# 引数をそのまま返却
proc Java_HelloJNI_hello*(env: JNIEnvPtr, me:jobject, arg:jstring ): jstring {.cdecl,exportc,dynlib.} =
return arg
コンパイル&実行
$ nim c --app:lib --noMain -o:libHelloJNI.so jnim_call.nim
$ java -Djava.library.path=. HelloJNI "呼べてる?"
呼べてる?
というわけで、以下、ゴミ記事となりました。
※※ さらに追記 ※※
上記Nimのソースにて、JNIメソッド引数をjstring -> cstring -> string と変換したところ、何故かJVMがコアダンプ吐いて異常終了してしまいました。
初期バージョンにてNimMainを最初に呼んで初期化しないと動かなかったので、そのへんが原因かと思われます。
※※※ さらにさらに追記 ※※※
自分でNimMain呼べば良いかなと思い、emitプラグマを使って以下のように記述してみると、コアダンプ吐くこともなく、うまく呼び出すことができました!!
import jnim
import dynlib
# 引数をそのまま返却
proc Java_HelloJNI_hello*(env: JNIEnvPtr, me:jobject, arg:jstring ): jstring {.cdecl,exportc,dynlib.} =
# Nimの初期化を行う
{.emit: """
NimMain();
""".}
# 引数のString -> cstring変換
var argNim:cstring = env.GetStringUTFChars(env,arg,nil)
# 終了時に開放
defer:
env.ReleaseStringUTFChars(env,arg,argNim)
# cstring -> string 変換
var ss = "Hello " & $argNim
# 文字列を生成して終了
return env.NewStringUTF(env,ss)
これでJavaからpegも呼べそうです。
※※※※ さらにさらにさらに追記 ※※※※
毎回NimMainを呼ばないとダメなのか調べるために、ループで1万回くらいJNIメソッドを呼び出したところ、
"cannot register global variable; too many global variables"というエラーが吐かれて、異常終了しました。
最初の1回だけNimMainを呼べばよいみたいなので、jnimに定義されているグローバル変数を利用することにしました。
この対応により、NimによるJNIメソッドを10万回呼び出してもエラーが出なくなりました!!!
import jnim
import dynlib
# 引数をそのまま返却
proc Java_HelloJNI_hello*(env: JNIEnvPtr, me:jobject, arg:jstring ): jstring {.cdecl,exportc,dynlib.} =
# Nimの初期化を行う
if theEnv == nil :
{.emit: """
NimMain();
""".}
theEnv = env
# 引数のString -> cstring変換
var argNim:cstring = env.GetStringUTFChars(env,arg,nil)
# 終了時に開放
defer:
env.ReleaseStringUTFChars(env,arg,argNim)
# cstring -> string 変換
var ss = "Hello " & $argNim
# 文字列を生成して終了
return env.NewStringUTF(env,ss)
※※※※ さらにx4追記 ※※※※
Nimのフォーラムにて、jnimでのネイティブ呼び出しにて、Segmentation faultが発生するという投稿がありました。
私の記事でも作成したネイティブメソッドでも同様な現象が発生しました。
解決策としてコンパイラオプションに-d:noSignalHandlerを渡すことで問題を回避することができました。
jnimによるネィティブ用SOファイル(DLL)を作成するコンパイルオプションについて明記します。
nim c --out:libHelloJNI.so \
--app:lib \
--nomain \
--threads:on \
-d:release \
-d:noSignalHandler \
hello.nim
Java->Nim
@shsnow23 さんのこの記事を読んでJava->Nim呼び出しもできそうな気がしたので、サンプルを作ってみました。
JNAじゃなくてJNI
Nimの中から文字列を返却したあとのメモリの開放やら考えると、JNIのメソッドの中でJavaのオブジェクト生成を使ったほうが安全かなーと思ったので、JNIで実装してみました。
手順
Java側
- JNI定義を持つJavaクラスを作成する
- javacでコンパイル
- javahでヘッダー作成
- cファイルを作成する
// JNIテストクラス
public class HelloJNI {
// ライブラリのロード
static {
System.loadLibrary("HelloJNI");
}
// JNIメソッド
private native String hello(String str);
// テスト用メイン関数
public static void main(String[] args){
HelloJNI h = new HelloJNI();
System.out.println(h.hello(args[0]));
}
}
Nim側
- {.exportc.}プラグマを使った関数を定義
- cソースにコンパイルします
proc hello_nim(s:cstring) : cstring {.exportc.} =
# 文字列を連結して返却
var ss:string = ""
ss = "hello " & $s
return ss
C側
ヘッダはjavahで自動生成されるので、修正する必要はないです。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: hello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_HelloJNI_hello
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
C(JNI)のソースです。nimの関数を呼ぶ前に、渡された引数をネイティブ型に変換したりします。
NimMainという関数を呼ばないとNim側の初期化が行われず、実行時エラーとなってしまいました。
JNIの実装の仕方はこちらを参考にさせてもらいました。
#include "HelloJNI.h"
// Nim側の関数定義
#include "nimcache/hello.h"
JNIEXPORT jstring JNICALL Java_HelloJNI_hello
(JNIEnv * env, jobject pThis, jstring arg)
{
// 呼び出しは必須
NimMain();
// 引数を char* に変換
NCSTRING argNim = (*env)->GetStringUTFChars(env,arg,NULL);
// Nimの関数を呼び出す
NCSTRING ret = hello_nim(argNim);
// 確保したメモリを開放
(*env)->ReleaseStringUTFChars(env,arg,argNim);
// Nimの戻り値から文字列を構築して、返却
return (*env)->NewStringUTF(env,ret);
}
ビルドスクリプト
情弱なので、makefileが作れません・・・
NIM_HOMEは、Nimをインストールしたディレクトリです。
sharedlib=libHelloJNI.so
# 前回の生成物をとりあえず削除
rm -rf nimcache
rm ${sharedlib}
# Nimのコンパイル&Cソース出力 nimcacheにソースが出力されます
nim c --noMain --noLinking --header:hello.h Hello.nim
# JNIのCソースとともにnimcache/*.cもコンパイル
gcc -shared -fPIC \
-I ${JAVA_HOME}/include \
-I ${JAVA_HOME}/include/linux \
-I ${NIM_HOME}/lib \
-o ${sharedlib} \
HelloJNI.c nimcache/*.c
# Javaもついでにコンパイル
javac HelloJNI.java
# テスト実行
java -Djava.library.path=. HelloJNI
コンパイル後のファイル状態
こんな状態になります。
.
├── compile.sh
├── HelloJNI.java
├── HelloJNI.class
├── HelloJNI.h
├── HelloJNI.c
├── Hello.nim
├── libHelloJNI.so
└── nimcache
├── Hello.c
├── Hello.o
├── hello.h
├── stdlib_system.c
└── stdlib_system.o
んで、実行結果
イヤッッホォォォオオォオウ
$ java -Djava.library.path=. HelloJNI "Nim ニムニム"
hello Nim ニムニム