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

비밀번호

댓글 작성자
 




... 151  152  153  154  155  156  157  158  159  160  [161]  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1023정성태4/20/201130062.NET Framework: 210. Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 [1]
1022정성태4/19/201125622디버깅 기술: 38. .NET Disassembly 창에서의 F11(Step-into) 키 동작파일 다운로드1
1021정성태4/18/201127911디버깅 기술: 37. .NET 4.0 응용 프로그램의 Main 함수에 BreakPoint 걸기
1020정성태4/18/201128541오류 유형: 117. Failed to find runtime DLL (mscorwks.dll), 0x80004005
1019정성태4/17/201129186디버깅 기술: 36. Visual Studio의 .NET Disassembly 창의 call 호출에 사용되는 주소의 의미는? [1]파일 다운로드1
1018정성태4/16/201132814오류 유형: 116. 윈도우 업데이트 오류 - 0x8020000E
1017정성태4/14/201127633개발 환경 구성: 115. MSBuild - x86/x64, .NET 2/4, debug/release 빌드에 대한 배치 처리파일 다운로드1
1016정성태4/13/201143657개발 환경 구성: 114. Windows Thin PC 설치 [2]
1015정성태4/9/201128998.NET Framework: 209. AutoReset, ManualReset, Monitor.Wait의 차이파일 다운로드1
1014정성태4/7/2011106439오류 유형: 115. ORA-12516: TNS:listener could not find available handler with matching protocol stack [2]
1013정성태4/7/201124259Team Foundation Server: 45. SharePoint 2010 + TFS 2010 환경에서 ProcessGuidance.html 파일 다운로드 문제
1012정성태4/6/201133004.NET Framework: 208. WCF - 접속된 클라이언트의 IP 주소 알아내는 방법 [1]
1011정성태3/31/201135378오류 유형: 114. 인증서 갱신 오류 - The request contains no certificate template information.
1010정성태3/30/201126146개발 환경 구성: 113. 응용 프로그램 디자인 스케치 도구 - SketchFlow [4]
1009정성태3/29/201138455개발 환경 구성: 112. Visual Studio 2010 - .NET Framework 소스 코드 디버깅 [4]
1008정성태3/27/201130830.NET Framework: 207. C# - Right operand가 음수인 Shift 연산 결과 [2]
1007정성태3/16/201131675개발 환경 구성: 111. Excel - XML 파일 연동 [5]파일 다운로드1
1006정성태3/15/201125437.NET Framework: 206. XML/XSD - 외래키처럼 참조 제한 거는 방법파일 다운로드1
1005정성태3/11/201135277개발 환경 구성: 110. 엑셀 매크로 함수 관련 오류 [2]
1004정성태3/3/201124470개발 환경 구성: 109. SharePoint Health Analyzer 디스크 부족 경고 제어
1003정성태3/3/201125504오류 유형: 113. SQL Server - DB Attach 시 Parameter name: nColIndex 오류 발생
1002정성태3/2/201123898Team Foundation Server: 44. TFS 설치 후, Team Portal의 Dashboard를 빠르게 확인하는 방법
1001정성태3/2/201127928Team Foundation Server: 43. TFS 2010 + SharePoint 2010 설치
1000정성태3/1/201132876오류 유형: 112. Remote FX RDP 연결 시 오류 유형 2가지 [5]
999정성태2/28/201146422개발 환경 구성: 108. RemoteFX - Windows 7 가상 머신에서 DirectX 9c 환경을 제공 [5]
998정성태2/27/201120119Team Foundation Server: 42. TFS Application-Tier만 재설치
... 151  152  153  154  155  156  157  158  159  160  [161]  162  163  164  165  ...