- PR -

DLLから文字列を取得する方法

投稿者投稿内容
いろは
常連さん
会議室デビュー日: 2005/05/11
投稿数: 31
投稿日時: 2006-07-13 17:22
"Dr. GUI と COM オートメーション"を読んでみました。
SysStringAllocを使う時はUnmanagedType.BStrでCoTaskMemAllocを使う時はUnmanagedType.LPWStrの方が良いと(自分の中では)理解しました。

MarshalAsでBStrを指定することで、DLL側ではBSTR*なのでCoTaskMemAllocを使うより、SysStringAllocを使って文字列を設定すべきという結論を導き出しました。

CoTaskMemFreeで例外が出るという問題が残ってしまいますが、とりあえずBlueさんに最初に提示していただいたソースで今回は行こうと思います。

とりあえず閉めさせていただきます。
もし補足などがありましたらコメントをお願いいたします。

Blueさん ありがとうございました。
Blue
大ベテラン
会議室デビュー日: 2005/09/12
投稿数: 230
お住まい・勤務地: 知っている人は知っている
投稿日時: 2006-07-14 00:30
一応まとめてみました。
ただし、GetMessageという関数名はすでにあるWin APIとかぶるので変えてあります。

VC(VS2005)
コード:


HRESULT WINAPI GetMsg( LPWSTR* msg )
{
const WCHAR* pwcs = L"Hello World";
const size_t cb = ( wcslen( pwcs ) + 1 ) * sizeof( WCHAR );
LPVOID pv = NULL;

pv = ::CoTaskMemAlloc( cb );
if ( pv != NULL )
{
memcpy( pv, pwcs, cb );
::CoTaskMemFree( *msg );
*msg = static_cast< LPWSTR >( pv );
return S_OK;
}
return E_OUTOFMEMORY;
}



VC#(VS2005)
コード:


[DllImport("XXX.dll")]
[return: MarshalAs(UnmanagedType.U4)]
public static extern UInt32 GetMsg([MarshalAs(UnmanagedType.LPWStr)] ref string msg);

static void Main(string[] args)
{
string msg = "";
if (GetMsg(ref msg) == 0)
{
Console.WriteLine(s);
}
}



一応これで動いているようです。

自分の書き込みより
引用:

ということは、SysAllocStringを使う場合「LPWStr」で受けるのは大丈夫なのかな?
これがまずいと、SysAllocStringByteLen+LPStrもまずくなるような。。。


SysAllocStringByteLenの場合は「AnsiBStr」でうけるっぽいです。

[ メッセージ編集済み 編集者: Blue 編集日時 2006-07-14 00:30 ]
yayadon
常連さん
会議室デビュー日: 2003/07/23
投稿数: 41
投稿日時: 2006-07-15 00:11
# ATLとかは使ってないんですよね?

そのDLLが,
COM仕様だったり,さらに,
オートメーションをサポートしているとしたら,
参照設定して使います。

[DllImport("xxx.dll")] ... static extern ... は,
ふつうのDLLの時に使います。

文字列の場合の in/out は,マーシャラに任せて,
そのまま StringBuilder でやります。

LPWSTR のところは WCHAR* でも同じです。
いずれも,それでポインタで,ポインタのポインタにする必要はないです。

DLL側
コード:
// stdafx.h の #include <windows.h> の下あたりに

#include <malloc.h>    // realloc
#include <stdlib.h>    // realloc

#include <wchar.h>     // wmemcpy_s


// 例えば,DLLCode.h に

#define EXPORT_C extern "C" __declspec(dllexport)

EXPORT_C int GetMsg(LPWSTR);
 

// 例えば,DLLCode.cpp に

int GetMsg(LPWSTR msg)
{
    LPWSTR tmp = L"ほげほげ";

    msg = (LPWSTR) realloc(msg, (wcslen(tmp) + 1) * sizeof(WCHAR));
    if(msg == NULL) return -1;

    errno_t err = wmemcpy_s(msg, wcslen(tmp) + 1, tmp, wcslen(tmp) + 1);
    if(err) return -1;

    return 0;  // 正常終了
}



C#側
コード:

    [DllImport("xxx.dll")]
    public static extern int GetMsg([MarshalAs(UnmanagedType.LPWStr)]StringBuilder msg);


    private void button1_Click(object sender, EventArgs e)
    {
        StringBuilder msg = new StringBuilder("");
        int rc = GetMsg(msg);
        if(rc == 0)
        {
            MessageBox.Show(msg.ToString());
        }
    }




CoTaskMemなんとか は,COM絡みの時に
片方が確保したメモリを片方が開放したりとかで有効で
ふつうのDLL時は,
マーシャラにとっては関係ないような気がします。

BSTR 自体は,文字列先頭をポイントしています。
結果として,OLECHAR* / WCHAR* と同じ。
ただ,オートメーションの時に特に有効で,
今回の場合は,COM仕様でもないので,
面倒なので使わなくてもいいような気がします。
SysAllocStringは,OLECHAR* の文字列の前に文字列のバイト長を付加して,
でも,文字列の先頭へのポインタ(OLECHAR* / WCHAR* / BSTR)を返すものです。
Blue
大ベテラン
会議室デビュー日: 2005/09/12
投稿数: 230
お住まい・勤務地: 知っている人は知っている
投稿日時: 2006-07-15 01:35
引用:

稍丼さんの書き込み (2006-07-15 00:11) より:

LPWSTR のところは WCHAR* でも同じです。
いずれも,それでポインタで,ポインタのポインタにする必要はないです。


本当にこのコードうまくいきますか?(実際にコピペしてみましたがエラーになりました)

LPWSTR* で受けないと、reallocした値はGetMsg関数から戻ってきたときにmsgの値に
反映されないと思うのですが。

コード:

int test(int* p){
    p = (int*)malloc(sizeof(int));
}

int main(){
    int* p = NULL;
    test(p);
    *p = 10; // pの値はNULLのまま
}


これと一緒。

LPWSTRで受けるのは、WINAPIのようにLPWSTRの指す領域に単に文字列をコピーする
時にしか使えないのではないのでしょうか?
yayadon
常連さん
会議室デビュー日: 2003/07/23
投稿数: 41
投稿日時: 2006-07-15 03:26
realloc でポイント先が変わってしまうようだとダメですね。

ただ,今回は,out でなく in/out なので,
あらかじめ EnsureCapacity で確保しておいてやれば,
その容量に応じてマーシャラが確保したブロックと,別物になるとは思えないので,
良さそうな気もするんですけどね...

実際ダメなら,却下と言うことで。
yayadon
常連さん
会議室デビュー日: 2003/07/23
投稿数: 41
投稿日時: 2006-07-15 06:01
realloc が具合が悪いなら,

コード:

EXPORT_C int GetMsg(LPWSTR, UINT);


と文字数を指定できるようにしてやって,

コード:

// DLL側
int GetMsg(LPWSTR msg, UINT uCount)
{
LPWSTR tmp = L"ほげほげまげまげ";
if(wcslen(tmp) > uCount){
return -1;
}
errno_t err = wmemcpy_s(msg, uCount + 1, tmp, wcslen(tmp) + 1);
if(err) return -1;

return 0; // 正常終了
}

// C#側
[DllImport("xxx.dll")]
public static extern int GetMsg(
[MarshalAs(UnmanagedType.LPWStr)]StringBuilder msg, uint uCount);

private void button4_Click(object sender, EventArgs e)
{
int capacity = 20; // 最大文字数
StringBuilder msg = new StringBuilder(capacity);
int rc = GetMsg(msg, (uint)msg.Capacity);
MessageBox.Show(msg.ToString());
}


かなぁ...

[ メッセージ編集済み 編集者: 稍丼 編集日時 2006-07-15 06:13 ]
yayadon
常連さん
会議室デビュー日: 2003/07/23
投稿数: 41
投稿日時: 2006-07-17 13:04
# 質問者のVS2003でなくVS2005でなのでアレなんですが...

ポインタのポインタ版を実際に試してみると,
確かに受け取れます。
ただ,ポインタのポインタにした場合,
DLL側が確保した領域にある文字列を
マーシャラが開放していないようです。

DLL側を新規作るのだったり,いじれたりするんだったら
ポインタとStringBuilderの組み合わせでやるのが無難な気がするんですけどね。
(StringBuilderのcapacityより多くの文字列は受け取れないので
注意が必要ですが)
マーシャラがDLL呼び出し前にバッファの確保と返ってきてからの開放を
ちゃんとやっているので,その方が安心でしょう。
いろは
常連さん
会議室デビュー日: 2005/05/11
投稿数: 31
投稿日時: 2006-07-18 09:50
質問者です。

稍丼さま Blueさま ありがとうございます。
週末はちょっとばたばたしていて書き込みを見ることが出来ませんでした。
内容に一部分からないところもありますが、勉強のつもりで何とかついていきたいと思いますので、よろしくお願いします。

引用:

稍丼さんの書き込み (2006-07-17 13:04) より:
# 質問者のVS2003でなくVS2005でなのでアレなんですが...



今はVS2003ですが、現在VS2005に移るべく対応中です。
EXEもDLLもソース修正は可能です。DLL自体はCOMではありません。

引用:

稍丼さんの書き込み (2006-07-17 13:04) より:
ポインタのポインタ版を実際に試してみると,確かに受け取れます。
ただ,ポインタのポインタにした場合,DLL側が確保した領域にある文字列をマーシャラが開放していないようです。



さすがにこれはまずいですよね。

引用:

DLL側を新規作るのだったり,いじれたりするんだったらポインタとStringBuilderの組み合わせでやるのが無難な気がするんですけどね。
(StringBuilderのcapacityより多くの文字列は受け取れないので注意が必要ですが)
マーシャラがDLL呼び出し前にバッファの確保と返ってきてからの開放をちゃんとやっているので,その方が安心でしょう。



確かにこれだけやり取りが面倒なのであれば、素直に提示していただいたStringBuilderを使用して文字数も入力した方が安全なのかもしれませんね。そもそもの発想が『CoTaskMemAllocを使えば良いんだ』っと言うところでしたので。
まだサンプルを試していませんので、後ほどやってみて結果報告いたします。

スキルアップ/キャリアアップ(JOB@IT)