jextractをGradleのタスクで実行するプラグイン作った

タイトル通りのgradleプラグインを作りました。
gradleのリポジトリには現在申請中です。
ソースコードはこちら

+ [jnr-ffi Plugin](https://github.com/Javakky/jnr)

# jnr-ffi Plugin

このプラグインは[Project Panama](https://openjdk.java.net/projects/panama/)で開発中(Java13で実装予定)の機能を使いやすくするものです。

特に、[JEP 191: Foreign Function Interface](https://openjdk.java.net/jeps/191)の利便化を目的としています。

JNR関連記事はこちらのQiitaにまとめたりしているのでよければどうぞ

+ [jnr-ffi関係記事リンク集](https://qiita.com/Kakky/items/bce3d6c9c47899b000a3)

## 動作環境
システムのJDK・プロジェクトのJDKともにバージョン13以上?

動作保証のJDKは [Project Panama Early-Access Builds](http://jdk.java.net/panama/)
だけです。

JDKのバージョンが12以下の場合警告文、jextractがシステムのJDKに存在しない場合にはエラー文を出します。

## jextract

### jextractとは？

jextractとは、C言語などの.hファイルから対応するJavaのインターフェースを生成するツールです。

[early-access版のJDK](http://jdk.java.net/panama/)に付属しており、[ServiceLoader](https://docs.oracle.com/javase/jp/10/docs/api/java/util/ServiceLoader.html)を利用することでプログラムからも呼び出すことができます。


### jextractタスク
このプラグインではjextractというタスクが定義されています。

jextractタスクでは、指定されたフォルダ内にある.hファイルからjextractでjarファイル(対応するインターフェース)を生成するものです。

(入れ子になっているファイルも検索し、ディレクトリをパッケージと対応させてjarを生成します)

jextractタスクのオプションは、build.gradleで以下のように指定できます。

```groovy:build.gradle
jextract{
    // .hファイルを含むディレクトリのパス
    // 初期値は"src/main/resources/"
    sourceRoot = "head/"
    // 生成したjarを置くディレクトリのパス
    // 初期値は"libs/"
    outPath = "jar"
    // 生成するインターフェースが属するパッケージのルート名
    // <packageRoot>.<ディレクトリ名>.<ディレクトリ名>という風に命名される
    // 初期値は""
    packageRoot = "pkg"
    // パッケージ名にsourceRootディレクトリを含むかどうか
    // falseにすると、SourceRoot直下の.hファイルのパッケージはpackageRootとなる
    // 初期値はfalse
    includeRoot = false
}
```

# 制作過程
## jextractを呼び出す

ServiceLoaderを利用してjextractを呼び出します。
ちなみに、com.sun.tools.jextract.Main$JextractToolProviderを直接呼び出そうとするとコンパイルエラーとなります。
(resources/Message.propertiesのせい？)

では、Javaのコードを書いていきます。

```java:Test.java
public class Test{
    public static void main(String[] args){
        // ServiceLoaderでToolProviderの実装一覧を取得する。
        // jshellやjextractなど、JDKのbinに入っているやつらが返ってくる。
        // jextractのツール名は"jextract"なので一致するものを利用する。
        ServiceLoader<ToolProvider> providers = load(ToolProvider.class);
        ToolProvider provider = null;
        for (ToolProvider tool : providers) {
            if (tool.name().equals("jextract")) provider = tool;
        }
        if(provider == null) System.out.println("エラー: jextractが存在しません。");
        // 最後の引数の配列にjextractを呼び出すときのコマンドライン引数を渡す。
        // 空白で区切る代わりに要素を分ける。
        provider.run(System.out, System.err, String[]{"src/main/resources/test.h"});
    }
}
```

```c:test.h
int hoge(void);
```

src/main/resources/にtest.hを作成してTest.mainを実行すると、プロジェクトの直下にtest.h.jarというファイルが作成されます。

+ [jnr-ffi使った(使いやすくするなどした)](https://qiita.com/Kakky/items/a54ccc68365707765a5a#interface%E7%94%9F%E6%88%90%E3%81%AE%E8%87%AA%E5%8B%95%E5%8C%96)

また、今回利用したオプションを抜粋します。

| オプション | 引数 | 説明 |
|:---|:---|:---|
| -t | パッケージ名 | パッケージに生成したインターフェースが属するようになる |
| -o | ファイル名(パスも含む) | ファイル名(パス)に生成したjarファイルが置かれる |
|  | ファイル名(パスも含む) | 変換したい.hファイルのファイル名(パス) <br> 複数選択可能だが、必ず同一パッケージとして生成される |

例えば、

```shell
jextract -t pkg -o out.jar test.h
```

というコマンドを実行した時、または

```java:Test.java
public class Test{
    public static void main(String[] args){
        ServiceLoader<ToolProvider> providers = load(ToolProvider.class);
        ToolProvider provider = null;
        for (ToolProvider tool : providers) {
            if (tool.name().equals("jextract")) provider = tool;
        }
        provider.run(System.out, System.err, String[]{"-o", "out.jar", "-t", "pkg", "test.h"});
    }
}
```

というプログラムを実行した時、

```c:test.h
int hoge(void);
```

というファイルがあれば、out.jarというファイルが生成されます。

```
out.jar
|-META-INF
| \jextract.properties
\pkg
  \test.class
```

out.jarの内部構造は以下のようになっており、pkgというパッケージディレクトリが生成されていることが分かります。

[jad](https://varaneckas.com/jad/)を使ってTest.classを逆コンパイルすると、以下のようになっていることが分かります。

```java:Test.jad
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 

package pkg;


public interface test
{

    public abstract int hoge();
}

```

しっかり.hファイルがinterfaceにトランスパイルされていますね。

あと、プラグインにしたときにServiceLoaderがjextractを見つけてくれなかったのでシステムのJDKを13にした後、呼び出しを少し変えました。

```java
public class Test{
    public static void main(String[] args){
        // システムのクラスローダーを使う
        ServiceLoader<ToolProvider> providers = load(ToolProvider.class, ClassLoader.getSystemClassLoader());
        ToolProvider provider = null;
        for (ToolProvider tool : providers) {
            if (tool.name().equals("jextract")) provider = tool;
        }
        provider.run(System.out, System.err, String[]{"-o", "out.jar", "-t", "pkg", "test.h"});
    }
}
```

## プラグインを作る
こちらの記事を参考にしました。

+ [Gradle のプラグインの作り方メモ](https://qiita.com/opengl-8080/items/fcf60ba5daf658f39951)

###気を付けたこと
+ テストプロジェクトでjarをプラグインに追加するとき、依存関係のあるライブラリもクラスパスに追加した。
+ テストプロジェクトとシステムのJDKも13にした。
+ gradleにプラグインを上げるときにgroupを指定しないと失敗した。