Peta Walkers

HOME: 開発日記

2003.10.19(日) - FileMakerプラグイン"PetaExecute"を作る

 小規模データベースの構築に便利なFileMakerを拡張するプラグインを作ってみた。

資料集め

 Webには資料なんてほとんど無いです。

 今回の環境。

 いまどきのMacの環境が無いのでWindows限定です。 Mac版FileMakerはAppleScriptがあるので他のアプリケーションとの連携が楽でうらやましいですね。

序章

 FileMaker Developer 6は購入してから1年弱というのにランタイムソリューションはおろか、データベースすら作っていませんでした。 なんともったいない! 最近Java一辺倒で、ペタクローンの改良もSWTの壁にぶつかったので気分転換にFileMakerのプラグインを作ってみることにしたのです。

 そういうわけで遅ればせながらデベロッパーズガイドを読み返してみました。

プラグインの概要

 FileMakerプラグインは計算フィールドやスクリプトステップ中の計算式で呼び出すことができる外部関数と、 FileMakerのアイドル時に呼び出されるタイプのものがあります。 今回は外部関数を作ってみることにします。

 プラグインの開発はサンプルのプロジェクトを元に行います。 Windows向けにはCodeWarriorのプロジェクトとVisual C++のプロジェクトがあります。 プロジェクトを開いてビルドすると"(Output)"フォルダにFMExample.fmxが出来上がります。

 出来上がったプラグインはFileMakerフォルダのSystemフォルダに入れると使用できるようになります。

今回のプラグインの仕様

 Mac版のFileMakerはAppleScriptによって外部のアプリケーションと柔軟に通信することができますが、Windows版のFileMakerは基本的に双方向の通信はできません(たぶん)。

 そこでコマンドラインを渡し、その実行結果を得る外部関数を作ってみます。 このシンプルなプラグインによってFileMakerの世界が大幅に拡張されること間違いなしです^_^;;。

 次の形式で呼び出したいと思います。

External("Peta-Execute", parameter)

プラグインインターフェース

 プラグインは入力も出力も文字列で行います。 サンプルプロジェクトのFMFunct.cに定義されているDo_ExternalFunction()をカスタマイズすることで独自の関数を作れるようになります。 ここらへんのことはデベロッパーズガイドに載っているので割愛します。

void Do_ExternalFunction(
    long functionID,   // == リソースID - 144
    FHandle parameter, // External("Peta-Execute", parameter)
    FHandle result     // 外部関数の戻り値
);

不要な機能の削除とリソース編集

 設定ダイアログが必要ないので削除します。 アプリケーション初期設定の設定ボタンを無効にしなければいけませんので、 FMExample.rcを開いてID131の文字列(フィーチャーストリング)を書き換えます。

 Xmpl1YYYYnYの(1オリジンで)6番目の文字をnに書き換え、Xmpl1nYYYnYにします。

 XmplとかFMExampleになっているところも好きな名前に書き換えます(本当は固有の名前にするためにAppleとFileMakerに登録が必要です)。

FileMakerのAPI

 とりあえず確認したのはメモリ操作です。

FHandle FMX_NewHandle(long size);
void    FMX_SetHandleSize(FHandle h, long size);
long    FMX_GetHandleSize(FHandle h);
void    FMX_DisposeHandle(FHandle h);
他

 デバッグプリント。APIというよりおまけで付いている関数です。

void DisplayNumber(int messageid, int value);
void DisplayString(int messageid, char *cstr);

機能の実装

 Do_ExternalFunction()で受け取ったパラメータをコマンドとして実行し、その標準出力を取得し、FHandle resultに格納する部分を実装します。

 getCommandOutput()の実装はモルタルコのプログラマ日記 2000年5月21日を元に作成しました。 コメントが//形式になっているのは元がC++だからですが、基本的に私の手抜きです。ご容赦を。

/*
    外部関数
*/
void Do_ExternalFunction(long functionId, FHandle parameter, FHandle result) 
{
    switch (functionId) 
    {
        case 0:
            PetaExecute(parameter, result);
        break;
        default:
        break;
    }
}

 単にPetaExecute()を呼び出していますが、functionIdを判別することによって1つのプラグインで複数の関数を提供することができます。

 FHandleの実体はunsigned char*へのポインタです。 プラグインは入力も出力も文字列で行います。

/*
    PetaExecuteの実装
*/
void PetaExecute(FHandle parameter, FHandle result) 
{
    long          hdlsize, resultSize;
    FHandle temp;
    FHandle command;
    /* Only do something if we got something in the parameter */
    hdlsize = FMX_GetHandleSize(parameter);
    
    if (hdlsize <= 0){
        return;
    }
    /* パラメータの最後に\0をつける */
    command = FMX_NewHandle(hdlsize + 1);
    BlockMoveData(*parameter, *command, hdlsize);
    (*command)[hdlsize] = '\0';

    /* コマンドを実行 */
    temp = getCommandOutput(*command);
    FMX_DisposeHandle(command);
    if (temp == NULL){
        return;
    }
    resultSize = FMX_GetHandleSize(temp);
    
    FMX_SetHandleSize(result, resultSize);
    if (FMX_MemoryError() == 0) {
        BlockMoveData(*temp, *result, resultSize);
    }
    FMX_DisposeHandle(temp);
}

 パラメータをC文字列に変換し、コマンドの実行結果をresultにコピーしています。

 FHandleはメモリの長さを知っているのでNULLターミネートしていません。 LPTSTRはC文字列なのでNULLターミネートしている必要があります。

/*
    コマンドを実行して標準出力を得る
*/
FHandle getCommandOutput(LPTSTR command){
    // 結果
    FHandle result = NULL;
    
    // 読み込んだ合計バイト数
    DWORD curReadSize = 0;
    DWORD curPosition = 0;

    // コマンドの終了コード
    DWORD res;
    
    // 標準出力用パイプ
    HANDLE hReadPipe;
    HANDLE hWritePipe;

    // エラー出力用パイプ
    HANDLE hErrReadPipe    = NULL,    hErrWritePipe =    NULL;

    // コマンドライン
    LPTSTR lpCommandLine = command;
    
    // 起動情報
    STARTUPINFO    StartupInfo;
    PROCESS_INFORMATION    ProcessInfo;
    
    // セキュリティ属性(ハンドル継承を指定)
    SECURITY_ATTRIBUTES    sa;
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.lpSecurityDescriptor    = NULL;
    sa.bInheritHandle =    TRUE;

    // 標準出力パイプの作成
    CreatePipe(&hReadPipe, &hWritePipe, &sa, 8192);

    // エラー出力パイプの作成
    CreatePipe(&hErrReadPipe, &hErrWritePipe, &sa, 8192);


    // メモリを確保して0で初期化
    ZeroMemory(&StartupInfo, sizeof(STARTUPINFO));
    StartupInfo.cb = sizeof(STARTUPINFO);

    // ハンドルの継承を指定
    StartupInfo.dwFlags    = STARTF_USESTDHANDLES;

    // DOS窓を表示しない
    StartupInfo.wShowWindow    = SW_HIDE;

    // 標準出力ハンドルとエラー出力ハンドルを設定
    StartupInfo.hStdOutput = hWritePipe;
    StartupInfo.hStdError  = hErrWritePipe;

    // コンソールアプリ起動
    if (CreateProcess(
        NULL,
        lpCommandLine,
        &sa,
        NULL,
        TRUE,             // ハンドルの継承
        DETACHED_PROCESS, // DOS窓を表示しないための指定
        NULL,
        NULL,
        &StartupInfo, &ProcessInfo))
    {
        // パイプ内容受け取り用バッファ
        char bufStdOut[8192], bufErrOut[8192];
        DWORD dwStdOut = 0, dwErrOut = 0;
        DWORD dwRet;

        // メモリを確保
        result = FMX_NewHandle(1);

        // プロセス起動中のパイプ内容受け取り処理
        while (
            (dwRet = WaitForSingleObject(ProcessInfo.hProcess, 0))
                != WAIT_ABANDONED)
        {
            memset(bufStdOut, 0, sizeof(bufStdOut));
            memset(bufErrOut, 0, sizeof(bufErrOut));

            // 標準出力パイプの内容を調べる
            PeekNamedPipe(hReadPipe, NULL, 0,    NULL, &dwStdOut, NULL);
            if (dwStdOut > 0)
            {
                //    内容が存在すれば、読み取る
                ReadFile(
                    hReadPipe,
                    bufStdOut,
                    sizeof(bufStdOut) - 1,
                    &dwStdOut,
                    NULL);
                //    bufStdOut にパイプ出力が入る
                
                // 読み込み済みのサイズ
                curReadSize += dwStdOut;
                // メモリを再割り当て
                FMX_SetHandleSize(result, curReadSize);
                // コピー
                dwStdOut = (DWORD)copyWithoutLF(bufStdOut, *result + curPosition, dwStdOut);
                
                // もう一度調節
                curReadSize = curPosition + dwStdOut;
                FMX_SetHandleSize(result, curReadSize);
                
                // 位置を進めておく
                curPosition = curReadSize;
            }

            if (dwRet == WAIT_OBJECT_0)
                break;
        }

        // プロセスハンドルとスレッドハンドルを閉じる
        GetExitCodeProcess(ProcessInfo.hProcess, &res);
        CloseHandle(ProcessInfo.hProcess);
        CloseHandle(ProcessInfo.hThread);
    }

    // すべてのパイプを閉じる
    CloseHandle(hWritePipe);
    CloseHandle(hReadPipe);
    CloseHandle(hErrWritePipe);
    CloseHandle(hErrReadPipe);

    return result;
}

 パイプとプロセスを生成してコマンドを実行し、標準出力をFHandleにコピーして返します。 リファクタリングが必要ですね。

/*
    LF以外をコピーする
    改行コードは\rだけで十分。
*/
long copyWithoutLF(unsigned char *src, unsigned char *dest, long length){
    long i;
    long pos = 0;
    for (i = 0; i < length; i++){
        if (src[i] == '\x0a'){
            continue;
        }
        dest[pos] = src[i];
        pos++;
    }
    return pos;
}

 もともとMac生まれのFileMakerは改行コードに\rだけを使います。 copyWithoutLF()はそのための関数です。

PetaExecuteの応用範囲

 PetaExecuteの応用範囲は文字列で入出力できるものすべてです。 たとえばPerlスクリプトを呼び出すことによってファイル操作やネットワーク処理を行うことができるようになります。

 FileMakerのPluginsに掲載されているプラグインにはかないませんが、FileMaker側から何かを実行したいという場面では役に立つでしょう。

あとがき

 今回FileMaker関連の情報を検索して気になったのはFileMakerのユーザーが作ったページです。 ほとんど更新が停止してから数年が経過しているページばかりで、 一時期の活気がまったくなくなっているのは何となく寂しい気がします。 まあ、みんな熟練して交換するほどの情報が無くなってしまったんだと思いますけど。

 自分的にはこれから使う機会が減っていくとは思いますが、 FileMakerほど簡単に習得できて短時間で構築できるデータベースは他に無いですからFileMaker Inc.には頑張ってもらいたいものです。

※コンパイルしたプラグインは後日公開したいと思います。→公開しました→PetaExecute

HOME: 開発日記