てきとうなさいと べぇたばん

Windows上のCでSQLiteをいじる

Microsoft Visual C++でSQLiteをいじる

UTF-16を使うようにする。

Microsoft Visual Studio Community 2017。

Windowsの文字コード

Windowsの文字コードはUNICODEモードにした上で行う。内部ではUTF-16となる。_setmode関数を使う。その場合はワイド関数を使う。

ナロー関数(printfとか)を使うと、プログラムが途中で止まることに注意(注意というか、そもそもしてはいけない https://www.jpcert.or.jp/sc-rules/c-str38-c.html

UTF-16をWindows上で扱う場合、wchar_tを使うのが一般的なようだ。プロジェクトを作るとwchar.hがincludeされる。

気をつけること

ワイド文字として扱わせるので、文字列にはLを加える

SQLiteの関数は末尾のプレフィックスに16の関数を使う

  • sqlite3_open16
  • sqlite3_prepare16_v2
  • sqlite3_errmsg16
  • sqlite3_column_text16

こんなものかな。

_setmode関数を使ってstdin、stdout、stderrを_O_U16TEXTにセットする こうすることで内部でUTF-16、出力を正しい形に変換できるようだ。

_setmode(_fileno(stdout), _O_U16TEXT);
_setmode(_fileno(stdin), _O_U16TEXT);
_setmode(_fileno(stderr), _O_U16TEXT);

出力に使う関数としてはこのあたりを使った

  • fwprintf
  • fputws

ハマったこと

時系列が前後するが、ここまでたどり着くのに結構苦労した。UTF-16で扱おうとなんとか色々やって表示できたと思ったら、何故かコマンドプロンプト上の実行結果で全角文字のような表示をしてた。例えばこんな感じ。(たしか_setmode_O_BINARYを指定した)

C r e a t e  t a b l e  e r r o r

これを、リダイレクトしてファイルに出力したあとにバイナリエディタで見てみるとこうなっていた

43 00 72 00 65 00 61 00 74 00 65 00 20 00 74 00 61 00 62 00 6c 00 65 00 20 00 65 00 72 00 72 00 6f 00 72 00 3a 00 20 00 6e 00 65 00 61 00 72 00 6f 00 72 00

これはUTF-16だな。それをASCIIとして読んでるから、00がNULとして解釈されている。このときの_setmode関数は_O_BINARYとしてて、変換しないようにしてた。

だから、この方法では正しくないということで、_O_U16TEXTなどのUNICODEモードで表示させないとだめだということになった。

コード

SQLiteをWindows上で扱うというか、ワイド文字の扱いをどのようにするべきかという事になってしまったけど本編はこっち。

今回はWin32 コンソールアプリケーションとしてプロジェクトを作成。

ヘッダーファイルを使用する方法でソリューションエクスプローラーにソースファイルへsqlite3.cを、ヘッダーファイルをsqlite3.hを入れてstdafx.hにincludeした。

// stdafx.h : 標準のシステム インクルード ファイルのインクルード ファイル、または
// 参照回数が多く、かつあまり変更されない、プロジェクト専用のインクルード ファイル
// を記述します。
//

#pragma once

#include "targetver.h"

#include <stdio.h>
#include <wchar.h>
#include <stdlib.h>
#include <io.h>
#include <fcntl.h>


// TODO: プログラムに必要な追加ヘッダーをここで参照してください
#include "sqlite3.h"

ソースファイル側。

#include "stdafx.h"

includeしてるのはstdafx.h。これは元からそうだった

int version(sqlite3 **db) {
    sqlite3_stmt *res;
    int rc;

    fwprintf(stdout, L"SQLite3 lib number version: %d\n", sqlite3_libversion_number());
    fwprintf(stdout, L"SQLite3 version: %hs\n", sqlite3_version);

    rc = sqlite3_prepare16(*db, L"SELECT SQLITE_VERSION();", -1, &res, 0);

    if (rc != SQLITE_OK)
    {
        const wchar_t* err = (const wchar_t*)sqlite3_errmsg16(*db);
        fwprintf(stderr, L"SELECT VERSION Error: %s\n", err);
        sqlite3_close(*db);
        return rc;
    }

    rc = sqlite3_step(res);

    if (rc == SQLITE_ROW)
    {
        wprintf(L"%ls\n", (const wchar_t*)sqlite3_column_text16(res, 0));
        return SQLITE_OK;
    }

    sqlite3_close(*db);
    return rc;
}

version関数に、SQLiteのバージョンを表示させるようにした。

int create_table_flavor(sqlite3 **db) {
    sqlite3_stmt *res;
    const wchar_t* err;
    int rc;

    rc = sqlite3_prepare16(*db, L"DROP TABLE IF EXISTS flavor", -1, &res, 0);

    if (rc != SQLITE_OK)
    {
        err = (const wchar_t*)sqlite3_errmsg16(*db);
        fwprintf(stderr, L"Drop table prepare error: %ls\n", err);
        sqlite3_close(*db);
        return rc;
    }

    rc = sqlite3_step(res);

    if (rc != SQLITE_DONE)
    {
        err = (const wchar_t*)sqlite3_errmsg16(*db);
        fwprintf(stderr, L"Drop table error: %ls\n", err);
        sqlite3_close(*db);
        return rc;
    }

    rc = sqlite3_prepare16(*db, L"CREATE TABLE flavor (id integer PRIMARY KEY AUTOINCREMENT, name TEXT);", -1, &res, 0);

    if (rc != SQLITE_OK)
    {
        err = (const wchar_t*)sqlite3_errmsg16(*db);
        fwprintf(stderr, L"Create table error: %ls\n", err);
        sqlite3_close(*db);
        return rc;
    }

    rc = sqlite3_step(res);

    if (rc == SQLITE_DONE)
    {
        return SQLITE_DONE;
    }

    sqlite3_close(*db);
    return rc;
}

create_table_flavor関数。flavorというテーブルを作る。予めあったらflavorというテーブルを削除する。

int insert_table_flavor(sqlite3 **db)
{
    sqlite3_stmt *res;
    const wchar_t* err;
    int rc;

    rc = sqlite3_prepare16_v2(*db, L"INSERT INTO flavor(name) VALUES(?)", -1, &res, 0);

    if (rc != SQLITE_OK)
    {
        err = (const wchar_t*)sqlite3_errmsg16(*db);
        fwprintf(stderr, L"Insert table prepare error: %ls\n", err);
        sqlite3_close(*db);
        return rc;
    }

    wchar_t* name = L"あいうおえ";
    sqlite3_bind_text16(res, 1, name, (int)(sizeof(wchar_t) * (wcslen(name) + 1)), SQLITE_TRANSIENT);

    rc = sqlite3_step(res);

    if (rc != SQLITE_DONE)
    {
        sqlite3_close(*db);
        return rc;
    }

    return rc;
}

insert_table_flavor関数では、flavorテーブルに値を挿入している。ワイド文字でも挿入ができることを確認。sqlite3_bind_text16では、第4引数に長さを指定する必要があったけれども、これは(int)(sizeof(wchar_t) * (wcslen(name) + 1))で対処した。

int wmain()
{
    _setmode(_fileno(stdout), _O_U16TEXT);
    _setmode(_fileno(stdin), _O_U16TEXT);
    _setmode(_fileno(stderr), _O_U16TEXT);

    sqlite3 *db;
    sqlite3_stmt *res;
    int rc;

    rc = sqlite3_open16(L"test.db", &db);

    if (rc != SQLITE_OK)
    {
        fwprintf(stderr, L"Could not open database: %ls\n", (const wchar_t*)sqlite3_errmsg16(db));
        sqlite3_close(db);
        return 1;
    }

    rc = version(&db);

    if (rc != SQLITE_OK)
    {
        fwprintf(stderr, L"Could not print version: %ls\n", (const wchar_t*)sqlite3_errmsg16(db));
        return 1;
    }

    rc = sqlite3_prepare16_v2(db, L"SELECT SQLITE_VERSION();", -1, &res, 0);

    if (rc != SQLITE_OK)
    {
        fwprintf(stderr, L"Failed to fetch data: %ls\n", (const wchar_t*)sqlite3_errmsg16(db));
        sqlite3_close(db);
        return 1;
    }

    rc = sqlite3_step(res);

    if (rc == SQLITE_ROW)
    {
        wprintf(L"%ls\n", (const wchar_t*)sqlite3_column_text16(res, 0));
    }

    fputws(L"version end\n", stdout);

    rc = create_table_flavor(&db);
    if (rc != SQLITE_DONE)
    {
        fwprintf(stderr, L"Could not create Table: %ls\n", (const wchar_t*)sqlite3_errmsg16(db));
        return 1;
    }

    rc = insert_table_flavor(&db);

    if (rc != SQLITE_DONE)
    {
        fwprintf(stderr, L"Could not insert table: %ls\n", (const wchar_t*)sqlite3_errmsg16(db));
        return 1;
    }

    rc = sqlite3_prepare16_v2(db, L"SELECT * FROM flavor;", -1, &res, 0);

    if (rc != SQLITE_OK)
    {
        fwprintf(stderr, L"Failed to fetch data: %ls\n", (const wchar_t*)sqlite3_errmsg16(db));
        sqlite3_close(db);
        return 1;
    }

    rc = sqlite3_step(res);

    if (rc == SQLITE_ROW)
    {
        wprintf(L"%ls\n", (const wchar_t*)sqlite3_column_text16(res, 1));
    }

    sqlite3_finalize(res);
    sqlite3_close(db);

    return 0;
}

wmain関数。wmainなのは、Microsoft固有の仕様ということで、ワイド文字バージョンのmain関数を定義する必要があるためとのこと。https://docs.microsoft.com/ja-jp/cpp/c-language/using-wmain?view=vs-2019

_setmode関数で_O_U16TEXTを指定して、変換モードを変更することで正しく文字化けを防いだ。

sqlite3_errmsg16などの関数は、void *を返すことになっているので、キャストして格納する必要があったと解釈しているけど、ここはconst wchar_t *として、fwprintf関数の指定には%lsを使った。

参考