こんにちは。
先日
http://madia.world.coocan.jp/cgi-bin/Vcbbs/wwwlng.cgi?print+200804/08040016.txt
で質問させていただいた件の続きです。
VC++、VB、VBAから関数を呼び出せるDLLは作れました。
ただ、引数に文字列を扱おうと、VBAではうまくいかないんです(VBではまだ未確認です)。
例えば、DLLから呼び出す関数を
void DllFunk(BSTR Input, BSTR *Output)
{
MessageBoxW(NULL, Input, L"でばっぐ", MB_OK);
if(*Output != NULL)SysFreeString(*Output);
*Output = SysAllocString(Input);
}
とし(__stdcallなどは省略しました。)、VBA側で
Declare Sub・・・ DllFunk(ByVal a As String, ByRef b As String)
みたいな感じで定義し、VBAでこの関数をCallしてみると、メッセージボック
ス内の引数Inputの部分が文字化けしていました。
試しにDLLのBSTRをchar*にしてみると、引数としては文字化けせずに受け取れ
ました。
また、DLL側の引数はBSTRのまま、VBAの関数でCallするときにStrConv関数を
使い試してみたところ、一部の文字は表示されましたが、文字化けしてしまう
文字も多々ありました(例えば"わゐうゑを" → "??う??"となります)。
やはり文字コードが違うようです。
VC++、VB、VBAで万能に使えるDLLを作成したかったのですが、DLLを作成する
際に、何かしかればならない事があるのでしょうか?
それとも、各言語側で引数に入れる前に文字コードの変換などを行う用にする
しかないのでしょうか?
ご存じの方いらっしゃいましたら、お願いいたします。
ちなみに環境は
OS:Windows XP Pro with SP2
VC++、VB:Visual Studio .NET 2005 Team Edition
Access:2003 with SP3
です。
VBAのDeclare宣言をどうしていますか?
VB6以前やVBAではString型にしてしまうと、自動的にchar*で扱う文字コードに変換されてしまいます。
Unicode文字列を渡すのであれば、StrPtr関数・Long型で渡すようにしてください。
(VB.NETからは文字コードを指定できます。)
過去ログ
http://madia.world.coocan.jp/cgi-bin/Vcbbs/wwwlng.cgi?print+200606/06060060.txt
というか、なぜBSTRなのですか?
VBから呼ぶからと言ってBSTRである必要はありませんよ。
LPCSTR か、Unicode である必要があるのなら LPCWSTR が一般的でしょう。
Blueさん、シャノンさん、いつもありがとうございます!!
>Blueさん
そうだったんですね。
確かに
Declare Sub・・・ DllFunk(ByVal a As String, ByRef b As String)
→Declare Sub・・・ DllFunk(ByVal a As Long, ByRef b As String)
とし、
Call DllFunk(String1, String2, ReturnCode)
→Call DllFunk(StrPtr(String1), String2, ReturnCode)
としたら、DLL内のでバック用メッセージボックスの方は正しく文字を受け取
ることができました。
ただ、2つめの引数は、DLL側ではBSTR型のポインタにしており、DLL内で処理
した文字列をアプリ側に返すための変数なのですが・・・
こちらも何か工夫をしなければならないのでしょうか?
(DLL側の引数をLPCSTR *にする?、VBA側でStrPtrと逆の処理をする??)
VBA側で、戻ってきた変数をStrConvで変換してみたのですがうまく行かず・・・
>シャノンさん
初めてVB、VC++共通のDLLを作成しようとしたとき、
http://m--takahashi.com/bbs/pastlog/08600/08523.html
等を見て、「VC++でVBでも使えるDLLを作成するには文字列はBSTR型を使わな
いとダメなんだ・・・Unicodeじゃ内と行けないって事なんだな〜」と思い込
んでしまっていました。
で、DLLは作成したものの、VBやVBAでは動作確認をしておらず・・・。
どうにでもなるんですね。
ちなみに、LPCSTR はchar *、LPCWSTRは WCHAR *とほぼ同等ですよね?
一度、DLLの関数を
void DllFunk(char *Input, char **Output)
としてみたのですが、いったん*Outputを初期化してから必要領域を確保しよ
うと
if(*Output != NULL)
{
delete[] *Output;
*Output = NULL;
}
としていたのですが、どうやらここで処理がおかしくなっているらしく、VBA
側からは完全に応答がなくなる、という現象が発生しました。
BSTRを使っていると問題なくいけているのですが(SysFreeString関数)・・・
恥ずかしながら、自作の関数などでLPCSTR、LPCWSTR等を使ったことがなく、
char *、WCHAR *しかほぼ経験がなくて・・・
何か特殊な扱い方があるのでしょうか?
長々とすみません・・・
よろしくお願いいたします。
ごめんなさい、訂正です。
>ちなみに、LPCSTR はchar *、LPCWSTRは WCHAR *とほぼ同等ですよね?
ちなみに、LPCSTR はconst char *、LPCWSTRはconst WCHAR *とほぼ同等ですよね?
でした。
>2つめの引数は、DLL側ではBSTR型のポインタにしており、DLL内で処理
>した文字列をアプリ側に返すための変数なのですが・・・
リンク先はまさにその例ですが。→Sample03A関数
文字列格納はBSTRを使うのが普通なのでは?(LPCWSTRはconstだから参照のみに使うという意味だと思うが。)
内部でCoTaskMemAlloc関数とうでメモリを確保して、文字列をコピーして
ポインタを返してもも、VBA側ではその中身を見れないです。
(ポインタを扱えないから当然です。MoveMemory APIをつかうなら元からBSTRにしろってこと。)
Blueさん、レスありがとうございます。
リンクは拝見していて、以下のようにテスト関数を作ってみたのですが
【DLL側】
#define DllExport extern "C" __declspec(dllexport)
DllExport void __stdcall TestFunc(BSTR Input, BSTR *ReturnValue, UINT *ErrorCode)
{
if(*ReturnValue != NULL)
{
SysFreeString(*ReturnValue);
*ReturnValue = NULL;
}
MessageBoxW(NULL, Input, L"引数1", MB_OK);
*ReturnValue = SysAllocString(Input);
MessageBoxW(NULL, *ReturnValue, L"引数2にコピーした結果", MB_OK);
*ErrorCode = 0;
return;
}
【VBA側】
Declare Sub TestFunc Lib "MyDll.dll" (ByVal InputString As Long, ByRef OutputString As String, ByRef ReturnCode As Integer)
'ボタンクリック時にテスト実行
Private Sub Button1_Click()
Dim RCode As Integer
Dim TestOutput As String
Call GetPCModel(StrPtr("あいうえおわゐうゑを"), TestOutput, RCode)
MsgBox TestOutput
End Sub
としてみると、DLL内部のデバッグ用メッセージボックスはきちんと「あいう
えおわゐうゑを」とどちらも表示されるのですが、VBA側の方では「B0D0F0H0J
0・・F0・・」と表示されます。
試しに
MsgBox StrConv(TestOutput, vbFromUnicode)
としてみると「あいうえお??う??」と表示されてしまいます。
文字コードが違うみたい??です。
どうしてなのでしょう??
> どうにでもなるんですね。
はい。
VBA は知りませんが、VB6 でも、LP(C)STR を引数に取る関数は呼べます。
> LPCSTR はconst char *、LPCWSTRはconst WCHAR *とほぼ同等ですよね?
ほぼと言うか、完全に同じです。
> if(*Output != NULL)
> {
> delete[] *Output;
> *Output = NULL;
> }
delete[] は new[] で確保したメモリを開放するのにしか使えません。
VB 側のメモリは new[] で確保されているとは限らないので、これは使えません。
VB と DLL で文字列をやり取りする場合、メモリは VB 側で確保します。
それを DLL 側で解放したり、DLL 側で確保したりは…BSTR と関連 API を使えばできるかもしれませんが、やったことはありませんし、やらないほうがいい気がします。
呼び出し側でメモリを確保する方が設計もシンプルになりますし。
なお、上記サンプルのように文字列をコピーするだけであれば、ReturnValue を解放する必要も無いように思いますが(そこに解放しなければならないポインタを渡さないというのは呼び出し側の責任ではないでしょうか)。
>*ReturnValue = SysAllocString(Input);
なぜSysAllocString?
過去ログのサンプルのSample03Aは「SysAllocStringByteLen」を使っていますが
試されましたか?
VBAでは Sample03W 関数のように SysAllocString をつかってUnicodeで格納する
ものは使えません。(VB.NETからはできる)
>シャノンさん
>> LPCSTR はconst char *、LPCWSTRはconst WCHAR *とほぼ同等ですよね?
> ほぼと言うか、完全に同じです。
そうだったんですね。
MSDNで、ぱっと見たときに
typedef __nullterminated CONST CHAR *LPCSTR;
とあったので「__nullterminatedってなんじゃらホイ??ま〜ほぼ一緒って事
で」って言う感じで認識してました(汗)
>Blueさん
SysAllocStringByteLenは、第一引数はLPCSTRですよね?
ということは、いったんマルチバイトに変換してからSysAllocStringByteLen
を行うという事でしょうか(コピー元の引数がBSTR型なので)?
そうなるとSample03Wが該当しますよね?
VBAにわたすときにString型では勝手にAnsi→Unicode変換がおこなわれてしまう。→(1)
よって、BSTRにUnicodeで文字列を格納しても、UnicodeをAnsiとしてUnicode変換してしまうようです。
(よって全く使えない文字列になる)
ですので、BSTRだけれどもAnsi文字列を格納する必要があるため、
SysAllocStringではなくSysAllocStringByteLenを使うのです。
>ということは、いったんマルチバイトに変換してから>SysAllocStringByteLen
>を行うという事でしょうか(コピー元の引数がBSTR型なので)?
そうです。
>そうなるとSample03Wが該当しますよね?
ですから、VBAからは(1)が自動的に行われるのでサンプルで示した 〜W系 の関数は使えません。
(VB.NETはMarshalAs属性を使って指定できます。)
> Blueさん
ありがとうございます。
確かにこれで、うまく値を持ってくることができました。
そこで新たな疑問です。
このDLLを、VC++で使った場合、このままでは使えませんよね?
ということは、DLL内に変換後の文字列変数の型を変えて、VC++用、VBA用とい
ったように関数を分けるか、void **のように、型を特定しないような扱いに
して、関数の引数に戻す文字列の型を指定する、という形にせざるを得ないと
いう事で間違いないでしょうか?
あくまでも目標が「VC++、VB、VBAで使えるDLLの作成」なので・・・
よろしくお願いいたします。
> __nullterminatedってなんじゃらホイ??
SAL 注釈というやつですね。
http://msdn2.microsoft.com/ja-jp/library/ms235402(VS.80).aspx
利用者に対して意図を明確にしたり、対応環境であれば不正な使い方をしたときにコンパイル時警告が表示されたりします。
実行時には何ら影響を及ぼしません。
で…そこまで BSTR にこだわるのは何故ですか?
> シャノンさん
> で…そこまで BSTR にこだわるのは何故ですか?
当初はBSTRでなければならない、と思っていたのですが、そうでなかったとし
ても、可能な限りこういう場合に一般的な型を使いたいな〜、と思っています。
で、Blueさんから
> 文字列格納はBSTRを使うのが普通なのでは?
というお言葉もあり、可能であればBSTRでいこうかな、と思っていたことと、
この関数の内部ではすでにBSTRでやり取りする事を前提に作り込んでしまって
いて(DLL内部に多数の関数が存在します)、入り口と出口だけをchar *にする
のもソースが汚いような気がするし、かといって全部作り直すのもな〜、と言
う気持ちも若干(汗)
汎用性を高めるのって、難しいですね・・・。
> この関数の内部ではすでにBSTRでやり取りする事を前提に作り込んでしまっていて
という事情があるならばやむを得ませんが、VB6 等の互換性まで考えるならば、LP(C)STR にしておくのが、一番互換性が高そうな気がします。
>シャノンさん
そうなると、文字列の引数を
DllExport void __stdcall TestFunc(LPCSTR Input, LPSTR *ReturnValue, UINT *ErrorCode)
のようにして、VBA側で
Dim InputString As String
Dim OutputString As String
OutputString = String(必要な長さ, Chr(0))
とバッファを確保した上で、DLLの関数を呼び出すようにすればよいのですよね?
と言うことは、この関数で必要な文字列長を出せるようにしておくと、安全ですね。
ありがとうございます。
こっちの方法も試してみます。
どちらにせよ、うまく行ったらご報告&解決させていただきます。
Blueさん、シャノンさん。
いつも本当にありがとうございます!!
何とかうまく行くようになりました。
結局、BSTRを使う形にし、関数の引数に
BOOL fAnsi
を追加し、TRUEの場合は、SysAllocStringByteLen、そうでない場合はSyaAllocString
を使うようにしました。
VBA側でこのフラグをBoolern型にするとうまく動作しなかったので、As Long
として、直接1と入力するようにしました。
長々とお付き合いいただき、ありがとうございました。
これで万事うまく行けそうな気がします。
今更だけど、Variantでやり取りすればどちらもうまくいきそう。
試してみた。
void WINAPI Sample01(VARIANT* v)
{
::VariantClear(v);
v->vt = VT_BSTR;
v->bstrVal = ::SysAllocString(L"ほげ");
}
' VBA Call
Public Declare Sub Sample01 Lib "DllTest" (ByRef v As Variant)
Sub test()
Dim v As Variant
ChDrive ThisWorkbook.Path
ChDir ThisWorkbook.Path
Sample01 v
MsgBox v
End Sub
// C Call(動的リンク)
int main(void)
{
HMODULE hDll = ::LoadLibrary(TEXT("DllTest"));
if (hDll)
{
void (WINAPI *pFunc)(VARIANT*) = (void (WINAPI *)(VARIANT*))::GetProcAddress(hDll, "Sample01");
if (pFunc)
{
VARIANT v;
(*pFunc)(&v);
::MessageBoxW(NULL, v.bstrVal, L"", MB_OK);
::VariantClear(&v);
}
::FreeLibrary(hDll);
}
return 0;
}
>Blueさん
おぉ、VARIANT型という手もありましたね。
こっちの方が楽かな・・・
ありがとうございます!!
時間見つけて、この方法もテストしてみます!!
ツイート | ![]() |