Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)

C# - Marshal.GetNativeVariantForObject 사용 시 메모리 누수(Memory Leak) 발생 및 해결 방법

GetNativeVariantForObject 메서드는,

Marshal.GetNativeVariantForObject Method
; https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.getnativevariantforobject

닷넷 객체를 COM 환경의 Variant 구조체로 바꿔주는 역할을 합니다. (지난 글에 다룬 VARIANTARGVARIANT의 typedef입니다.)

typedef struct tagVARIANT {
  union {
    struct {
      VARTYPE vt; // typedef unsigned short VARTYPE;
      WORD    wReserved1;
      WORD    wReserved2;
      WORD    wReserved3;
      union {
        LONGLONG     llVal;
        LONG         lVal;
        BYTE         bVal;
        SHORT        iVal;
        FLOAT        fltVal;
        DOUBLE       dblVal;
        VARIANT_BOOL boolVal;
        VARIANT_BOOL __OBSOLETE__VARIANT_BOOL;
        SCODE        scode;
        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;
        VARIANT_BOOL *__OBSOLETE__VARIANT_PBOOL;
        SCODE        *pscode;
        CY           *pcyVal;
        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;

정의가 복잡한데, 중간의 union을 최대 크기인 16바이트로 놓고 보면,

typedef struct tagVARIANT {
  union {
    struct {
      VARTYPE vt; // == 2바이트 (typedef unsigned short VARTYPE;)
      WORD    wReserved1; // == 2바이트
      WORD    wReserved2; // == 2바이트
      WORD    wReserved3; // == 2바이트
      union {
        LONGLONG     llVal;
        // ...[생략]...        
        IUnknown     **ppunkVal;
        // ...[생략]...        
        struct {  // x64의 경우 16바이트, x86의 경우 8바이트
          PVOID       pvRecord;
          IRecordInfo *pRecInfo;
        } __VARIANT_NAME_4;
      } __VARIANT_NAME_3;
    } __VARIANT_NAME_2;
    DECIMAL decVal;
  } __VARIANT_NAME_1;
} VARIANT;

약식으로 C#에서도 다음과 같은 식으로 정의할 수 있습니다.

[StructLayout(LayoutKind.Sequential)]
public struct Variant
{
    public ushort vt;
    public ushort wReserved1;
    public ushort wReserved2;
    public ushort wReserved3;
    public IntPtr data01;
    public IntPtr data02;
}

따라서 tagVARIANT의 크기는 x86의 경우 16바이트, x64의 경우 24바이트이기 때문에, GetNativeVariantForObject의 2번째 인자에 넘겨주는 메모리의 할당 크기는 당연히 16 또는 24바이트이기만 하면 됩니다.

object obj = new object();

int varSize = sizeof(Variant); // unsafe 문맥 필요
            
IntPtr pAlloc = Marshal.AllocHGlobal(varSize); // 16 또는 24
{
    Marshal.GetNativeVariantForObject(obj, pAlloc);
    Marshal.FreeHGlobal(pAlloc);
}

실제로 pAlloc에 할당된 메모리를 조사해 보면,

0x0000022D6923A900  09 00 00 00 00 00 00 00  ........
0x0000022D6923A908  18 00 35 69 2d 02 00 00  ..5i-...
0x0000022D6923A910  00 00 00 00 00 00 00 00  ........

처음 2바이트의 (이 글의 하단에 실은 VARENUM으로 표현되는) vt 값(0009)은 VT_DISPATCH에 해당하는 VARENUM 값이므로 이후의 0000022d69350018 값은 "IDispatch *pdispVal" 값으로 해석할 수 있습니다.

반면, object가 아니라 상숫값을 넣어 보면,

int varSize = sizeof(Variant);
            
IntPtr pAlloc = Marshal.AllocHGlobal(varSize);
{
    Marshal.GetNativeVariantForObject(5, pAlloc);
    Marshal.FreeHGlobal(pAlloc);
}

GetNativeVariantForObject는 pAlloc을 다음과 같이 채우고,

0x00000218A1D60420  03 00 00 00 00 00 00 00  ........
0x00000218A1D60428  05 00 00 00 00 00 00 00  ........
0x00000218A1D60430  00 00 00 00 00 00 00 00  ........

vt == 3은 VT_I4에 해당하므로 5는 그대로 "LONG lVal" 필드에 값이 보존된 것을 확인할 수 있습니다.




이제 GetNativeVariantForObject에서 어떻게 메모리 누수가 발생하는지 살펴볼까요? ^^ 우선, 간단하게 Variant 변수 내에 "값"을 담을 수 있는 경우라면 메모리 누수가 발생하지 않습니다. 일례로 다음과 같은 식으로 테스트해 보면,

unsafe static int _varSize = sizeof(Variant);

static unsafe void Main(string[] args)
{
    while (true) // 무한 루프
    {
        TestHandleWitInt();
    }
}

private unsafe static void TestHandleWitInt()
{
    int varSize = sizeof(Variant);

    IntPtr pAlloc = Marshal.AllocHGlobal(_varSize);
    {
        Marshal.GetNativeVariantForObject(5, pAlloc);
        Marshal.FreeHGlobal(pAlloc);
    }
}

작업 관리자를 통해 메모리가 안정적으로 유지되는 것을 확인할 수 있습니다. 반면, vt 필드가 VT_DISPATCH나 VT_IUNKNOWN 등으로 설정되는 경우에는 메모리 누수가 발생하는 것을 다음의 코드로 확인할 수 있습니다.

static unsafe void Main(string[] args)
{
    int i = 0;
    while (true)
    {
        TestHandleWithLeak();

        i++;

        if (i > 1000)
        {
            GC.Collect();
            GC.Collect();
            i = 0;
        }
    }
}

private unsafe static void TestHandleWithLeak()
{
    object obj = new object();
    IntPtr pAlloc = Marshal.AllocHGlobal(_varSize);
    {   
        Marshal.GetNativeVariantForObject(obj, pAlloc);
        Marshal.FreeHGlobal(pAlloc);
    }
}

여러가지로 테스트해 본 결과 이 문제를 해결하려면 oleaut32.dll의 VariantClear 함수를,

VariantClear function
; https://learn.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-variantclear

명시적으로 호출해줘야만 했습니다.

[DllImport("oleaut32.dll", PreserveSig = true)]
private static extern int VariantClear(IntPtr pObject);

private unsafe static void TestHandleWithoutLeak()
{
    object obj = new object();

    IntPtr pAlloc = Marshal.AllocHGlobal(_varSize);
    {
        Marshal.GetNativeVariantForObject(obj, pAlloc);

        VariantClear(pAlloc);
        Marshal.FreeHGlobal(pAlloc);
    }
}

혹시, 다른 방법을 알고 계신 분은 덧글 부탁드립니다. ^^

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




참고로, windbg + sos를 이용하는 경우 이에 대한 진단을 !gchandles를 통해 알 수 있습니다. 가령 VariantClear를 호출하지 않아 메모리 누수가 발생하는 상황이라면 다음과 같은 식으로 "Ref Count Handles"가 GetNativeVariantForObject 메서드의 호출 횟수만큼 누적이 됩니다.

0:000> !gchandles -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffcd8745ef0        1           48 System.SharedStatics
00007ffcd8746bb0        1           64 System.Security.PermissionSet
00007ffc7ce468e0        1           64 Microsoft.VisualStudio.Debugger.Runtime.Main+_ThrowCrossThreadMessageException
00007ffcd8745cf8        1          160 System.ExecutionEngineException
00007ffcd8745c80        1          160 System.StackOverflowException
00007ffcd8745c08        1          160 System.OutOfMemoryException
00007ffcd8745b70        1          160 System.Exception
00007ffcd8745f60        1          216 System.AppDomain
00007ffcd8745d70        2          320 System.Threading.ThreadAbortException
00007ffcd87459c0        2          646 System.String
00007ffcd8745e70        4        35320 System.Object[]
00007ffcd8745dd8   125126      3003024 System.Object
Total 125142 objects

Handles:
    Strong Handles:       11
    Pinned Handles:       5
    Ref Count Handles:    125125
    Weak Long Handles:    1




본문에서 vt 필드의 값은 (wtypes.h에 정의된 VARENUM과 동일한) System.Runtime.InteropServices의 VARENUM 값입니다.

public enum VarEnum // C/C++ - typedef enum VARENUM
{
    VT_EMPTY = 0,
    VT_NULL = 1,
    VT_I2 = 2,
    VT_I4 = 3,
    VT_R4 = 4,
    VT_R8 = 5,
    VT_CY = 6,
    VT_DATE = 7,
    VT_BSTR = 8,
    VT_DISPATCH = 9,
    VT_ERROR = 10,
    VT_BOOL = 11,
    VT_VARIANT = 12,
    VT_UNKNOWN = 13,
    VT_DECIMAL = 14,
    VT_I1 = 0x10,
    VT_UI1 = 17,
    VT_UI2 = 18,
    VT_UI4 = 19,
    VT_I8 = 20,
    VT_UI8 = 21,
    VT_INT = 22,
    VT_UINT = 23,
    VT_VOID = 24,
    VT_HRESULT = 25,
    VT_PTR = 26,
    VT_SAFEARRAY = 27,
    VT_CARRAY = 28,
    VT_USERDEFINED = 29,
    VT_LPSTR = 30,
    VT_LPWSTR = 0x1F,
    VT_RECORD = 36,
    VT_FILETIME = 0x40,
    VT_BLOB = 65,
    VT_STREAM = 66,
    VT_STORAGE = 67,
    VT_STREAMED_OBJECT = 68,
    VT_STORED_OBJECT = 69,
    VT_BLOB_OBJECT = 70,
    VT_CF = 71,
    VT_CLSID = 72,
    VT_VECTOR = 0x1000,
    VT_ARRAY = 0x2000,
    VT_BYREF = 0x4000
}




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 10/27/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 61  62  63  [64]  65  66  67  68  69  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12047정성태11/12/201914408Windows: 163. 안전하게 eject시킨 USB 장치를 물리적인 재연결 없이 다시 인식시키는 방법
12046정성태10/29/201910385오류 유형: 577. windbg - The call to LoadLibrary(...\sos.dll) failed, Win32 error 0n193
12045정성태10/27/20199727오류 유형: 576. mstest.exe 실행 시 "Visual Studio Enterprise is required to execute the test." 오류 - 두 번째 이야기
12044정성태10/27/20199936오류 유형: 575. mstest.exe - System.Resources.MissingSatelliteAssemblyException: The satellite assembly named "Microsoft.VisualStudio.ProductKeyDialog.resources.dll, ..."
12043정성태10/27/201910755오류 유형: 574. Windows 10 설치 시 오류 - 0xC1900101 - 0x4001E
12042정성태10/26/201911157오류 유형: 573. OneDrive 하위에 위치한 Documents, Desktop 폴더에 대한 권한 변경 시 "Unable to display current owner"
12041정성태10/23/201911164오류 유형: 572. mstest.exe - The load test results database could not be opened.
12040정성태10/23/201911426오류 유형: 571. Unhandled Exception: System.Net.Mail.SmtpException: Transaction failed. The server response was: 5.2.0 STOREDRV.Submission.Exception:SendAsDeniedException.MapiExceptionSendAsDenied
12039정성태10/22/20199826스크립트: 16. cmd.exe의 for 문에서는 ERRORLEVEL이 설정되지 않는 문제
12038정성태10/17/20199384오류 유형: 570. SQL Server 2019 RC1 - SQL Client Connectivity SDK 설치 오류
12037정성태10/15/201915611.NET Framework: 867. C# - Encoding.Default 값을 바꿀 수 있을까요?파일 다운로드1
12036정성태10/14/201916358.NET Framework: 866. C# - 고성능이 필요한 환경에서 GC가 발생하지 않는 네이티브 힙 사용파일 다운로드1
12035정성태10/13/201912494개발 환경 구성: 461. C# 8.0의 #nulable 관련 특성을 .NET Framework 프로젝트에서 사용하는 방법 [2]파일 다운로드1
12034정성태10/12/201911837개발 환경 구성: 460. .NET Core 환경에서 (프로젝트가 아닌) C# 코드 파일을 입력으로 컴파일하는 방법 [1]
12033정성태10/11/201915534개발 환경 구성: 459. .NET Framework 프로젝트에서 C# 8.0/9.0 컴파일러를 사용하는 방법
12032정성태10/8/201912008.NET Framework: 865. .NET Core 2.2/3.0 웹 프로젝트를 IIS에서 호스팅(Inproc, out-of-proc)하는 방법 - AspNetCoreModuleV2 소개
12031정성태10/7/20199444오류 유형: 569. Azure Site Extension 업그레이드 시 "System.IO.IOException: There is not enough space on the disk" 예외 발생
12030정성태10/5/201915719.NET Framework: 864. .NET Conf 2019 Korea - "닷넷 17년의 변화 정리 및 닷넷 코어 3.0" 발표 자료 [1]파일 다운로드1
12029정성태9/27/201915859제니퍼 .NET: 29. Jennifersoft provides a trial promotion on its APM solution such as JENNIFER, PHP, and .NET in 2019 and shares the examples of their application.
12028정성태9/26/201911593.NET Framework: 863. C# - Thread.Suspend 호출 시 응용 프로그램 hang 현상을 해결하기 위한 시도파일 다운로드1
12027정성태9/26/20198847오류 유형: 568. Consider app.config remapping of assembly "..." from Version "..." [...] to Version "..." [...] to solve conflict and get rid of warning.
12026정성태9/26/201912529.NET Framework: 862. C# - Active Directory의 LDAP 경로 및 정보 조회
12025정성태9/25/201910875제니퍼 .NET: 28. APM 솔루션 제니퍼, PHP, .NET 무료 사용 프로모션 2019 및 적용 사례 (8) [1]
12024정성태9/20/201912312.NET Framework: 861. HttpClient와 HttpClientHandler의 관계 [2]
12023정성태9/18/201912744.NET Framework: 860. ServicePointManager.DefaultConnectionLimit와 HttpClient의 관계파일 다운로드1
12022정성태9/12/201915801개발 환경 구성: 458. C# 8.0 (Preview) 신규 문법을 위한 개발 환경 구성 [3]
... 61  62  63  [64]  65  66  67  68  69  70  71  72  73  74  75  ...