空腹おやじのログと備忘録

VBA(主にExcel)でいろいろな実験的な事とか、Linuxのコマンドとか設定とかについて忘れないように、あれこれと・・・

ExcelのVBAで使えるDLLを、C++(Visual Studio 2017)で作る。・・・その5 構造体の受け渡し

初めに

これまで、数値、文字列、配列、バリアントと各種の型の受け渡しとやってきましたが、今回は、構造体です。

構造体は、VBAとDLLのそれぞれでの定義を間違えると、

  • 正しいデータの受け渡しが出来ない
  • 正しくデータを読み込めない
  • 正しくデータを更新できない

といった事になりかねません。
VBA側、DLL側の両方で、正しい定義を行い、十分な確認をすることが必要です。

事前準備

アライメント

構造体を扱う場合、注意が必要なのはアライメントです。

アライメントについては、以下のサイトに詳しく書かれているので、参考にしてください。
www7b.biglobe.ne.jp
www5d.biglobe.ne.jp

アライメントを調整する方法としては、

  • #pragma pack(n) を使用する
  • 構造体のメンバーに、アライメントを調整するためのダミーメンバーを加える

などがあります。

#pragma pack の使用については、Microsoft のサイト内に、以下のような記述があります。

VBA では、ユーザー定義データ型のデータ要素は 4 バイト境界にパッキングされます。
Visual Studio では、このデータ要素が既定で 8 バイト境界にパッキングされます。
そのため、C/C++ 構造体の定義は

#pragma pack(4)
//ここに構造体を定義
#pragma pack()

ブロックで囲んで要素の配置がずれないようにする必要があります。

https://docs.microsoft.com/ja-jp/office/client-developer/excel/how-to-access-dlls-in-excel#argument-types-in-cc-and-vba

#pragma pac による要素の配置への影響の確認

以下のような構造体を定義。
VBA

Private Type SampleType
    iValue  As Integer
    dValue  As Double
    lValue  As Long
End Type

Private Type SampleTypeWithDummy
    iValue      As Integer
    byDummy(0 To 5)  As Byte
    dValue      As Double
    lValue      As Long
End Type

Private Type SampleTypeWithDummy2
    iValue      As Integer
    byDummy(0 To 5)  As Byte
    dValue      As Double
    lValue      As Long
    lDummy      As Long
End Type

DLL

struct SampleNoPack
{
    short   nValue;
    double  dValue;
    long    lValue;
};

struct SampleNoPackWithDummy
{
    short   nValue;
    char    cDummy[6];
    double  dValue;
    long    lValue;
};

struct SampleNoPackWithDummy2
{
    short   nValue;
    char    cDummy[6];
    double  dValue;
    long    lValue;
    long    lDummy;
};

#pragma pack(4)
struct SamplePack
{
    short   nValue;
    double  dValue;
    long    lValue;
};
#pragma pack()

VBAから、DLLの関数に渡してみる。

    __declspec(dllexport) void WINAPI SetStructP(SamplePack* pst);
    __declspec(dllexport) void WINAPI SetStructNP(SampleNoPack* pst);
    __declspec(dllexport) void WINAPI SetStructNPWD(SampleNoPackWithDummy* pst);
    __declspec(dllexport) void WINAPI SetStructNPWD2(SampleNoPackWithDummy2* pst);

なお、各構造体のメンバーには、以下の値を設定しました。

メンバー 色(下図)
iValue 0x1234
dValue 3.5 マゼンタ
lValue 0x56789ABC シアン

それぞれのDLL内でのメモリ上の状態は、以下のようになりました。

pacあり、ダミーメンバーなし
配置は同一となった
VBAから渡した場合 (SampleType → SamplePack)
f:id:Z1000S:20200115115736j:plain
DLL内で宣言した場合
f:id:Z1000S:20200115115752j:plain

pacなし、ダミーメンバーなし
配置は異なる
というか、構造体のサイズ自体が異なる。

  • LenB(SampleType):16
  • sizeof(SampleNoPack):24

このため、正しい値を渡すことが出来ない

VBAから渡した場合(SampleType → SampleNoPack)
f:id:Z1000S:20200115115855j:plain
DLL内で宣言した場合
f:id:Z1000S:20200115115904j:plain

プロシージャを抜ける際に、以下のエラーメッセージが表示された。
f:id:Z1000S:20200115150139j:plain

pacなし、ダミーメンバーあり
配置は同一となったように見える
こちらも、構造体のサイズ自体が異なる。

  • LenB(SampleTypeWithDummy):20
  • sizeof(SampleNoPackWithDummy):24

前述のパターンと違い、こちらの場合は、メンバーの配置が同一のため、値の受け渡しは出来た。

VBAから渡した場合(SampleTypeWithDummy → SampleNoPackWithDummy)
f:id:Z1000S:20200115120817j:plain
DLL内で宣言した場合
f:id:Z1000S:20200115115939j:plain

pacなし、ダミーメンバーあり2
配置は同一となった
こちらは、構造体のサイズが同じ。

  • LenB(SampleTypeWithDummy2):24
  • sizeof(SampleNoPackWithDummy2):24

VBAから渡した場合(SampleTypeWithDummy2 → SampleNoPackWithDummy2)
f:id:Z1000S:20200115115955j:plain
DLL内で宣言した場合
f:id:Z1000S:20200115120007j:plain
VBAから渡した方の最後の部分4Byteが0x00 × 4 でないのは、VBA側で値を設定したためなので、ここは気にしないで下さい。

メモ

アライメントの調整によりメンバー間に発生する領域は、

  • VBAから渡された場合、0x00で埋められている。
  • DLLで生成した変数の場合、0xCCで埋められている。(Visual Studio C++ では、未初期化の場合、この値になるようです。)

となり、全く同じにはなっていない。(DLL側で、変数宣言時に、0x00で初期化すれば、VBAと同じ状態にすることは可能)

構造体の構成によっては、アライメントの調整は不要となる場合もありえるが、後々変更が発生する可能性があるのであれば、予め対応しておいた方が良さそう。

コード

DLL

AccessibleFromVBA.h

#pragma once

extern "C"
{
#define ACCESSIBLEFROMVBA_API __declspec(dllexport)

//中略
#pragma pack(4)
    struct SamplePack
    {
        short   nValue;
        double  dValue;
        long    lValue;
    };
#pragma pack()

    ACCESSIBLEFROMVBA_API void WINAPI GetStructP(SamplePack* pst);
    ACCESSIBLEFROMVBA_API void WINAPI GetStructPArray(LPSAFEARRAY* ppsa);

    ACCESSIBLEFROMVBA_API void WINAPI SetStructP(const SamplePack* pst);
    ACCESSIBLEFROMVBA_API void WINAPI SetStructPArray(const LPSAFEARRAY* ppsa);
}

AccessibleFromVBA.cpp
追加分

ACCESSIBLEFROMVBA_API void WINAPI SetStructP(const SamplePack* pst)
{
    std::wstringstream ss;

    ss << pst->nValue << L"\n"
       << pst->dValue << L"\n"
       << pst->lValue << L"\n";

    MessageBox(NULL, ss.str().c_str(), L"SetStructP", MB_OK | MB_ICONINFORMATION);

    return;
}

ACCESSIBLEFROMVBA_API void WINAPI SetStructPArray(const LPSAFEARRAY* ppsa)
{
    //格納されているデータ型の確認
    VARTYPE vt;
    HRESULT hResult = SafeArrayGetVartype(*ppsa, &vt);

    if (SUCCEEDED(hResult))
    {
        //VBAから構造体を渡した場合、hResult は、E_INVALIDARG が返るので
        //FAILED(hResult) で弾かない。
        return;
    }

    //要素のサイズ
    UINT uiElemBytes = SafeArrayGetElemsize(*ppsa);

    if (uiElemBytes != sizeof(SamplePack))
    {
        //構造体のサイズと異なる場合、処理しない。
        return;
    }

    //次元数
    UINT uiDims = SafeArrayGetDim(*ppsa);

    std::wstringstream ss;

    for (UINT i = 1; i <= uiDims; ++i)
    {
        LONG lLBound, lUBound;
        hResult = SafeArrayGetLBound(*ppsa, i, &lLBound);
        hResult = SafeArrayGetUBound(*ppsa, i, &lUBound);

        ss << i << L"次元\n"
           << L" LBound:" << lLBound << L"\n"
           << L" UBound:" << lUBound << L"\n";
    }

    ss << L"データ型:SamplePack\n";

    if (uiDims == 1)
    {
        LONG lIndex;

        LONG lLBound;
        LONG lUBound;

        hResult = SafeArrayGetLBound(*ppsa, 1, &lLBound);
        hResult = SafeArrayGetUBound(*ppsa, 1, &lUBound);

        for (LONG i = lLBound; i <= lUBound; ++i)
        {
            SamplePack sp;
            hResult = SafeArrayGetElement(*ppsa, &i, &sp);

            ss << sp.nValue << L"\n"
               << sp.dValue << L"\n"
               << sp.lValue << L"\n"
               << L"\n";
        }
    }

    MessageBox(NULL, ss.str().c_str(), L"SetStructPArray", MB_OK | MB_ICONINFORMATION);

    return;
}

ACCESSIBLEFROMVBA_API void WINAPI GetStructP(SamplePack* pst)
{
    pst->nValue *= 2;
    pst->dValue *= 2;
    pst->lValue *= 2;

    return;
}

ACCESSIBLEFROMVBA_API void WINAPI GetStructPArray(LPSAFEARRAY* ppsa)
{
    //格納されているデータ型の確認
    VARTYPE vt;
    HRESULT hResult = SafeArrayGetVartype(*ppsa, &vt);

    if (SUCCEEDED(hResult))
    {
        //VBAから構造体を渡した場合、hResult は、E_INVALIDARG が返るので
        //FAILED(hResult) で弾かない。
        return;
    }

    //要素のサイズ
    UINT uiElemBytes = SafeArrayGetElemsize(*ppsa);

    if (uiElemBytes != sizeof(SamplePack))
    {
        //構造体のサイズと異なる場合、処理しない。
        return;
    }

    //次元数
    UINT uiDims = SafeArrayGetDim(*ppsa);

    if (uiDims == 1)
    {
        LONG lLBound;
        LONG lUBound;

        hResult = SafeArrayGetLBound(*ppsa, 1, &lLBound);
        hResult = SafeArrayGetUBound(*ppsa, 1, &lUBound);

        for (LONG i = lLBound; i <= lUBound; ++i)
        {
            SamplePack sp;
            hResult = SafeArrayGetElement(*ppsa, &i, &sp);

            sp.nValue *= 2;
            sp.dValue *= 2;
            sp.lValue *= 2;

            hResult = SafeArrayPutElement(*ppsa, &i, &sp);
        }
    }

    return;
}

AccessibleFromVBA.def

LIBRARY AccessibleFromVba

EXPORTS
    DoNothing
    GetNumberI
    GetNumberI2
    SetString
    SetStringS
    GetStringByParam
    GetStringByParamS
    GetStringByRetVal
    GetStringByRetValS
    GetArrayPE
    GetArrayAD
    GetArray2
    SetArrayGE
    SetArrayAD
    GetArrayV
    SetArrayV
    GetStructP
    GetStructPArray
    SetStructP
    SetStructPArray
VBA
Private Type SampleType
    iValue  As Integer
    dValue  As Double
    lValue  As Long
End Type

Private Declare Sub SetStructP Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" (ByRef udtSample As SampleType)
Private Declare Sub SetStructPArray Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" (ByRef udtSample() As SampleType)

Private Declare Sub GetStructP Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" (ByRef udtSample As SampleType)
Private Declare Sub GetStructPArray Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" (ByRef udtSample() As SampleType)


Public Sub DllTestSetStruct()

    Dim udtSample   As SampleType

    udtSample.iValue = 18
    udtSample.dValue = 3.5
    udtSample.lValue = 52

    Call SetStructP(udtSample)

End Sub

Public Sub DllTestSetStructArray()

    Dim udtSample(1)   As SampleType

    udtSample(0).iValue = 1234
    udtSample(0).dValue = 3.5
    udtSample(0).lValue = 56789

    udtSample(1).iValue = 3456
    udtSample(1).dValue = 4.5
    udtSample(1).lValue = 7890

    Call SetStructPArray(udtSample)

End Sub

Public Sub DllTestGetStruct()

    Dim udtSample   As SampleType

    udtSample.iValue = 1000
    udtSample.dValue = 3.5
    udtSample.lValue = 200000

    Debug.Print "Before"
    Debug.Print udtSample.iValue, udtSample.dValue, udtSample.lValue

    Call GetStructP(udtSample)

    Debug.Print "After"
    Debug.Print udtSample.iValue, udtSample.dValue, udtSample.lValue

End Sub

Public Sub DllTestGetStructArray()

    Dim udtSample(1)    As SampleType
    Dim i               As Long

    udtSample(0).iValue = 1000
    udtSample(0).dValue = 3.5
    udtSample(0).lValue = 2000000

    udtSample(1).iValue = 3000
    udtSample(1).dValue = 4.5
    udtSample(1).lValue = 6000000

    Debug.Print "Before"
    For i = LBound(udtSample) To UBound(udtSample)
        Debug.Print udtSample(i).iValue, udtSample(i).dValue, udtSample(i).lValue
    Next i

    Call GetStructPArray(udtSample)

    Debug.Print "After"
    For i = LBound(udtSample) To UBound(udtSample)
        Debug.Print udtSample(i).iValue, udtSample(i).dValue, udtSample(i).lValue
    Next i

End Sub

実行結果

DllTestSetStruct
f:id:Z1000S:20200115204055j:plain

DllTestSetStructArray
f:id:Z1000S:20200115203437j:plain

DllTestGetStruct

Before
 1000          3.5           200000 
After
 2000          7             400000 

DllTestGetStructArray

Before
 1000          3.5           2000000 
 3000          4.5           6000000 
After
 2000          7             4000000 
 6000          9             12000000 

まとめ

構造体の受け渡しでは、アライメントに十分に気をつける必要があることが確認できました。

構造体の配列の受け渡しは、SAFEARRAYで処理しようとした場合、これまでのやり方のように、vtで型を判断することが出来ず、構造体のサイズで判断しました。
調べていると、VT_RECORD というキーワードが出てくるが、VBAではなく、VBでの処理であり、いろいろと面倒な手続きがあるようで、VBAでの方法を見つけられず、上記のような処理で妥協していまいました。

次回予告

次回はDLLのデバッグの方法を簡単に説明して、このシリーズを終了とするつもりです。