urushi blog

プログラマでゲーマーのうるしが綴るメモ

Java のコードを Objective-C に? J2ObjC を試してみた。

前回の記事に引き続き、クロスコンパイル関連の記事になります。前回の記事では Embeddinator-4000 を紹介しました。

結論

今回もまずは結論から記載すると、今回の J2ObjCギリギリ使えそうな感じでした。自分の目的であるクロスプラットフォームで、同一のコードのネットワークライブラリを作るという目的をなんとか達成できるポテンシャルがあると思っています。

動機

前回の Embeddinator-4000iOS 向けの Objective-C コード生成においてジェネリクスが用いることができないという、非常に大きな欠点を抱えており、解決にはまだ時間がかかりそう (本当に解決するかも不明) なので、クロスプラットフォーム向けのコード生成機能が他にもないかを探していたところ、見つけたのがこの J2ObjC でした。

J2ObjC とは?

ざっくり言えば、Java のコードを Objective-C に変換するツールです。Embeddinator-4000 はざっくり言えば、ランタイムを乗っけてその上で動かしてしまえ、というちょっと雑な手法でしたが、J2ObjC は真面目に Java のコードをガッツリ Objective-C に変換します。言語仕様レベルの変換も非常に難しい問題ですが、一番面倒かつ重要になってくるのが、各種標準ライブラリの存在になります。 Java には非常に多くの標準ライブラリが備わっており、ネットワーク周りから計算ライブラリまで一通りのものは揃っています。 J2ObjC はそこが非常に大きな問題になっています。 J2ObjC では JavaJRE における標準ライブラリの Objective-C 実装を非常に広範囲で提供しており (その提供自体にも J2ObjC が用いられている箇所も少なくない) それを用いることで標準ライブラリを用いたコードも Objective-C に変換することが可能です。

開発者

J2ObjC の開発元は Google です。GoogleJavaPython の文化だと思うので、そこでその資源を活かして iOS macOS アプリを記述したかったのだと思います。 Objective-C に変換されたコード片にライブラリを付け足していく形でアプリを実現しているのだと思います。そのためか、コードの質も高くメンテも高頻度で行われています。

使用方法

J2ObjC を用いてざっくりコンパイルするのであれば、その公式サイトを参照してください。共有ライブラリ等を作成するのであれば、J2ObjCでJavaとSwiftを連携させる あたりの記事が詳しいです。

自分は Twitter4J を使ったライブラリを書きたいので、その依存関係を一緒に Objective-C に変換してくれるサポートがほしかったので、Gradle Plugin を用いたライブラリプロジェクトを作成しました。(Gradle Plugin は何故か開発が止まっているのですが、Maven Plugin よりは断然使いやすそうだったのでこちらを採用しました…)

使用感

ライブラリで使用しているライブラリも合わせて Objective-C に変換するためには、Maven レポジトリからソースコードを拾ってくる必要があります。基本的に Gradle Plugin がなんとかしてくれるので、特に意識する必要はないですが、そのためソースコードが提供されていないライブラリは使用できません。(Kotlin で書かれたコードも当然変換できない。(class に落としてデコンパイルとかすればいけるのかもしれないけど底までしたくない)) 自分の書いたコードはあっさり Objective-Cコンパイル完了。Twitter4J についてもあっさりコンパイル完了。意外とコンパイルに時間がかかると思っていたので、拍子抜けでした。

落とし穴

さて、Gradle Plugin は cocoapod 用の podspec ファイルを吐いてくれるので、それを参照すれば iOS のプロジェクトでも簡単に依存関係に追加することができます。(吐かれる podspec ファイルには色々足りないので、最新の cocoapod で用いるためには修正が必須) と思って追加してビルドしたらリンカエラーになってしまい、調べてみたらリンカフラグが足りなかった。(podspec の情報が足りない -> Gradle Plugin がメンテされていないことの影響)

そしていざ、iOSTwitter4J を実行してみたら、ClassNotFound のエラーで落ちた。調べてみると、Twitter4J にはあるクラスが何故か J2ObjCObjective-C に変換されていなかった。J2ObjC はオンデマンドにクラスを Objective-C に変換するため、どのクラスからも import されていないクラスは Objective-C に変換されない。そのため Class.forName()Java 内で動的にロードされたクラスについては、変換対象にならなかったので ClassNotFound になっていました。そのため、Class.forName() で動的ロードしていた部分を普通にインスタンスを生成するように変更しました。

その後に立ちはだかったのは、HmacSHA1 が使えないという問題。確かに変換後の Objective-CSecurity.getProviders() で取得した内容から、HmacSHA1 が使えないことは分かったんですが、じゃあ何故 JVM で提供されているものが使えないのか?という問題があるのですが、どうやら US の輸出規制の問題だとかで提供されていないようでした。ので、OpenJDK から HmacCore.java を移植し、ついでに MessageDigit.java のバグで、複数回ハッシュを生成した時に状態を持ち越してしまう問題 (MessageDigitIssue) があり、正しく HmacSHA1 が生成できない問題があったので、SHA1 を生成する毎に手動で状態をリセットするように変更。して、やっと Twitter にリクエストすることに成功しました。

そして最後に立ちはだかったのが、Accept-Encoding: gzip でリクエストしているのに、何故かレスポンスが Objective-C に変換後のコードだと gzip ではないとエラーを吐いてしまう事。これは帰ってきたライブラリを見てわかったのですが、Objective-C に変換後は HTTP リクエストを飛ばす際に、勝手に Accept-Encoding: gzip で勝手に送信して、レスポンスも勝手に gzip を紐解いてしまうという、ライブラリ間の挙動の差でした。ので、始めの2バイトを確認して、gzip であるかを判断して、その後の処理を分岐するように変更。そしてやっとレスポンスを得ることに成功しました。

依存関係がほぼない Twitter4J でもこれぐらいかかったので、他に追加でもしようものならもっと大変だろうと思います。ですがなんとか成功したので、もう少し頑張って iOS Android 共用ライブラリの夢をみようと思います。ついでに、 Twitter4J をフォークして J2ObjC で動作可能にしたものは uakihir0Twitter4J です。(Hmac の対応はここレポジトリにはありません) JitPack 等で使ってください。

結論

J2ObjC はモデルの共通化とか、外部ライブラリに依存しないコードなら検討の価値あり。外部ライブラリを頑張ろうとすると、そもそもソースコードがなかったりと地獄を見るが、なんとかぎりぎり頑張れる。