📚

【C#】CsSqlite - .NET / Unity向けのハイパフォーマンスなSQLiteライブラリ

に公開

CsSqlite、というC#向けのSQLiteバインディング実装を作りました。sqlite3.hが巨大すぎる故にリポジトリの言語がCに侵食されてる上、何故かRustまで居座ってますが、歴としたC#向けのライブラリです。

https://github.com/nuskey8/CsSqlite

C#でSQLite、というかDBを扱うにはEntityFramework Core(EFCore)を使うのが基本です。知らない人のために説明しておくと、EntityFramework Coreは.NET向けのO/Rマッパーです。LINQと動的生成を用いたクエリの生成、マイグレーションの機能などなど、DBを扱う上で必要なもの全部入りのようなフレームワークで、その辺の機能が欲しい時には強力なんですが、ローカルDBとしてSQLiteを利用するだけ、みたいなケースで使うにはフレームワークとして重厚すぎるんですよね。

一応EFCoreのSQLite実装はMicrosoft.Data.Sqliteという別ライブラリに分離されているため、これを単独で利用することは可能です。ただ、こちらの実装はADO.NETの抽象化を意識した設計になっているため、バインディング層としてのパフォーマンス的に優れているか、というと微妙なところです。

また、System.Data.SQLiteという実装も存在しますが、これはMicrosoft.Data.Sqliteが登場する以前のADO.NET向けのSQLiteプロバイダで、SQLiteの開発チームによってメンテナンスされていたものになります。最初の実装が2005年とかなり古いため、今となってはMicrosoft.Data.Sqliteを使ったほうが良いでしょう。この辺りは公式ドキュメントのSystem.Data.SQLite との比較という記事が詳しいです。

もう一つの問題はUnityサポートです。上のようなライブラリは基本的にモダンな.NETランタイム向けのもので、Unity上での動作はサポートされていません。UnityでSQLiteを扱うにはSQLiteUnityKitを使う方法が一般的らしいですが、これの実装としてはかなり微妙で、正直自分で使う気にはならないかな...という雰囲気です。sqlite-netという.NET向けのライブラリを利用する手もありますが、こちらはUnityでの動作はあまり考慮されていないため、持ち込むには結構手間がかかります。

というわけで、CsSqliteは最小限かつ高速なSQLiteバインディングを提供しつつ、Unityでの動作もしっかりサポートする、という方針で一から設計し直したものになっています。

使い方

基本的にはREADMEを読んでもらえればいいのですが、軽く使い方を説明しておきましょう。

using CsSqlite;

// SqliteConnectionの初期化
using var connection = new SqliteConnection("example.db");
connection.Open();

// SQLの実行
connection.ExecuteNonQuery("""
CREATE TABLE IF NOT EXISTS user (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    age INTEGER NOT NULL,
    name TEXT NOT NULL
);
""");

// UTF-8文字列もそのまま流せる
connection.ExecuteNonQuery("""
INSERT INTO user (id, name, age)
VALUES (1, 'Alice', 18),
       (2, 'Bob', 32),
       (3, 'Charlie', 25);
"""u8);

// Readerの作成
using var reader = connection.ExecuteReader("""
SELECT name
FROM user
""");

// 一行ごとに読んでいく
while (reader.Read())
{
    Console.WriteLine($"Hello, {reader.GetString(0)}!");
}

また、クエリを再利用したい時や、パラメータを追加したい時にはSqliteCommandが利用できます。外部入力から動的にクエリを構築する際には、インジェクション対策のために文字列補完ではなくこちらを利用すると良いでしょう。

using var command = conn.CreateCommand("INSERT INTO t(val) VALUES($foo);");
command.Parameters.Add("$foo", "foo");
command.ExecuteNonQuery();

API自体はMicrosoft.Data.Sqliteとほとんど変わりません、というかほぼ同じになるように揃えました。パフォーマンスの都合上、CommandTextプロパティが存在しないなどの若干の変更は加えていますが、感覚としてはほぼ同じように使えるはずです。

また、CsSqliteはあくまでSQLiteの操作を高レベルAPIでラップするライブラリなので、O/Rマッパーのような機能はありません。必要であれば都度拡張してもらえると良いでしょう。

パフォーマンス

ベンチマークも見ていきましょう。このベンチマークでは以下のようなコードを複数のライブラリで実行しています。

using var conn = new CsSqlite.SqliteConnection(":memory:");

conn.ExecuteNonQuery("CREATE TABLE t(id INTEGER PRIMARY KEY, val TEXT);");

using var cmd = conn.CreateCommand("INSERT INTO t(val) VALUES ($foo), ($bar);");
cmd.Parameters.Add("$foo"u8, "foo"u8);
cmd.Parameters.Add("$bar"u8, "bar"u8);
cmd.ExecuteNonQuery();

using var reader = conn.ExecuteReader("SELECT * FROM t;");
while (reader.Read())
{
    _ = reader.GetInt(0);
    _ = reader.GetString(1);
}

見ての通り、CREATE TABLEINSERTSELECTを順に実行するだけのコードです。そして結果は以下の通り。

実行速度自体も最速ですが、特筆すべきはアロケーションの少なさです。この120BはSqliteConnection自体のアロケーションとreader.GetString(1)で作成した文字列の分なので、SQL操作時にはC#側のGCアロケーションは一切ありません。

バインディングならどのライブラリでもパフォーマンスは大差ない、と思いきや、バインディング層で余計なアロケーションを起こしたりしていると意外と遅くなり得ます。CsSqliteは徹底的にアロケーションを削って丁寧に最適化しているため、高レベルなAPIを扱いつつも、C APIを直接叩くのに近いパフォーマンスを発揮できています。

csbindgenの活用

バインディング部分はCsSqlite.Nativeパッケージに分離されていますが、この中身のNativeMethodsはCysharpさんのライブラリcsbindgenによってRust経由で自動生成されています。

fn main() {
    // bindgenで.hからrustバインディングを生成
    bindgen::Builder::default()
        .header("native/sqlite3.h")
        .generate()
        .unwrap()
        .write_to_file("src/sqlite3.rs")
        .unwrap();

    // 生成したrustバインディングからC#バインディングを生成
    let builder = csbindgen::Builder::default()
        .input_bindgen_file("src/sqlite3.rs")
        .method_filter(|x| !x.starts_with("sqlite3_win32"))
        .csharp_namespace("CsSqlite")
        .csharp_method_prefix("")
        .csharp_class_accessibility("public")
        .csharp_use_function_pointer(false)
        .csharp_dll_name("sqlite3");

    builder
        .generate_to_file(
            "src/sqlite3_csbindgen.rs",
            "../CsSqlite.Native/NativeMethods.g.cs",
        )
        .unwrap();

    // Unity用
    builder
        .csharp_dll_name_if("(UNITY_IOS || UNITY_WEBGL) && !UNITY_EDITOR", "__Internal")
        .csharp_dll_name_if("UNITY_ANDROID && !UNITY_EDITOR", "sqliteX")
        .generate_csharp_file("../CsSqlite.Unity/Assets/CsSqlite.Unity/Native/NativeMethods.g.cs")
        .unwrap();
}

ただ、現在のcsbindgenではcsharp_dll_name_if()を複数使いたいケースに対応できなかったので、それに対応したPRを出してみました。

https://github.com/Cysharp/csbindgen/pull/114

SQLiteはプラットフォーム毎に組み込みで用意されているケースが多いため、それを直接使うとするとDllImportに幾つかの分岐が必要で...

Unity

CsSqliteはUnity向けのパッケージも提供しています。v1.0.1でSystem.Runtime.CompilerServices.Unsafeの依存を無くしたので、Package Managerからgit URLで入れるだけで動くようになっています。

対応プラットフォームはWindows、macOS、Linux、iOS、Androidです。SQLite Wasmを使えばブラウザでも動かせないことはないんですが、UnityのWebGLでwasmを動かすのは結構大変なので一旦そのままにしています。需要がありそうなら対応するかな...

まとめ

元はと言えば、Unityでローカルにデータを保存するのにSQLiteを使うとよい、みたいなことを聞いたのが作ったきっかけだったりします。なるほど確かに良さそうと思って調べてみると、意外と決定打になるライブラリが不在だったので、じゃあ作ればいいか、と。バインディング部分は自動生成で済んだので、作業の片手間に作っていたら普通に1日くらいで完成してました。csbindgenすごい。

というわけでCsSqlite、結構いい感じになっているので、是非是非使ってみて下さい!

Discussion