SqliteにアクセスするDLLの作成


TradeStation(EasyLanguage)から外部Sqlite3データベースを利用するためのDLLを作成してみました。
以前ブログに書いたものを、いちいちテーブルを作成変更したときにDLLを改変する必要がないように汎用性があるものに書き換えてみました。

環境
Windows10 Pro 64bit(H270 chipset) and Windows10 Home 32bit(OSX-Elcapitan PararellsDesktop)
VisualStudio2017

作成手順
1  VisuslStudio2017で空のC++プロジェクトを新規作成(Sqlite)
2  プロパティから出力をdllに変更
3  「リンカー」「入力」「追加の依存ファイル」にwinsqlite3.libを追加
* DebugからRelease等構成を変更した場合には再度2、3のプロパティ修正が必要
* プラットフォームはWin32
* winsqlite3が利用できない場合は、sqliteのダウンロードとsqlite3.libの作成が必要

4  .defファイルを追加(Sqlite.def)
5  プロパティで「リンカー」「定義ファイル」にSqlite.defが追加されていることを確認
6  Sqlite.hとSqlite.cppを追加
7 コードをコピペしてビルド

//--------- Sqlite.def
LIBRARY Sqlite

EXPORTS
    ExecuteQuery
//--------- Sqlite.h
#pragma once
//以下はTradeStationのオブジェクトを参照するときに必要の様だ
//#import "C:/Program Files/TradeStation 9.5/Program/tskit.dll" //for 32bit
//#import "C:/Program Files (x86)/TradeStation 9.5/Program/tskit.dll"   //for 6

const char * __stdcall ExecuteQuery(const char * dbpath, const char * query, const char * column_separator, const char * newline_separator);
//--------- Sqlite.cpp
//DLL
//select,create table,insert,replace,updateコマンド対応
//db fileがなければ作成される
#include "Sqlite.h"
#include <winsqlite/winsqlite3.h>
#include <cstdio>
#include <string>
#include <algorithm>

#pragma execution_character_set("utf-8")    //charはUTF-8になる

#define SQLITE_OK           0   /* Successful result */
/* beginning-of-error-codes */
#define SQLITE_ERROR        1   /* Generic error */
#define SQLITE_INTERNAL     2   /* Internal logic error in SQLite */
#define SQLITE_PERM         3   /* Access permission denied */
#define SQLITE_ABORT        4   /* Callback routine requested an abort */
#define SQLITE_BUSY         5   /* The database file is locked */
#define SQLITE_LOCKED       6   /* A table in the database is locked */
#define SQLITE_NOMEM        7   /* A malloc() failed */
#define SQLITE_READONLY     8   /* Attempt to write a readonly database */
#define SQLITE_INTERRUPT    9   /* Operation terminated by sqlite3_interrupt()*/
#define SQLITE_IOERR       10   /* Some kind of disk I/O error occurred */
#define SQLITE_CORRUPT     11   /* The database disk image is malformed */
#define SQLITE_NOTFOUND    12   /* Unknown opcode in sqlite3_file_control() */
#define SQLITE_FULL        13   /* Insertion failed because database is full */
#define SQLITE_CANTOPEN    14   /* Unable to open the database file */
#define SQLITE_PROTOCOL    15   /* Database lock protocol error */
#define SQLITE_EMPTY       16   /* Not used */
#define SQLITE_SCHEMA      17   /* The database schema changed */
#define SQLITE_TOOBIG      18   /* String or BLOB exceeds size limit */
#define SQLITE_CONSTRAINT  19   /* Abort due to constraint violation */
#define SQLITE_MISMATCH    20   /* Data type mismatch *
#define SQLITE_MISUSE      21   /* Library used incorrectly */
#define SQLITE_NOLFS       22   /* Uses OS features not supported on host */
#define SQLITE_AUTH        23   /* Authorization denied */
#define SQLITE_FORMAT      24   /* Not used */
#define SQLITE_RANGE       25   /* 2nd parameter to sqlite3_bind out of range */
#define SQLITE_NOTADB      26   /* File opened that is not a database file */
#define SQLITE_NOTICE      27   /* Notifications from sqlite3_log() */
#define SQLITE_WARNING     28   /* Warnings from sqlite3_log() */
#define SQLITE_ROW         100  /* sqlite3_step() has another row ready */
#define SQLITE_DONE        101  /* sqlite3_step() has finished executing */
/* end-of-error-codes */

int status = SQLITE_OK;

void SetMessage(int status, char** message) {
    switch (status)
    {
    case SQLITE_OK:
        *message = "SQLITE_OK:Successful result";
        break;
    case SQLITE_ERROR:
        *message = "SQLITE_ERROR:SQL error or missing database";
        break;
    case SQLITE_INTERNAL:
        *message = "SQLITE_INTERNAL:Internal logic error in SQLite";
        break;
    case SQLITE_PERM:
        *message = "SQLITE_PERM:Access permission denied";
        break;
    case SQLITE_ABORT:
        *message = "SQLITE_ABORT:Callback routine requested an abort";
        break;
    case SQLITE_BUSY:
        *message = "SQLITE_BUSY:The database file is locked";
        break;
    case SQLITE_LOCKED:
        *message = "SQLITE_LOCKED:A table in the database is locked";
        break;
    case SQLITE_NOMEM:
        *message = "SQLITE_NOMEM:A malloc() failed";
        break;
    case SQLITE_READONLY:
        *message = "SQLITE_READONLY:Attempt to write a readonly database";
        break;
    case SQLITE_INTERRUPT:
        *message = "SQLITE_INTERRUPT:Operation terminated by sqlite3_interrupt()";
        break;
    case SQLITE_IOERR:
        *message = " Some kind of disk I/O error occurred";
        break;
    case SQLITE_CORRUPT:
        *message = "SQLITE_CORRUPT:The database disk image is malformed";
        break;
    case SQLITE_NOTFOUND:
        *message = "SQLITE_NOTFOUND:Unknown opcode in sqlite3_file_control()";
        break;
    case SQLITE_FULL:
        *message = "SQLITE_FULL:Insertion failed because database is full";
        break;
    case SQLITE_CANTOPEN:
        *message = "SQLITE_CANTOPEN:Unable to open the database file";
        break;
    case SQLITE_PROTOCOL:
        *message = "SQLITE_PROTOCOL:Database lock protocol error";
        break;
    case SQLITE_EMPTY:
        *message = "SQLITE_EMPTY:Database is empty";
        break;
    case SQLITE_SCHEMA:
        *message = "SQLITE_SCHEMA:The database schema changed";
        break;
    case SQLITE_TOOBIG:
        *message = "SQLITE_TOOBIG:String or BLOB exceeds size limit";
        break;
    case SQLITE_CONSTRAINT:
        *message = "SQLITE_CONSTRAINT:Abort due to constraint violation";
        break;
    case SQLITE_MISMATCH:
        *message = "SQLITE_MISMATCH:Data type mismatch";
        break;
    case SQLITE_MISUSE:
        *message = "SQLITE_MISUSE:Library used incorrectly";
        break;
    case SQLITE_NOLFS:
        *message = "SQLITE_NOLFS:Uses OS features not supported on host";
        break;
    case SQLITE_AUTH:
        *message = "SQLITE_AUTH:Authorization denied";
        break;
    case SQLITE_FORMAT:
        *message = "SQLITE_FORMAT:Auxiliary database format error";
        break;
    case SQLITE_RANGE:
        *message = "SQLITE_RANGE:2nd parameter to sqlite3_bind out of range";
        break;
    case SQLITE_NOTADB:
        *message = "SQLITE_NOTADB:File opened that is not a database file";
        break;
    case SQLITE_NOTICE:
        *message = "SQLITE_NOTICE:Notifications from sqlite3_log()";
        break;
    case SQLITE_WARNING:
        *message = "SQLITE_WARNING:Warnings from sqlite3_log()";
        break;
    case SQLITE_ROW:
        *message = "SQLITE_ROW:sqlite3_step() has another row ready";
        break;
    case SQLITE_DONE:
        *message = "SQLITE_DONE:sqlite3_step() has finished executing";
        break;
    default:
        *message = "unknown error";
        break;
    }
}

const char * __stdcall ExecuteQuery(const char * dbpath, const char * query, const char * column_separator, const char * newline_separator)
{

    char * errorMessage;
    sqlite3_stmt *statement;
    ::sqlite3* db = 0;
    status = ::sqlite3_open(dbpath, &db);
    if (status != SQLITE_OK) {
        SetMessage(status, &errorMessage);
        return errorMessage;
    }
    static std::string buf;
    buf.clear();
    buf = query;
    transform(buf.begin(), buf.end(), buf.begin(), tolower);

    if (buf.substr(0, 6) == "select") {
        buf.clear();

        status = sqlite3_prepare_v2(db, query, -1, &statement, &query);
        if (status != SQLITE_OK) {
            sqlite3_close(db);
            SetMessage(status, &errorMessage);
            return errorMessage;
        }


        int r;
        int j = 0;
        double prev = 0;

        while (SQLITE_ROW == (r = sqlite3_step(statement))) {
            if (j > 0)
                buf += newline_separator;
            for (int i = 0; i < sqlite3_column_count(statement); i++)
            {
                std::string name = sqlite3_column_name(statement, i);
                int columnType = sqlite3_column_type(statement, i);
                sqlite3_int64 value_int = 0;
                double value_double = 0;
                switch (columnType) {
                case SQLITE_NULL:
                    //http://program.station.ez-net.jp/special/iphone/db/sqlite.null.asp
                    buf += "NULL";
                    break;
                case SQLITE_INTEGER:
                    value_int = sqlite3_column_int64(statement, i);
                    buf += std::to_string(value_int);
                    break;
                case SQLITE_FLOAT:
                    value_double = sqlite3_column_double(statement, i);
                    buf += std::to_string(value_double);
                    break;
                case SQLITE_BLOB:
                    buf += "[[";
                    buf += (const char*)sqlite3_column_text(statement, i);
                    buf += "]]";
                    break;
                default:
                    buf += (const char*)sqlite3_column_text(statement, i);
                    break;
                }
                if (i < sqlite3_column_count(statement) - 1)
                    buf += column_separator;
            }
            j++;
        }
        sqlite3_reset(statement);
        sqlite3_finalize(statement);

        if (SQLITE_DONE != r) {
            buf += "execute error";
        }


    }
    else {      //select以外のクエリ
        buf.clear();
        status = sqlite3_exec(db, query, nullptr, nullptr, &errorMessage);
        if (status != SQLITE_OK) {
            if (errorMessage != NULL) sqlite3_free(errorMessage);
        }
        SetMessage(status, &errorMessage);
        buf = errorMessage;
    }

    sqlite3_close(db);

    return buf.c_str();
}

注:
1 TradestationはUTF-8の文字列を認識するようで、文字化け解消のためDLLもcharをUTF-8として扱っている。
2 c++で文字列を返す方法を十分に理解していないため、メモリリークがあるかもしれません。
3 汎用にするために、クエリを渡す方法にしました。
4 selectステートメントは結果またはエラーを、その他のステートメントはStatus(エラー)を返します。
5 データ型としてBlobの場合もtextで取得するする様に書きましたが、実際にできるかは不明です。


トレステで作成したSqliteクエリ実行アプリ

C#で確認する方法
1 VisualStudioでC# Consoleプロジェクトを追加
2 プラットフォームはx86に変更
3 コードをコピペしてデバッグ(自動的にデータベースファイルが作成される)
4 stackoverflowにUTF-8の文字化け解消方法がアップされていました

using System;
using System.Text;
using System.Runtime.InteropServices;

namespace csConsole
{
    class Program
    {
        [DllImport("c:\\data\\Tradestation\\dll\\Sqlite.dll")]
        private static extern IntPtr ExecuteQuery(string dbpath, IntPtr query, string column_separator, string row_separator);
         private static string dbfile = "c:\\data\\Tradestation\\trade.db";
        static void Main(string[] args)
        {
            RunQuery("create table if not exists brands (code integer primary key, name text, market text, 上場年月日 text, 時価総額 numeric);");
            string query = "replace into brands(code, market, name, 上場年月日, 時価総額) values"
                + " (6696, 'マザーズ', 'トランザス', '2017/08/09', 9472)"
                + ", (3989, 'マザーズ', 'シェアリングテクノロジー', '2017/08/03', 12712)";
            RunQuery(query);

            query = "select code, 上場年月日, name, market from brands;";
            RunQuery(query);

            Console.WriteLine();
            Console.WriteLine("    hit any key then quit");
            Console.ReadKey();
        }

        private static void RunQuery(string query)
        {
            IntPtr pOut = ExecuteQuery(dbfile, NativeUtf8FromString(query), ",", "\n");
            String result = StringFromNativeUtf8(pOut);
            Console.WriteLine(result);
        }
        //これでUtf-8相互変換できる!
        //https://stackoverflow.com/questions/10773440/conversion-in-net-native-utf-8-managed-string  Conversion in .net: Native Utf-8 <-> Managed String
        private static IntPtr NativeUtf8FromString(string managedString)
        {
            int len = Encoding.UTF8.GetByteCount(managedString);
            byte[] buffer = new byte[len + 1];
            Encoding.UTF8.GetBytes(managedString, 0, managedString.Length, buffer, 0);
            IntPtr nativeUtf8 = Marshal.AllocHGlobal(buffer.Length);
            Marshal.Copy(buffer, 0, nativeUtf8, buffer.Length);
            return nativeUtf8;
        }

        private static string StringFromNativeUtf8(IntPtr nativeUtf8)
        {
            int len = 0;
            while (Marshal.ReadByte(nativeUtf8, len) != 0) ++len;
            byte[] buffer = new byte[len];
            Marshal.Copy(nativeUtf8, buffer, 0, buffer.Length);
            return Encoding.UTF8.GetString(buffer);
        }

    }
}

 

本当はc++/clrで作成したかったのですが、MySQLやPhamtomJSなどと同様にNuGet等からインストールしたドライバーを利用した場合、コンソールアプリは動くのですがDLLにすると「ハンドルされていない例外: System.IO.FileLoadException: ファイルまたはアセンブリ ...、またはその依存関係の 1 つが読み込めませんでした。」のようなエラーで動きません。
ググってみたところ、VC関連の依存DLLが読み込めないことが原因のようで、マニフェストファイルを埋め込むことで解消できそうに思えました。
しかし、このマニフェストの具体的な埋め込み方法がよくわからずc++/clrは断念しました。

Reference
* Windowsデベロッパーセンター - Windows アプリの開発 - データ アクセス - SQLite データベース
* Public symbols in winsqlite3.dll
* SQLite Official Page
* SQLite Result Codes
* char*の戻り値を返すC++のdll関数呼び出し
* STACK OVERFLOW Conversion in .net: Native Utf-8 <-> Managed String