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

비밀번호

댓글 작성자
 




... 166  167  168  169  170  171  172  173  [174]  175  176  177  178  179  180  ...
NoWriterDateCnt.TitleFile(s)
650정성태1/19/200931652COM 개체 관련: 21. C/C++ 프로젝트에 /clr 옵션 적용으로 인한 COM 개체 사용 오류
649정성태1/18/200929237Windows: 38. Q1U UMPC에 Windows 7 베타 설치하기
648정성태1/18/200927955Windows: 37. Windows PE를 USB 메모리에 적용
647정성태1/18/200938093Windows: 36. Windows PE ISO 이미지 만들기 [1]
646정성태1/18/200931046디버깅 기술: 23. COMPLUS_ZapDisable - JIT 최적화 코드 생성 제어 [1]
645정성태1/11/200929855Windows: 35. 서명되지 않은 드라이버 로딩 방법
644정성태1/11/200920929Windows: 34. VPC 설치 후기 [2]
643정성태1/10/200926284Windows: 33. Windows 7 베타와 VMA 충돌 [1]
642정성태1/8/200925015개발 환경 구성: 34. Sysinternals의 모든 툴을 한번에 업데이트 하는 방법 [1]
641정성태1/7/200922145기타: 27. D820 - A09 바이오스 업데이트 프로그램 패치 [2]
640정성태1/4/200923852Team Foundation Server: 29. ClickOnce 응용 프로그램 배포를 Team Build에 추가.
639정성태1/4/200921802Team Foundation Server: 28. PFX 코드 서명을 포함한 프로젝트의 팀 빌드 실패 - MSB4018
638정성태1/3/200924928.NET Framework: 119. WPF - 의존 속성 정의에서 XamlParseException 발생하는 예 [2]
637정성태1/1/200927102기타: 26. 2008년 인기 순위 정리
636정성태12/31/200822259.NET Framework: 118. 2진 검색을 이용한 리스트 정렬 삽입파일 다운로드1
635정성태12/29/200824848오류 유형: 66. 파일 암호화 오류 - Recovery policy configured for this system contains invalid recovery certificate
634정성태12/29/200839180기타: 25. 가상 키보드 관련 정리 [4]
633정성태12/20/200824695기타: 24. RMClock for x64 [2]
632정성태12/19/200833366기타: 23. D820 - 배터리 없이 바이오스 업데이트 방법 [2]파일 다운로드1
631정성태12/10/200841975VC++: 36. Detours 라이브러리를 이용한 Win32 API - Sleep 호출 가로채기 [3]
630정성태12/9/200822751.NET Framework: 117. WPF - TreeView에서 항목이 펼쳐질 때 Cursors.Wait 사용파일 다운로드1
629정성태12/7/200831955.NET Framework: 116. 소켓 연결 시간 제한
628정성태12/6/200820846.NET Framework: 115. Marshal 타입 관련 2가지 자원 해제 메서드파일 다운로드1
627정성태12/6/200823254VS.NET IDE: 58. VS.NET IDE 팁 - 커서 위치 이동 [1]
626정성태12/6/200823482오류 유형: 65. TF53018: The application tier XXXXXXX is attempting to connect to a data tier with an incompatible version
625정성태12/6/200823634오류 유형: 64. TFS 2008 SP1 설치 - MsiApplyMultiplePatches returned 0x643
... 166  167  168  169  170  171  172  173  [174]  175  176  177  178  179  180  ...