jnimを利用してNimからJavaのメソッドを呼び出す


## jnim

jnimは、jni apiをラップしたライブラリで、nimからJavaを呼び出すことができます。
[jnim](https://github.com/yglukhov/jnim)に関するいくつかの記事がQiitaにもあります。

* [Nimで他の言語との連携方法まとめ](https://qiita.com/snowlt23/items/1696687cca61bfedb1cc)
* [JavaからJNI経由でNimを呼び出してみる](https://qiita.com/6in/items/c8ea07dde7b0df995480)

本家サイトを見ると、`System.out.println`のサンプルしか書いておらず、Mavenリポジトリに登録されているようなライブラリを利用するにはどうしたら良いのかググってもそれらしい記事も見当たりませんでした。

今回は、外部Jarファイルに入っているJavaのクラスをNimから呼び出すためのサンプルソースを解説する記事となります。

```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を生成します。

https://mvnrepository.com/artifact/com.fasterxml.uuid/java-uuid-generator

### 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に追加します。

```groovy:build.gradle
/*
 * 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フォルダが作成され、２つのJarファイルが格納されています。

```shell
> 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.}`を定義します。

```nim:uuid_classes.nim
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を出力しています。

```nim:sample.nim
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`ともに取得されていることが確認できました。

```shell
> 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にアタッチさせる必要がありました。

```nim
# スレッドから呼び出し
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

```shell
nim c --threads:on --threadAnalysis:off -r sample
```

出力結果

```shell
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](https://github.com/nim-lang/c2nim)のように、自動でJavaクラスから定義ファイルを作るようなものがあると良いかなと思いました。

また、利用したいライブラリがあってもすべての機能が必要というわけでもないため、Gradleのライブラリプロジェクトを作成して、ライブラリをラップした簡易なクラスを定義して、それをNimから呼び出すといった方式が良さそうな気がします。(ついでにFatJar化するとベターでしょうかね) 

でもまあ、そこまでしてJavaのクラス使いたいなら、Java/Groovy/Kotlin/Scala/Clojureとかで作るよね。

#### おまけ
Javaのクラスの実装をNimで作成できるjexportマクロがあるので、Javaからのコールバックなどの処理もできるようです。

```Nim
type
  MyObjData = ref object
    a: int

  MyObj = ref object of JVMObject
    data: MyObjData

jexport MyObjSub extends MyObj:
  proc new = super()

  proc stringMethod(): string =
    "Nim"
```