PowerGres on Windows V1.x、V2.x で C 言語関数を作るには

このページに記載の情報は PowerGres on Windows V1.x、V2.x を対象としています。
V3.x 以降ではベースとする PostgreSQL と同様の方法で C 言語関数を作成できます。

目次

はじめに

PostgreSQL と同様に、PowerGres でも C 言語 (もしくは C++ のような C と互換性のある言語) で記述した関数を呼び出すことができるようになって います。

PowerGres 上で C 言語関数を作成する方法は、基本的な部分については PostgreSQL と同じです。 たとえば、PostgreSQL では C 言語関数を作成する際に、 呼び出せるようにしたい関数をマクロ PG_FUNCTION_INFO_V1, PG_FUNCTION_ARGS や Datum 型を用いて次のように定義しますが、 この辺のことは PowerGres でも同じです。


#include <fmgr.h>

extern Datum userfunc(PG_FUNCTION_ARGS);

PG_FUNCTION_INFO_V1(userfunc);

Datum
userfunc(PG_FUNCTION_ARGS)
{
    /* 省略 */
}

PostgreSQL での C 言語関数の作成方法についての詳細は、PowerGres 製品版 に付属の PostgreSQL 和訳ドキュメントの「SQL の拡張:関数」に記されて おります。(評価版にはドキュメントが添付されていません。) まずは、そちらをご覧下さい。

この文書は PowerGres 上で C 言語関数を作成する方法について解説したもの ですが、 読者は PostgreSQL 上での C 言語関数の作成方法を理解していることを前提とし、 PowerGres 特有の注意点を中心に解説します。 ご了承下さい。

PowerGres 上で C 言語関数を作成する上で、PostgreSQL にはない注意点は 次の 2 つです。

  1. 動的ロードを行うためには、関数を DLL にする必要がある
  2. 関数は、マルチスレッド環境下で動作できるようにする必要がある

次節より、それぞれの注意点について、詳しく説明していきます。

注意点 その 1: DLL にする

通常 UNIX では、C 言語関数をコンパイルしたオブジェクトの動的ロードを 行うために、オブジェクトを共有ライブラリの形式にします。 同様に、Windows ではオブジェクトを DLL (Dynamic Link Library) 形式に することになります。

Windows の DLL では、外部から呼び出せるようにしたい関数をエクスポート する必要があります。 Windows で関数をエクスポートするには、VC++ に用意されている __declspec(dllexport) 修飾子を用いるか、エクスポート したい関数名の一覧を記述した *.def ファイルを用意します。

以下、それぞれの方法について、PosgreSQL の contrib/spi/ に収録されている autoinc という C 言語関数を例にとって説明します。 autoinc のソースコードは、簡単に書くと次のようになって います。


extern Datum autoinc(PG_FUNCTION_ARGS);

PG_FUNCTION_INFO_V1(autoinc);

Datum
autoinc(PG_FUNCTION_ARGS)
{
    /* 省略 */
}

__declspec(dllexport) 修飾子を用いる場合

__declspec(dllexport) 修飾子を用いる場合は、ソースコード に __declspec(dllexport) を書き加えます。


extern __declspec(dllexport) Datum autoinc(PG_FUNCTION_ARGS);

__declspec(dllexport) PG_FUNCTION_INFO_V1(autoinc);

__declspec(dllexport) Datum
autoinc(PG_FUNCTION_ARGS)
{
    /* 省略 */
}

*.def ファイルを用意する場合

*.def ファイルを用意する場合は、次のような内容にします。


EXPORTS
   autoinc
   pg_finfo_autoinc

関数名 autoinc とは別に pg_finfo_ を頭に 付けた pg_finfo_autoinc という関数も指定する必要がある 点に注意して下さい。 この関数は、PG_FUNCTION_INFO_V1 マクロが展開されることに よって生成されます。

注意点 その2: マルチスレッド環境下で動作させる

PostgreSQL とは異なり、PowerGres では サーバ (postmaster.exe) がマルチスレッドで動作します。 このため、C 言語関数も、マルチスレッド環境で正しく動作できるもので なくてはなりません。

ここで、特に気をつけなくてはならないのは、C 言語関数内で使用している、 静的な領域を持った変数です。


/* 静的な変数の例 */
static int var = 0;

自動変数とは異なり、こうした変数は複数のスレッド間で共有されます。 また、対策を講じない限り各スレッドから非同期にアクセスされます。 どういうことかと言うと、たとえばスレッド A, B が次のような処理をほぼ 同時に行ったとします。


/* スレッド A が行う処理 */
var = 1;
printf("%d", var);

/* スレッド B が行う処理 */
var = 2;
printf("%d", var);

もし各処理の厳密な実行順序が次のようになった場合、どちらのスレッド も printf() で出力する値は 2 になります。


var = 1;             /* スレッド A が実行 */
var = 2;             /* スレッド B が実行 */
printf("%d", var);   /* スレッド A が実行 */
printf("%d", var);   /* スレッド B が実行 */

しかし、もし実行順序が次のようになった場合だと、スレッド A, B ともに printf() で出力する値は 1 になります。


var = 2;             /* スレッド B が実行 */
var = 1;             /* スレッド A が実行 */
printf("%d", var);   /* スレッド A が実行 */
printf("%d", var);   /* スレッド B が実行 */

こうした実行順序の微妙な違いによって、動作結果が異なってしまうことに なります。 C 言語関数をマルチスレッドで動作させるようにするには、スレッド同士が 非同期で変数にアクセスする可能性について、つねに注意を払わなくては なりません。

問題の解決方法ですが、変数の扱いをどうするかによって異なります。

では、それぞれの方法について、詳しく説明していくことにします。

変数をスレッド間で共有する場合

変数を複数のスレッド間で共有するのであれば、あとは非同期にアクセス しないようにする対策を施せば済むので、問題の解決はそれほど難しくは ありません。

対策方法としては、PowerGres のスピンロックや、Windows のクリティカル セクション等を使って、スレッド同士で排他制御を行いながら変数にアクセス するようにします。 ここでは、スピンロックを使った排他制御の方法について説明します。

まず、ロック用の変数を初期化する必要があります。 Windows では、スレッドが DLL をロード (アタッチ) したり、ロードした DLL を解放 (デタッチ) したりする際に、DllMain() が自動的 に呼び出されるので、この関数の中で初期化するのが良いでしょう。


#include <postgres.h>
#include <storage/spin.h>

static volatile slock_t mylock;

BOOL WINAPI
DllMain(HINSTANCE dll, DWORD reason, PVOID implicity)
{
    if (reason == DLL_PROCESS_ATTACH)
        SpinLockInit(&mylock);
    return TRUE;
}

スレッド間で共有する変数 var にアクセスする際は、ロック による排他制御を行います。 ある時点でロックを取得できるのは、一つのスレッドだけです。 あるスレッドがロック取得中に他のスレッドが取得を試みると、ロックを 取得できるまでブロックされます。


SpinLockAcquire_NoHoldoff(&mylock);   /* ロックの取得 */

/*
 * ここに、変数 var にアクセスする処理を書く。
 * 例:
 *    var = 1;             -- 変数への代入
 *    printf("%d", var);   -- 変数の値を参照
 */

SpinLockRelease_NoHoldoff(&mylock);   /* ロックの解放 */

以上に加えて、変数 var にも volatile 修飾子を付けておく 必要があるかも知れません。


static volatile int var = 0;

volatile 修飾子は、C コンパイラに対して、変数の値が予期せぬ間に 書き変わっている可能性があることを伝えます。

シングルスレッド環境 (プログラム内で動作するスレッドが常に一つしか ない場合) では、そのスレッドが変数の値を最後に参照したときと変数の値は 変わらないはずですので、基本的に volatile 修飾子は不要です。 しかしながら、マルチスレッド環境では、そのような前提が (厳密にはプログラム の内容によりますが) 通用しないため、適宜 volatile 修飾子を付ける必要が あります。

変数をスレッド毎に別々の値を保持する場合

変数をスレッド同士で共有せずに、各スレッドに独立した値を持たせたい場合 には、かなり工夫が必要です。

蛇足ですが、VC++ には変数に対してスレッド毎に別々の領域を与えるための 仕掛けとして、__declspec(thread) 修飾子があります。 しかし、今回のように DLL を動的にロードする場合、この修飾子は残念ながら 機能しません。


/* スレッド毎に領域を与える                */
/* (動的にロードした DLL 内では機能しない) */
__declspec(thread) static int var = 0;

したがって、変数に静的な領域をもたせるのではなく、 malloc() などで動的に確保した領域を使うように 書き改める必要があります。 ただし、malloc() で領域を確保しても、領域へのポインタ を保持するために、静的な領域を持った用意を使うわけにはいきません。


/* 間違った例 (結局 static 変数が必要になってしまう) */
static void *var_ptr = (int *)malloc(sizeof int);

そこで PowerGres では、この問題を解決するために、ポインタ値を登録 できるハッシュテーブルと、このハッシュテーブルを操作するための関数 を用意しています。

int TlsHashSet(const char *key, void *value);
文字列 key をキーとして、ハッシュテーブルに ポインタ値 value を登録します。
void *TlsHashGet(const char *key);
文字列 key をキーとして登録したポインタ値を、 ハッシュテーブルの中から探し出します。
void TlsHashUnset(const char *key);
文字列 key をキーとして登録したポインタ値を、 ハッシュテーブルから削除します。
void TlsHashInitialize(void);
ハッシュテーブルを初期化します。 各スレッドは、上記のいずれかの関数を呼び出す前に必ず一度呼び出す 必要があります。

これらの関数はサーバ (postmaster.exe) 内に用意されているため、 サーバにロードされた DLL からも呼び出せるようになっています。

以下は、ハッシュテーブルに変数を登録する処理を行う関数の例です。 int 型の変数一個分のメモリを確保し、それをハッシュテーブルに登録して います。


#include <powergres/tlshash.h>

static int
autoinc_initialize_tls(void)
{
    int *var;

    /* すでに初期化済みなら、何もせず関数を抜ける */
    if (TlsHashGet("mycfunc.var") != NULL)
        return 1;

    /* 変数 var 用の領域を malloc() で確保する */
    var = (int *) malloc(sizeof(int));
    if (var == NULL)
        return 0;
    *var = 0;

    /* var 用の領域をハッシュテーブルに登録する */
    if (!TlsHashSet("authinc.var", tls)) {
        free(var);
        return 0;
    }

    return 1;
}

この例では、ハッシュテーブルのキーとして "authinc.var" という文字列を 使っています。 この文字列は適当なものを付けて構いませんが、他の DLL と競合しないように して下さい。 PowerGres に同梱している DLL では、すべてキー文字列は "pg_" で始るようにしていますので、競合を避けるために "pg_" で始まるキー文字列は使用しないで下さい。

PostgreSQL から呼び出せるようにしたい C 関数では、冒頭で上記の関数を 呼び出すようにします。 こうすることで、C 関数が呼びだされた際に、先だって必ず上記の初期化が 行われるようになります。 なお、ハッシュテーブルに確保した領域は、スレッドが消滅する際に削除 されます。


Datum
autoinc(PG_FUNCTION_ARGS)
{
    /* 先ほどの関数を呼びだす */
    if (!autoinc_initialize_tls())
           elog(ERROR, "autoinc: memory exhausted");

   /* 以下省略 */    
}

ハッシュテーブルに登録した int 型変数へのアクセスは、次のようになります。


*(int *)TlsHashGet("mycfunc.var") = 0;      /* 値の代入 */
x = *(int *)TlsHashGet("mycfunc.var") + 1;  /* 値の参照 */

これではプログラムの見た目が複雑になってしまいますし、変数を使っている ようには見えません (実際のところ変数は使ってはいません)。 この欠点は、いささか強引な手法ですが、以下のようなマクロを用意すること で改善できます。


#define var (*(int *)TlsHashGet("mycfunc.var"))

このマクロを使えば、先ほどのプログラムは次のように書くことができる ようになり、外観上は変数を扱っているように見せることができます。


var = 0;       /* 値の代入 */
x = var + 1;   /* 値の参照 */

ハッシュテーブル操作関数の仕様

前節で紹介したハッシュテーブル操作関数の細かな仕様について記しておきます。 ここにあげた関数を使用するには、あらかじめ次のヘッダを include して下さい。


#include <powergres/tlshash.h>

void TlsHashInitialize(void);
ハッシュテーブルを初期化します。 各スレッドは他の操作関数を呼び出す前に、必ず一度この関数を呼び出す 必要があります。 この関数を呼び出さずに、他の操作関数を呼び出したときの動作は未定義 です。 一つのスレッドが複数回この関数を呼び出しても、二度目以降は何も しません。
int TlsHashSet(const char *key, void *value);
文字列 key をキーとしたハッシュテーブルに、 ポインタ値 value を登録します。 登録に成功すればゼロ以外の値が返り、失敗すればゼロが返ります。 すでにキーがテーブルに登録されている場合、登録は失敗し、ゼロが 返ります。 valueNULL でも構いませんが、 TlsHashGet() を使用する際に制約があります (詳しくは TlsHashGet() の項を参照のこと)。
void *TlsHashGet(const char *key);
文字列 key をキーとして登録したポインタ値を、 ハッシュテーブルの中から探し出します。 見つかればそのポインタ値を返し、見つからなければ NULL を返します。 登録したポインタの値が NULL の場合は、キーが 見つからなかった場合との区別がつきませんので、注意して下さい。
void TlsHashUnset(const char *key);
文字列 key をキーとして登録したポインタ値を ハッシュテーブルから削除します。 指定されたキーがテーブルに登録されていなければ、関数は何もせずに 返ってきます。

C 言語関数のコンパイル方法

C 言語関数をコンパイルして DLL を作成するには、PowerGres のヘッダ ファイルやライブラリが必要になります。

ただし、PowerGres インストール時にセットアップタイプに「カスタム」 を選んで、 「アプリケーション開発ツール」をインストールするように していないと、ヘッダファイルやライブラリはインストールされません。 インストールしていない場合は、Windows のコントロールパネルから 「プログラムの追加と削除」ないし「アプリケーションの追加と削除」を 選んで、「アプリケーション開発ツール」を追加でインストールして下さい。

ヘッダファイルは、PowerGres のインストール先の include フォルダに配置されます。 C コンパイラのプリプロセッサに関する設定項目を書き換え、ヘッダファイル の検索フォルダとして、このフォルダを追加して下さい。

同様に、生成する C 言語関数 DLL には PowerGres のライブラリ powergres.lib をリンクする必要があります。 このライブラリは、PowerGres のインストール先の lib フォルダに置かれています。 C コンパイラのリンカに関する設定項目を修正して、ライブラリの検索フォルダ として上記のフォルダを追加し、powergres.lib をリンクする ように指定して下さい。

付録: Windows プログラミングの参考書

(PowerGres の C 言語関数の作成に限らず) 一般に Windows で DLL を作成 するにあたって、プログラミングの参考となる書籍を紹介しておきます。

「Advanced Windows 改訂第 4 版」
Jeffrey Richter 著
株式会社ロングテール/長尾高弘 訳
株式会社アスキー 発行
ISBN 4-7561-3805-5
「Win32システムサービスプログラミング 改訂版」
マーシャル・ブレイン+ロン・リーブス 著
株式会社ドキュメントシステム 訳
株式会社ピアソン・エデュケーション 発行
ISBN 4-89471-371-3

最終更新: 2005 年 7月 21日
Copyright © 2005- SRA OSS, Inc. 日本支社 All rights reserved.