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

비밀번호

댓글 작성자
 




... 91  92  93  94  95  96  97  98  [99]  100  101  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11457정성태2/17/201823978.NET Framework: 732. C# - Task.ContinueWith 설명 [1]파일 다운로드1
11456정성태2/17/201829734.NET Framework: 731. C# - await을 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법 [7]파일 다운로드1
11455정성태2/17/201818629오류 유형: 451. ASP.NET Core - An error occurred during the compilation of a resource required to process this request.
11454정성태2/12/201827517기타: 71. 만료된 Office 제품 키를 변경하는 방법
11453정성태1/31/201819478오류 유형: 450. Azure Cloud Services(classic) 배포 시 "Certificate with thumbprint ... doesn't exist." 오류 발생
11452정성태1/31/201824992기타: 70. 재현 가능한 최소한의 예제 프로젝트란? [3]파일 다운로드1
11451정성태1/24/201819231디버깅 기술: 111. windbg - x86 메모리 덤프 분석 시 닷넷 메서드의 호출 인자 값 확인
11450정성태1/24/201834497Windows: 146. PowerShell로 원격 프로세스(EXE, BAT) 실행하는 방법 [1]
11449정성태1/23/201821874오류 유형: 449. 단위 테스트 - Could not load file or assembly 'Microsoft.VisualStudio.QualityTools.VideoRecorderEngine' or one of its dependencies. [1]
11448정성태1/20/201819359오류 유형: 448. Fakes를 포함한 단위 테스트 프로젝트를 빌드 시 CS0619 관련 오류 발생
11447정성태1/20/201820713.NET Framework: 730. dotnet user-secrets 명령어 [2]파일 다운로드1
11446정성태1/20/201821741.NET Framework: 729. windbg로 살펴보는 GC heap의 Segment 구조 [2]파일 다운로드1
11445정성태1/20/201819617.NET Framework: 728. windbg - 눈으로 확인하는 Workstation GC / Server GC
11444정성태1/19/201819712VS.NET IDE: 125. Visual Studio에서 Selenium WebDriver를 이용한 웹 브라우저 단위 테스트 구성파일 다운로드1
11443정성태1/18/201820284VC++: 124. libuv 모듈 살펴 보기
11442정성태1/18/201818105개발 환경 구성: 353. ASP.NET Core 프로젝트의 "Enable unmanaged code debugging" 옵션 켜는 방법
11441정성태1/18/201816635오류 유형: 447. ASP.NET Core 배포 오류 - Ensure that restore has run and that you have included '...' in the TargetFrameworks for your project.
11440정성태1/17/201819913.NET Framework: 727. ASP.NET의 HttpContext.Current 구현에 대응하는 ASP.NET Core의 IHttpContextAccessor/HttpContextAccessor 사용법파일 다운로드1
11439정성태1/17/201824732기타: 69. C# - CPU 100% 부하 주는 프로그램파일 다운로드1
11438정성태1/17/201819478오류 유형: 446. Error CS0234 The type or namespace name 'ITuple' does not exist in the namespace
11437정성태1/17/201818785VS.NET IDE: 124. Platform Toolset 설정에 따른 Visual C++의 헤더 파일 기본 디렉터리
11436정성태1/16/201821035개발 환경 구성: 352. ASP.NET Core (EXE) 프로세스가 IIS에서 호스팅되는 방법 - ASP.NET Core Module(AspNetCoreModule) [4]
11435정성태1/16/201822159개발 환경 구성: 351. OWIN 웹 서버(EXE)를 IIS에서 호스팅하는 방법 - HttpPlatformHandler (Reverse Proxy)파일 다운로드2
11434정성태1/15/201822514개발 환경 구성: 350. 사용자 정의 웹 서버(EXE)를 IIS에서 호스팅하는 방법 - HttpPlatformHandler (Reverse Proxy)파일 다운로드2
11433정성태1/15/201820576개발 환경 구성: 349. dotnet ef 명령어 사용을 위한 준비
11432정성태1/11/201826336.NET Framework: 726. WPF + Direct2D + SharpDX 출력 C# 예제파일 다운로드2
... 91  92  93  94  95  96  97  98  [99]  100  101  102  103  104  105  ...