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

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

ExcelのVBAで使えるDLLを、C++(Visual Studio 2017)で作る。・・・その6 デバッグ編

初めに

DLLを作ってきましたが、作ったからには、動作確認が必要です。
今回は、DLLのデバッグ方法についてです。

このシリーズの記事を読んでくださった方は、基本的にVBAデバッグ経験者と思います。
ブレークポイントを設定して、ステップ実行したり・・・
基本的な部分は、DLLのデバッグでも同様です。

私の場合、VBAのコードから、DLLのコードにどうやって入っていくのかが
最初の時、わからなかったのでそのあたりについて説明します。
VBAでのデバッグ経験があることを前提に、類似した細かい部分は、端折ります。)

キーワード

アタッチ

アタッチとは、

デバッガーがプロセスやタスクを監視対象にすること

https://www.wdic.org/w/TECH/%E3%82%A2%E3%82%BF%E3%83%83%E3%83%81

今回の場合、Excelに接続して、監視できるようにするといった感じでしょうか。

デタッチ

こちらは、アタッチの逆。
アタッチした対象を監視下から外して、切り離すことです。

実際にやってみる

操作の流れ
  1. DLLを呼び出すExcelファイルの起動
  2. Visual Studio(DLLソリューション)の起動
  3. アタッチ
  4. DLL側コードへのブレークポイント設定
  5. VBAからDLL関数の呼び出し
  6. DLLコードのステップ実行
  7. デタッチ
アタッチ
  1. 対象のExcelファイルと、DLLソリューションを開く。
    • Visual Studioソリューション構成が「Debug」になっているか、確認しておくこと。
      f:id:Z1000S:20200126090401j:plain
    • DLLのプロジェクトがビルド済みであること。
  2. Visual Studioのメニューから、プロセスにアタッチを選択。
    • デバッグ」-「プロセスにアタッチ」をクリック
      f:id:Z1000S:20200126150813j:plain
  3. プロセスにアタッチダイアログから、DLLの呼び出し元となるExcelを探して、選択し、アタッチボタンをクリック。
    f:id:Z1000S:20200126151749j:plain
ブレークポイント

デバッグを開始する部分に、ブレークポイントを設定する。
F9キーで、ブレークポイントの設定、解除ができます。
ブレークポイントを設定したら、VBA側からDLLの関数を呼び出すコードを実行します。

ステップイン

1コードステートメントを実行します。
対象コードステートメントが、関数の呼び出しの場合、呼び出し先の関数の中の行へ入っていきます。
ショートカットキーは、F11キーです。

ステップオーバー

1コードステートメントを実行します。
ステップインと違うのは、対象行が、関数の呼び出しの場合、
呼び出し先の関数を実行しますが、呼び出し先関数の内部コードをステップ実行しません。
ショートカットキーは、F10キーです。

ステップアウト

ステップインとは逆に、ステップインした関数から抜ける場合に使用します。
ショートカットキーは、Shift + F11キーです。

続行

次のブレークポイントがある場合には、次のブレークポイントまで実行します。
次のブレークポイントがない場合、最後まで処理を続行します。
ショートカットキーは、F5キーです。

デバッグの停止

f:id:Z1000S:20200126162032j:plain

全てデタッチ

f:id:Z1000S:20200126152334j:plain

プロセスに再アタッチ

一度アタッチしたプロセスをデタッチした後に、再度同じプロセスにアタッチしたい場合には、プロセスに再アタッチを選択すると、アタッチするプロセスを選択する手間が省けます。
ショートカットキーは、Shift + Alt + P です。
f:id:Z1000S:20200126151926j:plain

ウォッチ

変数を右クリックして、ウォッチの追加をクリック
f:id:Z1000S:20200126165524j:plain
確認してみる。
f:id:Z1000S:20200126164845j:plain

自動変数

ウォッチのように自分で追加しなくても、スコープに応じて表示される変数が変わっていく。
f:id:Z1000S:20200126165758j:plain

ローカル

ウォッチのように自分で追加しなくても、スコープに応じて表示される変数が変わっていく。
f:id:Z1000S:20200126165811j:plain

メモリ

変数のアドレスを指定したり、アドレスを直接指定して、該当アドレスのメモリ状態を確認することが出来ます。
構造体の回で、packの指定有無で、データの配置を確認したときは、この方法を使用しました。
f:id:Z1000S:20200126152500j:plain

変数ppsaを選択する様子
f:id:Z1000S:20200126163540j:plain
選択した変数のアドレスのメモリ状態
f:id:Z1000S:20200126163552j:plain

アドレスを指定しなければいけないので、ポインタ変数の場合はそのまま指定できますが、
通常の変数の場合には、先頭に & を付けて指定する必要があります。
例えば、
int hoge;
の場合には、
&hoge
と指定します。

ポインタ変数の場合には、ローカルウィンドウなどからドラッグ・アンド・ドロップでも指定できます。

主なショートカットキー

VBAVisual Studio では、微妙にショートカットキーが違うので・・・

VBE でF8キーを押して、Visula Studio に入っても、F8キーを押して、
「あれ、進まない?あっ、F10だった。」とかよくやってますwww

項目VBAVisual Studio備考
ブレークポイントの設定/解除F9F9
全てのブレークポイントの解除Ctrrl + Shift + F9Ctrrl + Shift + F9
ステップインF8F11
ステップオーバーShift + F8F10
ステップアウトCtrl + Shift + F8Shift + F11
カーソル行の前まで実行Ctrl + F8Ctrl + F10
デバッグの停止Shift + F5

その他(情報提供依頼)

Rubberduckというアドインがあって、便利そうだのだけど、
こいつをインストールしたところ、Visual Studioでアタッチしても、ExcelのVBEから入っていけない(デバッグできない)現象に遭遇。
現状アンイストールしか対応方法がわからずじまい。
解決策をご存知の方、いらっしゃいましたら教えてください。

最後に

今回で、VBAで使えるDLL作成に関する記事は終了です。
後半は、手抜き気味のような気もしますが、参考になれば幸いです。

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のデバッグの方法を簡単に説明して、このシリーズを終了とするつもりです。

ExcelのVBAで使えるDLLを、C++(Visual Studio 2017)で作る。・・・その4.3(非配列Variant型変数による配列受け渡し編)

初めに

前回の予告通り、今回は非配列のVariant型変数に配列を格納して、DLLとの受け渡しをしてみます。

処理

処理方法は、これまでにやってきた事の組み合わせで出来ます。

DLL側でVariant型を受け取る処理は、文字列の受け渡しを行った時と同じ。
受け取ってから、VARIANT型変数のvtを見て、配列かどうかの判断を行います。
配列であることの確認できれば、VARIANT型変数のparrayに目的のSAFEARRAYがあります。
SAFEARRAYがわかれば、あとは、前回の処理を使えます。

コード

DLL

AccessibleFromVBA.h
追加分

    ACCESSIBLEFROMVBA_API void WINAPI GetArrayV(LPVARIANT pv);
    ACCESSIBLEFROMVBA_API void WINAPI SetArrayV(const LPVARIANT pv);

AccessibleFromVBA.cpp

AccessibleFromVBA.def

LIBRARY AccessibleFromVba

EXPORTS
    DoNothing
    GetNumberI
    GetNumberI2
    SetString
    SetStringS
    GetStringByParam
    GetStringByParamS
    GetStringByRetVal
    GetStringByRetValS
    GetArrayPE
    GetArrayAD
    GetArray2
    SetArrayGE
    SetArrayAD
    GetArrayV
    SetArrayV
VBA
Private Declare Sub SetArrayV Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" (ByRef v As Variant)
Private Declare Sub GetArrayV Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" (ByRef v As Variant)

Public Sub DllTestSetArrayV()

    Dim v   As Variant
    Dim lArray(3, 1)  As Long
    Dim sArray()    As String
    Dim dArray(2)   As Double
    Dim i   As Long
    Dim j   As Long

    ReDim sArray(2)

    sArray(0) = "Z1000"
    sArray(1) = "ZX-10R"
    sArray(2) = "Kawasaki"

    v = sArray

    Call SetArrayV(v)

    ReDim sArray(2, 2)

    v = sArray

    Call SetArrayV(v)

    For i = LBound(lArray, 1) To UBound(lArray, 1)
        For j = LBound(lArray, 2) To UBound(lArray, 2)
            lArray(i, j) = (i + 1) * 10 + j
        Next j
    Next i

'Arrayを使った場合、DLL側では、vt が VT_VARIANT | VT_ARRAY になるので注意が必要
'以下のコメントしたコードでは、VT_I4 | VT_ARRAY にはならない。
'    v = Array(1&, 2&, 3&, 4&)
    v = lArray

    Call SetArrayV(v)

    v = dArray

    Call SetArrayV(v)

End Sub

Public Sub DllTestGetArrayV()

    Dim vL  As Variant
    Dim vS  As Variant
    Dim i   As Long

    ReDim vL(3) As Long

    vL(0) = 100
    vL(1) = 200
    vL(2) = 300
    vL(3) = 400

    Debug.Print "Before"

    For i = LBound(vL) To UBound(vL)
        Debug.Print i, vL(i)
    Next i

    Call GetArrayV(vL)

    Debug.Print vbCrLf & "After"

    For i = LBound(vL) To UBound(vL)
        Debug.Print i, vL(i)
    Next i

    Debug.Print ""

    ReDim vS(2) As String

    vS(0) = "Z1000"
    vS(1) = "ZX-10R"
    vS(2) = "Ninja 900R"

    Debug.Print "Before"

    For i = LBound(vS) To UBound(vS)
        Debug.Print i, vS(i)
    Next i

    Call GetArrayV(vS)

    Debug.Print vbCrLf & "After"

    For i = LBound(vS) To UBound(vS)
        Debug.Print i, vS(i)
    Next i

End Sub

実行結果

DllTestSetArrayV

f:id:Z1000S:20191229211734j:plain
f:id:Z1000S:20191229211759j:plain
f:id:Z1000S:20191229211822j:plain
f:id:Z1000S:20191229211837j:plain

DllTestGetArrayV
Before
 0             100 
 1             200 
 2             300 
 3             400 

After
 0             200 
 1             400 
 2             600 
 3             800 

Before
 0            Z1000
 1            ZX-10R
 2            Ninja 900R

After
 0            Z1000 GetArrayV 0
 1            ZX-10R GetArrayV 1
 2            Ninja 900R GetArrayV 2

まとめ

今回は、過去の応用のようなものなので、サクッと簡単に終わらせてしまいました。

今後の予定は、

をまとめられればいいなと思っています。

ExcelのVBAで使えるDLLを、C++(Visual Studio 2017)で作る。・・・その4.2(配列編)

初めに

前回は、予定を変更して、String型の受け渡しをする方法についてまとめました。

今回は、SAFEARRAYについてどのようなものか調べた前々回の続編です。
実際に配列データの受け渡しを行います。

SAFEARRAYに関する追加情報

配列要素のデータ型

前回の記事で、気が付いた人もいるかもしれないが、SAFEARRAY構造体のメンバーには配列要素の型情報がない
fFeatures に、VARIANT型およびBSTR型の場合に立つフラグがあるが、それ以外の型の場合、構造体メンバーだけでは判断ができない。

これについては、後述する型判定関数があるので、基本的な型であれば問題ない。

引数

VBAでは、配列はByValでは引数に指定できないので、ByRef指定となる。
そのため、DLL側では、SAFEARRAYの受け渡しをするために、引数は、
SAFEARRAYを指すポインタ(SAFEARRAY*)ではなく、
SAFEARRAYを指すポインタへのポインタ(SAFEARRAY**)とする。

一方、呼び出すVBA側では、Declare で受け渡しをする引数をどの様に書けばいいのか?
VBAには、SAFEARRYなるデータ型はない。
結論から言えば、通常のプロシージャと同様で、下記のようになる。

Declare Sub 関数名 Lib "DLL名" (ByRef 配列変数名() As データ型)

As Byteだろうが
As Longだろうが
As Variantだろうが
構わない。

言い換えれば、下記のように、Declare でAlias を設定すれば、DLLのひとつの関数で異なるデータ型の受け渡しも可能となる。
DLLの関数宣言

__declspec(dllexport) void WINAPI FuncX(LPSAFEARRAY* ppsa);

VBA側の宣言
上記のDLLの関数宣言に対し、以下の宣言は、いずれも問題ない。

Declare Sub XByte Lib "DLL名" Alias "FuncX" (ByRef 配列変数名() As Byte)
Declare Sub XLong Lib "DLL名" Alias "FuncX" (ByRef 配列変数名() As Long)
Declare Sub XVariant Lib "DLL名" Alias "FuncX" (ByRef 配列変数名() As Variant)

主な処理と使用する関数

HRESULTを返す関数の成否判定

いくつかの関数は復帰値の型が、HRESULTとなっている。
これらの関数は、成功すると S_OK (0x00000000)を返す。
成功を判定するマクロとして、SUCCEEDED マクロがあり、
失敗を判定するマクロとして、FAILED マクロがあるので、必要に応じて使えばよい。

使用例

HRESULT hResult = SafeArrayAccessData(*ppsa, (void**)&piValue);

if (FAILED(hResult))
    return;

HRESULT が取る値(抜粋?)については、こちらに記載されている。

SAFEARRAYに格納されている要素のデータ型分類値の取得

SafeArrayGetVartype

HRESULT SafeArrayGetVartype(
  SAFEARRAY *psa,
  VARTYPE   *pvt
);

VBAから渡した場合、VERTYPEは、以下のような値を取る。

項目データ型備考
VBAC++
VT_I2Integershort0x0002
VT_I4Longint0x0003
VT_R4Singlefloat0x0004
VT_R8Doubledouble0x0005
VT_CYCurrencyCY0x0006
VT_DATEDateDATE0x0007
VT_BSTRStringBSTR0x0008
VT_BOOLBooleanBOOL0x00B
VT_VARIANTVariantVARIANT0x00C
VT_UI1Byteunsigned char0x0011
VT_I8LongLonglong long0x001464bit版のみ

docs.microsoft.com

配列の次元数の取得

SafeArrayGetDim

UINT SafeArrayGetDim(
  SAFEARRAY *psa
);

docs.microsoft.com

配列の1要素のサイズの取得

SafeArrayGetElemsize

UINT SafeArrayGetElemsize(
  SAFEARRAY *psa
);

例えば、VBA側で、

Dim arryL(2) As Long
Dim arryV(2) As Variant
Dim arryS(2) As String

と宣言されていた場合、
arryLは、Longのサイズになるので、4 (Byte)が返る。
arryVは、Variantのサイズになるので、16 (Byte)が返る。
arrySは、Stringのポインタサイズ(?)になるので、4 (Byte)が返る。

docs.microsoft.com

指定した次元のインデックスの指定可能最小値(LBound)の取得

SafeArrayGetLBound

HRESULT SafeArrayGetLBound(
  SAFEARRAY *psa,
  UINT      nDim,
  LONG      *plLbound
);

nDim は、対象となる次元。
左端の次元が1となり、右に行くと増えていく。
指定方法が、SAFEARRAY メンバーの rgsabound[n].lLbound とは異なるので、何らかの理由で両関数を使い分ける必要がある場合には注意が必要。

Dim arryL(1 To 2, 3 To 5) As Long

の場合、

nDim Lbound
1 1
2 3

となる。

SAFEARRAYのメンバー、rgsabound[n].lLbound を参照して取得する方法もある。
指定方法は、前回のrgsaboundの説明を参照。
SafeArrayGetLBound の方が、直感的に指定しやすい。

docs.microsoft.com

指定した次元のインデックスの指定可能最大値(UBound)の取得

SafeArrayGetUBound

HRESULT SafeArrayGetUBound(
  SAFEARRAY *psa,
  UINT      nDim,
  LONG      *plUbound
);

SafeArrayGetLBound と同様。

こちらは、SAFEARRAYに直接取得できるメンバーはない。
rgsabound[n].lLbound + rgsabound[n].cElements - 1
で計算はできる。

docs.microsoft.com

配列のロックカウントをインクリメントと、配列データへのポインターの取得

SafeArrayAccessData

HRESULT SafeArrayAccessData(
  SAFEARRAY  *psa,
  void HUGEP **ppvData
);

ポインタを使って、要素にアクセスする場合に使用するものらしい。
配列データへのポインタを返すのと同時に、SafeArrayへのロックを行う。

SAFEARRAY ppvData 使用後、SafeArrayUnaccessData を呼び出さなければいけない

SafeArrayGetElement および SafeArrayPutElement を使用するよりも高速に処理ができるらしい。

This approach is faster than using SafeArrayGetElement and SafeArrayPutElement.

https://docs.microsoft.com/ja-jp/windows/win32/api/oleauto/nf-oleauto-safearrayaccessdata?f1url=https%3A%2F%2Fmsdn.microsoft.com%2Fquery%2Fdev15.query%3FappId%3DDev15IDEF1%26l%3DJA-JP%26k%3Dk(OLEAUTO%2FSafeArrayAccessData)%3Bk(SafeArrayAccessData)%3Bk(DevLang-C%2B%2B)%3Bk(TargetOS-Windows)%26rd%3Dtrue#examples

多次元配列の場合、前回の記事に記載の通り、ポインタのインクリメントと配列のインデックスに注意が必要。

HUGEP は正直なところ、よくわかっていない。
16bit時代(?)に使われていたみたいだが・・・
省略して、単に void** にキャストしても、データは取得できた。
とりあえず、使わない方向で行くことに・・・

docs.microsoft.com

配列のロックカウントをデクリメントと、SafeArrayAccessDataによって取得されたポインターの無効化

SafeArrayUnaccessData

HRESULT SafeArrayUnaccessData(
  SAFEARRAY *psa
);

SafeArrayAccessData によりロックした配列のアンロックを行う。

docs.microsoft.com

配列記述子と配列内のすべてのデータの破棄

SafeArrayDestroy

HRESULT SafeArrayDestroy(
  SAFEARRAY *psa
);
配列の単一の要素の取得

SafeArrayGetElement

HRESULT SafeArrayGetElement(
  SAFEARRAY *psa,
  LONG      *rgIndices,
  void      *pv
);

rgIndices
配列の各次元のインデックスのベクトル。
右端(最下位)の次元はrgIndices [0]
左端の次元は、rgIndices [psa-> cDims – 1]

2次元以上の配列の場合、要素数が配列の次元数のlong型の配列を用意して、
取得したい要素の各次元のインデックスを格納して、関数に渡す。

Dim  arry(3, 3, 3) As Long

という配列があり、arry(1, 2, 3) の要素を取得したい場合には、以下のようにすればよい。

long  lIndex[] = {1, 2, 3};
int iValue;
SafeArrayGetElement(*ppsa, lIndex, &iValue);

pv
取得した要素を格納する変数

docs.microsoft.com

データ要素を配列内の指定された場所に保存

SafeArrayPutElement

HRESULT SafeArrayPutElement(
  SAFEARRAY *psa,
  LONG      *rgIndices,
  void      *pv
);

使い方は、SafeArrayGetElementと同様。(pvは、書き込むデータ)

long lValue = getSomeValue();
SafeArrayPutElement(*ppsa, &lIndex, &lValue);

ただし、BSTRの場合は注意が必要

BSTR bstr;
bstr = SysAllocStringByteLen(pszReturn, lenByte);
//&bstrではないので注意!!!(& は不要)
SafeArrayPutElement(*ppsa, &lIndex, bstr);

docs.microsoft.com

配列内のすべてのデータの破棄

SafeArrayDestroy

HRESULT SafeArrayDestroy(
  SAFEARRAY *psa
);

既存の配列記述子と配列内のすべてのデータを破棄します。オブジェクトが配列に格納されている場合、配列内の各オブジェクトでReleaseが呼び出されます。
docs.microsoft.com

VBAからDLLへ渡す

VBAから渡された配列の値を、メッセージボックスで表示してみました。

処理の流れ
  1. 格納されているデータ型の確認
  2. 配列の次元数の確認
  3. 各次元の要素数、インデックスの上下限の確認
  4. データ読み込み
  5. 後処理

DLLからVBAへ返す

VBAから受け取った配列に、何らかの値を格納して返してみました。

処理の流れ
  1. 格納されているデータ型の確認
  2. 配列の次元数の確認
  3. 各次元の要素数、インデックスの上下限の確認
  4. データ書き込み
  5. 後処理

コード

DLL

AccessibleFromVBA.h
AccessibleFromVBA.cpp
追加部分のみ

プロトタイプ宣言

std::wstring	convMbc2Wstr(const char* lpcszSrc);
std::wstring	convMbcBstr2Wstr(const BSTR& bstr);

DLLに配列を渡す処理

DLLで配列を更新して返す処理

文字列変換処理

AccessibleFromVBA.def

VBA

実行結果

getArrayPETest
 2             3             406 
 2             4             408 
 3             3             606 
 3             4             608 
 4             3             806 
 4             4             808 
 5             3             1006 
 5             4             1008 

GetArrayPE:1
GetArrayPE:2
GetArrayPE:3
GetArrayPE:4
GetArrayPE:5
getArrayADTest
 2             3            0x23
 2             4            0x24
 3             3            0x33
 3             4            0x34
 4             3            0x43
 4             4            0x44
 5             3            0x53
 5             4            0x54

 2             3            0x69
 2             4            0x6C
 3             3            0x99
 3             4            0x9C
 4             3            0xC9
 4             4            0xCC
 5             3            0xF9
 5             4            0xFC

GetArrayAD0
GetArrayAD1
GetArrayAD2
GetArrayAD3
GetArrayAD4

GetArrayAD_BSTR0
 4 
GetArrayAD_EMPTY2
GetArrayAD_EMPTY3
setArrayADTest

SetArrayGE
f:id:Z1000S:20191222112853j:plain
f:id:Z1000S:20191222112930j:plain
f:id:Z1000S:20191222112941j:plain
f:id:Z1000S:20191222112954j:plain

SetArrayAD
f:id:Z1000S:20191222113008j:plain
f:id:Z1000S:20191222113020j:plain
f:id:Z1000S:20191222113032j:plain
f:id:Z1000S:20191222113045j:plain

次回予告

次回は、Variant型の非配列変数に、配列を格納して、DLLと受け渡しをする予定です。

ExcelのVBAで使えるDLLを、C++(Visual Studio 2017)で作る。・・・その3.2(String型による文字列の受け渡し)

初めに

前回の予告で、「次回は、実際に配列の受け渡しを行ってみます。」などと書いたのだけれど、
「やっぱりString型の配列の受け渡しも欲しいよな。」
となったので、予定を変更して前々回の続編として、
String型で文字列の受け渡しをまとめることにしました。

BSTR

VBAのString型は、BSTR型です。
まずは、BSTRについて調べてみました。

構造

BSTRは、

  1. 長さのプレフィックス
  2. データ文字列
  3. ターミネータ

で構成される複合データ型です。
C++のヘッダを見ると、BSTRは、上記の「データ文字列」部なのですが・・・。詳細は下記)

項目データ型説明
長さプレフィックスULONG次のデータ文字列のバイト数。符号なし4バイト整数。
データ文字列の最初の文字の直前に配置されます。
この値には、ターミネーターは含まれません。
データ文字列WCHAR[n]複数の埋め込みNULL文字が含まれる場合があります。
データ文字列の終端がNULLである必要はありません。
ターミネーターWCHAR0x0000 (WCHAR)
BSTRは(WCHAR型の)ポインターです。 ポインターが指しているのは、長さプレフィックスではなく、データ文字列の最初の文字です。

メモリ上の配置イメージは、以下のようになります。
f:id:Z1000S:20191204105312j:plain

ヘッダ内では、以下のように定義されています。

typedef WCHAR OLECHAR;
typedef OLECHAR* BSTR;
typedef BSTR* LPBSTR;
メモリ管理

BSTRは、std::wstringとは違って、

  1. 使用する前にメモリの割り当て
  2. 使用後にメモリの解放

が必要です。

メモリの割り当てには、以下の関数を使用します。

メモリの開放には、以下の関数を使用します。

メモリの割り当て、開放を誰が担当するのかは、ケースによって変わってきます。

BSTRがインターフェイス内にとどまっている場合、操作が完了したらメモリを解放する必要があります。ただし、BSTRがインターフェイスを通過すると、受信オブジェクトがメモリ管理を担当します。

  • BSTR引数を必要とする関数を呼び出す場合、呼び出しの前にBSTRにメモリを割り当てて、後で解放する必要があります。
  • BSTRを返す関数を呼び出すときは、自分で文字列を解放する必要があります。
  • BSTRを返す関数を実装する場合、文字列を割り当てますが、解放しないでください。関数を受信すると、メモリが解放されます。
https://docs.microsoft.com/ja-jp/cpp/atl-mfc-shared/allocating-and-releasing-memory-for-a-bstr?f1url=https%3A%2F%2Fmsdn.microsoft.com%2Fquery%2Fdev15.query%3FappId%3DDev15IDEF1%26l%3DJA-JP%26k%3Dk(WTYPES%2FBSTR);k(BSTR);k(DevLang-C%2B%2B);k(TargetOS-Windows)%26rd%3Dtrue&view=vs-2019

以下は別のサイトに記載されていたもので、原文は英語ですが、Googleさんにお願いして翻訳しました。

インターフェースは契約です。 呼び出し元と呼び出し先に期待される動作を記述します。 「このメモリを解放するのは誰ですか?」 その契約の一部であるため、それを解放する所有者を決定し、それをインターフェイスのドキュメントに書き込みます。
ただし、通常は次のことが予想されます。

  • BSTRが「in」パラメータである場合
    • 通常、呼び出し側がそれを解放します。 呼び出し先がそれを所有したい場合、呼び出し先はコピーを作成してコピーを所有できます。
  • BSTRが「out」パラメータである場合
    • エントリでnullである必要があります。そのため、解放する必要はありません。また、明らかに、呼び出し側は結果の文字列を所有します。
  • BSTRが「in / out」パラメータである場合
    • 呼び出し先は渡された値を解放し、置き換えます。 呼び出し元は、新しい値を解放して所有します。
  • BSTRが「out ret」パラメーターである場合
    • 明らかに呼び出し側はそれを解放します。

VBはこれらのルールを予期し、呼び出し側であり、呼び出し側が文字列を解放している場合、ユーザーに代わって文字列を解放します。

https://blogs.msdn.microsoft.com/ericlippert/2003/09/12/erics-complete-guide-to-bstr-semantics/#comment-5180

ポインタを介してのデータの書き換え
前述の通り、BSTRは、(WCHARの)ポインタですが、ポインタを使用して、その内容を直接書き換えてはいけません。
後述する関数から用途に合ったものを使用して行います。

内部のデータ文字列の状態

BSTR は既知のバイト数であるため、ゼロで文字列を終了するという規則は必要ありません。 したがって、ゼロは BSTR 内の正当な値 です。 これは、 BSTR がバイナリイメージを含む任意のデータを含むことができることを 意味し ます。 このため、 BSTR は、文字列に加えてバイナリデータをマーシャリングする便利な方法としてよく使用されます。 これは 、いくつかの奇妙な状況で は、 BSTR が奇数バイトになる場合があることを 意味し ます。 まれですが、可能性に注意する必要があります。

https://blogs.msdn.microsoft.com/ericlippert/2003/09/12/erics-complete-guide-to-bstr-semantics/

VBAでString型とVariant型の変数に文字列を設定してDLLに渡した場合、DLL内で見たメモリの内容は以下のようになりました。
BSTR型で受けた場合、char[]相当の内容になっています。
VARIANT型で受けた場合、WCHAR[]相当の内容になっています。

渡した文字列は、

  • Z1000R:半角英数、偶数文字数
  • ZX-10RR:半角英数記号、奇数文字数
  • カワサキ:全角カタカナ

の3種類です。

受け取る型
BSTRVARIANT
渡す型StringZ1000R
f:id:Z1000S:20191204150121j:plain
ZX-10RR
f:id:Z1000S:20191204150138j:plain
カワサキ
f:id:Z1000S:20191204150154j:plain
Z1000R
f:id:Z1000S:20191204145611j:plain
ZX-10RR
f:id:Z1000S:20191204145734j:plain
カワサキ
f:id:Z1000S:20191204145807j:plain
Variant-Z1000R
f:id:Z1000S:20191204150839j:plain
ZX-10RR
f:id:Z1000S:20191204150855j:plain
カワサキ
f:id:Z1000S:20191204150911j:plain

アンダーラインの部分の各色の部分は、以下の内容を示しています。
黄色:長さのプレフィックス
シアン:データ文字列
マゼンタターミネーター

カワサキ」の各文字の文字コードは、以下の通りです。
(CPUがリトルエンディアンなので、メモリのイメージ図上では、上位と下位が入れ替わって配置されています。)

種別
Shift-JIS 0x834A 0x838F 0x8354 0x834C
UNICODE 0x30AB 0x30EF 0x30B5 0x30AD

「ZX-10RR」は、終端のNULLを除いて、7文字になりますが、String型→BSTR型の場合、前述の通り奇数バイトの値が長さとして設定されているのがわかります。

ヘッダ

WTypes.h
docs.microsoft.com

関数

VBAのString型を使って、DLLとやり取りする場合、主に使用するのは以下の2つ。

  • SysAllocStringByteLen
  • SysFreeString

SysAllocStringByteLen
ANSI文字列を入力として受け取り、ANSI文字列を含むBSTRを返します。
ANSIからUnicodeへの変換を実行しません。

VBA側が、String型で文字列を受け取る場合、この関数を使用します。

BSTR SysAllocStringByteLen(
  LPCSTR psz,
  UINT   len
);

lenは、pszの終端のNULLを含めないByte数を指定する。
また、この値は奇数であっても構わない。

この関数は、バイナリデータを含むBSTRを作成するために提供されています。 このタイプのBSTRは、ANSIからUnicode、またはその逆に変換されない状況でのみ使用できます。

pszがNullの場合、要求された長さの文字列が割り当てられますが、初期化されません。 文字列pszにはヌル文字を埋め込むことができ、ヌルで終わる必要はありません。

docs.microsoft.com

SysAllocString
新しい文字列を割り当て、渡された文字列をコピーします。
VBA側が、Variant型で文字列を受け取る場合、この関数を使用します。

BSTR SysAllocString(
  const OLECHAR *psz
);

docs.microsoft.com

SysAllocStringLen
新しい文字列を割り当て、渡された文字列から指定された数の文字をコピーし、NULL終了文字を追加します。

BSTR SysAllocStringLen(
  const OLECHAR *strIn,
  UINT          ui
);

uiは、コピーする文字数。
docs.microsoft.com

SysFreeString
VARIANTの時にも使ったやつ。
SysAllocString、SysAllocStringByteLen、SysReAllocString、SysAllocStringLen、またはSysReAllocStringLenによって以前に割り当てられた文字列の割り当てを解除します。
docs.microsoft.com

SysStringLen

BSTRの長さを返します。
終端のNULL文字を含まないbstrの文字数。 bstrがnullの場合、戻り値はゼロです。

BSTRにNULL文字が埋め込まれている場合、戻り値はstrlen(bstr)と異なる場合があります。 この関数は、BSTRの割り当てに使用されるSysAllocStringLen関数のcchパラメーターで指定された文字数を常に返します。

https://docs.microsoft.com/ja-jp/windows/win32/api/oleauto/nf-oleauto-sysstringlen#remarks
UINT SysStringLen(
  BSTR pbstr
);

docs.microsoft.com

SysStringByteLen

BSTRの長さ(バイト単位)を返します。
終端のNULL文字を含まないbstrのバイト数。 bstrがnullの場合、戻り値はゼロです。

BSTRにNULL文字が埋め込まれている場合、戻り値はstrlen(bstr)と異なる場合があります。 この関数は、BSTRの割り当てに使用されるSysAllocStringByteLen関数のlenパラメーターで指定されたバイト数を常に返します。

https://docs.microsoft.com/ja-jp/windows/win32/api/oleauto/nf-oleauto-sysstringbytelen#remarks
UINT SysStringByteLen(
  BSTR bstr
);

docs.microsoft.com

SysAllocStringとSysAllocStringByteLenの違い

SysAllocStringSysAllocStringByteLenの両関数に同じ内容の文字列(全く同じものではありません)を渡して生成されたBSTRを、VBAString型で受け取った場合、どのような違いが出るのか試してみました。

DLL側でのBSTR生成 VBA側でStringで受けた後の状態 備考
SysAllocString(L"Z1000R"); Z 1 0 0 0 R 文字の間に'\0'が入っており、スペースが挟まれているように見える
SysAllocStringByteLen("Z1000R", 6); Z1000R 入力したデータが取得できている
SysAllocString(L"カワサキ"); ォ0・オ0ュ0 文字化けしている
SysAllocStringByteLen("カワサキ", 8); カワサキ 入力したデータが取得できている

目的に合った関数を使用しましょう。

コード

DLL

AccessibleFromVBA.h

#pragma once

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

    ACCESSIBLEFROMVBA_API void WINAPI DoNothing();
    ACCESSIBLEFROMVBA_API int WINAPI GetNumberI(int i);
    ACCESSIBLEFROMVBA_API void WINAPI GetNumberI2(int* pi);
    ACCESSIBLEFROMVBA_API void WINAPI SetString(VARIANT vString);
    ACCESSIBLEFROMVBA_API void WINAPI SetStringS(const BSTR sString);
    ACCESSIBLEFROMVBA_API void WINAPI GetStringByParam(VARIANT* pvString);
    ACCESSIBLEFROMVBA_API void WINAPI GetStringByParamS(BSTR* pbstr);
    ACCESSIBLEFROMVBA_API VARIANT WINAPI GetStringByRetVal();
    ACCESSIBLEFROMVBA_API BSTR WINAPI GetStringByRetValS();
}

AccessibleFromVBA.cpp
追加分のみ

ACCESSIBLEFROMVBA_API void WINAPI GetStringByParamS(BSTR* pbstr)
{
    if (!pbstr)
        return;

    //まず開放
    SysFreeString(*pbstr);

    //返す文字列(std::wstringは使用しない)
    std::string sReturn("GetStringByParamS 返却データ文字列");

    //BSTR生成
    *pbstr = SysAllocStringByteLen(sReturn.c_str(), sReturn.length());

    return;
}

ACCESSIBLEFROMVBA_API BSTR WINAPI GetStringByRetValS()
{
    //返す文字列(std::wstringは使用しない)
    std::string s("GetStringByRetValS 返却データ文字列");

    //BSTR生成
    BSTR bstr = SysAllocStringByteLen(s.c_str(), s.length());

    return bstr;
}

ACCESSIBLEFROMVBA_API void WINAPI SetStringS(const BSTR sString)
{
    if (!sString)
    {
        MessageBox(NULL, L"Argment is NULL.", L"DLL", MB_OK | MB_ICONERROR);

        return;
    }

    //VBAからの文字列は、char*で格納されている
    //BSTRの途中に'\0'がある事は想定していない。
    std::string s((char*)sString);

    MessageBoxA(NULL, s.c_str(), "DLL", MB_OK | MB_ICONINFORMATION);

    return;
}

AccessibleFromVBA.def

LIBRARY AccessibleFromVba

EXPORTS
    DoNothing
    GetNumberI
    GetNumberI2
    SetString
    SetStringS
    GetStringByParam
    GetStringByParamS
    GetStringByRetVal
    GetStringByRetValS

stdafx.h

#pragma once

#include "targetver.h"

#define WIN32_LEAN_AND_MEAN    // Windows ヘッダーから使用されていない部分を除外します。
// Windows ヘッダー ファイル:
#include <windows.h>

// TODO: プログラムに必要な追加ヘッダーをここで参照してください
#include <WTypes.h>    //BSTR
#include <atlstr.h>    //VARIANT
#include <iostream>
#include <string>
VBA
Private Declare Sub SetStringS Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" (ByVal s As String)
Private Declare Sub GetStringByParamS Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" (ByRef s As String)
Private Declare Function GetStringByRetValS Lib "C:\Datas\MyDatas\Developer\VisualStudioComunity2017\DllForVBA\ForTest\AccessibleFromVBA.dll" () As String

Public Sub DllCallTest()

    Dim s   As String

    Call GetStringByParamS(s)
    Debug.Print "GetStringByParamS:" & s

    s = ""
    s = GetStringByRetValS
    Debug.Print "GetStringByRetValS:" & s

    s = "Z1000R"
    Call SetStringS(s)

End Sub

実行結果

f:id:Z1000S:20191208113751j:plain

まとめ

使用する文字をShift-JISの範囲内等に限定できるのであれば、
VBA側の型をString型としても、DLLとの文字列の受け渡しは(文字化けせずに)できる。
ただし、その場合は、DLL側で使用する関数が、Unicodeで返す場合とは変わってくる。

DLL側のインターフェイスを、

  • VARIANT型
  • BSTR型

どちらにするのか、DLLを作る前にきちんと検討、確認をして決めましょう。

Unicode文字をつかうなら、VARIANT一択なんですけど・・・

次回予告

次回こそ、実際に配列の受け渡しをおこないます。

たぶん・・・

【VBA】非配列のVariant型変数に格納した配列のLBoundや要素数を変えてみた

SafeArrayRedim を使うと、
非配列のVariant型変数に格納した配列でも、LBoundとかElementsとかの変更ができる。

だまって、
ReDim Preserve v(2 To 4)
とかすればいいんだけど・・・

ソース
Private Type SAFEARRAYBOUND
    cElements   As Long
    lLbound     As Long
End Type

Private Declare Function SafeArrayRedim Lib "OleAut32" (ByVal psa As Long, ByRef psaboundNew As SAFEARRAYBOUND) As Long

Private Declare Sub MoveMemory Lib "Kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)


Public Sub redimVariantArray()

    Dim v   As Variant

    v = Array(1, 2, 3)

    Dim psa As Long
    Dim isDynamicArray  As Boolean

    psa = getSafeArrayPointer(v, isDynamicArray)

    If psa = 0 Then
        Debug.Print "Target is not initialized."

        Exit Sub
    ElseIf Not isDynamicArray Then
        Debug.Print "Target is not Dynamic Array."

        Exit Sub
    End If

    Debug.Print "Before"
    Debug.Print "LBound:" & LBound(v)
    Debug.Print "UBound:" & UBound(v)
    Debug.Print ""

    Dim sab As SAFEARRAYBOUND

    sab.cElements = UBound(v) - LBound(v) + 1
    sab.lLbound = LBound(v) + 2

    Call SafeArrayRedim(Not Not psa, sab)

    Debug.Print "After"
    Debug.Print "LBound:" & LBound(v)
    Debug.Print "UBound:" & UBound(v)

End Sub

Public Sub redimVariantRefArray()

    Dim v   As Variant
    Dim lArray() As Long

    ReDim lArray(3)

    lArray(0) = 2
    lArray(1) = 3
    lArray(2) = 4
    lArray(3) = 5

    v = lArray

    Dim psa As Long
    Dim isDynamicArray  As Boolean

    psa = getSafeArrayPointer(v, isDynamicArray)

    If psa = 0 Then
        Debug.Print "Target is not initialized."

        Exit Sub
    ElseIf Not isDynamicArray Then
        Debug.Print "Target is not Dynamic Array."

        Exit Sub
    End If

    Debug.Print "Before"
    Debug.Print "LBound:" & LBound(v)
    Debug.Print "UBound:" & UBound(v)
    Debug.Print ""

    Dim sab As SAFEARRAYBOUND

    sab.cElements = UBound(v) - LBound(v) + 2
    sab.lLbound = LBound(v) + 2

    Call SafeArrayRedim(Not Not psa, sab)

    Debug.Print "After"
    Debug.Print "LBound:" & LBound(v)
    Debug.Print "UBound:" & UBound(v)

End Sub

Public Function getSafeArrayPointer(ByRef v As Variant, ByRef isDynamicArray As Boolean) As Long

    Const VT_ARRAY          As Integer = &H2000
    Const VT_BYREF          As Integer = &H4000

    Const FADF_STATIC       As Integer = &H2
    Const FADF_FIXEDSIZE    As Integer = &H10

    Const OFFSET_VT         As Long = 0&
    Const OFFSET_PARRAY     As Long = 8&
    Const OFFSET_FFEATURES  As Long = 2&

    Dim pArray  As Long
    Dim vt      As Integer

    isDynamicArray = False

    Call MoveMemory(vt, v, Len(vt))

    If (vt And VT_ARRAY) = 0 Then
        '配列ではない
        Exit Function
    End If

    'VARIANTのparrayのアドレス
    Call MoveMemory(pArray, ByVal VarPtr(v) + OFFSET_PARRAY, Len(pArray))

    If (vt And VT_BYREF) = VT_BYREF Then
        'VARIANTのpparrayから、parrayのアドレスを引っ張ってくる
        Call MoveMemory(pArray, ByVal pArray, Len(pArray))
    End If

    If pArray = 0 Then
        'Redimがまだか、Eraseした後
        Exit Function
    End If

    Dim fFeatures   As Integer

    Call MoveMemory(fFeatures, ByVal pArray + OFFSET_FFEATURES, Len(fFeatures))

    isDynamicArray = CBool((fFeatures And (FADF_STATIC Or FADF_FIXEDSIZE)) <> (FADF_STATIC Or FADF_FIXEDSIZE))

    getSafeArrayPointer = pArray

End Function
実行結果

redimVariantArray

Before
LBound:0
UBound:2

After
LBound:2
UBound:4

f:id:Z1000S:20191206233348g:plain

redimVariantRefArray

Before
LBound:0
UBound:3

After
LBound:2
UBound:6

ExcelのVBAで使えるDLLを、C++(Visual Studio 2017)で作る。・・・その4.1(配列 準備編)

はじめに

前回の文字列の受け渡しでは、VARIANTを使いました。(自分でもびっくり)

今回の配列でも、int* とか short* とかを使うのかと思いきや
SAFEARRAYなるものを使うことになります。

VBA の配列は OLE の SafeArrays です。C/C++ については、Windows OLE/COM ヘッダー ファイル内で SAFEARRAY として定義されています。

https://docs.microsoft.com/ja-jp/office/client-developer/excel/how-to-access-dlls-in-excel

ということで、配列の1回目は、SAFEARRAYとはどのようなものなのか、調べてみます。

SAFEARRAY

docs.microsoft.com

SAFEARRAY構造体

こんな感じで定義されています。

typedef struct tagSAFEARRAY {
  USHORT         cDims;
  USHORT         fFeatures;
  ULONG          cbElements;
  ULONG          cLocks;
  PVOID          pvData;
  SAFEARRAYBOUND rgsabound[1];
} SAFEARRAY;

メンバ内容
cDims配列の次元数
fFeaturesフラグ
cbElements配列の1要素のサイズ
cLocks対応するロック解除なしでアレイがロックされた回数。
原文:The number of times the array has been locked without a corresponding unlock.
よくわかりません・・・ m(_ _)m
pvData配列要素の先頭データを指すポインタ
rgsabound配列の指定次元の
1.要素数
2.指定できるインデックスの下限

cDims
配列の次元数が格納されている。
1次元配列なら、1
2次元配列なら、2
3次元配列なら、3
と言った具合です。

fFeatures
項目備考
FADF_AUTO0x0001配列はスタック上に割り当てられる。
FADF_STATIC0x0002配列は静的に割り当てられる。
FADF_EMBEDDED0x0004配列は構造体に組み込まれる。
FADF_FIXEDSIZE0x0010配列はリサイズ、再割り当てできない。
FADF_RECORD0x0020レコードを含む配列。
FADF_HAVEIID0x0040インターフェイスを識別するIIDを持つ配列。FADF_DISPATCHまたはFADF_UNKNOWNも設定されている場合にのみ設定される。
FADF_HAVEVARTYPE0x0080VARIANT型を持つ配列。
FADF_BSTR0x0100BSTR型の配列。
FADF_UNKNOWN0x0200IUnknown*型の配列。
FADF_DISPATCH0x0400IDispatch*型の配列。
FADF_VARIANT0x0800VARIANT型の配列。
FADF_RESERVED0xF008将来のために予約済み。
VBA側から渡す配列によって異なり、以下のようになるようです。
f:id:Z1000S:20191113210606j:plain
DateTime型、Currency型はいずれも数値型と同じになりました。

cbElements
配列の要素1個のサイズ。
VBAのIntegerの配列なら2、Longの配列なら4
MSのサイトには、「データオブジェクトのサイズは含まれない」と書かれている(が理解できていない・・・)。

pvData
データを指すポインタ。

1次元配列の場合は、普通にインクリメントしても(範囲内であれば)大丈夫。
2次元以上の配列の場合、インクリメントしても、おそらくほとんどの人が意図した要素のアドレスを指していないと思う。

なぜなら、メモリ上の要素の並びが以下のようになっているから。

元データ(Byte型の3次元配列)

x(0, 0, 0) = &H0
x(0, 0, 1) = &H1
x(0, 1, 0) = &H10
x(0, 1, 1) = &H11
x(1, 0, 0) = &H20
x(1, 0, 1) = &H21
x(1, 1, 0) = &H30
x(1, 1, 1) = &H31

SAFEARRAYのpvDataの状態(メモリデータ)
f:id:Z1000S:20191114215409j:plain
次項のrgsaboundでも書いているが、インデックスの並びが左右逆になっている。

混乱する元になるので、多次元配列の場合は、ポインタを使わないようにした方がよさそう・・・

rgsabound
各次元の境界情報
下記構造体で定義されている。

typedef struct tagSAFEARRAYBOUND {
  ULONG cElements;
  LONG  lLbound;
} SAFEARRAYBOUND, *LPSAFEARRAYBOUND;

単一要素の配列となっているのは、
多次元配列の場合に、インデックスに指定する値を変えることで各次元の情報が得られるような仕組みとするためと思われる。
指定できるインデックスは、0 ~ cDims -1
ただし、インデックスの指定には注意が必要。
3次元配列を例にすると、下表のようになる。
配列の1番右の次元がのインデックスがとなる。

rgsabound
インデックス
210
 
配列(1次元目,2次元目,3次元目)

VBA側で、下記配列を用意した場合、下表のような値となる。

Dim lArray(1 To 2, 2 To 4, 3) As Long

項目rgsabound
インデックス
012
cElements432
lLbound021


docs.microsoft.com

次回予告

SAFEARRAYの基礎知識を得たところで、
次回は、実際に配列の受け渡しを行ってみます。
さらに、第3回は、VARIANTの非配列変数に、配列を格納して受け渡しをしてみる予定です。