COM を学ぶ(6) : C言語でExcel を操作する(Mingw)

C++Haskell から呼ぶのが大変だというのが分ったので、Ruby の win32ole.c を参考にC言語Excel を操作する関数を書いてみました。
これらの関数を Haskell から呼べば HaskellExcel を操作出来ます(^^)/

  • C++ ではシートから行・列を指定してCellsオブジェクトを取得しましたが、C で同じ方法だと取得に成功はするものの文字化け。 シートからRengeとセル名 "C2" を指定してDISPID_PROPERTYGET を指定して invoke 関数を呼びました。
  • DISPID_PROPERTYPUT のときは渡すパラメータが別に必要のようです。
  • VARIANT の配列を作って渡す方法が分らなくて泣きそうだった・・・。プログラマー's研究所/研究日誌 COMの呼び方に感謝です。
  • "Renge" "C2" を指定するのは win32ole のサンプルで知りました。
/* 
 * gcc exltest.c -lole32 -loleaut32 -luuid -o exltest
 */
#include <stdio.h>
#include <malloc.h>
#include <windows.h>

// 参考 ruby.h
#define ALLOCA_N(type,n) (type*)alloca(sizeof(type)*(n))

IDispatch *mallocDispatch(){
    return (struct IDispatch *)malloc( sizeof(struct IDispatch) );
}

// COM は内部でBSTRを使用しています。
BSTR BSTRfromCstring(char* cstring ){
    int    cstringlen, out_size;
    BSTR   wstr;

    cstringlen = strlen(cstring);
    out_size   = MultiByteToWideChar(CP_ACP, 0, cstring, cstringlen, NULL, 0);
    wstr       = SysAllocStringLen(NULL, out_size);
    MultiByteToWideChar(CP_ACP, 0, cstring, cstringlen, wstr, out_size);
    return wstr;
}

char* CSTRfromBSTR(BSTR bstr){
    int    out_size;
    char   *cstring;

    out_size = WideCharToMultiByte(CP_ACP, 0, (OLECHAR*)bstr, -1, NULL, 0, NULL, NULL);
    cstring  = (char*)malloc((out_size+1) * sizeof(char));
    WideCharToMultiByte(CP_ACP, 0, (OLECHAR*)bstr, -1, cstring, out_size, NULL, NULL);
    return cstring;
}

IDispatch *Variant2Dispatch(VARIANT *pVariant){
    IDispatch *pDispatch;

    if (V_ISBYREF(pVariant))
        pDispatch = *V_DISPATCHREF(pVariant);
    else
        pDispatch = V_DISPATCH(pVariant);

    return pDispatch;
}
// 常にインタフェーステーブルへアクセスする。
// Open,Close などのコマンドからテーブルのディスパッチIDを求め
// 実行する。
HRESULT ComInvoke( PVOID *p, char *ComString ,VARIANTARG *param, int nArgs, USHORT wFlags, VARIANT *result){
    IDispatch   *pDisp;
    DISPID      dispID;
    HRESULT     hr;
    unsigned    short *ucPtr; 
    UINT        puArgErr = 0;
    EXCEPINFO   excepinfo;

    // http://msdn.microsoft.com/ja-jp/library/x6828bcx%28v=VS.80%29.aspx
    // Win32OLE 製作過程の雑記 : invoke メソッドの引数
    // http://homepage1.nifty.com/markey/ruby/win32ole/win32ole03.html#invoke-param
    DISPPARAMS  dispParams = { NULL, NULL, 0, 0 };
    dispParams.rgvarg            = param;  // 引数の配列への参照を表します。
    dispParams.rgdispidNamedArgs = NULL;   // 名前付き引数の dispID の配列(未使用)
    dispParams.cArgs             = nArgs;  // 引数の数を表します。 
    dispParams.cNamedArgs        = 0;      // 名前付き引数の数 (未使用)
    // 参考:ruby win32ole.c  ole_invoke2 関数
    if (wFlags & DISPATCH_PROPERTYPUT) {
        dispParams.cNamedArgs = 1;
        dispParams.rgdispidNamedArgs    = ALLOCA_N( DISPID, 1 );
        dispParams.rgdispidNamedArgs[0] = DISPID_PROPERTYPUT;
    }

    memset( &excepinfo, 0, sizeof(EXCEPINFO));
    pDisp = (IDispatch   *)p;

    // コマンド文字列からディスパッチID取得
    ucPtr = BSTRfromCstring( ComString );
    hr=pDisp->lpVtbl->GetIDsOfNames((IDispatch  *)pDisp, &IID_NULL, &ucPtr, 1, LOCALE_USER_DEFAULT, (DISPID*)&dispID);
    //printf("GetIDsOfNames nArgs:%d  %-10s = %04d hr:%08lx\n",  nArgs , ComString, dispID, hr);

    // ここが肝心のInvokeを実行する部分。
    VariantInit(result);
    hr = pDisp->lpVtbl->Invoke(
             pDisp,                    // 参考: Ruby付属の「OLE View」
             dispID,                   // arg1 - I4 dispidMember        [IN]
             &IID_NULL,                // arg2 - GUID riid              [IN]
             LOCALE_SYSTEM_DEFAULT,    // arg3 - UI4 lcid               [IN]
             wFlags,                   // arg4 - UI2 wFlags             [IN]
             &dispParams,              // arg5 - DISPPARAMS pdispparams [IN]
             result,                   // arg6 - VARIANT pvarResult     [OUT]
             &excepinfo,               // arg7 - EXCEPINFO pexcepinfo   [OUT]
             &puArgErr );              // arg8 - UINT puArgErr          [OUT]
     // printf("Invoke %-10s dispID:%4d hr:%08x puArgErr:%d\n",ComString, dispID, hr,puArgErr);
     SysFreeString(ucPtr);
     return hr;
}

// ProgID("Excel.Application")からCLSID({00024500-0000-0000-C000000000000046})
// を求め、CoCreateInstance APIを呼びます。
IDispatch *InstanceNew(char *ComName){
    IDispatch  *pDisp;
    BSTR       name;
    CLSID      clsid;
    HRESULT    hr=0;

    pDisp = mallocDispatch();
    name  = BSTRfromCstring( ComName );
    hr    = CLSIDFromProgID(name, &clsid);
    // HRESULTは最上位ビットで OK ,NG を表現します。
    // FAILED は hr が 0 より小さいかどうかチェックするマクロ。
    if(FAILED(hr)) {
        hr = CLSIDFromString(name, &clsid);
    }
    hr = CoCreateInstance(&clsid, NULL, CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER, &IID_IDispatch, (void **)&pDisp);
    SysFreeString(name);
    return pDisp;
}

IDispatch *CreateNewObject( PVOID *parentDisp, char *ObjName){
    VARIANT    param, result;
    DISPID     dispID;
    HRESULT    hr = 0;

    VariantInit(&param);
    VariantInit(&result);
    param.vt = VT_EMPTY;
    hr = ComInvoke((void **)parentDisp, ObjName, &param, 0, DISPATCH_PROPERTYGET | DISPATCH_METHOD,&result);
    // printf("CreateNewObject   ObjName:%-14s hr:%08lx\n",ObjName,hr);
    VariantClear(&param);
    return Variant2Dispatch(&result);
}

char *Date2String(DATE date){
    char *buf;
    SYSTEMTIME st;

    VariantTimeToSystemTime(date, &st);
    buf = (char*)malloc(20 * sizeof(char));
    sprintf(buf,"%04d/%02d/%02d %02d:%02d:%02d",
                       st.wYear,st.wMonth,st.wDay,st.wHour,st.wMinute,st.wSecond);
    return buf;
}
char *Number2String(long num){

    char *buf;
    buf = (char*)malloc(30 * sizeof(char));
    sprintf(buf,"%d",num);
    return buf;
}

char *Double2String(double num){
    char *buf;
    buf = (char*)malloc(30 * sizeof(char));
    sprintf(buf,"%f",num);
    return buf;
}

char *Variant2String(VARIANT *result){
    switch(V_VT(result)){
        case VT_I2:  // short
            return Number2String((long)V_I2(result));
            break;
        case VT_I4:  // long
            return Number2String((long)V_I4(result));
            break;
        case VT_R4:  // float
            return Double2String(V_R4(result));
            break;
        case VT_R8:  // double
            return Double2String(V_R8(result));
            break;
        case VT_BOOL: //(True -1,False 0)
            return (V_BOOL(result) ? "True" : "False");
            break;
        case VT_BSTR:
            return CSTRfromBSTR(V_BSTR(result));
            break;
        case VT_DATE:
            return Date2String( V_DATE(result));
            break;
    }
}
// ver = ReadProperty((void **)pExl, "Version");
char *ReadProperty(PVOID *cell, char *PropertyName){
    VARIANT    param, result;

    VariantInit(&param);
    param.vt      = VT_EMPTY;
    ComInvoke((void **)cell, PropertyName,&param, 0, DISPATCH_PROPERTYGET | DISPATCH_METHOD, &result);
    SysFreeString(param.bstrVal);
    return Variant2String(&result);
}
// hr = PutProperty((void **)cell, "Value","ほげ");
HRESULT PutProperty(PVOID *cell, char *PropertyName, char *String){
    VARIANT     result;
    VARIANTARG  param[1];
    BSTR        bstr;
    HRESULT     hr = 0;

    bstr = BSTRfromCstring(String);
    VariantInit(&param[0]);  param[0].vt = VT_BSTR|VT_BYREF;  param[0].pbstrVal = &bstr;
    hr = ComInvoke((void **)cell, PropertyName, param, 1, DISPATCH_PROPERTYPUT, &result);
    VariantClear(&result);
    VariantClear(&param[0]);
    return hr;
}

// GetAbsolutePathName メソッドをコールし、パス名を含めたファイル名を取得
char *GetPathName(IDispatch *fDisp, char *fileName){
    VARIANT    param, result;
    HRESULT    hr = 0;
    char *name, *fullPathName;


    VariantInit(&param);
    param.vt      = VT_BSTR;
    param.bstrVal = BSTRfromCstring(fileName);

    hr = ComInvoke((void **)fDisp, "GetAbsolutePathName", &param, 1, DISPATCH_METHOD, &result);
    name = CSTRfromBSTR(result.bstrVal);
    fullPathName = malloc(strlen(name)+1 );
    strcpy(fullPathName, name);
    free(name);
    VariantClear(&param);
    VariantClear(&result);
    return fullPathName;
}
// Scripting.FileSystemObject を作りパス名を含めたファイル名を取得
// in  : fileName
// out : fullPathName
char *getFullPathName(char *fileName){
    IDispatch  *fileSystemObj;

    fileSystemObj = InstanceNew("Scripting.FileSystemObject");
    return GetPathName(fileSystemObj, fileName);
}

HRESULT WorkBooksOpen(PVOID *workBooks, char *fullPathName){
    VARIANT     result;
    VARIANTARG  param[1];
    BSTR        bstr;
    HRESULT     hr = 0;

    //http://www.hcn.zaq.ne.jp/no-ji/reseach/990905.htm
    // プログラマー's研究所/研究日誌 [COM] COMの呼び方
    bstr = BSTRfromCstring(fullPathName);
    VariantInit(&param[0]);  param[0].vt = VT_BSTR|VT_BYREF;  param[0].pbstrVal = &bstr;

    hr = ComInvoke((void **)workBooks, "Open", param, 1, DISPATCH_METHOD, &result);
    VariantClear(&result);
    return hr;
}

IDispatch *GetSheetObject(PVOID *workSheets, long n){
    VARIANT     result;
    VARIANTARG  param[1];
    HRESULT     hr = 0;

    VariantInit(&param[0]);  param[0].vt = VT_I4;   param[0].lVal = n;
    ComInvoke((void **)workSheets, "Item", param, 1, DISPATCH_PROPERTYGET , &result);
    return Variant2Dispatch(&result);
}

// cell = getCellObject((void **)sheet, "C2");
IDispatch *GetCellObject(PVOID *sheet, char *cells){
    VARIANT     result;
    VARIANTARG  param[1];
    BSTR        bstr;
    HRESULT     hr = 0;

    bstr = BSTRfromCstring(cells);

    VariantInit(&param[0]);  param[0].vt = VT_BSTR|VT_BYREF;   param[0].pbstrVal = &bstr;
    ComInvoke((void **)sheet, "Range", param, 1, DISPATCH_PROPERTYGET , &result);
    return Variant2Dispatch(&result);
}

// call DispatchMethod((void **)workBooks, "Close");
// call DispatchMethod((void **)pExl, "Quit");
HRESULT DispatchMethod(PVOID *pDisp, char *command){
    VARIANT    param, result;
    HRESULT    hr = 0;

    VariantInit(&param);
    param.vt      = VT_EMPTY;
    hr = ComInvoke((void **)pDisp, command, &param, 0, DISPATCH_METHOD,&result);
    VariantClear(&param);
    VariantClear(&result);
    return hr;
}

void ReleaseObject( PVOID *pDisp ){
    ((IDispatch  *)pDisp)->lpVtbl->Release( (void *)pDisp);
}

int main(int argc, char* argv[]){
    IDispatch  *pExl, *workBooks, *ActiveWBook, *workSheets, *sheet, *cell;
    char       *ver, *celldat;
    HRESULT    hr = 0;

    OleInitialize(0);
    pExl        = InstanceNew("Excel.Application");
    ver         = ReadProperty((void **)pExl, "Version");
    printf("Version:%s\n",ver);  //=> Version:9.0
    workBooks   = CreateNewObject((void **)pExl,       "Workbooks");
    hr          = WorkBooksOpen(  (void **)workBooks,  getFullPathName("sample2.xls"));
    ActiveWBook = CreateNewObject((void **)pExl,        "ActiveWorkbook");
    workSheets  = CreateNewObject((void **)ActiveWBook, "Worksheets");
    sheet       = GetSheetObject( (void **)workSheets,  2);       // 2 番目のシート
    cell        = GetCellObject(  (void **)sheet,       "C2");    // C2 セル
    hr = PutProperty((void **)cell, "Value","ほげ");
    celldat     = ReadProperty(   (void **)cell,        "Value");
    printf("celldat:%s\n",celldat); //=> ほげ
    DispatchMethod((void **)ActiveWBook, "Save");
    DispatchMethod((void **)workBooks,   "Close");
    DispatchMethod((void **)pExl,        "Quit");
    ReleaseObject((void **)cell);
    ReleaseObject((void **)sheet);
    ReleaseObject((void **)workSheets);
    ReleaseObject((void **)ActiveWBook);
    ReleaseObject((void **)workBooks);
    ReleaseObject((void **)pExl);
    OleUninitialize();
    return 0;
}