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

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

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一択なんですけど・・・

次回予告

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

たぶん・・・