JavaのソースコードからJavaScript / Objective-Cのソースコードを生成する
はじめまして。フロントエンドエンジニアのらくさんです。
弊社は、Flash Lite 1.1のSWFをスマートフォンで再生できるようにする、JswfPlayerというサービスを提供しています。私はそのプレーヤー開発を行っています。
JswfPlayerはWebブラウザ上で動作するため、ネイティブアプリで使用するにはWebView内で動かすことになりますが、せっかくネイティブアプリにするならJswfPlayerもJavaやObjective-Cでコードを書きOpenGLで描画を行い高速に動作させたくなります。
しかし、JavaScript/Java/Objective-Cそれぞれにフルスクラッチで開発するのは大変です。そこで、コードを出来るだけ共有する方法を模索しており、最近GWT-Exporterとj2objcを試してみたので使用方法をまとめてみました。(ちなみに、JswfPlayerのネイティブ版を開発する計画は今のところありません…)
GWT-Exporterとは
GWTはご存知の方も多いと思いますが、Google Web Toolkitの略称で「Javaを使ってウェブ用Ajaxアプリケーションを開発できるオープンソースのJavaソフトウェア開発フレームワーク」(Wikipediaより) です。Javaで書いたソースコードのうち、クライアント側で動作するものについてはJavaScriptに変換されて実行されます。
GWT-Exporterは、GWTでJavaScriptに変換されたコードを手書きのJavaScriptから利用できるようにするためのGWTモジュールです。今回はこのGWT-Exporterを使ってJavaからJavaScriptに変換することだけが目的なので、通常のGWTを用いた開発については触れません。
■GWT-Exporterのプロジェクトサイト
http://code.google.com/p/gwt-exporter/
j2objcとは
j2objcは、JavaのソースコードをObjective-Cに変換するツールで、2012年9月にGoogleからリリースされたばかりのものです。まだ開発途上であり頻繁に更新されているようですが、Google内のいくつかのプロジェクトでは使用されているようです。
■j2objcのプロジェクトサイト
http://code.google.com/p/j2objc/
目次
GWT-Exporterの使い方
GWTのインストール
GWTの開発環境はEclipseのプラグインとして提供されているので、Eclipseが必要です。この記事ではEclipse3.7を使用しています。
GWTのインストール方法は割愛します。
私は次のページを参考にしました。
Eclipse を用いた GWT 開発環境の構築方法 – Google Web Toolkit (GWT) 入門
GWTプロジェクトの作成
Fileメニューから「New」→「Other…」を選び、Googleフォルダ内の「Web Application Project」を選択します。
プロジェクト名とパッケージ名を入力し、「Use Google App Engine」と「Generate project sample」は不要なのでチェックを外します。Finishボタンを押すとGWTプロジェクトが作成されます。
GWT-Exporterのダウンロードとビルドパスの設定
http://code.google.com/p/gwt-exporter/downloads/list
ここから gwtexporter-2.4.0.jar をダウンロードします。
ダウンロードした gwtexporter-2.4.0.jar をプロジェクトに追加し、ビルドパスに設定します。
GWT-Exporterを使用するための設定
Fileメニューから「New」→「Other…」を選び、Google Web Toolkitフォルダ内の「Module」を選択します。
パッケージ名にはプロジェクト作成時に指定したものを入力します。モジュール名は適当な名前を付けます。Inherited modulesにデフォルトで指定されている「com.google.gwt.user.Uesr」を取り除き、「org.timepedia.exporter.Exporter」を追加します。
「モジュール名.gwt.xml」というファイルが作成されるので、それを開き、次の3行を追加します。
1 2 3 4 5 |
[sourcecode language="xml"] <set-property name="export" value="yes"/> <entry-point class="モジュールのパッケージ名.client.適当なクラス名"/> <add-linker name="xs"/> [/sourcecode] |
上記のentry-pointで指定したクラスを、次の内容で作成します。
- EntryPointインターフェースを実装する
- onModuleLoadメソッド内でExporterUtil.exportAll()を呼ぶ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[sourcecode language="java"] package com.sonicmoov.example.gwtx.client; import org.timepedia.exporter.client.ExporterUtil; import com.google.gwt.core.client.EntryPoint; public class ExportAll implements EntryPoint { @Override public void onModuleLoad() { ExporterUtil.exportAll(); } } [/sourcecode] |
Javaのソースコード
JavaScriptから呼び出したいクラスは、Exportableインターフェースを実装している必要があります。さらに、@Exportアノテーションをクラスまたはメソッドに指定する必要があります。
また、JavaScript側でのパッケージ名を@ExportPackageアノテーションで指定することができます。
詳しくは下記をご覧ください。
http://code.google.com/p/gwt-exporter/wiki/GettingStarted
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[sourcecode language="java"] package com.sonicmoov.example.gwtx.client; import org.timepedia.exporter.client.Export; import org.timepedia.exporter.client.ExportPackage; import org.timepedia.exporter.client.Exportable; @ExportPackage("") @Export public class Hello implements Exportable { public String sayHello() { return "HELLO!!"; } } [/sourcecode] |
既存のJavaコードがあり、それに手を入れたくない場合は Exportable の代わりに ExportOverlay を使うこともできます。今回は後述のj2objcを使用する際にこの方が都合が良いので、こちらを採用します。
1 2 3 4 5 6 7 8 9 10 |
[sourcecode language="java" padlinenumbers="2"] package com.sonicmoov.example.gwtx.client; public class Hello { public String sayHello() { return "HELLO!!"; } } [/sourcecode] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[sourcecode language="java"] package com.sonicmoov.example.gwtx.client; import org.timepedia.exporter.client.Export; import org.timepedia.exporter.client.ExportOverlay; import org.timepedia.exporter.client.ExportPackage; @ExportPackage("") @Export public abstract class HelloOverlay implements ExportOverlay<Hello> { public abstract String sayHello(); } [/sourcecode] |
JSNI (JavaScript Native Interface)
Java側からJavaScriptを呼ぶことができます。JNIと同様にnative修飾子を付けたメソッドを宣言し、次のように /*-{
と }-*/
で囲ったコメント内にJavaScriptのコードを記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[sourcecode language="java" highlight="9,10,11"] package com.sonicmoov.example.gwtx.client; public class Hello { public String sayHello() { return "HELLO!!"; } public native void sayHelloNative() /*-{ alert("HELLO Native!!"); }-*/; } [/sourcecode] |
オーバーレイクラスの方も修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[sourcecode highlight="13" language="java"] package com.sonicmoov.example.gwtx.client; import org.timepedia.exporter.client.Export; import org.timepedia.exporter.client.ExportOverlay; import org.timepedia.exporter.client.ExportPackage; @ExportPackage("") @Export public abstract class HelloOverlay implements ExportOverlay<Hello> { public abstract String sayHello(); public abstract void sayHelloNative(); } [/sourcecode] |
コンパイル
Package Explorer内のプロジェクトを右クリックし、「Google」→「GWT Compile」を選択するとダイアログが出てきます。そのままCompileボタンを押してください。
しばらく待つと次のようなファイルが出来上がります。
JavaScriptから呼び出す
コンパイルしてできた「***.nocache.js」というファイルをscriptタグで読み込むと、@Exportアノテーションを指定したクラスやメソッドが使用できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[sourcecode language="html"] <html> <head> <script type="text/javascript" src="war/com.sonicmoov.example.gwtx.export/com.sonicmoov.example.gwtx.export.nocache.js"></script> </head> <script type="text/javascript"> function sayHello() { var hello = new Hello(); alert(hello.sayHello()); } function sayHelloNative() { var hello = new Hello(); hello.sayHelloNative(); } </script> <body> <input type="button" onclick="sayHello()" value="sayHello"> <input type="button" onclick="sayHelloNative()" value="sayHelloNative"> </body> </html> [/sourcecode] |
j2objc
j2objcのビルド
http://code.google.com/p/j2objc/downloads/list
ここからj2objcのソースコードをダウンロードします。バイナリ版もありますが、私の環境では正常に動作しませんでした。
ビルドには次の環境が必要です。
- Xcode 4以上
- Java for OS X
- Apache Maven
次の手順でビルドします。
1 2 3 4 5 6 |
[sourcecode language="text" gutter="false"] $ unzip j2objc-0.5.6-src.zip $ cd j2objc-0.5.6 $ make dist [/sourcecode] |
Javaのソースコード
GWT-Exporterの解説で使用したHelloクラスにmainメソッドを追加します。また、JSNIで記述していたsayHelloNativeメソッドも書き換えます(詳細は後述)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
[sourcecode language="java" highlight="11-13,15-20"] package com.sonicmoov.example.gwtx.client; public class Hello { public String sayHello() { return "HELLO!!"; } public native void sayHelloNative() /*-{ alert("HELLO Native!!"); }-*/ /*-[ NSLog(@"HELLO Native!!"); ]-*/; public static void main(String[] args) { Hello hello = new Hello(); String str = hello.sayHello(); System.out.println(str); hello.sayHelloNative(); } } [/sourcecode] |
j2objcでObjective-Cに変換、j2objccでコンパイル
次のコマンドで上記のHelloクラスをObjective-Cに変換します。
1 2 3 |
[sourcecode language="text" gutter="false"] $ j2objc -d objc src/com/sonicmoov/example/gwtx/client/Hello.java [/sourcecode] |
このコマンドによりobjcディレクトリ内(のcom/sonicmoov/example/gwtx/client)に Hello.h と Hello.m が作成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[sourcecode language="objc"] // // Generated by the J2ObjC translator. DO NOT EDIT! // source: src/com/sonicmoov/example/gwtx/client/Hello.java // // Created by rakusan on 12/12/10. // @class IOSObjectArray; #import "JreEmulation.h" @interface ComSonicmoovExampleGwtxClientHello : NSObject { } - (NSString *)sayHello; - (void)sayHelloNative; @end [/sourcecode] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
[sourcecode language="objc"] // // Generated by the J2ObjC translator. DO NOT EDIT! // source: src/com/sonicmoov/example/gwtx/client/Hello.java // // Created by rakusan on 12/12/10. // #import "IOSObjectArray.h" #import "com/sonicmoov/example/gwtx/client/Hello.h" @implementation ComSonicmoovExampleGwtxClientHello - (NSString *)sayHello { return @"HELLO!!"; } - (void)sayHelloNative { NSLog(@"HELLO Native!!"); } @end int main( int argc, const char *argv[] ) { int exitCode = 0; NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; IOSObjectArray *args = JreEmulationMainArguments(argc, argv); ComSonicmoovExampleGwtxClientHello *hello = [[[ComSonicmoovExampleGwtxClientHello alloc] init] autorelease]; NSString *str = [((ComSonicmoovExampleGwtxClientHello *) NIL_CHK(hello)) sayHello]; NSLog(@"%@", str); [((ComSonicmoovExampleGwtxClientHello *) NIL_CHK(hello)) sayHelloNative]; [pool release]; return exitCode; } [/sourcecode] |
次のコマンドで Hello.h, Hello.m から実行バイナリ hello を生成します。
1 2 3 |
[sourcecode language="text" gutter="false"] $ j2objcc -I objc -o hello objc/com/sonicmoov/example/gwtx/client/Hello.m [/sourcecode] |
出来上がったバイナリを実行します。
1 2 3 4 5 |
[sourcecode language="text" gutter="false"] $ ./hello 2012-12-10 18:25:45.593 hello[25504:60b] HELLO!! 2012-12-10 18:25:45.595 hello[25504:60b] HELLO Native!! [/sourcecode] |
OCNI (Objective-C Native Interface)
さきほど、JSNIで記述していたsayHelloNativeメソッドを次のように書き換えました。
1 2 3 4 5 6 7 |
[sourcecode language="java" highlight="3,4,5"] public native void sayHelloNative() /*-{ alert("HELLO Native!!"); }-*/ /*-[ NSLog(@"HELLO Native!!"); ]-*/; [/sourcecode] |
j2objcは、ネイティブメソッド用のコメントデリミタにGWTのJSNIとは異なる /*-[
と ]-*/
を使用することで、Objective-C、GWT、そしてJava(JNI)で同じネイティブメソッドを共有できるようになっています。
メモリ管理について
JavaからObjective-Cに変換する場合、メモリ管理がどのようになるのか気になるところです。j2objcでは、明示的なリファレンスカウント、ARC、GCの3つから選べるようです。デフォルトは明示的なリファレンスカウントです。
詳細は下記をご覧ください。
http://code.google.com/p/j2objc/wiki/MemoryManagement
まとめ
GWT-Exporterとj2objcの基本的な使い方は以上になります。JavaからJavaScript/Objective-Cに変換できるのは、java.langパッケージとjava.utilパッケージの一部のみのようなので、GWT-Exporterやj2objcがどんなプロジェクトでも使用できるわけではありませんが、ビジネスロジックをJavaで記述しJavaScript/Objective-Cに変換、UI等はそれぞれに書くことで、Webアプリ、Android、iOS間で一部のソースコードを共有できるようになるのではないでしょうか。
今回は非常に単純なコードで試しただけですが、もっと大きく複雑なものでのテストも行いパフォーマンスの計測等も行う予定です。またj2objcはXcodeに統合して使うこともできるようなので、それについても調査し改めて記事にしたいと思います。