SQLiteVFSの作り方

こんにちは @mecha_g3 です。

SQLiteは軽量でコンパクトなRDBMSです。KLabではスマートフォン向けのゲームでゲームのパラメータやユーザーの設定を保存するのに使用しています。

今回はそんなSQLiteの一番低レイヤにある、OS毎の実装を抽象化しているインターフェース (VFS) を実装する方法を解説します。

SQLiteのWebサイトにVFS実装のサンプルがいくつかあります。より詳細で正確な情報はこちらを参照してください。

参考: The SQLite OS Interface or "VFS"

VFSでできること

SQLiteの実装のうちOSとファイルシステムに関連する部分、つまり Open, Read, Write, Close などの関数をすべて自作のものに差し替える事ができます。

例えば、特殊な制約があるファイルシステムを持っているデバイス向けに特化した実装を行うことができたり、dbファイルを自作の関数で難読化して保存したりすることができたり、ネットワーク越しにSQLiteのファイルを読み書きすることも頑張り次第で可能です。

デフォルトのVFSと自作のVFS

SQLiteはデフォルトでVFSをいくつか持っています。Windows向けにビルドしたSQLiteなら "win32"、Unix向けにビルドされたものは "unix" がデフォルトのVFSのとして使われます。

参考: Multiple VFSes

VFSに手を加えるということは、SQLiteのソースを拡張して改造版のSQLiteのライブラリを作らないといけないのかと思うかもしれません (僕もそうでした) が、VFSはうまく抽象化されていて、SQLiteのライブラリ (libsqlite3)には手を加えないまま、自作VFSを実行時に追加することができます。

今回はC言語での使用例を掲載しますが、共有ライブラリとしてビルドすれば他のプログラミング言語から使用することもできます。

鍵となる構造体

VFSを実装するにあたって、使い方/使われ方を理解しないといけない構造体が3つあります。まずはその3つを紹介します。

sqlite3_vfs 構造体

1つのVFSを表す構造体です。ファイルを開く、ファイルを削除する、現在時刻を取得するなど、OSが提供するべき機能を関数ポインタとして持っています。

自作の関数を各関数ポインタに設定した sqlite3_vfs を引数にしてsqlite3_vfs_register関数を呼び出すことで自作VFSを登録することができます。

// https://www.sqlite.org/c3ref/vfs.html
typedef struct sqlite3_vfs sqlite3_vfs;
typedef void (*sqlite3_syscall_ptr)(void);
struct sqlite3_vfs {
  int iVersion;            /* Structure version number (currently 3) */
  int szOsFile;            /* Size of subclassed sqlite3_file */
  int mxPathname;          /* Maximum file pathname length */
  sqlite3_vfs *pNext;      /* Next registered VFS */
  const char *zName;       /* Name of this virtual file system */
  void *pAppData;          /* Pointer to application-specific data */
  int (*xOpen)(sqlite3_vfs*, const char *zName, sqlite3_file*, int flags, int *pOutFlags);
  int (*xDelete)(sqlite3_vfs*, const char *zName, int syncDir);
  int (*xAccess)(sqlite3_vfs*, const char *zName, int flags, int *pResOut);
  int (*xFullPathname)(sqlite3_vfs*, const char *zName, int nOut, char *zOut);
  void *(*xDlOpen)(sqlite3_vfs*, const char *zFilename);
  void (*xDlError)(sqlite3_vfs*, int nByte, char *zErrMsg);
  void (*(*xDlSym)(sqlite3_vfs*,void*, const char *zSymbol))(void);
  void (*xDlClose)(sqlite3_vfs*, void*);
  int (*xRandomness)(sqlite3_vfs*, int nByte, char *zOut);
  int (*xSleep)(sqlite3_vfs*, int microseconds);
  int (*xCurrentTime)(sqlite3_vfs*, double*);
  int (*xGetLastError)(sqlite3_vfs*, int, char *);
  /*
  ** The methods above are in version 1 of the sqlite_vfs object
  ** definition.  Those that follow are added in version 2 or later
  */
  int (*xCurrentTimeInt64)(sqlite3_vfs*, sqlite3_int64*);
  /*
  ** The methods above are in versions 1 and 2 of the sqlite_vfs object.
  ** Those below are for version 3 and greater.
  */
  int (*xSetSystemCall)(sqlite3_vfs*, const char *zName, sqlite3_syscall_ptr);
  sqlite3_syscall_ptr (*xGetSystemCall)(sqlite3_vfs*, const char *zName);
  const char *(*xNextSystemCall)(sqlite3_vfs*, const char *zName);
  /*
  ** The methods above are in versions 1 through 3 of the sqlite_vfs object.
  ** New fields may be appended in future versions.  The iVersion
  ** value will increment whenever this happens.
  */
};

sqlite3_file 構造体

sqlite3_file は SQLiteによって開かれた1つのファイルを表す構造体です。

sqlite3_file のためのメモリはVFSではなくSQLite側でmalloc/freeされます。確保されるサイズはsqlite3_vfs の szOsFile によって指定することができます。

メンバは pMethods だけです。sqlite3_io_methods 構造体は次に紹介します。

// https://www.sqlite.org/c3ref/file.html
typedef struct sqlite3_file sqlite3_file;
struct sqlite3_file {
  const struct sqlite3_io_methods *pMethods;  /* Methods for an open file */
};

sqlite3_io_methods 構造体

sqlite3_file に対して操作を行う関数を、関数ポインタとして持っている構造体です。簡単にいうと sqlite3_file のメソッド群です。

自作した関数を設定した sqlite3_io_methods を用意しておき、sqlite3_file の pMethods に設定して使用します。

// https://www.sqlite.org/c3ref/io_methods.html
typedef struct sqlite3_io_methods sqlite3_io_methods;
struct sqlite3_io_methods {
  int iVersion;
  int (*xClose)(sqlite3_file*);
  int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst);
  int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst);
  int (*xTruncate)(sqlite3_file*, sqlite3_int64 size);
  int (*xSync)(sqlite3_file*, int flags);
  int (*xFileSize)(sqlite3_file*, sqlite3_int64 *pSize);
  int (*xLock)(sqlite3_file*, int);
  int (*xUnlock)(sqlite3_file*, int);
  int (*xCheckReservedLock)(sqlite3_file*, int *pResOut);
  int (*xFileControl)(sqlite3_file*, int op, void *pArg);
  int (*xSectorSize)(sqlite3_file*);
  int (*xDeviceCharacteristics)(sqlite3_file*);
  /* Methods above are valid for version 1 */
  int (*xShmMap)(sqlite3_file*, int iPg, int pgsz, int, void volatile**);
  int (*xShmLock)(sqlite3_file*, int offset, int n, int flags);
  void (*xShmBarrier)(sqlite3_file*);
  int (*xShmUnmap)(sqlite3_file*, int deleteFlag);
  /* Methods above are valid for version 2 */
  int (*xFetch)(sqlite3_file*, sqlite3_int64 iOfst, int iAmt, void **pp);
  int (*xUnfetch)(sqlite3_file*, sqlite3_int64 iOfst, void *p);
  /* Methods above are valid for version 3 */
  /* Additional methods may be added in future releases */
};

VFS実装の難しいポイント

sqlite3_file 構造体の継承

難しいポイント1つ目です。

C言語にはオブジェクト指向言語のようなクラスの継承やメソッドのオーバーロードといった機能はありませんが、似たようなことはできます。VFSの実装にもこのテクニックが使われているので理解する必要があります。

sqlite3_file はSQLiteから開かれたファイルを表すベースとなる構造体です。実際には各VFSは sqlite3_file を継承した構造体を使用しています。

どうやって継承を実現しているのでしょうか? sqlite3_file に一つメンバを追加した構造体を作ってみましょう。以下の例は、sqlite3_file を継承し、var1 メンバを追加した SampleVfsFile を定義しています。

typedef struct SampleVfsFile SampleVfsFile;
struct SampleVfsFile {
  sqlite3_file base; // 必ず最初のメンバとして置く
  int var1;
};

SampleVfsFile の最初のメンバに sqlite3_file 型のメンバを置くことで、SampleVfsFile 型の変数の先頭アドレスから sizeof(sqlite3_file) バイトは、sqlite3_file 型と同じレイアウトになります。

同じメモリレイアウトになっているということは、ポインタをキャストすることで sqlite3_file として振る舞う事ができます。

SampleVfsFile *pSampleVfsFile = ...; // 適当に初期化された SampleVfsFile へのポインタ
sqlite3_file *pFile = (sqlite3_file*)pSampleVfs; // ポインタをキャストすることで sqlite3_file として振る舞うことができる

こうすることで、SampleVfsFile 型は sqlite3_file* を引数にとるあらゆる関数で利用可能になります。

また、sqlite3_file のメンバにある pMethods はまさに sqlite3_file* を引数にとる関数のあつまりでしたから、pMethodsを自作のものに差し替えるとまるごとメソッドを置き換えることができます。このようにして継承とオーバーライドを実現しています。

自作VFSからデフォルトVFSを利用する

難しいポイント2つ目です。

先の例で SampleVfsFile を作りましたが、これはデフォルトVFSのsqlite3_fileメンバを継承していません。このままでは自作VFSですべてのプラットフォーム向けの実装をしないといけないです。これはしんどいので、自作VFSの内側でデフォルトのVFS実装を利用できるような形を作ります。

実行時にデフォルトのVFSを取得して保持するような実装を行います。必要なのは以下の2つです。

  1. デフォルトVFS実装の sqlite3_vfs のアドレス
  2. デフォルトVFSが使用する sqlite3_file の容量確保

1 に関しては、sqlite3_vfs 構造体には自由に使えるpAppDataというメンバがあるので、ここにデフォルトVFSのアドレスを保持しておくことができます。

2 に関しては、sqlite3_file として何バイトのメモリを確保するべきかをsqlite3_vfs の szOsFileメンバで指定することができます。これを利用してsizeof(自作sqlite3_file) + sizeof(デフォルトVFSのsqlite3_file)だけメモリ確保してもらうようにして、2つの構造体のデータを連続したメモリ領域に埋め込みます。

sqlite3_file memory layout image

画像 左: デフォルトVFS使用時のsqlite3_file, 右: 自作VFS使用時のsqlite3_file

この2つが揃えば、自作VFSの中でデフォルトVFSの関数を呼び出すことができます。デフォルトVFSの関数を呼び出す際に使用するsqlite3_fileはデフォルトVFSのものを渡してあげましょう。上図とサンプル実装ではORGFILE(pFile) がデフォルトVFSの sqlite3_file の先頭アドレスを取得するマクロです。

※ 2 に関してレイアウトの決め方は自由です。デフォルトVFSの直後に自作の構造体が来るようなメモリレイアウトにしてpMethods を差し替える形でも良いでしょう。

自作VFS LgVfsを作ってみよう

VFSのサンプルとして、xOpen, xRead, xWrite, xSync 関数が呼び出された際にログ出力するVFSを作ってみました。

少し長いですが、先述の難しいポイントを2つとも押さえておけば理解できるはずです。

// lg_vfs.h
#ifndef LG_VFS_H
#define LG_VFS_H

// lgvfs をSQLiteに登録します。
// 登録に成功した場合 SQLITE_OK を返却します。
int lg_vfs_register();

#endif
// lg_vfs.c
#include "lg_vfs.h"

#include <stdio.h>
#include <string.h>

#include "sqlite3.h"

typedef struct sqlite3_vfs LgVfs;
typedef struct LgFile LgFile;

// sqlite3_vfs* からデフォルトVFSの sqlite3_vfs* を取得するマクロ
// LgVfsのpAppDataにはデフォルトのVFS実装のアドレスが格納される (lg_vfs_register 参照)
#define ORIGVFS(p) ((sqlite3_vfs *)((p)->pAppData))

// sqlite3_file* からデフォルトVFS実装の sqlite3_file* を取得するマクロ
// LgFileの直後にデフォルトVFSのsqlite3_fileが格納される (lg_vfs_register, lgOpen 参照)
#define ORIGFILE(p) ((sqlite3_file *)(((LgFile *)(p)) + 1))

// sqlite3_file* から LgFile* を取得するマクロ
#define LGFILE(p) ((LgFile *)(p))

// LgFile構造体 (sqlite3_fileを継承)
struct LgFile {
  sqlite3_file base;      // 継承のため必ず最初に配置する
  char *file_path;  // 開かれたファイルパス
};

// LgFile構造体のメソッド(宣言)
static int lgClose(sqlite3_file *pFile);
static int lgRead(sqlite3_file *pFile, void *zBuf, int iAmt, sqlite_int64 iOfst);
static int lgWrite(sqlite3_file *pFile, const void *zBuf, int iAmt, sqlite_int64 iOfst);
static int lgTruncate(sqlite3_file *pFile, sqlite_int64 size);
static int lgSync(sqlite3_file *pFile, int flags);
static int lgFileSize(sqlite3_file *pFile, sqlite_int64 *pSize);
static int lgLock(sqlite3_file *pFile, int eLock);
static int lgUnlock(sqlite3_file *pFile, int eLock);
static int lgCheckReservedLock(sqlite3_file *pFile, int *pResOut);
static int lgFileControl(sqlite3_file *pFile, int op, void *pArg);
static int lgSectorSize(sqlite3_file *pFile);
static int lgDeviceCharacteristics(sqlite3_file *pFile);
static int lgShmMap(sqlite3_file *pFile, int iPg, int pgsz, int bExtend, void volatile **pp);
static int lgShmLock(sqlite3_file *pFile, int offset, int n, int flags);
static void lgShmBarrier(sqlite3_file *pFile);
static int lgShmUnmap(sqlite3_file *pFile, int deleteFlag);
static int lgFetch(sqlite3_file *pFile, sqlite3_int64 iOfst, int iAmt, void **pp);
static int lgUnfetch(sqlite3_file *pFile, sqlite3_int64 iOfst, void *pPage);

// LgVfsのメソッド(宣言)
static int lgOpen(sqlite3_vfs *pVfs, const char *zName, sqlite3_file *pFile, int flags, int *pOutFlags);
static int lgDelete(sqlite3_vfs *pVfs, const char *zPath, int dirSync);
static int lgAccess(sqlite3_vfs *pVfs, const char *zPath, int flags, int *pResOut);
static int lgFullPathname(sqlite3_vfs *pVfs, const char *zPath, int nOut, char *zOut);
static void *lgDlOpen(sqlite3_vfs *pVfs, const char *zPath);
static void lgDlError(sqlite3_vfs *pVfs, int nByte, char *zErrMsg);
static void (*lgDlSym(sqlite3_vfs *pVfs, void *p, const char *zSym))(void);
static void lgDlClose(sqlite3_vfs *pVfs, void *pHandle);
static int lgRandomness(sqlite3_vfs *pVfs, int nByte, char *zBufOut);
static int lgSleep(sqlite3_vfs *pVfs, int nMicro);
static int lgCurrentTime(sqlite3_vfs *pVfs, double *pTimeOut);
static int lgGetLastError(sqlite3_vfs *pVfs, int a, char *b);
static int lgCurrentTimeInt64(sqlite3_vfs *pVfs, sqlite3_int64 *p);
static int lgSetSystemCall(sqlite3_vfs *pVfs, const char *zName, sqlite3_syscall_ptr pCall);
static sqlite3_syscall_ptr lgGetSystemCall(sqlite3_vfs *pVfs, const char *zName);
static const char *lgNextSystemCall(sqlite3_vfs *pVfs, const char *zName);

// LgVfs
static sqlite3_vfs lg_vfs = {
    3,                  /* iVersion (currently 3) */
    0,                  /* szOsFile (set when registered) */
    1024,               /* mxPathname */
    0,                  /* pNext */
    "lgvfs",            /* zName */
    0,                  /* pAppData (set when registered) */
    lgOpen,             /* xOpen */
    lgDelete,           /* xDelete */
    lgAccess,           /* xAccess */
    lgFullPathname,     /* xFullPathname */
    lgDlOpen,           /* xDlOpen */
    lgDlError,          /* xDlError */
    lgDlSym,            /* xDlSym */
    lgDlClose,          /* xDlClose */
    lgRandomness,       /* xRandomness */
    lgSleep,            /* xSleep */
    lgCurrentTime,      /* xCurrentTime */
    lgGetLastError,     /* xGetLastError */
    lgCurrentTimeInt64, /* xCurrentTimeInt64 */
    lgSetSystemCall,    /* xSetSystemCall */
    lgGetSystemCall,    /* xGetSystemCall */
    lgNextSystemCall    /* xNextSystemCall */
};

// LgVfs用 sqlite3_io_methods
static const sqlite3_io_methods lg_io_methods = {
    3,                       /* iVersion */
    lgClose,                 /* xClose */
    lgRead,                  /* xRead */
    lgWrite,                 /* xWrite */
    lgTruncate,              /* xTruncate */
    lgSync,                  /* xSync */
    lgFileSize,              /* xFileSize */
    lgLock,                  /* xLock */
    lgUnlock,                /* xUnlock */
    lgCheckReservedLock,     /* xCheckReservedLock */
    lgFileControl,           /* xFileControl */
    lgSectorSize,            /* xSectorSize */
    lgDeviceCharacteristics, /* xDeviceCharacteristics */
    lgShmMap,                /* xShmMap */
    lgShmLock,               /* xShmLock */
    lgShmBarrier,            /* xShmBarrier */
    lgShmUnmap,              /* xShmUnmap */
    lgFetch,                 /* xFetch */
    lgUnfetch                /* xUnfetch */
};

// LgFile構造体のメソッド(実装)
static int lgClose(sqlite3_file *pFile) {
  fprintf(stderr, "> lgClose %s\n", LGFILE(pFile)->file_path);
  sqlite3_free(LGFILE(pFile)->file_path);
  LGFILE(pFile)->file_path = 0;
  return ORIGFILE(pFile)->pMethods->xClose(ORIGFILE(pFile));
}
static int lgRead(sqlite3_file *pFile, void *zBuf, int iAmt, sqlite_int64 iOfst) {
  fprintf(stderr, "> lgRead %s %d %lld\n", LGFILE(pFile)->file_path, iAmt, iOfst);
  return ORIGFILE(pFile)->pMethods->xRead(ORIGFILE(pFile), zBuf, iAmt, iOfst);
}
static int lgWrite(sqlite3_file *pFile, const void *zBuf, int iAmt, sqlite_int64 iOfst) {
  fprintf(stderr, "> lgWrite %s %d %lld\n", LGFILE(pFile)->file_path, iAmt, iOfst);
  return ORIGFILE(pFile)->pMethods->xWrite(ORIGFILE(pFile), zBuf, iAmt, iOfst);
}
static int lgTruncate(sqlite3_file *pFile, sqlite_int64 size) {
  fprintf(stderr, "> lgTruncate %s %lld\n", LGFILE(pFile)->file_path, size);
  return ORIGFILE(pFile)->pMethods->xTruncate(ORIGFILE(pFile), size);
}
static int lgSync(sqlite3_file *pFile, int flags) {
  fprintf(stderr, "> lgSync %s 0x%x\n", LGFILE(pFile)->file_path, flags);
  return ORIGFILE(pFile)->pMethods->xSync(ORIGFILE(pFile), flags);
}
static int lgFileSize(sqlite3_file *pFile, sqlite_int64 *pSize) {
  return ORIGFILE(pFile)->pMethods->xFileSize(ORIGFILE(pFile), pSize);
}
static int lgLock(sqlite3_file *pFile, int eLock) {
  return ORIGFILE(pFile)->pMethods->xLock(ORIGFILE(pFile), eLock);
}
static int lgUnlock(sqlite3_file *pFile, int eLock) {
  return ORIGFILE(pFile)->pMethods->xUnlock(ORIGFILE(pFile), eLock);
}
static int lgCheckReservedLock(sqlite3_file *pFile, int *pResOut) {
  return ORIGFILE(pFile)->pMethods->xCheckReservedLock(ORIGFILE(pFile), pResOut);
}
static int lgFileControl(sqlite3_file *pFile, int op, void *pArg) {
  return ORIGFILE(pFile)->pMethods->xFileControl(ORIGFILE(pFile), op, pArg);
}
static int lgSectorSize(sqlite3_file *pFile) {
  return ORIGFILE(pFile)->pMethods->xSectorSize(ORIGFILE(pFile));
}
static int lgDeviceCharacteristics(sqlite3_file *pFile) {
  return ORIGFILE(pFile)->pMethods->xDeviceCharacteristics(ORIGFILE(pFile));
}
static int lgShmMap(sqlite3_file *pFile, int iPg, int pgsz, int bExtend, void volatile **pp) {
  return ORIGFILE(pFile)->pMethods->xShmMap(ORIGFILE(pFile), iPg, pgsz, bExtend, pp);
}
static int lgShmLock(sqlite3_file *pFile, int offset, int n, int flags) {
  return ORIGFILE(pFile)->pMethods->xShmLock(ORIGFILE(pFile), offset, n, flags);
}
static void lgShmBarrier(sqlite3_file *pFile) {
  ORIGFILE(pFile)->pMethods->xShmBarrier(ORIGFILE(pFile));
}
static int lgShmUnmap(sqlite3_file *pFile, int deleteFlag) {
  return ORIGFILE(pFile)->pMethods->xShmUnmap(ORIGFILE(pFile), deleteFlag);
}
static int lgFetch(sqlite3_file *pFile, sqlite3_int64 iOfst, int iAmt, void **pp) {
  return ORIGFILE(pFile)->pMethods->xFetch(ORIGFILE(pFile), iOfst, iAmt, pp);
}
static int lgUnfetch(sqlite3_file *pFile, sqlite3_int64 iOfst, void *pPage) {
  return ORIGFILE(pFile)->pMethods->xUnfetch(ORIGFILE(pFile), iOfst, pPage);
}

// LgVfsのメソッド(実装)
static int lgOpen(sqlite3_vfs *pVfs, const char *zName, sqlite3_file *pFile, int flags, int *pOutFlags) {
  fprintf(stderr, "> lgOpen %s\n", zName ? zName : "(NULL)");

  // この時点でpFileはszOsFileだけ確保されている

  // pFileのうち前半のLgFileの部分を初期化
  memset(pFile, 0, sizeof(LgFile));

  // LgFile->file_path にファイル名をコピー
  int n = strlen(zName ? zName : "(NULL)") + 1;
  LGFILE(pFile)->file_path = sqlite3_malloc(n);
  strncpy(LGFILE(pFile)->file_path, zName ? zName : "(NULL)", n);

  // 自作のlg_io_methodsを使用する
  pFile->pMethods = &lg_io_methods;

  // デフォルトVFSのxOpenを呼び出す
  int rc = ORIGVFS(pVfs)->xOpen(ORIGVFS(pVfs), zName, ORIGFILE(pFile), flags, pOutFlags);
  if (rc != SQLITE_OK) {
    sqlite3_free(LGFILE(pFile)->file_path);
    LGFILE(pFile)->file_path = 0;
  }
  return rc;
}
static int lgDelete(sqlite3_vfs *pVfs, const char *zPath, int dirSync) {
  return ORIGVFS(pVfs)->xDelete(ORIGVFS(pVfs), zPath, dirSync);
}
static int lgAccess(sqlite3_vfs *pVfs, const char *zPath, int flags, int *pResOut) {
  return ORIGVFS(pVfs)->xAccess(ORIGVFS(pVfs), zPath, flags, pResOut);
}
static int lgFullPathname(sqlite3_vfs *pVfs, const char *zPath, int nOut, char *zOut) {
  return ORIGVFS(pVfs)->xFullPathname(ORIGVFS(pVfs), zPath, nOut, zOut);
}
static void *lgDlOpen(sqlite3_vfs *pVfs, const char *zPath) {
  return ORIGVFS(pVfs)->xDlOpen(ORIGVFS(pVfs), zPath);
}
static void lgDlError(sqlite3_vfs *pVfs, int nByte, char *zErrMsg) {
  ORIGVFS(pVfs)->xDlError(ORIGVFS(pVfs), nByte, zErrMsg);
}
static void (*lgDlSym(sqlite3_vfs *pVfs, void *p, const char *zSym))(void) {
  return ORIGVFS(pVfs)->xDlSym(ORIGVFS(pVfs), p, zSym);
}
static void lgDlClose(sqlite3_vfs *pVfs, void *pHandle) {
  ORIGVFS(pVfs)->xDlClose(ORIGVFS(pVfs), pHandle);
}
static int lgRandomness(sqlite3_vfs *pVfs, int nByte, char *zBufOut) {
  return ORIGVFS(pVfs)->xRandomness(ORIGVFS(pVfs), nByte, zBufOut);
}
static int lgSleep(sqlite3_vfs *pVfs, int nMicro) {
  return ORIGVFS(pVfs)->xSleep(ORIGVFS(pVfs), nMicro);
}
static int lgCurrentTime(sqlite3_vfs *pVfs, double *pTimeOut) {
  return ORIGVFS(pVfs)->xCurrentTime(ORIGVFS(pVfs), pTimeOut);
}
static int lgGetLastError(sqlite3_vfs *pVfs, int a, char *b) {
  return ORIGVFS(pVfs)->xGetLastError(ORIGVFS(pVfs), a, b);
}
static int lgCurrentTimeInt64(sqlite3_vfs *pVfs, sqlite3_int64 *p) {
  return ORIGVFS(pVfs)->xCurrentTimeInt64(ORIGVFS(pVfs), p);
}
static int lgSetSystemCall(sqlite3_vfs *pVfs, const char *zName, sqlite3_syscall_ptr pCall) {
  return ORIGVFS(pVfs)->xSetSystemCall(ORIGVFS(pVfs), zName, pCall);
}
static sqlite3_syscall_ptr lgGetSystemCall(sqlite3_vfs *pVfs, const char *zName) {
  return ORIGVFS(pVfs)->xGetSystemCall(ORIGVFS(pVfs), zName);
}
static const char *lgNextSystemCall(sqlite3_vfs *pVfs, const char *zName) {
  return ORIGVFS(pVfs)->xNextSystemCall(ORIGVFS(pVfs), zName);
}

// lgvfs をSQLiteに登録する関数
// 実行時にユーザーから呼ばれる想定
int lg_vfs_register() {
  // デフォルトのVFS取得する
  sqlite3_vfs *org_vfs = sqlite3_vfs_find(0);
  if (org_vfs == 0) {
    return SQLITE_ERROR;
  }

  // ORIGVFS マクロのために pAppData にデフォルトVFSのアドレスを記録しておく
  lg_vfs.pAppData = org_vfs;

  // sqlite3_file のために確保されるサイズを指定する
  // LgFileに加えて、デフォルトのVFSの sqlite3_file
  // を連続した領域に保持しておくため合計サイズを用いる
  lg_vfs.szOsFile = sizeof(LgFile) + org_vfs->szOsFile;

  return sqlite3_vfs_register(&lg_vfs, 0);
}

VFSを利用する側のサンプルコードです。 SQLite使用前に lg_vfs_register を呼び出してVFSを登録し、sqlite3_open_v2 の最後の引数に "lgvfs" を指定して使用します。

// main.c
#include <stdio.h>
#include <stdlib.h>

#include "lg_vfs.h"
#include "sqlite3.h"

int main() {
  if (remove("test.db") == 0) {
    fprintf(stderr, "test.db deleted\n");
  }

  fprintf(stdout, "lg_vfs_register\n");
  int rc = lg_vfs_register();
  if (rc != SQLITE_OK) {
    fprintf(stderr, "lg_vfs_register failure\n");
    exit(1);
  }

  fprintf(stdout, "sqlite3_open_v2\n");
  sqlite3 *db;
  rc = sqlite3_open_v2("test.db", &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, "lgvfs");
  if (rc != SQLITE_OK) {
    fprintf(stderr, "sqlite3_open_v2 failure: %s\n", sqlite3_errmsg(db));
    exit(1);
  }

  fprintf(stdout, "sqlite3_exec CREATE TABLE\n");
  rc = sqlite3_exec(db, "CREATE TABLE users (user_id INTEGER, user_name TEXT, age INTEGER)", NULL, NULL, NULL);
  if (rc != SQLITE_OK) {
    fprintf(stderr, "CREATE TABLE failure: %s\n", sqlite3_errmsg(db));
    exit(1);
  }

  fprintf(stdout, "sqlite3_exec INSERT\n");
  rc = sqlite3_exec(db, "INSERT INTO users (user_id, user_name, age) VALUES (1, 'tarou', 18), (2, 'jirou', 16)", NULL,
                    NULL, NULL);
  if (rc != SQLITE_OK) {
    fprintf(stderr, "INSERT failure: %s\n", sqlite3_errmsg(db));
    exit(1);
  }

  fprintf(stdout, "sqlite3_prepare_v2\n");
  sqlite3_stmt *stmt;
  rc = sqlite3_prepare_v2(db, "SELECT user_id, user_name, age FROM users", 128, &stmt, NULL);
  if (rc != SQLITE_OK) {
    fprintf(stderr, "SELECT failure %d\n", rc);
    exit(1);
  }

  fprintf(stdout, "sqlite3_step\n");
  while (sqlite3_step(stmt) == SQLITE_ROW) {
    int user_id = sqlite3_column_int(stmt, 0);
    const unsigned char *user_name = sqlite3_column_text(stmt, 1);
    int age = sqlite3_column_int(stmt, 2);
    fprintf(stdout, "%d\t%s\t%d\n", user_id, user_name, age);
  }

  fprintf(stdout, "sqlite3_finalize\n");
  rc = sqlite3_finalize(stmt);
  if (rc != SQLITE_OK) {
    fprintf(stderr, "sqlite3_finalize failure: %s\n", sqlite3_errmsg(db));
    exit(1);
  }

  fprintf(stdout, "sqlite3_close\n");
  rc = sqlite3_close(db);
  if (rc != SQLITE_OK) {
    fprintf(stderr, "sqlite3_close failure: %s\n", sqlite3_errmsg(db));
    exit(1);
  }

  return 0;
}

コンパイルして実行します。今回はサンプルコードとVFSのコードを一緒にビルドしてしまいます。

実行結果を見てみましょう。

$ gcc lg_vfs.c main.c -lsqlite3 && ./a.out
test.db deleted
lg_vfs_register
sqlite3_open_v2
> lgOpen /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 100 0
sqlite3_exec CREATE TABLE
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 16 24
> lgOpen /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 512 0
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 8 512
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 0x2
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 12 0
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 0x2
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 4096 0
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 4096 4096
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 0x2
> lgClose /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal
sqlite3_exec INSERT
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 16 24
> lgOpen /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 512 0
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 4 512
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 4096 516
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 4 4612
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 4 4616
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 4096 4620
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 4 8716
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 8 9216
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 0x2
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 12 0
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 0x2
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 4096 0
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 4096 4096
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 0x2
> lgClose /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal
sqlite3_prepare_v2
sqlite3_step
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 16 24
1   tarou   18
2   jirou   16
sqlite3_finalize
sqlite3_close
> lgClose /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db

無事に自作VFSの関数が呼ばれてログが出力されています。

書き込みを行う前にロールバック用のジャーナルファイルが作られている様子がわかります。こう見ると小さいサイズの書き込みが多いですね。

そういえば、SQLiteのジャーナルモードに WALモード (Write-Ahead Logging) という設定があり、これを使うと追記のみのジャーナルファイルが使われてINSERTが速くなると聞いたことがあります。せっかくなので挙動を見てみましょう。

以下のコードを挿入してジャーナルモードをWALに設定します。

sqlite3_exec(db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL);
lg_vfs_register
sqlite3_open_v2
> lgOpen /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 100 0
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 16 24
> lgOpen /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 512 0
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 8 512
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 12 0
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal 0x2
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 4096 0
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 0x2
> lgClose /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-journal
sqlite3_exec CREATE TABLE
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 16 24
> lgOpen /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 4096 0
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal 32 0
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal 0x3
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal 4120 32
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal 4120 4152
sqlite3_exec INSERT
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal 4120 8272
sqlite3_prepare_v2
sqlite3_step
1   tarou   18
2   jirou   16
sqlite3_finalize
sqlite3_close
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal 0x3
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal 4096 56
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 4096 0
> lgRead /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal 4096 8296
> lgWrite /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 4096 4096
> lgTruncate /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 8192
> lgSync /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db 0x3
> lgTruncate /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal 0
> lgClose /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db-wal
> lgClose /Users/anon/projects/KLabGamesTechBlog/contents/20220617_anon/test.db

Open時になにやら操作回数が増えていますが、CREATE TABLE や INSERT 時に wal ファイルにしか書き込んでいません。walファイルに溜まった変更差分はCloseのタイミングでdbファイルに反映しているようです。特にINSERTが1回の書き込みのみになっていてとても速そうです。こうした挙動を観察できるのも面白いですね!

さいごに

SQLiteのVFSの作り方を解説しました。実際に動くサンプルコードも用意したので、是非手元で試してみて下さいね。

https://www.sqlite.org/vfs.html により詳細なドキュメントといくつかの参考になるサンプル実装が含まれています。今回の lg_vfs の実装は appendvfs.c を参考にして作りました。

本記事中のソースコードは、SQLiteに倣って Public Domain とします。商用または非商用を問わず、あらゆる手段で、誰でも自由にコピー、変更、公開、使用、コンパイル、販売、または配布できます。

VFSを実際に作ってみると、色々とハマりポイントがあります。例えば、xOpenはテンポラリファイルの作成にも使われ、その際にはファイル名がNULLでxOpenが呼ばれます。xRead/xWriteもテンポラリファイルを考慮に入れておかないといけません。VFSを自作するときは注意してください。

SQLiteのリポジトリには大量のテストコードがあるので、それの一部を自作VFSの為に利用することができるかもしれません。僕は試せていませんが、もし詳しい方や例があれば教えて下さい。

最後まで読んでいただきありがとうございました!

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。

関連記事

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。