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

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

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

その1その2と書いたものの
1年以上放置して、いまさらの第3段。

今回は、文字列の受け渡しです。

初めに

初回に書いた設定に要修正箇所があります。
「プロジェクトの設定」の
3.「構成プロパティ」-「全般」-「プロジェクトの既定値」-「文字セット」を
"Unicode 文字セットを使用する"に変更して下さい。

使用する文字範囲が、Shift-JIS の範囲で限定されるような場合には、
マルチ バイト文字セットを使用する
でも良いのですが、Unicode 文字や、JIS 第三水準、第四水準の文字が含まれる可能性が排除できない場合には、
Unicode 文字セットを使用する
としておいた方が、後でトラブルになりにくいと思います。

文字列を受け渡しするためのデータ型

VBAでは、文字列を扱うデータ型は、String です。
これまで、ネットで見つかるコードを見ると、DLL側は、

  • char*
  • const char*
  • wchar_t*
  • BSTR
  • BSTR*

といった型での受け渡しをしているものが大半を締めていたようです。


しかし、今回の記事では、これらの型を使いません。
使うのは、VARIANT です。

理由は、以下の通りです。

VBAString は、
ByVal 渡しの場合はバイト文字列 BSTR 構造体へのポインターとして渡されます。
ByRef 渡しの場合はポインターへのポインターとして渡されます。


文字列を格納している VBAVariant は、
ByVal 渡しの場合は Unicode ワイド文字文字列 BSTR 構造体へのポインターとして渡されます。
ByRef 渡しの場合はポインターへのポインターとして渡されます。

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

Excel は、ワイド文字 Unicode 文字列を使用して内部で動作しています。VBA ユーザー定義関数が String 引数を取るように宣言されている場合、Excel は指定した文字列をロケール固有の方法でバイト文字列に変換します。関数に Unicode 文字列を渡す場合、VBA ユーザー定義関数は String 引数の代わりにバリアント型を受け入れる必要があります。その後、DLL 関数は、VBA から バリアント BSTR ワイド文字列を受け入れることができます。

DLL から VBAUnicode 文字列を返すには、バリアント文字列引数を修正する必要があります。これが機能するには、C/C++ コードでバリアントへのポインターを使用するよう DLL 関数を宣言し、VBA コードで引数を ByRef varg As Variant として宣言する必要があります。古い文字列のメモリを解放し、OLE Bstr 文字列を使用して作成された新しい文字列値は DLL でのみ機能すべきです。

DLL から VBA にバイト文字列を返すには、バイト文字列 BSTR 引数をインプレースで変更する必要があります。これが機能するには、C/C++ コードで BSTR へのポインターへのポインターを使用するよう DLL 関数を宣言し、VBA コードで引数を「ByRef varg As String」として宣言する必要があります。

https://docs.microsoft.com/ja-jp/office/client-developer/excel/how-to-access-dlls-in-excel#variant-and-string-arguments
VARIANT型

VARIANT型は、構造体(VBAでは、Typeステートメントを使って定義するユーザー定義の型と思ってもらえば、とりあえずはOK。厳密に言えば違うんですけど。)で、以下の様になっています。

typedef struct tagVARIANT {
  union {
    struct {
      VARTYPE vt;
      WORD    wReserved1;
      WORD    wReserved2;
      WORD    wReserved3;
      union {
        LONGLONG     llVal;
        LONG         lVal;
        BYTE         bVal;
        SHORT        iVal;
        FLOAT        fltVal;
        DOUBLE       dblVal;
        VARIANT_BOOL boolVal;
        SCODE        scode;
        VARIANT_BOOL __OBSOLETE__VARIANT_BOOL;
        CY           cyVal;
        DATE         date;
        BSTR         bstrVal;
        IUnknown     *punkVal;
        IDispatch    *pdispVal;
        SAFEARRAY    *parray;
        BYTE         *pbVal;
        SHORT        *piVal;
        LONG         *plVal;
        LONGLONG     *pllVal;
        FLOAT        *pfltVal;
        DOUBLE       *pdblVal;
        VARIANT_BOOL *pboolVal;
        SCODE        *pscode;
        CY           *pcyVal;
        VARIANT_BOOL *__OBSOLETE__VARIANT_PBOOL;
        DATE         *pdate;
        BSTR         *pbstrVal;
        IUnknown     **ppunkVal;
        IDispatch    **ppdispVal;
        SAFEARRAY    **pparray;
        VARIANT      *pvarVal;
        PVOID        byref;
        CHAR         cVal;
        USHORT       uiVal;
        ULONG        ulVal;
        ULONGLONG    ullVal;
        INT          intVal;
        UINT         uintVal;
        DECIMAL      *pdecVal;
        CHAR         *pcVal;
        USHORT       *puiVal;
        ULONG        *pulVal;
        ULONGLONG    *pullVal;
        INT          *pintVal;
        UINT         *puintVal;
        struct {
          PVOID       pvRecord;
          IRecordInfo *pRecInfo;
        } __VARIANT_NAME_4;
      } __VARIANT_NAME_3;
    } __VARIANT_NAME_2;
    DECIMAL decVal;
  } __VARIANT_NAME_1;
} VARIANT;

結構たくさんの要素があるのですが、この中で、今回大切なのは、以下の3つです。

データ型 変数 備考
VARTYPE vt VARIANTの変数に格納されているデータの型等の情報
BSTR bstrVal 文字列データ
BSTR* pbstrVal 文字列データを指すポインタ

気が付いた方もいるかもしれませんが、
VARIANTの中に格納される文字列の型は、BSTR型です。

vt
vtに格納される値は、VARENUMの中のいずれかが使用されます。
ただし、一部は単独で使用されず、他の値とORを取るフラグとして使用されるものもあります。

VARIANTの変数に、非配列の文字列が格納されている場合、vt は、下表のいずれかの値となります。

定義 備考
VT_BSTR0x0008文字列
VT_BSTR | VT_BYREF0x4008文字列(参照)
数値、オブジェクト等文字列以外のデータが格納されている場合や初期化されていないような場合には、上記とは異なる値が設定されています。

bstrVal
vtに、VT_BSTRが設定されている時、bstrValには、文字列が格納されています。
pbstrVal
vtに、VT_BSTR | VT_BYREF が設定されている時、pbstrValには、文字列へのポインタが格納されています。


以下の2つの違いについては、後述します。

  • VT_BSTR
  • VT_BSTR | VT_BYREF


docs.microsoft.com
docs.microsoft.com
docs.microsoft.com

関数

DLL側でのVARIANT内部形式の判断

VARIANTとBSTRの変換を行う際に、気をつけなければいけないのが、vt の値です。
文字列が格納されている場合の vt の値は、以下の2つがある事を書きました。
これらは、DLLの関数に渡すVBAの変数の型に依存します。

vtVBAの変数の型呼出例備考
VT_BSTR
[ 0x0008 ]
内部処理形式が Stringの Variant の場合Dim v As Variant
v = ""
Call DllFunc(v)
vがemptyの状態で渡すと、VT_EMPTYとなる
VT_BSTR | VT_BYREF
[ 0x4008 ]
String の場合Dim s As String
Call DllFunc(s)
 
VT_BYREF フラグの状態によって以下の処理方法を変える必要があるため、vtの値の確認は重要です。

  1. VBAからDLLへパラメータとして文字列を受け取る場合
  2. DLLからVBAへパラメータに文字列を設定して返す場合
VARIANTの文字列の変換

VARIANT→BSTR
std::wstring のコンストラクタを使って初期化します。
パラメータ v が、VT_BYREF の有無で処理方法が変わってきます。
vt文字列情報が格納されているメンバー左記要素に格納されている内容
VT_BSTRv.bstrVal文字列
VT_BSTR | VT_BYREFv.pstrVal格納されている文字列を指すポインタ

コンストラクタの第2引数には、文字列数を渡します。
文字数の取得には、SysStringLen 関数を使用します。
docs.microsoft.com

std::wstring	convVstr2Wstr(const VARIANT v)
{
	std::wstring ws;

	if (v.vt == VT_BSTR)
	{
		ws = std::wstring(v.bstrVal, SysStringLen(v.bstrVal));
	}
	else if (v.vt == (VT_BSTR | VT_BYREF))
	{
		ws = std::wstring(*v.pbstrVal, SysStringLen(*v.pbstrVal));
	}

	return ws;
}

wstring→VARIANT
VARIANT変数に文字列を設定する場合、以下の手順で行います。

  1. vt に VT_BSTR または、VT_BSTR | VT_BYREF をセットする。
  2. bstrVal または、pbstrVal に文字列をセットする

VARIANT変数 を vString とする。
vString.vt == VT_BSTR の場合
SysAllocString に、C文字列を指すポインタを渡して、帰ってきた値(BSTR)を直接、vString.bstrVal に代入します。

	std::wstring ws(L"設定する文字列");

	vString.vt = VT_BSTR ;
	vString.bstrVal = SysAllocString(ws.c_str());

	//以下の方法では、正しく文字列が返らない
	//BSTR bs = SysAllocString(ws.c_str());
	//vString.bstrVal = bs;
	//SysFreeString(bs);

vt == VT_BSTR | VT_BYREF の場合
一旦、BSTRの変数に対して、SysAllocString を使って文字列を格納します。
上記変数をSysReAllocString に渡し、文字列データをセットします。
SysFreeString を使用して、上記BSTR変数を開放します。

	std::wstring ws(L"設定する文字列");

	vString.vt = VT_BSTR | VT_BYREF;

	BSTR bs = SysAllocString(ws.c_str());
	SysReAllocString(vString.pbstrVal, bs);
	SysFreeString(bs);

docs.microsoft.com
docs.microsoft.com
docs.microsoft.com

VBAからDLLへ文字列を渡す

VBAからDLLへ文字を渡すだけ(一方通行)の場合、

呼出元/先引数指定備考
VBAByVal vString As Variant 
DLLVARIANT vString 

DLLからVBAへ文字列を返す(パラメータ)

呼出元/先引数指定備考
VBAByRef vString As Variant 
DLLVARIANT* pvString*を忘れないこと

DLLからVBAへ文字列を返す(復帰値)

呼出元/先宣言備考
VBADeclare Function FuncName Lib LIB_PATH () As Variant 
DLL__declspec(dllexport) VARIANT WINAPI FuncName 

コード

DLL

AccessibleFromVBA.h
AccessibleFromVBA.cpp
AccessibleFromVBA.def
stdafx.h

VBA

実行サンプル

SetString実行時
f:id:Z1000S:20191010204255j:plain

GetStringByParam、GetStringByRetVal実行結果
f:id:Z1000S:20191009163339j:plain

最後に

文字列はVARIANTで受け渡しですよ!!!
バ・リ・ア・ン・ト


次は配列当たりでしょうか?
では、また1年半後にwww


z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com






プリコンパイル済みヘッダ使わなきゃよかった・・・