C++
SQLite3
cocos2d-x

wxSQLite3.3.1+cocos2d-x v3.16の組み込み手順

概要

自分の備忘のためのメモ投稿です。調べたらwxSQLite3とcocos2d-xの組み込みの記事は色々とあるのですが、自分でビルドした際のことをまとめてます。
(備忘が投稿のメイン目的なので、多少は公開も意識して記載してますが、結構解説とかテキトーです、すみません)

やってること

  • Macで暗号化されたSQLiteファイルを作れるようにインストール
  • Google Spread Sheetでマスタ管理
  • cocos2d-xに組み込む
  • 簡単なSQLiteのORMのラッパークラス作成

動作環境

  • Mac OS X
  • cocos2d-x v3.16
  • wxSQLite v3.3.1

参考

cocos2d-xでwxSqlite3を使ってみた
wxSqlite3で暗号化 sqlite3をセキュアに利用する
ローカルにファイルダウンロード

wxSQLite3のダウンロード

https://sourceforge.net/projects/wxcode/files/Components/wxSQLite3/
2018/4/20現在はv3.3.1が最新のためおとしてみた。

解凍&Macへインストール

パスワード付のsqliteファイルを作れるようにMacにもインストールする。

$ tar -zxvf ./wxsqlite3-3.3.1.tar.gz
$ cd wsqlite3-3.3.1/sqlite3/secure/src/
$ gcc -o sqlite3 -DSQLITE_HAS_CODEC=1 -DSQLITE_USER_AUTHENTICATION=0 shell.c sqlite3secure.c -ldl -lpthread

In file included from sqlite3secure.c:13:
./sqlite3.c:18709:17: warning: 'OSAtomicCompareAndSwapPtrBarrier' is deprecated: first deprecated in macOS 10.12 - Use atomic_compare_exchange_strong() from <stdatomic.h> instead
      [-Wdeprecated-declarations]
      success = OSAtomicCompareAndSwapPtrBarrier(NULL, newzone, 
                ^
/usr/include/libkern/OSAtomicDeprecated.h:547:6: note: 'OSAtomicCompareAndSwapPtrBarrier' has been explicitly marked deprecated here
bool    OSAtomicCompareAndSwapPtrBarrier( void *__oldValue, void *__newValue, void * volatile *__theValue );
        ^
In file included from sqlite3secure.c:58:
./rijndael.c:1127:79: warning: '&' within '|' [-Wbitwise-op-parentheses]
          iv[3][3] = (UINT8) ((iv[3][3] << 1) | (outBuffer[k/8] >> (7-(k&7))) & 1);
                                              ~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~
./rijndael.c:1127:79: note: place parentheses around the '&' expression to silence this warning
          iv[3][3] = (UINT8) ((iv[3][3] << 1) | (outBuffer[k/8] >> (7-(k&7))) & 1);
                                                                              ^
                                                (                                )
./rijndael.c:1281:75: warning: '&' within '|' [-Wbitwise-op-parentheses]
          iv[3][3] = (UINT8) ((iv[3][3] << 1) | (input[k/8] >> (7-(k&7))) & 1);
                                              ~ ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~
./rijndael.c:1281:75: note: place parentheses around the '&' expression to silence this warning
          iv[3][3] = (UINT8) ((iv[3][3] << 1) | (input[k/8] >> (7-(k&7))) & 1);
                                                                          ^
                                                (                            )
3 warnings generated.

Warningでたけど大丈夫かな。とりあえず非推奨ってだけっぽいのでそのままにする。

動作確認

wxSqlite3で暗号化 sqlite3をセキュアに利用する に書いてるのとまったく同じことをやってみる。あと、ついでにSQLファイルも読み込んだりできるのかどうか。

$ pwd
/Users/*****/wxsqlite3-3.3.1/sqlite3/secure/src

$ ./sqlite3  test.db
>PRAGMA KEY='test'; 
>create table test(a,b);
>insert into test(a,b)values(1,2);
>select * from test;
1|2
>.q

$ ./sqlite3 test.db 
>select * from test;
Error: file is encrypted or is not a database
>PRAGMA KEY='test'; # エラーが出たあとでキー設定してみる
>select * from test;
Error: file is encrypted or is not a database # へー、だめなんだ
>.q

$ ./sqlite3 test.db
>PRAGMA KEY='test';
>select * from test;
1|2
>.q

$ cat test.sql 
PRAGMA KEY='test';
insert into test(a,b) values(4,5);
select * from test;

$ ./sqlite3 test.db < test.sql 
1|2
4|5

$ ./sqlite3 test.db
> .read test.sql
1|2
4|5
4|5

# これだとMacにプリインストールされた別のsqlite3が起動するためNGになる
$ sqlite3 test.db < test.sql
Error: near line 2: file is encrypted or is not a database
Error: near line 3: file is encrypted or is not a database

Google Spread Sheetでマスタ管理

Google Spread Sheet でデータ管理して、 Google Apps Scriptを用いてSQLを作成するようなのを作っておくと簡単に管理できそう。
(最後ローカルでDBを作る手間はあるけど)

すごい簡単なスクリプトはこんな感じ。
GASのダウンロードの実装は以下を参考。
http://googleappsscript.hatenablog.com/entry/2017/09/06/120000

Google Apps Scriptのコード例(コード.gs)

// SQLite3のパスワード
function getSQLKey() {
 return "password";
}

// SQLを作成して出力する
function showDialogDownloadSQL() {
   var contentType = "text/plain";
   var charSet = "UTF-8";
   var lineDelimiter = ",";
   var newLineChar = "\n";  
   var fileName = "masterdata.sql";
   var folderId = 'XXXX' // 保存先(https://drive.google.com/drive/folders/XXXXXXX のXXXXXXX部分)
   var folder = DriveApp.getFolderById(folderId);
   var sheet = SpreadsheetApp.getActive().getSheetByName("XXXX");
   var last = sheet.getLastRow();
   // download.html で利用する変数(var宣言しない)
   body  = "PRAGMA KEY='" + getSQLKey() + "';\n"; 
   body += "DROP TABLE IF EXISTS TEST_TABLE;\n"; 
   body += "CREATE TABLE TEST_TABLE (id text primary key , name text, value text);\n"; 
   for(var row = 1; row <= last; row++) {
     if(!sheet.getRange(row, 1).isBlank()) {
       // この例だと1列目〜3列目にデータがあるイメージ
       var rows = sheet.getRange(row, 1 , 1 ,3).getValues();
       var columns = rows[0];
       var id = columns[0];
       var name = columns[1];
       var value = columns[2];
       body += "INSERT INTO TEST_TABLE (id,name,value) VALUES ('" + id + "','" + name + "','" + value + "');\n";
     }
   }

  var blob = Utilities.newBlob("", contentType, fileName).setDataFromString(body, charSet);
  var fileId = DriveApp.getFolderById(folderId).createFile(blob).getId();
  Browser.msgBox("Google Driveに作成しました。");

  url = "https://drive.google.com/uc?export=download&id=" + fileId;

  var output = HtmlService.createTemplateFromFile('download');
  var html = output.evaluate().setHeight(420).setWidth(600);
  var ui = SpreadsheetApp.getUi();

  ui.showModalDialog(html, fileName);
}

// SpleadSheetオープン時にメニューを追加する
function onOpen() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var entries = [{
                   name : "SQLファイル出力",
                   functionName : "showDialogDownloadSQL"
                 }];
  spreadsheet.addMenu("書き出し", entries);
};

Google Apps Scriptのコード例(download.html)

<style type="text/css">
#json {
  height: 320px;
  overflow: scroll;
  border:1px solid #ddd;
  padding:10px;
}
</style>

<pre align="left" id="json">
<?
output.append(body);
?>
</pre>

<div align="center">
<?
output.append("<a href='" +  url + "' target='_blank'>ダウンロード</a>");
?>
</div>

image.png

image.png

image.png

Cocos2d-xに組み込む

他でも使いそうなため、とりあえずはProjectRoot直下に以下のような形でいれる。
そのうち別のGitレポジトリとしてサブモジュール化してもいいかも。

ディレクトリ構成

/ProjectRoot
├/proj.ios_mac
├/proj.android-studio
├(省略)
├/Resources
├/Classes
└/vendor
 └/wxsqlite3-3.3.1
  ├Android.mk (あとで作成)
  └/src
   └/sqlite3
        ├ (もともと wxsqlite3-3.3.1/sqlite3/secure/src に入ってたソース一式)
    ・

image.png

iOS側の設定

Build Settingsの変更

まずは Other C Flags のDebug / Releaseに -DSQLITE_HAS_CODEC を追加する。
wxsqlite3のソースコード上で SQLITE_HAS_CODECの定数の有無をみているため、これしないと暗号化のが使えない(と思う。ちゃんと調べてないけど)

image.png

Header Search Pathssqlite3 のソースを配置したパスを追加する。

image.png

プロジェクトにも、 sqlite3 のソースを配置したフォルダを追加する。
ただし、その際には以下だけを追加し、他は参照からはずす。

image.png

理由は、 sqlite3secure.c内でsqlite3.cをIncludeしてたり必要のないソースがあったりする ためっぽい。あんまりちゃんと調べてないけどとりあえず他はビルド設定からはずしてもちゃんと必要なものだけincludeされてビルドされるので問題なさそう。

Android側の設定

上記に示したディレクトリにAndroid.mkを作成する。

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := wxsqlite3_static
LOCAL_MODULE_FILENAME := libwxsqlite3
LOCAL_CFLAGS += -DSQLITE_HAS_CODEC
LOCAL_SRC_FILES := src/sqlite3/sqlite3secure.c
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/src/sqlite3
LOCAL_C_INCLUDES := $(LOCAL_PATH)/src/sqlite3
include $(BUILD_STATIC_LIBRARY)

proj.andorid-studio/app/jni/Android.mk を書き換える。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

$(call import-add-path,$(LOCAL_PATH)/../../../cocos2d)
$(call import-add-path,$(LOCAL_PATH)/../../../cocos2d/external)
$(call import-add-path,$(LOCAL_PATH)/../../../cocos2d/cocos)
$(call import-add-path,$(LOCAL_PATH)/../../../cocos2d/cocos/audio/include)
+ $(call import-add-path,$(LOCAL_PATH)/../../../vendor)
$(call import-add-path,$(LOCAL_PATH))

(省略)

+ LOCAL_CFLAGS := -DSQLITE_HAS_CODEC // たぶん必要ないけど念のため
(省略)
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../Classes
(省略)
+ LOCAL_C_INCLUDES += $(LOCAL_PATH)/../../../vendor/wxsqlite3-3.3.1/src

# _COCOS_HEADER_ANDROID_BEGIN
# _COCOS_HEADER_ANDROID_END

+ LOCAL_WHOLE_STATIC_LIBRARIES += wxsqlite3_static

LOCAL_STATIC_LIBRARIES := cocos2dx_static

# _COCOS_LIB_ANDROID_BEGIN
# _COCOS_LIB_ANDROID_END

include $(BUILD_SHARED_LIBRARY)

$(call import-module,.)
$(call import-module,./android)
(省略)
+ $(call import-module, ./wxsqlite3-3.3.1)

# _COCOS_LIB_IMPORT_ANDROID_BEGIN
# _COCOS_LIB_IMPORT_ANDROID_END

proj.andorid-studio/app/jni/Application.mk を書き換える。

APP_STL := gnustl_static

APP_CPPFLAGS := -frtti -DCC_ENABLE_CHIPMUNK_INTEGRATION=1 -std=c++11 -fsigned-char -fexceptions
APP_LDFLAGS := -latomic

APP_ABI := armeabi
APP_SHORT_COMMANDS := true
+ APP_CFLAGS := -DSQLITE_HAS_CODEC

ifeq ($(NDK_DEBUG),1)
  APP_CPPFLAGS += -DCOCOS2D_DEBUG=1
  APP_OPTIM := debug
else
  APP_CPPFLAGS += -DNDEBUG
  APP_OPTIM := release
endif

たぶんここで定数登録しとけば、あとの LOCAL_CFLAGS の設定はいらないような気がしなくもない。

proj.android-studio/app/build.gradle を書き換える。

(省略)

        externalNativeBuild {
            ndkBuild {
                if (!project.hasProperty("PROP_NDK_MODE") || PROP_NDK_MODE.compareTo('none') != 0) {
                    // skip the NDK Build step if PROP_NDK_MODE is none
                    targets 'MyGame'
                    arguments 'NDK_TOOLCHAIN_VERSION=4.9'
                    arguments 'APP_PLATFORM=android-'+PROP_APP_PLATFORM

                    def module_paths = [project.file("../../cocos2d").absolutePath,
                                        project.file("../../cocos2d/cocos").absolutePath,
                                        project.file("../../cocos2d/external").absolutePath,
+                                       project.file("../../vendor").absolutePath ]
                    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
                        // should use '/'
                        module_paths = module_paths.collect {it.replaceAll('\\\\', '/')}
                        arguments 'NDK_MODULE_PATH=' + module_paths.join(";")
                    }
                    else {
                        arguments 'NDK_MODULE_PATH=' + module_paths.join(':')
                    }

                    arguments '-j' + Runtime.runtime.availableProcessors()
                    abiFilters.addAll(PROP_APP_ABI.split(':').collect{it as String})
                }
            }
        }
    }


(以下略)

あとはビルドしたら使えるようになる。

簡単なSQLiteのラッパークラス作成

どうせならORMっぽく使える方がいいので、こんなのを作成。

SQLite3Helper.hpp

#ifndef SQLite3Helper_hpp
#define SQLite3Helper_hpp

#include "sqlite3/sqlite3.h"
#include "cocos2d.h"

using ColumnKey = std::string;

/** 結果のエンティティ */
class SQLiteEntity {
   private:
    friend class SQLite3Helper;
    cocos2d::ValueMap m_data;

   public:
    operator cocos2d::ValueMap() const {
        return m_data;
    }
    SQLiteEntity() : m_data(){};
    bool isNull() const;
    int getInt(const ColumnKey& key, int defaultValue = 0) const;
    float getFloat(const ColumnKey& key, float defaultValue = 0.f) const;
    std::string getString(const ColumnKey& key,
                          const std::string& defaultValue = "") const;
    cocos2d::Value get(const ColumnKey& key) const;
};

class SQLite3Helper {
   private:
    std::string m_secret;
    std::string m_filePath;
    sqlite3* m_db;

   public:
    SQLite3Helper(const std::string& filePath, const std::string& secret = "")
    : m_filePath(filePath), m_secret(secret), m_db(nullptr) {};

    /** 主キー検索(主キーがない場合なにも帰らない) */
    SQLiteEntity getEntityById(const std::string& tableName, const std::string& id);


   private:
    void close();
    void reset();
    void open();
};

#endif /* SQLite3Helper_hpp */

SQLite3Helper.cpp

#include "SQLite3Helper.hpp"
USING_NS_CC;

bool SQLiteEntity::isNull() const {
    return m_data.empty();
}

int SQLiteEntity::getInt(const ColumnKey &key, int defaultValue) const {
    auto res = get(key);
    if(res.isNull()) {
        return defaultValue;
    }
    return res.asInt();
}

float SQLiteEntity::getFloat(const ColumnKey &key, float defaultValue) const {
    auto res = get(key);
    if(res.isNull()) {
        return defaultValue;
    }
    return res.asFloat();
}

std::string SQLiteEntity::getString(const ColumnKey &key,
                                    const std::string &defaultValue) const {
    auto res = get(key);
    if(res.isNull()) {
        return defaultValue;
    }
    return res.asString();
}

Value SQLiteEntity::get(const ColumnKey &key) const {
    auto it = m_data.find(key);
    if(it != m_data.end()) {
        return it->second;
    }
    return Value::Null;
}

#pragma mark SQLite3Helper

SQLiteEntity SQLite3Helper::getEntityById(
    const std::string &tableName, const std::string &id) {
    open();
    auto sql = StringUtils::format("select * from %s where id = '%s'",
                                   tableName.c_str(), id.c_str());
    sqlite3_stmt *stmt = nullptr;
    SQLiteEntity res;
    if (sqlite3_prepare_v2(m_db, sql.c_str(), -1, &stmt, nullptr) ==
        SQLITE_OK) {
        if(sqlite3_step(stmt) == SQLITE_ROW) {
            for (int i = 0; i < sqlite3_column_count(stmt); i++) {
                std::string name = sqlite3_column_name(stmt, i);
                int columnType = sqlite3_column_type(stmt, i);
                switch (columnType) {
                    // 実用化の際にはちゃんと他のケースも見る
                    case SQLITE_INTEGER:
                        res.m_data[name] = sqlite3_column_int(stmt, i);
                        break;
                    case SQLITE_FLOAT:
                        res.m_data[name] = (float)sqlite3_column_double(stmt,i);
                        break;
                    case SQLITE_TEXT: {
                        auto val = sqlite3_column_text(stmt, i);
                        auto s = StringUtils::format("%s", val);
                        res.m_data[name] = s;
                    }
                        break;
                    default:
                        break;
                }
            }
        }
        sqlite3_reset(stmt);
        sqlite3_finalize(stmt);
        close();
    } else {
        throw std::runtime_error("failed to run query.");
    }
    return res;
}

void SQLite3Helper::open() {
    if(m_db != nullptr) {
        reset();
    }
    auto status = sqlite3_open(m_filePath.c_str(), &m_db);
    if(status != SQLITE_OK) {
        throw std::runtime_error("failed to open db.");
    }
    if (m_secret != "") {
        status =
            sqlite3_key(m_db, m_secret.c_str(), (int)strlen(m_secret.c_str()));
        if(status != SQLITE_OK) {
            throw std::runtime_error("failed to set encrypt key.");
        }
    }
}

void SQLite3Helper::close() {
    if (m_db != nullptr) {
        auto status = sqlite3_close(m_db);
        if (status != SQLITE_OK) {
            throw std::runtime_error("failed to close db.");
        }
        m_db = nullptr;
    }
}

void SQLite3Helper::reset() {
    close();
}

実際に使う

auto db = Const::ASSET("master.db");
SQLite3Helper helper(db,"password");
auto entity = helper.getEntityById("TEST_TABLE", "id1");
if (entity.isNull()) {
    CCLOG("entity not found.");
} else {
    CCLOG("value :%s" , entity.getString("value","error").c_str());
}

まだテキトーな作りだけど、テーブル構造を解析するようにしたら汎用的なプライマリーキー検索や FindByConditions みたいなのとかも作れそう。