jnim
jnimは、jni apiをラップしたライブラリで、nimからJavaを呼び出すことができます。
jnimに関するいくつかの記事がQiitaにもあります。
本家サイトを見ると、System.out.println
のサンプルしか書いておらず、Mavenリポジトリに登録されているようなライブラリを利用するにはどうしたら良いのかググってもそれらしい記事も見当たりませんでした。
今回は、外部Jarファイルに入っているJavaのクラスをNimから呼び出すためのサンプルソースを解説する記事となります。
import jnim
# Import a couple of classes
jclass java.io.PrintStream of JVMObject:
proc println(s: string)
jclass java.lang.System of JVMObject:
proc `out`: PrintStream {.prop, final, `static`.}
# Initialize JVM
initJNI()
# Call!
System.`out`.println("This string is printed with System.out.println!")
サンプルソース
今回のサンプルソースは、GitHubに保存しています。
https://github.com/6in/jnim-sample
サンプルソースの内容ですが、Mavenリポジトリで公開されているUUIDライブラリを参照して、NimからUUIDを生成するJavaのクラスを呼び出して、UUIDを生成します。
JAVA_HOMEの設定
jnimはJAVA_HOME環境変数をチェックしJVMの場所を取得するため、JAVA_HOMEを設定してください。
ソース概要
ファイル・フォルダ | 説明 |
---|---|
sample.nimble | nimbleファイル |
sample.nim | メインソース |
uuid_classes.nim | jnim用のJava Class定義 |
gradlew | gradle wrapper for *nix |
gradlew.bat | gradle wapper for windows |
build.gradle | gradle定義ファイル |
jars/ | 利用したいJarファイルがここにコピーされます |
Gradleを利用してJarを取得する
Gradle(Wrapper)を利用して、Maven CentralからJarを取得し依存関係を含めたJarファイルを1か所のフォルダに格納します。
利用したいライブラリはbuild.gradleのdependenciesに追加します。
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Groovy library project to get you started.
* For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle
* User Manual available at https://docs.gradle.org/6.7.1/userguide/building_java_projects.html
*/
plugins {
id 'java-library'
}
repositories {
// Use JCenter for resolving dependencies.
jcenter()
}
// 利用したいライブラリを追加
dependencies {
compile group: 'com.fasterxml.uuid', name: 'java-uuid-generator', version: '4.0.1'
}
// dependenciesで定義されているJarおよび関連するJarをjarsにコピーする
task collectJars(type: Copy) {
into "jars"
from configurations.runtime
}
定義されているカスタムタスク(collectJars)を実行するとJarsフォルダが作成され、2つのJarファイルが格納されています。
> nimble collectJars
> dir jars
jnim-sample\jars
12/13/2020 02:46 PM <DIR> .
12/13/2020 02:46 PM <DIR> ..
12/13/2020 02:46 PM 37,457 java-uuid-generator-4.0.1.jar
12/13/2020 02:46 PM 41,424 slf4j-api-1.7.29.jar
2 File(s) 78,881 bytes
2 Dir(s) 252,181,385,216 bytes free
利用したいクラスをNimで定義する
以下のような感じで利用したいクラスとメソッドの定義を行います。
staticメソッドの場合は{.static.}
を定義します。
import jnim
# 利用するJavaクラスの定義(をいちいち定義しないといけない)
jclass java.util.UUID * of JVMObject:
proc toString*(): string
jclass com.fasterxml.uuid.impl.RandomBasedGenerator * of JVMObject:
proc generate*(): UUID
jclass com.fasterxml.uuid.impl.NameBasedGenerator * of JVMObject:
proc generate*(name: string): UUID
jclass com.fasterxml.uuid.Generators * of JVMObject:
proc randomBasedGenerator*(): RandomBasedGenerator {.`static`.}
proc nameBasedGenerator*(): NameBasedGenerator {.`static`.}
NimでJavaの初期化&呼び出しを行う
Javaでは、別のJarに入っているクラスを呼び出す場合には環境変数CLASSPATH
にJarファイルへのパスを設定するか、Java起動オプションのCLASSPATH
の設定を行う必要がありますが、jnimは環境変数であるCLASSPATH
を参照しないため、JVMの初期化関数であるinitJNI
の引数にオプションとして渡す必要があります。
具体的には、-Djava.class.path=/path/to/jar
といった形式でJarへのパスを指定します。
下記のサンプルでは、jars
フォルダに格納されているJar
ファイルを取得し、ファイルセパレータで連結した文字列を組み立てて、initJNI
を呼び出します。
uuid_classes.nim
で定義していたGeneratorsクラスのstaticメソッドを呼び出して、生成されたUUIDを出力しています。
import os
import jnim
import strutils
import ./uuid_classes
# jars/に入っているjarファイルを列挙
var paths: seq[string] = @[]
for f in walkDir("jars"):
paths.add f.path
when defined(windows):
const sep = ";"
else:
const sep = ":"
# Initialize JVM with JVMOption
initJNI(JNIVersion.v1_8, @["-Djava.class.path=" & paths.join(sep)])
# call method
var randomBased = Generators.randomBasedGenerator().generate().toString()
var nameBased = Generators.nameBasedGenerator().generate("name").toString()
echo fmt"{ randomBased = }"
echo fmt"{ nameBased = }"
出力結果
SLF4Jのログが表示されてますが、ramdomBased
と nameBased
ともに取得されていることが確認できました。
> nimble run
Verifying dependencies for sample@0.1.0
Info: Dependency on jnim@>= 0.5.1 already satisfied
Verifying dependencies for jnim@0.5.1
Building sample/sample.exe using c backend
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
randomBased = 7a95887d-b10b-4008-9588-7db10bc008e2
nameBased = 6ae99955-2a0d-5dca-94d6-2e2bc8b764d3
Nimのスレッドからの呼び出し
NimのスレッドからのJavaメソッドを呼び出すときは、initJNIThread
をスレッド処理の最初に呼び出して、すでに初期化済みのJVMにアタッチさせる必要がありました。
# スレッドから呼び出し
proc threadFunc(param: tuple[a, b: int]) {.thread, gcsafe.} =
# この呼出しが必ず必要(すでに初期化済みのVMにアタッチする)
initJNIThread()
defer:
# 終了時に呼び出す
deinitJNIThread()
var randomBased = ""
var nameBased = ""
for x in param.a..param.b:
randomBased = Generators.randomBasedGenerator().generate().toString()
nameBased = Generators.nameBasedGenerator().generate("name").toString()
echo fmt"{ randomBased = }"
echo fmt"{ nameBased = }"
block:
defer:
echo "end threads"
var thr: array[0..1, Thread[tuple[a, b: int]]]
echo "start threads"
thr[0].createThread(threadFunc, (1, 1000))
thr[1].createThread(threadFunc, (1, 1000))
sleep(1000)
echo "wait threads"
joinThreads(thr)
ビルドは、threadsスイッチと、threadAnalysisスイッチを指定。
参考:
Nimマニュアルを日本語訳してみた
https://qiita.com/tauplus/items/80afbd47f3a44158ea1f
nim c --threads:on --threadAnalysis:off -r sample
出力結果
start threads
randomBased = c0466bea-6769-49c1-866b-ea6769e9c170
nameBased = 6ae99955-2a0d-5dca-94d6-2e2bc8b764d3
randomBased = 9032c0ad-1afc-4bfe-b2c0-ad1afc4bfe47
nameBased = 6ae99955-2a0d-5dca-94d6-2e2bc8b764d3
wait threads
end threads
まとめ
jnimを利用して既存のJavaライブラリを利用するサンプルを作成しました。
特徴としては、
- gradleで利用したいライブラリをMavenCentralなどから取得する設定を記述する
- Mavenでもいいですけどね。お好きなものを使ってください。
- gradle タスクで、依存するライブラリをすべて取得する
- 利用したいJavaクラス定義をNimで定義する
- JNIの初期化において、利用するすべてのJarへのパスを構築して引数(
java.class.path
)として渡す - Javaクラスおよびメソッドを呼び出す。
- Nimのマルチスレッドからも呼び出せる。
感想
利用したいJavaクラスの定義をいちいち書かないといけないというのがやはり面倒で、c2nimのように、自動でJavaクラスから定義ファイルを作るようなものがあると良いかなと思いました。
また、利用したいライブラリがあってもすべての機能が必要というわけでもないため、Gradleのライブラリプロジェクトを作成して、ライブラリをラップした簡易なクラスを定義して、それをNimから呼び出すといった方式が良さそうな気がします。(ついでにFatJar化するとベターでしょうかね)
でもまあ、そこまでしてJavaのクラス使いたいなら、Java/Groovy/Kotlin/Scala/Clojureとかで作るよね。
おまけ
Javaのクラスの実装をNimで作成できるjexportマクロがあるので、Javaからのコールバックなどの処理もできるようです。
type
MyObjData = ref object
a: int
MyObj = ref object of JVMObject
data: MyObjData
jexport MyObjSub extends MyObj:
proc new = super()
proc stringMethod(): string =
"Nim"