RoboVMはGroovyの夢を見るか

はじめに

これはG*アドベントカレンダーの最終日、第25日目の記事です。

巷では「AndroidはGroovyの夢を見るか」という話がありますが、もう一つの雄である、iOSでGroovyは何とかならんのか、というのが今回のネタです。

もう少し具体的に言うと、JavaiOS用アプリを開発するソフトウェアとしてRoboVMがありますが、Groovyで使えないか?を探ります。

結論から言うと、力及ばず、夢は夢のままでした... (¦3[___]
#私の環境だけかもしれませんが...

この記事では、次の環境で動作をさせています。

OS: OSX 10.9.5
Java: 1.8.0_25
Groovy: 2.4.0-rc-1
Gradle: 2.2.1
XCode: 6.1.1
RoboVM: 1.0.0-beta-01

ここからは、私が何をして、何ができて、何ができなかったのか、第一歩でコケた話をお楽しみください。

RoboVMのインストール

RoboVMのインストール自体は簡単です。RoboVMの「Download the latest RoboVM release」からtar+gzファイルをダウンロードし、適当なディレクトリに展開します。

tar zxvf robovm-1.0.0-beta-01.tar.gz

あとは、展開してできるディレクトリを環境変数ROBOVM_HOMEに、${ROBOVM_HOME}/binを環境変数PATHに、それぞれ設定します。

先人の知恵を参考にする

既に「RoboVMをGroovyで」という先人が何人かいらっしゃるようで、今回はここから一式クローンして試してみます。

git clone https://github.com/msgilligan/GradleGroovyRobot.git

名前からわかるように、Gradleのプロジェクトになっています。build.gradleにちょっと手を入れ、次のように変更しました。

apply plugin: 'groovy'

repositories {
    mavenCentral()
    flatDir { dirs 'lib' }
}

def robovmver = "1.0.0-beta-01"
project.ext.set("robovmver", robovmver)

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.4.0-rc-1'
    compile "org.robovm:robovm-rt:$robovmver"
    compile "org.robovm:robovm-objc:$robovmver"
    compile "org.robovm:robovm-cocoatouch:$robovmver"
}

project.ext.set("robovm_home", "/Users/rhapsody/Tools/RoboVM/robovm-$robovmver")

// main class
String mainClass = "IOSDemo"
def groovyJar = ""

task findGroovyJar << {
    groovyJar = project.configurations.compile.find { it.name.startsWith("groovy-all-") }
}

// compile bytecode to llvm and run in the ios simulator
task run (dependsOn: [build, findGroovyJar]){

    doFirst {
        println(">> Running RoboVM")
        String cmd = "$project.robovm_home/bin/robovm -verbose -arch x86 -os ios -cp $project.robovm_home/lib/robovm-objc.jar:$project.robovm_home/lib/robovm-cocoatouch.jar:$projectDir/build/classes/main/:$groovyJar -run $mainClass"
        def proc = cmd.execute()

        proc.in.eachLine {line -> println line}
        proc.err.eachLine {line -> System.err.println( 'ERROR: ' + line)}
        proc.waitFor()
    }
}

サンプルのコード

サンプルのコードですが、クローンしたプロジェクトに含まれていますが、ここに載っているJavaのコードをGroovyで書きなおしてみました。ポイントは、@CompileStaticを使うことです。

import org.robovm.apple.coregraphics.CGRect
import org.robovm.apple.foundation.NSAutoreleasePool
import org.robovm.apple.uikit.UIApplication
import org.robovm.apple.uikit.UIApplicationDelegateAdapter
import org.robovm.apple.uikit.UIApplicationLaunchOptions
import org.robovm.apple.uikit.UIButton
import org.robovm.apple.uikit.UIButtonType
import org.robovm.apple.uikit.UIColor
import org.robovm.apple.uikit.UIControl
import org.robovm.apple.uikit.UIControlState
import org.robovm.apple.uikit.UIEvent
import org.robovm.apple.uikit.UIScreen
import org.robovm.apple.uikit.UIWindow
import groovy.transform.CompileStatic

@CompileStatic
class IOSDemo extends UIApplicationDelegateAdapter {

    UIWindow window = null
    int clickCount = 0

    @Override
    boolean didFinishLaunching(UIApplication application,
            UIApplicationLaunchOptions launchOptions) {

        final UIButton button = UIButton.create(UIButtonType.RoundedRect)
        button.frame = new CGRect(115.0f, 121.0f, 91.0f, 37.0f)
        button.setTitle('Click me!', UIControlState.Normal)

/*
        button.addOnTouchUpInsideListener(new UIControl.OnTouchUpInsideListener() {
            @Override
            public void onTouchUpInside(UIControl control, UIEvent event) {
                button.setTitle('Click #' + (++clickCount), UIControlState.Normal)
            }
        })
*/

        window = new UIWindow(UIScreen.mainScreen.bounds)
        window.backgroundColor = UIColor.lightGray()
        window.addSubview(button)
        window.makeKeyAndVisible()

        true
    }

    static void main(String[] args) {
        NSAutoreleasePool pool = new NSAutoreleasePool()
        UIApplication.main(args, null, IOSDemo)
        pool.close()
    }
}

これで実行の準備が整いました。では実行してみましょう。

gradle run

初回はバイトコードをネイティブなコードに変換しているようで時間がかかり、また私の環境だけかもしれませんが、エラーでiOSシミュレータが起動しなかったりする場合があります。そのような場合は、再度実行すると、次のようにiOSシミュレータが起動し、アプリが実行されます。

f:id:touchez_du_bois:20141226021149p:plain

ここまではできるのですが、ここから先には進めませんでした...orz

サンプルのコード、その2

先程のサンプルコードですが、コメントアウトしている箇所があります。

        button.addOnTouchUpInsideListener(new UIControl.OnTouchUpInsideListener() {
            @Override
            public void onTouchUpInside(UIControl control, UIEvent event) {
                button.setTitle('Click #' + (++clickCount), UIControlState.Normal)
            }
        })

ボタンにリスナーを登録して、ボタンがクリックされた場合に、ボタンのタイトルを再設定することをしています。

では、このコメントアウトしている箇所を有効にして実行してみましょう。iOSシミュレータが起動しますが、先程のようにアプリは実行されず、次のようなメッセージが出力されてしまいます。
#ちなみに、クローンしたプロジェクトのサンプルコードも基本的に同じで、リスナーを登録するコードはありませんでした。

[WARN] android.System: A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks.

[WARN] android.System: java.lang.Throwable: Explicit termination method 'close' not called
    at dalvik.system.CloseGuard.open(CloseGuard.java)
    at java.io.RandomAccessFile.<init>(RandomAccessFile.java)
    at java.io.RandomAccessFile.<init>(RandomAccessFile.java)
    at java.util.zip.ZipFile.<init>(ZipFile.java)
    at java.util.zip.ZipFile.<init>(ZipFile.java)
    at java.lang.PathClassLoader.init(PathClassLoader.java)
    at java.lang.PathClassLoader.findResource(PathClassLoader.java)
    at java.lang.ClassLoader.getResource(ClassLoader.java)
    at java.lang.ClassLoader.getResourceAsStream(ClassLoader.java)
    at org.codehaus.groovy.reflection.GeneratedMetaMethod$DgmMethodRecord.loadDgmInfo(GeneratedMetaMethod.java)
    at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.registerMethods(MetaClassRegistryImpl.java)
    at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java)
    at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java)
    at groovy.lang.GroovySystem.<clinit>(GroovySystem.java)
    at org.codehaus.groovy.runtime.InvokerHelper.<clinit>(InvokerHelper.java)
    at groovy.lang.GroovyObjectSupport.<init>(GroovyObjectSupport.java)
    at groovy.lang.Reference.<init>(Reference.java)
    at IOSDemo.didFinishLaunching(IOSDemo.groovy)
    at org.robovm.apple.uikit.UIApplicationDelegate$ObjCProxy.$cb$application$didFinishLaunchingWithOptions$(Unknown Source)
    at org.robovm.apple.uikit.UIApplication.main(Native Method)
    at org.robovm.apple.uikit.UIApplication.main(UIApplication.java)
    at IOSDemo.main(IOSDemo.groovy)


[WARN] java.lang.Class: Class.forName() failed to load 'java.lang.ClassValue'. Use the -forcelinkclasses command line option or add <forceLinkClasses><pattern>java.lang.ClassValue</pattern></forceLinkClasses> to your robovm.xml file to link it in.

Loading class 'java.util.logging.ConsoleHandler' failed
java.lang.ClassNotFoundException: java.util.logging.ConsoleHandler

java.lang.ExceptionInInitializerError
    at org.codehaus.groovy.runtime.InvokerHelper.<clinit>(InvokerHelper.java)
    at groovy.lang.GroovyObjectSupport.<init>(GroovyObjectSupport.java)
    at groovy.lang.Reference.<init>(Reference.java)
    at IOSDemo.didFinishLaunching(IOSDemo.groovy)
    at org.robovm.apple.uikit.UIApplicationDelegate$ObjCProxy.$cb$application$didFinishLaunchingWithOptions$(Unknown Source)
    at org.robovm.apple.uikit.UIApplication.main(Native Method)
    at org.robovm.apple.uikit.UIApplication.main(UIApplication.java)
    at IOSDemo.main(IOSDemo.groovy)
Caused by: java.lang.NullPointerException
    at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java)
    at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java)
    at groovy.lang.GroovySystem.<clinit>(GroovySystem.java)
    ... 8 more


BUILD SUCCESSFUL

Total time: 34.537 secs

リスナーが登録できなければ、先に進めません...

時間もないので、今回はここまでです...

最後に

今後、やる気があれば、できたバイトコードデコンパイルしたり、Groovyのソースを追っかけてみるつもりです。

Android用のコードが入っているので、同じようにRoboVM用のコードを追加してなんとかなるといいのですが。