🚧

jnigenであそぼう

2023/03/28に公開

Flutter 3.7のブログにて、jnijnigenが開発中であることが明かされました。

https://medium.com/flutter/whats-next-for-flutter-b94ce089f49c

On Android, we’re using JNI to bridge to Jetpack libraries written in Kotlin. With a new command, Dart automatically creates bindings for cross-language interoperation and converting data classes appropriately.

面白そうだったので、色々と試行錯誤して動かしてみました。この記事では、簡単にjnijnigenの動かし方を紹介し、遊んでみる人を増やしたいと思っています。

注意

筆者が遊んでいるのは、下記のバージョンになります。
アップデートによって遊び方が更新されると思われますので、必ず最新の情報を確認するようにしてください。

jni/jnigenとは

より正確な資料に当たりたい場合は、公式ドキュメントを確認して、次のブロックまで文章をスキップしてください🙏

https://github.com/dart-lang/jnigen/wiki/Architecture-&-Design-Notes


JNI、つまりJava Native InterfaceはJavaからネイティブなライブラリ(例えばCやC++で記述されたライブラリ)を呼び出すための仕組みの呼び名です。
手頃なドキュメントとしては、Oracleの「Java Native Interface仕様の目次」が適切かなと思います。

https://docs.oracle.com/javase/jp/8/docs/technotes/guides/jni/spec/jniTOC.html

Androidアプリケーションの開発においても、JNIを利用することで、CやC++で記述されたライブラリを呼び出すことができます。
Androidのアプリケーション開発として「jni」を検索すると、こちらの.soファイルをリンクする方法がよく引っかかります。

https://developer.android.com/training/articles/perf-jni?hl=ja


Dartの文脈では、「JavaやKotlinのAPIを呼び出すためにpackage:jnipackage:jnigenが利用できる」としています。

https://dart.dev/guides/libraries/java-interop

Dart mobile, command-line, and server apps running on the Dart Native platform, on Android, Windows, macOS, and Linux can use package:jni and package:jnigen to call Java and Kotlin APIs.

後述する内部の仕組みを見ると、大文字のJNI、つまりJava Native Interfaceのような振る舞いをしている箇所もあります。このため、以後大文字のJNIと記述した場合にはJava Native Interfaceを、小文字のjniと記述した場合にはpackage:jniを示すものとします。
package:jnigenについては(開発中なため変更される可能性も大いにありますが)、v0.3時点では、「JavaのライブラリをDartから呼び出すためのコードを生成する」ライブラリとされています。

https://github.com/dart-lang/jnigen/tree/e8b5d881d64a723c262e21770a4adadbbfbba7f7

Experimental bindings generator for Java bindings through dart:ffi and JNI.

It generates C and Dart bindings which enable calling Java libraries from Dart. C bindings call the Java code through JNI, Dart bindings in turn call these C bindings through FFI.

より詳細な概要は、後ほど。

Method Channelとjnigen

Flutterにおいて、DartからJavaやKotlinのコードを呼び出す手段としては、Method Channelが存在します。

https://docs.flutter.dev/development/platform-integration/platform-channels

2023年3月現在では、「どちらを使うべき」かと言えば「Method Channelを使うべき」です。というのも、jnigenはまだstableになっている機能ではない、ためです。

https://github.com/dart-lang/jnigen/issues/201#issuecomment-1475673363

こちらのコメントの通り、jnijnigenよりJavaのコードを簡単に呼び出せるという利点があります。実際にOkHttpをbindするコードを書いてみた感想としても、こちらの意見には納得感があります。

jniとjnigenで遊ぶ

リポジトリはこちらです。色々と遊んでいるので、補足を入れながら遊び方を紹介します。

https://github.com/koji-1009/fox_http

セットアップ

リポジトリの作成には、flutterのpluginテンプレートを利用します。
このため、新規にプロジェクトを作成する場合には、次のコマンドでプロジェクトを作成してください。今回は、fox_httpというプロジェクト名で、android向けのみの実装としています。

flutter create --template plugin --platforms android fox_http

続いて、pubspec.yamljnijnigenを追加します。jnidependenciesjnigendev_dependenciesです。バージョンについては、ここでは^0.3.0としておきます。

jnigen.yaml

jniによってAndroid向けの実装をする場合、さまざまな設定を行うファイルがjnigen.yamlになります。
ひとまず、公式のサンプルからコードをコピーしてきましょう。

https://github.com/flutter/samples/tree/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/experimental/pedometer

なお、それぞれのパラメーターは次のとおりです。必要に応じて、修正を加えていきます。

https://github.com/dart-lang/jnigen/blob/e8b5d881d64a723c262e21770a4adadbbfbba7f7/README.md#yaml-configuration-reference

output: bindings_type: c_based

Cのソースコードを作成する方法です。

こちらの設定を有効にする場合、CMakeの設定をプロジェクトに追加する必要があります。
/android/build.gradleの中でndkのバージョンを設定したり、cmakeのpathを設定する箇所をexampleを参考にしながら追加する必要があります。そして、CMakeの設定には/srcディレクトリの中身が必要となります。
Androidのプロジェクトで必要な/src内のファイルは、下記のとおりです。

  • .clang-format
    • ライセンスを確認しつつ、exampleからそのままコピー
  • CMakeLists.txt
    • プロジェクトに合わせて、pathや生成するファイル名を変更
  • dartjni.h
    • ライセンスを確認しつつ、exampleからそのままコピー

また、これらの設定と整合性を保つよう、output: c:output: dart:のpath設定を行う必要がります。文章で見ると大変そうですが、v0.3.0の現在では、exampleを参考に記述をするとそこまで大変ではありません。(ガイドがないと大変ですが……)

output: bindings_type: dart_only

Cのソースコードを作成しない方法です。
こちらの設定とした場合、前述のCMake/srcディレクトリの作成が不要になります。生成しないことによるトレードオフについては、公式ドキュメントを参照してください。

https://github.com/dart-lang/jnigen/blob/e8b5d881d64a723c262e21770a4adadbbfbba7f7/README.md#pure-dart-bindings


個人的な見解としては、v0.3.0の状態で遊ぶのであれば、dart_onlyの方が設定が少ないのでいいのではと思います。もちろん、これを本番環境に導入する場合には、慎重な検討が必要です。

Java/Kotlin APIの呼び出し

基本的にはclasses:で呼び出したいAPIのあるクラスパスの指定を行い、変換がうまくいかないメソッドやフィールドをexclude: methodsexclude: fields:で除外していくことになります。

Android API

実装例はpedometerを見てください。

https://github.com/flutter/samples/tree/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/experimental/pedometer

この時、classes.jarを参照していることに気づくと思われます。これはどうやって生成したか記述されていないため、推測するしかありません。(後ほどドキュメントが拡充されることを祈っています。)
ファイル名からは、Health Connect APIを利用できるように、GoogleのMaven Repositoryから落としてきたライブラリファイルかなと思われます。maven


詳細は下記の箇所にコメントされているのですが、Android APIの取得周りはまだまだ検討中の項目になっているようです。

https://github.com/dart-lang/jnigen/blob/e8b5d881d64a723c262e21770a4adadbbfbba7f7/README.md#android-core-libraries

基本的にはandroid_sdk_configにexample dirを指定しておけば良さそうです。

Java/Kotlin libs API

fox_httpではsquare社のOkHttpを参照しています。AndroidアプリをJava/Kotlinで書いたことのある方なら、説明は不要だと思われます。

https://github.com/square/okhttp

基本的には先述の話と同じように、必要なjarファイルをmaven repositoryなどから取得することになります。fox_httpでは、/jarに必要なファイルをまとめています。
注意点としては、後述のクラス定義を自前でパースしていく都合上、必要が生じた依存関係は全て自分で解決する必要があります。OkHttpでは、一部のクラスをOkioからimportしているため、自前で適切なバージョンのokioを取得します。

最後に、/android/build.gradleに必要なライブラリ、ここではimplementation 'com.squareup.okhttp3:okhttp:4.10.0'を追加することで、準備が完了します。

クラス、メソッドの生成

このあとは、Dartで書きたい処理に応じてclassesにパスを追加し、処理に失敗する箇所をexcludeに追加する作業を繰り返します。利用したいメソッド利用したいメソッドの戻り値の型、そして利用したいメソッドの引数の型を、必要を満たせるまで追加するイメージです。
fox_httpでは、大量のexclude処理を書いています。これはOkHttp v4において「Kotlinのコードにてdeprecatedになったプロパティに対して、-deprecatedというprefixをつけて、Javaのコードにする」という処理が記載されていたところ、Dartにて「メソッドやフィールドは-から始められない」という制限に引っかかることになってしまったためです。

https://github.com/square/okhttp/blob/parent-4.10.0/okhttp/src/main/kotlin/okhttp3/OkHttpClient.kt#L287

また、自動生成されるDartのコードではtypeというプロパティが追加されます。これもJavaやKotlinでtypeプロパティやtype()メソッドが追加されている場合に、衝突することとなります。

https://github.com/square/okhttp/blob/parent-4.10.0/okhttp/src/main/kotlin/okhttp3/MediaType.kt#L34

生成されたDartのコードを眺めて、エラーが起きている箇所を1つ1つチェックしていくことが重要になります。

Jクラスの扱い

DartとJava/Kotlinの間で処理を行う場合、生成したインスタンスのdelete()が必要になります。
Javaの世界で取得したインスタンスを、破棄が可能であるとコード上で処理する、ためだと筆者は理解しています。

https://github.com/dart-lang/jnigen/issues/131

なおリネームの議論がされているため、今後のバージョンアップでは別名になる可能性もあります。


もう一つ、注意しておきたいのがJavaにおけるintStringの違いです。
Javaではintはプリミティブ型ですが、Stringはプリミティブ型ではありません。このため、intが引数や戻り値になっている場合と、Stringが引数や戻り値になっている場合では、処理が異なってきます。

https://github.com/dart-lang/jnigen/blob/e8b5d881d64a723c262e21770a4adadbbfbba7f7/jni/lib/src/jprimitives.dart

https://github.com/dart-lang/jnigen/blob/e8b5d881d64a723c262e21770a4adadbbfbba7f7/jni/lib/src/jstring.dart

実装は、上記2クラスをざっと見比べてみれば把握できます。
なお、JStringクラスにはtoDartStringメソッドが用意されており、引数でbool deleteOriginalを設定できるようになっています。Java/KotlinからJStringを取得し、単にDartでStringとして利用したい場合には、このメソッドを利用すれば良さそうです。
(StringクラスにtoJStringメソッドが拡張関数として追加されていたりするので、実際に利用する場合には、そこまでいろいろなことを覚えなくても良さそうです)

遊んでみた感想

手引きがサンプルコードしかなかったため大変でしたが、慣れてきてからはサクサク実装できました。とは言え、proguardルールをうまく同梱できなかったりと、まだまだ実運用には厳しい印象があります。
一方で、JavaやKotinのライブラリ、そしてAndroid SDKのAPIを呼び出すのは非常に簡単です。exampleには「Javaで書いたコードを、Dartから呼び出す」例などもあるので、採用の幅は広そうだなと思います。精度や速度は気になりますが、java.util.UUIDを呼び出せるようになると、色々と捗りそうな気もしてきますし。。


今回OkHttpを導入してみようと思ったのは、cupertino_httpの次の一文を見たためです。

Using the Foundation URL Loading System, rather than the socket-based dart:io HttpClient implemententation, has several advantages:

  1. It automatically supports iOS/macOS platform features such VPNs and HTTP proxies.
  2. It supports many more configuration options such as only allowing access through WiFi and blocking cookies.
  3. It supports more HTTP features such as HTTP/3 and custom redirect handling.

実のところ、Flutterは端末で設定されたproxyの設定に問題を(2023年3月現在でも)問題を抱えています。

https://github.com/dart-lang/http/issues/26

https://github.com/flutter/flutter/issues/26359

system_proxyパッケージを利用することで回避できる問題ではありますが、ネイティブと同じ仕組みで通信を行うライブラリがあると安心感もあるな、と思っています。ただ、ネイティブでHTTP通信を行なってしまうと、dev_tools上で通信処理を閲覧できなくなる、という問題を抱えてしまうのですが……。
このアプローチの手段として、jnijnigenは悪くないのでは、と思いました。今後の発展、並びに安定化をお祈りする次第です。

GitHubで編集を提案

Discussion