マルチ OS エンジンを使用した固定記憶域の操作 (テクノロジー・プレビュー) – パート 2

この記事は、インテル® デベロッパー・ゾーンに公開されている「Working with persistent storage using Multi-OS Engine (Technology Preview) – Part 2」 (https://software.intel.com/en-us/articles/working-with-persistent-storage-part-2) の日本語参考訳です。


この記事の PDF 版はこちらからご利用になれます。

この記事は、固定記憶域の操作に関するチュートリアルのパート 2 です。ここでは、sqlite.h ファイルのバインディングを生成することで、iOS* および Android* で MOE を用いて SQLite ライブラリーを利用し、sqlite.h ファイルにアクセスする方法を示します。このチュートリアルのパート 1 はこちらからご覧になれます。ここでは、パート 1 で使用したアプリケーションを基に作業します。sqlite Web サイトから sqlite3.h をダウンロードして、共通ライブラリーのディレクトリーに配置します。

C ヘッダーファイルから Java* のバインディングを生成するには、[MOE Actions] > [Generate Bindings] メニューを選択します。

MOE により、sqlite に関連するすべての NatJ バインディングを含む新しいディレクトリーが作成されます。このバインディングを使用してデータベース・インスタンスを作成し、両プラットフォームに共通の CRUD 操作を実行します。

生成されたクラスファイルにアノテーション @Library(“sqlite3”) を追加します@Library アノテーションは、マークしたクラスが動作するためにロードする必要があるネイティブ・ライブラリーの名前を指定します。NatJ.register() が呼び出されると、NatJ は呼び出し元のクラスでこのアノテーションを検索し、NatJ.lookUpLibrary(…) で指定されたライブラリーのロードを試みます。

@Library("sqlite3")
@Runtime(CRuntime.class)
public final class Globals {
    static {
        NatJ.register();
    }

ネイティブ C ポインターにアクセスするため、MOE の NatJ バインディングも必要です。依存項目として、natj-api.jar ライブラリーを共通ライブラリー・フォルダーに追加します。この jar は github プロジェクトにあり、アプリケーションのプロジェクトに直接インポートすることができます。

次に、データベースのデータモデルとなる Note クラスを作成します。

public class Note implements Comparable<Note>{
    private Integer id;
    private String note;

    public void setId(int i){
        id =i;
    }

    public Integer getId(){
        return id;
    }

    public void setNote(String s){
        note = s;
    }
    public String getNote(){
        return note;
    }

    @Override
    public int compareTo(Note o) {
        return this.id.compareTo(o.id);
    }

}

データベースの作成・管理とノートの保存を行うための共通コードを作成します。

データベース・インスタンスのオープンとクローズには SQLiteDatabaseHelper.java を使用します。コンストラクターに、プラットフォーム固有のデータベース・ファイルのディレクトリー・パスが渡されます。onCreate メソッドは、ノートテーブルが存在しない場合、テーブルを新規作成します。

SQLiteDatabase.java クラスは、挿入、削除、更新、および選択クエリーを処理します。このクラスのメソッドは、ネイティブ sqlite 操作を抽象化します。例えば、以下は挿入クエリーの実装です。

@Override
    public void insert(String note) {
        StringBuilder sql = new StringBuilder();
        sql.append("INSERT");
        sql.append(" INTO ");
        sql.append(TABLE);
        sql.append(" (note) values (");
        sql.append("\"");
        sql.append(note);
        sql.append("\")");
        execSQL(sql.toString());
    }

execSQL メソッドは、SQL クエリーを実行する SQL ステートメントを準備します。

@Override
    public void execSQL(String statement) {
        SQLiteStatement stmt = new SQLiteStatement(statement, null);
        if (stmt.prepare(dbHandle)) {
            if (!stmt.exec()) {
                System.err.println("Error executing - " + stmt.getLastError());
            }
        } else {
            System.err.println("Error executing - " + stmt.getLastError());
            System.err.println("\tin: " + stmt.getStatement());
        }
    }

SQLiteStatement.java は、prepare()、exec()、query()、strep()、close() のような sqlite ライブラリーのネイティブルーチンを処理します。このクラスは、バインディングによって生成されたネイティブ C API 呼び出しを使用します。以下は、prepare() メソッドと exec() メソッドの実装例です。

public boolean prepare(VoidPtr dbHandle) {
    if (dbHandle == null) {
        throw new NullPointerException();
    }
    this.dbHandle = dbHandle;

    @SuppressWarnings("unchecked")
    Ptr<VoidPtr> stmtRef = (Ptr<VoidPtr>) PtrFactory.newPointerPtr(
        Void.class, 2, 1, true, false);
    int err = Globals.sqlite3_prepare_v2(dbHandle, statement, -1, stmtRef,
        null);
    if (err != 0) {
        lastError = Globals.sqlite3_errmsg(dbHandle);
        return false;
    }
    stmtHandle = stmtRef.get();
    int idx = 0;
    for (Object bind : bindArgs) {
        idx++;
        if (bind instanceof String) {
            err = Globals.sqlite3_bind_text(stmtHandle, idx, (String)bind, -1, 
                new Globals.Function_sqlite3_bind_text(){
                    @Override
                    public void call_sqlite3_bind_text(VoidPtr arg0){}
            });
        } else if (bind instanceof Integer) {
            err = Globals.sqlite3_bind_int(stmtHandle, idx,  (Integer) bind);
        } else if (bind instanceof Long) {
            err = Globals.sqlite3_bind_int64(stmtHandle, idx,  (Long) bind);
        } else if (bind instanceof Double) {
            err = Globals.sqlite3_bind_double(stmtHandle, idx,  (Double) bind);
        } else if (bind == null) {
            err = Globals.sqlite3_bind_null(stmtHandle, idx);
        } else {
            lastError = "No implemented SQLite3 bind function found for " + 
                bind.getClass().getName();
            return false;
        }
        if (err != 0) {
            lastError = Globals.sqlite3_errmsg(dbHandle);
            return false;
        }
    }
    return true;
}
public boolean exec() {
    if (stmtHandle == null) {
        throw new RuntimeException("statement handle is closed");
    }
    //LOG.debug("Execing " + statement);
    int err = Globals.sqlite3_step(stmtHandle);
    if (err == 101 /* SQLITE_DONE */) {
        affectedCount = Globals.sqlite3_changes(dbHandle);
        lastInsertedID = Globals.sqlite3_last_insert_rowid(dbHandle);
    }
    close();
    if (err != 101 /* SQLITE_DONE */) {
        lastError = Globals.sqlite3_errmsg(dbHandle);
        return false;
    }
    return true;
}

上記のコード例を基に、sqlite ステートメント・クラスのさまざまなメソッドを実装できます。

SQLiteCursor.java クラスは、ステートメント・クラスのデータ抽出 API を処理します。以下のメソッドは、実行したクエリーから整数値と文字列を取得します。

@Override
    public String getString(int i) {
        if (stmt == null) {
            throw new RuntimeException("statement is closed");
        }
        return Globals.sqlite3_column_text(stmt.getStmtHandle(), i);
    }

@Override
    public int getInt(int i) {
        if (stmt == null) {
            throw new RuntimeException("statement is closed");
        }
        return Globals.sqlite3_column_int(stmt.getStmtHandle(), i);
    }

Android* には専用の SQLite バージョンがあり、その実装はネイティブバージョンとわずかに異なるため、両プラットフォーム間でのコードの再利用が制限されます。そのため、ここでは NatJ バインディングを使用して、Android* 用にカスタムバージョンの sqlite を作成します。Android* が SQLite のネイティブ C 呼び出しを処理できるように、ネイティブ SQLite のダイナミック共有オブジェクト・ライブラリーを作成して、ランタイムにリンクします。sqlite.so のほかにも、libnatj.so と libc++_shared.so の 2 つのライブラリーをプロジェクトにリンクします。libnatj.so は、MOE の NatJ 呼び出しを使用するための NatJ ブリッジです。libc++_shared.so は、Android* 4.x をサポートするためのものです。Android* 5.x では不要です。

.so ネイティブ・ライブラリーを使用できるように、build.gradle ファイルを変更する必要があります。

最初に、sourceSets で jnilibs に src ディレクトリーを指定します。

sourceSets {
    main {
        manifest.srcFile 'src/main/AndroidManifest.xml'
        java.srcDir 'src'
        res.srcDir 'res'
        assets.srcDir 'assets'

        jniLibs.srcDir 'src/main/native-libs'
        jni.srcDirs = []    // ndk-build の自動呼び出しを無効にする
    }
}

次に、ネイティブ・ライブラリーの jar ファイルを作成します。

task nativeLibsToJar(type: Zip, description: 'create a jar archive of the native libs') {
    destinationDir file("$buildDir/native-libs")
    baseName 'native-libs'
    extension 'jar'
    from fileTree(dir: 'src/main/native-libs', include: '**/*.so')
    into 'lib/'
}

そして、jar ファイルをビルドします。

tasks.withType(org.gradle.api.tasks.compile.JavaCompile) {
    compileTask -> compileTask.dependsOn nativeLibsToJar
}

clean.dependsOn 'cleanCopyNativeLibs'

tasks.withType(com.android.build.gradle.tasks.PackageApplication) {
    pkgTask ->
        pkgTask.jniFolders = new HashSet()
        pkgTask.jniFolders.add(new File(buildDir, 'native-libs'))
}

iOS* はネイティブバージョンの sqlite でビルドされているため、ライブラリーをリンクせずに直接使用できます。

次に、iOS* と Android* 向けに、データベース呼び出しのためのプラットフォーム固有のコードを作成します。

iOS* 固有コード

SQLiteDatabaseHelper 共通クラスを拡張する SQLiteDatabaseHelper クラスを作成します。iOS* 固有のドキュメント・ディレクトリーのパスを取得するため、getDocumentsPath() メソッドのみオーバーライドします。

@Override
    protected String getDocumentsPath() {
        NSArray paths = Foundation.NSSearchPathForDirectoriesInDomains(
                NSSearchPathDirectory.DocumentDirectory,
                NSSearchPathDomainMask.UserDomainMask, true);
        return (String) paths.firstObject();
    }

MasterViewController の viewDidLoad() メソッドで、コンストラクターにデータベースの filepath を渡して、データベースヘルパー・インスタンスを作成します。データベース・インスタンスを作成し、getWritableDatabase() メソッドでその参照を取得します。

    @Override
    @Selector("viewDidLoad")
    public void viewDidLoad() {
        // ビューをロード (通常は nib から) した後、追加のセットアップを行う
        navigationItem().setLeftBarButtonItem(editButtonItem());
        ISQLiteDatabaseHelper helper = new SQLiteDatabaseHelper(dbFileName);
        db = helper.getWritableDatabase();
}

追加ボタンにリスナーメソッドを追加し、データベースへの挿入クエリーを実行します。

@Selector("insertNewObject:")
    public void insertNewObject(Object sender){

        db.insert(defaultText);
        makeObjects();
        NSIndexPath indexPath = NSIndexPath.indexPathForRowInSection(0,0);
        tableView().insertRowsAtIndexPathsWithRowAnimation((NSArray) NSArray.arrayWithObject(indexPath), UITableViewRowAnimation.Automatic);
        performSegueWithIdentifierSender("showDetail", this);
    }

DetailViewController で、更新 API を呼び出してデフォルトのテキストへの変更を更新します。テキストが空白の場合はノートを削除します。

@Selector("doSaveNote:")
    public void doSaveNote( Object sender){
        if (detailNote == null || db == null) {
            return;
        }
        if(!dataText.text().equals("")){
            detailNote.setNote(dataText.text());
            db.update(detailNote);
        }else{
            db.delete(detailNote.getId());
        }

    }

Android* 固有コード

iOS* と同様に、AndroidDatabaseHelper クラスを作成し、Android* の sqlite ファイルのディレクトリー・パスを渡すように getDocumentsPath() をオーバーライドします。

@Override
    protected String getDocumentsPath() {
        return Environment.getExternalStorageDirectory().getPath();
    }

iOS* と同様に、データベースヘルパー・インスタンスとデータベース・インスタンスを作成します。つまり、iOS* の viewDidLoad() に相当する MainActivity.java の onCreate() を作成します。

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SQLiteDatabaseHelper dbHelper = new AndroidSQLiteDatabaseHelper(
            AndroidSQLiteDatabaseHelper.DB_NAME);
        db = dbHelper.getWritableDatabase();
...
}

追加ボタンにリスナーメソッドを追加し、データベースへの挿入を実行します。

public void insertNewObject() {
    db.insert(DEFAULT_TEXT);
    makeObjects();
}

ユーザー入力に応じて、EditorActivity.java は更新/削除クエリーを呼び出します。

private void finishEditing() {
    String newText = editor.getText().toString().trim();
    if (newText.equals("")){
        newText = DEFAULT_TEXT;
        db.delete(noteId);
    } else if (!newText.equals(note)){
        noteDetail.setNote(newText);
        db.update(noteDetail);
    }
    finish();
}

アプリケーションを実行すると、プラットフォームでデータベース・ファイルが作成されます。

このように、両プラットフォームで同じ sqlite 実装を利用して、コードのロジックを再利用できます。これにより、コーディングの労力を大幅に軽減でき、コードの保守が容易になります。

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

関連記事