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 です。
理由は、以下の通りです。
VBA の String は、
ByVal 渡しの場合はバイト文字列 BSTR 構造体へのポインターとして渡されます。
ByRef 渡しの場合はポインターへのポインターとして渡されます。
文字列を格納している VBA の Variant は、
https://docs.microsoft.com/ja-jp/office/client-developer/excel/how-to-access-dlls-in-excel
ByVal 渡しの場合は Unicode ワイド文字文字列 BSTR 構造体へのポインターとして渡されます。
ByRef 渡しの場合はポインターへのポインターとして渡されます。
Excel は、ワイド文字 Unicode 文字列を使用して内部で動作しています。VBA ユーザー定義関数が String 引数を取るように宣言されている場合、Excel は指定した文字列をロケール固有の方法でバイト文字列に変換します。関数に Unicode 文字列を渡す場合、VBA ユーザー定義関数は String 引数の代わりにバリアント型を受け入れる必要があります。その後、DLL 関数は、VBA から バリアント BSTR ワイド文字列を受け入れることができます。
DLL から VBA に Unicode 文字列を返すには、バリアント文字列引数を修正する必要があります。これが機能するには、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_BSTR | 0x0008 | 文字列 |
VT_BSTR | VT_BYREF | 0x4008 | 文字列(参照) |
bstrVal
vtに、VT_BSTRが設定されている時、bstrValには、文字列が格納されています。pbstrVal
vtに、VT_BSTR | VT_BYREF が設定されている時、pbstrValには、文字列へのポインタが格納されています。以下の2つの違いについては、後述します。
- VT_BSTR
- VT_BSTR | VT_BYREF
関数
DLL側でのVARIANT内部形式の判断
VARIANTとBSTRの変換を行う際に、気をつけなければいけないのが、vt の値です。
文字列が格納されている場合の vt の値は、以下の2つがある事を書きました。
これらは、DLLの関数に渡すVBAの変数の型に依存します。
vt | VBAの変数の型 | 呼出例 | 備考 |
---|---|---|---|
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) |
VARIANTの文字列の変換
VARIANT→BSTR
std::wstring のコンストラクタを使って初期化します。パラメータ v が、VT_BYREF の有無で処理方法が変わってきます。
vt | 文字列情報が格納されているメンバー | 左記要素に格納されている内容 |
---|---|---|
VT_BSTR | v.bstrVal | 文字列 |
VT_BSTR | VT_BYREF | v.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変数に文字列を設定する場合、以下の手順で行います。- vt に VT_BSTR または、VT_BSTR | VT_BYREF をセットする。
- 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);
コード
DLL
AccessibleFromVBA.h
AccessibleFromVBA.cpp
AccessibleFromVBA.def
stdafx.h
実行サンプル
SetString実行時
GetStringByParam、GetStringByRetVal実行結果
最後に
文字列はVARIANTで受け渡しですよ!!!
バ・リ・ア・ン・ト
次は配列当たりでしょうか?
では、また1年半後にwww
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
z1000s.hatenablog.com
プリコンパイル済みヘッダ使わなきゃよかった・・・