jnigenであそぼう
Flutter 3.7のブログにて、jni
とjnigen
が開発中であることが明かされました。
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.
面白そうだったので、色々と試行錯誤して動かしてみました。この記事では、簡単にjni
とjnigen
の動かし方を紹介し、遊んでみる人を増やしたいと思っています。
注意
筆者が遊んでいるのは、下記のバージョンになります。
アップデートによって遊び方が更新されると思われますので、必ず最新の情報を確認するようにしてください。
- jni:
0.3.0
- jnigen:
0.3.0
- flutter:
3.7.7
- Dart:
2.19.4
jni/jnigenとは
より正確な資料に当たりたい場合は、公式ドキュメントを確認して、次のブロックまで文章をスキップしてください🙏
JNI、つまりJava Native InterfaceはJavaからネイティブなライブラリ(例えばCやC++で記述されたライブラリ)を呼び出すための仕組みの呼び名です。
手頃なドキュメントとしては、Oracleの「Java Native Interface仕様の目次」が適切かなと思います。
Androidアプリケーションの開発においても、JNIを利用することで、CやC++で記述されたライブラリを呼び出すことができます。
Androidのアプリケーション開発として「jni」を検索すると、こちらの.so
ファイルをリンクする方法がよく引っかかります。
Dartの文脈では、「JavaやKotlinのAPIを呼び出すためにpackage:jni
やpackage:jnigen
が利用できる」としています。
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から呼び出すためのコードを生成する」ライブラリとされています。
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が存在します。
2023年3月現在では、「どちらを使うべき」かと言えば「Method Channelを使うべき」です。というのも、jnigen
はまだstableになっている機能ではない、ためです。
こちらのコメントの通り、jni
とjnigen
はよりJavaのコードを簡単に呼び出せるという利点があります。実際にOkHttpをbindするコードを書いてみた感想としても、こちらの意見には納得感があります。
jniとjnigenで遊ぶ
リポジトリはこちらです。色々と遊んでいるので、補足を入れながら遊び方を紹介します。
セットアップ
リポジトリの作成には、flutterのpluginテンプレートを利用します。
このため、新規にプロジェクトを作成する場合には、次のコマンドでプロジェクトを作成してください。今回は、fox_http
というプロジェクト名で、android向けのみの実装としています。
flutter create --template plugin --platforms android fox_http
続いて、pubspec.yaml
にjni
とjnigen
を追加します。jni
がdependencies
、jnigen
がdev_dependencies
です。バージョンについては、ここでは^0.3.0
としておきます。
jnigen.yaml
jniによってAndroid向けの実装をする場合、さまざまな設定を行うファイルがjnigen.yaml
になります。
ひとまず、公式のサンプルからコードをコピーしてきましょう。
なお、それぞれのパラメーターは次のとおりです。必要に応じて、修正を加えていきます。
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
ディレクトリの作成が不要になります。生成しないことによるトレードオフについては、公式ドキュメントを参照してください。
個人的な見解としては、v0.3.0の状態で遊ぶのであれば、dart_only
の方が設定が少ないのでいいのではと思います。もちろん、これを本番環境に導入する場合には、慎重な検討が必要です。
Java/Kotlin APIの呼び出し
基本的にはclasses:
で呼び出したいAPIのあるクラスパスの指定を行い、変換がうまくいかないメソッドやフィールドをexclude: methods
やexclude: fields:
で除外していくことになります。
Android API
実装例はpedometerを見てください。
この時、classes.jar
を参照していることに気づくと思われます。これはどうやって生成したか記述されていないため、推測するしかありません。(後ほどドキュメントが拡充されることを祈っています。)
ファイル名からは、Health Connect APIを利用できるように、GoogleのMaven Repositoryから落としてきたライブラリファイルかなと思われます。maven
詳細は下記の箇所にコメントされているのですが、Android APIの取得周りはまだまだ検討中の項目になっているようです。
基本的にはandroid_sdk_config
にexample dirを指定しておけば良さそうです。
Java/Kotlin libs API
fox_httpではsquare社のOkHttpを参照しています。AndroidアプリをJava/Kotlinで書いたことのある方なら、説明は不要だと思われます。
基本的には先述の話と同じように、必要な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にて「メソッドやフィールドは-
から始められない」という制限に引っかかることになってしまったためです。
また、自動生成されるDartのコードではtype
というプロパティが追加されます。これもJavaやKotlinでtype
プロパティやtype()
メソッドが追加されている場合に、衝突することとなります。
生成されたDartのコードを眺めて、エラーが起きている箇所を1つ1つチェックしていくことが重要になります。
J
クラスの扱い
DartとJava/Kotlinの間で処理を行う場合、生成したインスタンスのdelete()
が必要になります。
Javaの世界で取得したインスタンスを、破棄が可能であるとコード上で処理する、ためだと筆者は理解しています。
なおリネームの議論がされているため、今後のバージョンアップでは別名になる可能性もあります。
もう1つ、注意しておきたいのがJavaにおけるint
とString
の違いです。
Javaではint
はプリミティブ型ですが、String
はプリミティブ型ではありません。このため、int
が引数や戻り値になっている場合と、String
が引数や戻り値になっている場合では、処理が異なってきます。
実装は、上記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:
- It automatically supports iOS/macOS platform features such VPNs and HTTP proxies.
- It supports many more configuration options such as only allowing access through WiFi and blocking cookies.
- It supports more HTTP features such as HTTP/3 and custom redirect handling.
実のところ、Flutterは端末で設定されたproxyの設定に問題を(2023年3月現在でも)問題を抱えています。
system_proxyパッケージを利用することで回避できる問題ではありますが、ネイティブと同じ仕組みで通信を行うライブラリがあると安心感もあるな、と思っています。ただ、ネイティブでHTTP通信を行なってしまうと、dev_tools上で通信処理を閲覧できなくなる、という問題を抱えてしまうのですが…。
このアプローチの手段として、jni
とjnigen
は悪くないのでは、と思いました。今後の発展、並びに安定化をお祈りする次第です。
Discussion