質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Win32 API

Win32 APIはMicrosoft Windowsの32bitプロセッサのOSで動作するAPIです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

Q&A

解決済

2回答

1046閲覧

C++DLLでGlobalAllocしたメモリをC#のP/Invokeで受け取る際に文字列のマーシャリングをしていると勝手に解放される。

naitou

総合スコア141

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Win32 API

Win32 APIはMicrosoft Windowsの32bitプロセッサのOSで動作するAPIです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

0グッド

1クリップ

投稿2023/09/23 02:09

GlobalFreeを実行するとプログラムがクラッシュする場合があり、色々調べた結果、下記サンプルコードの様にGlobalFree相当の解放が勝手に行われているという結論に至りました。

文字列のマーシャリングをしているMallocEx1では「C++処理 MallocEx」というメッセージボックスが出ているときは、プロセスの仮想メモリ使用量(コミットサイズ)が500MiBを超え、「C#処理 MallocEx1呼び出し完了」というメッセージボックスが出るところまで行くと、GlobalFreeしていないのにメモリが勝手に解放されていることをタスクマネージャーなどで確認しました。

MallocEx2のようにマーシャリングをしない場合はメモリが解放されないため、メモリリークして仮想メモリ使用量(コミットサイズ)が増えて1000MiBを超えました。

GlobalAllocしたものが勝手に開放されることに大変驚いたのですが、このような挙動になることはCLRや.NET Frameworkの仕様で決められていることなのでしょうか。

C++DLL側
Visual Studio 2022
WindowsSDK 10.0.22000.0

C++

1#include<windows.h> 2char* tmp; 3void _stdcall MallocEx(int size,int value,char** ppRet) { 4 tmp = (char*)GlobalAlloc(GMEM_FIXED, size); 5 memset(tmp, value, size ); 6 MessageBox(NULL,"C++処理 MallocEx","",NULL); 7 if (size > 100) {//100文字で頭打ち 8 size = 100; 9 } 10 tmp[size-1] = 0; 11 *ppRet = tmp; 12} 13void _stdcall FreeEx() { 14 MessageBox(NULL, "C++処理 FreeEx", "", NULL); 15 GlobalFree(tmp); 16}

モジュール定義ファイル

C++

1LIBRARY 2EXPORTS 3 MallocEx 4 FreeEx

C++DLLを呼び出すC#側
Visual Studio 2022
.NET Framework4.8

C#

1using System; 2using System.Collections.Generic; 3using System.Linq; 4using System.Runtime.InteropServices; 5using System.Threading.Tasks; 6using System.Windows.Forms; 7using System.Text; 8using System.Text.RegularExpressions; 9 10namespace WindowsFormsApp1 { 11 internal static class Program { 12 13 [DllImport("TestDll.Dll",EntryPoint="MallocEx")] 14 extern public static void MallocEx1(int size,int value,ref string buff); 15 16 [DllImport("TestDll.Dll",EntryPoint="MallocEx")] 17 extern public static void MallocEx2(int size,int value,ref IntPtr buff); 18 19 [DllImport("TestDll.Dll")] 20 extern public static void FreeEx(); 21 22 [STAThread] 23 static void Main() { 24 int size = 1024*1024*500;//500MiB確保 25 string str1 = null; 26 IntPtr str2 = IntPtr.Zero; 27 28 MallocEx1(size,'0',ref str1); 29 MessageBox.Show("C#処理 MallocEx呼び出し完了 "+str1); 30 31 MallocEx1(size,'1',ref str1); 32 MessageBox.Show("C#処理 MallocEx呼び出し完了 "+str1); 33 34 MallocEx2(size,'0',ref str2); 35 MessageBox.Show("C#処理 MallocEx呼び出し完了 "+Marshal.PtrToStringAnsi(str2)); 36 37 MallocEx2(size,'1',ref str2); 38 MessageBox.Show("C#処理 MallocEx呼び出し完了 "+Marshal.PtrToStringAnsi(str2)); 39 40 } 41 } 42} 43 44

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ネイティブ相互運用性のベスト プラクティス

❌ 禁止: [Out] string パラメーターを使用しないでください。 文字列がインターン処理された文字列で、文字列パラメーターが [Out] 属性の値で渡された場合、ランタイムが不安定になる可能性があります。 文字列のインターン処理の詳細については、String.Intern のドキュメントを参照してください。

Windows 固有: [Out] 文字列の場合、CLR は文字列を解放するために既定で CoTaskMemFree を使用します。また、UnmanagedType.BSTR とマークされている文字列の場合は SysStringFree を使用します。

これらを踏まえた上で、下のURLの内容を見てください。異なるメモリアロケータでも、実装が同じになるケースは有りえるようで、たまたま違うメモリ解放関数でも解放されただけと推測されます。
CoTaskMemAlloc v malloc v AllocHGlobal

マーシャラーの挙動を完全に理解していたとしても、ネイティブDLL側で確保したメモリを ref string で受けるのは、個人的にかなり怖い作りだなと思います。

既定のマーシャリングの動作

使用しているメモリが CoTaskMemAlloc メソッドで割り当てられていない場合、IntPtr を使用し、適切なメソッドを使用して手動でメモリを解放する必要があります。

とあるので、自分でアロケータを制御する必要があるなら、MSのドキュメントに従ってIntPtrにするか、unsafeで素のポインタを使うのがいいと思います。

投稿2023/09/29 01:00

編集2023/09/29 01:12
nururi

総合スコア98

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

naitou

2023/09/29 06:26

まさに求めていた情報でした。よく読んでみます。ありがとうございます。
guest

0

ベストアンサー

(1) DLL の作りがおかしいです。
MallocEx を2回呼び出すと、tmp は置き換わっちゃうのでは?
1回目に呼び出したときに確保したメモリを解放できなくなっちゃいます。
FreeEx のほうはアドレスを渡して解放すべきです。

(2) MallocEx1 が ref string で受けている
ref string で受けるなら、BSTR* を返さないといけないのでは?
BSTR は char* にキャストできますが、その逆はダメだと思います。

(3) 1回目の MallocEx1 で string オブジェクト(モドキ)が作成されます。
2回目の MallocEx1 では 別の string オブジェクトが作成され、str1 に格納されますが、1回目の string オブジェクトは参照が切れるので GC が動くと解放されてもおかしくありません。


うまくいった組み合わせ

以下の組み合わせでメモリ破壊なく動きました。

csharp

1[DllImport("SampleDLL.Dll")] 2[return: MarshalAs(UnmanagedType.BStr)] 3extern public static string StringTest( 4 [MarshalAs(UnmanagedType.BStr)] ref string buff);

c++

1BSTR WINAPI StringTest(BSTR* ppRet) { 2 wchar_t str[] = L"Test!"; 3 if (*ppRet != nullptr) { 4 SysFreeString(*ppRet); 5 } 6 *ppRet = SysAllocStringLen(str, lstrlen(str)); 7 return SysAllocStringLen(str, lstrlen(str)); 8}

考察

ref string で渡したとき C# の動きは次のようになっていると思われます。

(1) 文字列を渡すとき、null でなければ BSTR 文字列(MarshalAs で指定された型)を作成し、そのアドレスを dll に渡す
(2) DLL から戻ってきたら返された BSTR から String 文字列を作成し、渡された BSTR は解放する

こう考えると辻褄が合います。

投稿2023/09/23 04:53

編集2023/09/24 02:22
KOZ6.0

総合スコア2626

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

KOZ6.0

2023/09/23 05:04 編集

2回目の MallocEx1 を別の変数で受ければ解放されないと思いますがいかがでしょう? もっとも ref string で受けてる時点で怪しいですけどね。
naitou

2023/09/23 08:28 編集

>>(1) DLL の作りがおかしいです。 現象を最小コードで再現するサンプルなのでご容赦下さい。 MallocEx を2回呼び出す前に必ずFreeExを呼び出す使用法のイメージです。 >>(2) MallocEx1 が ref string で受けている >>(3) 1回目の MallocEx1 で string オブジェクト(モドキ)が作成されます。 メッセージボックスが表示される度にメモリ使用量を確認すると、2回目の MallocEx1が呼ばれる前にメモリが解放されているため、そもそもマーシャリングでGlobalAllocしたメモリとの関係が切れてると考えています。 [DllImport("TestDll.Dll",EntryPoint="MallocEx")]と文字セットを指定しない場合は アスキー文字のマーシャリングになっているかと思っていました。 DLL側を void _stdcall MallocEx(int size,int value, wchar_t** pRet) とBSTR形式にして、C#側を [DllImport("TestDll.Dll",EntryPoint="MallocEx", CharSet = CharSet.Unicode)] にしてみましたが、同様のメモリ量増減動作となりました。 なおこの時にC#側を元の [DllImport("TestDll.Dll",EntryPoint="MallocEx")] としていると先頭の1文字しかstringに入っていないというおかしな動作でした。
naitou

2023/09/23 09:56 編集

すみません。「BSTR形式にして」と書いてますが、検証したのはBSTR形式ではなくただのwchar_tで、BSTRの先頭に含まれる4バイトのサイズ情報がありませんでした。先頭4バイトを入れて、5バイトからのアドレスを返してBSTR形式の確認をしようと思ったのですが、引数がref stringだとGlobalAllocしたメモリの先頭アドレスを渡さないとプログラムがクラッシュする現象が発生しました。たしかにもし勝手に開放してるなら先頭アドレスを渡さないと解放できないよなと少し納得してしまいました。 また以下のようにSysAllocStringで作ったBSTRをref stringで受けると、プログラムがクラッシュしました。ref stringで何かやってそうなのが関係してるのもかもしれないです。 void _stdcall MallocEx(int size, int value, BSTR* pRet) { BSTR tmp = SysAllocString(L"aaaaaaaaaa"); *pRet = tmp; }
KOZ6.0

2023/09/23 11:18 編集

そもそもの目的は何だったのでしょうか? 確実に受け取るのであれば、IntPtr を使えば良いです。 ref string で受け取りたいなら、ワンクッション置いて IntPtr を使えばいいです。 追及するのであれば、引数に MarshalAs 属性をつけて UnmanagedType を指定してみてください。 「MarshalAsAttribute クラス」 https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.marshalasattribute 「UnmanagedType 列挙型」 https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.unmanagedtype
naitou

2023/09/23 14:42

目的はこのような挙動になることがCLRや.NET Frameworkの仕様なのか見識のいる方からご教授頂けないかと思った次第です。実現方法論としてはIntPtrを用いるのが良さそうですね。
KOZ6.0

2023/09/24 04:34 編集

ヒープ領域の string にグローバルメモリ渡して大丈夫?え!渡せちゃう? 不変(immutable) なはずの string を書き換えならまだしも置き換えて挙動がおかしくならないの? と疑問点ばかりです。 あいまいな知識で回答して申し訳ありません。
naitou

2023/09/25 15:25

検証ありがとうございます。 [MarshalAs(UnmanagedType.BStr)] ref string buffでBSTRを受けれるようになりますね。 無指定だとアスキーなのですから、BSTRで受けるならそう明示的に書かないとダメですよね。 >SysAllocStringで作ったBSTRをref stringで受けると、プログラムがクラッシュしました というのは当たり前でしたね。 >(1) 文字列を渡すとき、null でなければ BSTR 文字列(MarshalAs で指定された型)を作成し、そのアドレスを dll に渡す その通りの様ですね。勉強になりました。しかしCLRでメモリ並び変えが実行されたらアドレスが変わりそうな気がしますね。 >(2) DLL から戻ってきたら返された BSTR から String 文字列を作成し、渡された BSTR は解放する 以下のように巨大なBSTRを作ってStringTestを何度も呼び出してみたのですが、 今度は逆にBSTRが解放されていませんでした。。。 SysFreeStringを呼び出してもタスクマネージャー読みでメモリ量が多いままでした。 なんだかもうref stringは使わない方が良い気がしてきました。。。 int size = 1024 * 1024 * 250; wchar_t *str = (wchar_t*)malloc(size); int len = size/2-1; for (int i = 0; i < len ;i++) { str[i] = L'a'; } str[len] = L'0'; if (*ppRet != nullptr) { SysFreeString(*ppRet); } *ppRet = SysAllocStringLen(str, lstrlenW(str)); free(str);
KOZ6.0

2023/09/25 16:58 編集

>しかしCLRでメモリ並び変えが実行されたらアドレスが変わりそうな気がしますね。 BSTR はアンマネージメモリに作られるので GC の対象外です。 >今度は逆にBSTRが解放されていませんでした。。。 解放した分、C# 側にメモリが増えてるからでは? 何回かやってみましたが、DLL を抜けるときに一瞬ワーキングセットが 500MB オーバーしてすぐに 270MB くらいに戻りましたよ。 メモリの無駄が多いので使わないほうが良いというのは同感です。
naitou

2023/09/27 09:07

検証ありがとうございます。 マネージドなstringオブジェクトがアンマネージメモリの参照を保持していて大丈夫なのか疑問に思いました。 またstringオブジェクトの参照が無くなったら解放されるのかも疑問でした。(開放されると逆にまずい) こちらで出てるメモリリークは実行コードの違いかもしれないですね。元の質問から離れて来ているので言及は辞めようと思います。もしかしたら別タイトルで質問を作成するかもしれません。
KOZ6.0

2023/09/27 10:09

String オブジェクトがアンマネージメモリを参照しているわけではないですよ? 管理しているのは、マネージとアンマネージの橋渡しをしているマーシャラーです。 メモリを解放するのも仕様ですね。 この辺、どうなるんだろうと思ってたのですが、私はスッキリしました。
naitou

2023/09/27 16:02

オブジェクトのメモリアドレスをダンプしつつ確認したところ、String オブジェクトがアンマネージメモリを参照しているわけではないことを確認し、かつC# 側のマネージドにメモリが増えており、繰り返し実行しているうちに遅れて解放されていることを確認しました。しかしC#側でなぜかstring変数へnullを代入してから、GC.Collect()を実行しないと全然解放されないと、それでも微妙にリークしてるような感じはあるんですが保留とします。 お付き合い頂きありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問