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)
DLL内で宣言した場合
pacなし、ダミーメンバーなし
配置は異なるというか、構造体のサイズ自体が異なる。
- LenB(SampleType):16
- sizeof(SampleNoPack):24
このため、正しい値を渡すことが出来ない。
VBAから渡した場合(SampleType → SampleNoPack)
DLL内で宣言した場合
プロシージャを抜ける際に、以下のエラーメッセージが表示された。
pacなし、ダミーメンバーあり
配置は同一となったように見えるこちらも、構造体のサイズ自体が異なる。
- LenB(SampleTypeWithDummy):20
- sizeof(SampleNoPackWithDummy):24
前述のパターンと違い、こちらの場合は、メンバーの配置が同一のため、値の受け渡しは出来た。
VBAから渡した場合(SampleTypeWithDummy → SampleNoPackWithDummy)
DLL内で宣言した場合
pacなし、ダミーメンバーあり2
配置は同一となったこちらは、構造体のサイズが同じ。
- LenB(SampleTypeWithDummy2):24
- sizeof(SampleNoPackWithDummy2):24
VBAから渡した場合(SampleTypeWithDummy2 → SampleNoPackWithDummy2)
DLL内で宣言した場合
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
DllTestSetStructArray
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のデバッグの方法を簡単に説明して、このシリーズを終了とするつもりです。