Scala で Android アプリ開発(NDK 編)

下準備

始めに /path/to/hello-world/project/build.scala を次のように修正する。

// ..snip..

object AndroidBuild extends Build {
  lazy val main = Project (
    "Hello World",
    file("."),
    settings = General.fullAndroidSettings ++
               AndroidNdk.settings // これを追加
  )

  // ..snip..
}

次に /path/to/hello-world/src/main/jni/ を作成する。

$ make -p /path/to/hello-world/src/main/jni/

ラッパークラスを作る

ものは試しにファイルの情報を取得するラッパ /path/to/hello-world/src/main/scala/FileStat.scala を作成する。

package com.github.cooldaemon.HelloWorld

import _root_.java.io.File
import _root_.java.util.{Map => JMap}
import _root_.scala.collection.JavaConversions._

object extendFileStat {
  implicit def fileToFileStat(file: File): FileStat = new FileStat(file)
}

class FileStat(file: File) {
  System.loadLibrary("file_stat")

  @native private[this] def getStat(name: String): JMap[String, Long]
  def stat: Map[String, Long] = mapAsScalaMap(getStat(file.toString)).toMap
}

C 関連のファイルを用意する

.class ファイルを作るため一度コンパイルし、その後、javah でラッパクラスから C ヘッダファイルを作成する。

$ cd /path/to/hello-world
$ sbt
> compile
> exit
$ javah -o ./src/main/jni/file_stat.h -classpath ./target/scala-2.9.1/classes com.github.cooldaemon.HelloWorld.FileStat

sbt 経由で javah を実行する方法は試していない。

Makefile として /path/to/hello-world/src/main/jni/Android.mk を作成する。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := file_stat
LOCAL_SRC_FILES := file_stat.c

#LOCAL_C_INCLUDES += $(SBT_MANAGED_JNI_INCLUDE)

include $(BUILD_SHARED_LIBRARY)

最後にファイル本体 /path/to/hello-world/src/main/jni/file_stat.c を作成する。

#include <sys/stat.h>
#include "file_stat.h"

jobject NewLong(JNIEnv* env, jlong value)
{
    jclass longClass = (*env)->FindClass(env, "java/lang/Long");
    jmethodID init = (*env)->GetMethodID(env, longClass, "<init>", "(J)V");
    jobject longObj = (*env)->NewObject(env, longClass, init, value);
    (*env)->DeleteLocalRef(env, longClass);
    return longObj;
}

JNIEXPORT jobject JNICALL Java_com_github_cooldaemon_HelloWorld_FileStat_getStat
  (JNIEnv *env, jobject obj, jstring name)
{
    jclass mapClass = (*env)->FindClass(env, "java/util/HashMap");
    jmethodID init = (*env)->GetMethodID(env, mapClass, "<init>", "(I)V"); 
    jobject mapObj = (*env)->NewObject(env, mapClass, init, 1);

    jmethodID put = (*env)->GetMethodID(env, mapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

    jboolean iscopy;
    const char *mfile = (*env)->GetStringUTFChars(env, name, &iscopy);    

    struct stat finfo;
    lstat(mfile, &finfo);

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "dev"),
        NewLong(env, finfo.st_dev)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "ino"),
        NewLong(env, finfo.st_ino)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "mode"),
        NewLong(env, finfo.st_mode)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "nlink"),
        NewLong(env, finfo.st_nlink)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "uid"),
        NewLong(env, finfo.st_uid)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "gid"),
        NewLong(env, finfo.st_gid)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "rdev"),
        NewLong(env, finfo.st_rdev)
    );

    (*env)->CallObjectMethod(env, mapObj, put,
        (*env)->NewStringUTF(env, "size"),
        NewLong(env, finfo.st_size)
    );

コンパイルしてみる。

$ sbt
> android:ndk-build

使用する

val f = new File(context.getFilesDir, "foo")
import extendFileStat._
f.stat foreach {case (k, v) => Log.d(k + ":" + v.toString)}