Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

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

GetNativeVariantForObject 메서드는,

Marshal.GetNativeVariantForObject Method
; https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.getnativevariantforobject?view=netframework-4.8

닷넷 객체를 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://docs.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
}




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

[연관 글]





[최초 등록일: ]
[최종 수정일: 2/27/2020 ]

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

비밀번호

댓글 쓴 사람
 




1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12359정성태10/7/2020160오류 유형: 665. Windows - 재부팅 후 iSCSI 연결이 끊기는 문제
12358정성태10/7/2020115오류 유형: 664. Web Deploy 설치 시 "A newer version of Microsoft Web Deploy 3.6 was found on this machine." 오류
12357정성태10/7/2020117오류 유형: 663. 이벤트 로그 - The storage optimizer couldn't complete retrim on New Volume
12356정성태10/7/2020158오류 유형: 662. ASP.NET Core와 500.19, 500.21 오류 (0x8007000d)
12355정성태10/3/2020166오류 유형: 661. Hyper-V Linux VM의 Internal 유형의 가상 Switch에 대한 IP 연결이 되지 않는 경우
12354정성태10/2/2020156오류 유형: 660. Web Deploy (msdeploy.axd) 실행 시 오류 기록
12353정성태10/7/2020167개발 환경 구성: 518. 비주얼 스튜디오에서 IIS 웹 서버로 "Web Deploy"를 이용해 배포하는 방법
12352정성태10/2/2020202개발 환경 구성: 517. Hyper-V Internal 네트워크에 NAT을 이용한 인터넷 연결 제공
12351정성태10/2/2020525오류 유형: 659. Nox 실행이 안 되는 경우 - Unable to bind to the underlying transport for ...
12350정성태11/30/2020515Windows: 175. 윈도우 환경에서 클라이언트 소켓의 최대 접속 수 [1]파일 다운로드1
12349정성태9/25/2020280Linux: 32. Ubuntu 20.04 - docker를 위한 tcp 바인딩 추가
12348정성태9/25/2020199오류 유형: 658. 리눅스 docker - Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
12347정성태9/25/2020393Windows: 174. WSL 2의 네트워크 통신 방법
12346정성태9/25/2020162오류 유형: 657. IIS - http://localhost 방문 시 Service Unavailable 503 오류 발생
12345정성태9/25/2020162오류 유형: 656. iisreset 실행 시 "Restart attempt failed." 오류가 발생하지만 웹 서비스는 정상적인 경우
12344정성태9/25/2020168Windows: 173. 서비스 관리자에 "IIS Admin Service"가 등록되어 있지 않다면?
12343정성태9/24/2020517.NET Framework: 945. C# - 닷넷 응용 프로그램에서 메모리 누수가 발생할 수 있는 패턴
12342정성태9/25/2020381디버깅 기술: 171. windbg - 인스턴스가 살아 있어 메모리 누수가 발생하고 있는지 확인하는 방법
12341정성태9/23/2020462.NET Framework: 944. C# - 인스턴스가 살아 있어 메모리 누수가 발생하고 있는지 확인하는 방법파일 다운로드1
12340정성태9/23/2020327.NET Framework: 943. WPF - WindowsFormsHost를 담은 윈도우 생성 시 메모리 누수
12339정성태9/21/2020239오류 유형: 655. 코어 모드의 윈도우는 GUI 모드의 윈도우로 교체가 안 됩니다.
12338정성태9/21/2020197오류 유형: 654. 우분투 설치 시 "CHS: Error 2001 reading sector ..." 오류 발생
12337정성태9/21/2020216오류 유형: 653. Windows - Time zone 설정을 바꿔도 반영이 안 되는 경우
12336정성태9/21/2020429.NET Framework: 942. C# - WOL(Wake On Lan) 구현
12335정성태10/28/2020498Linux: 31. 우분투 20.04 초기 설정 - 고정 IP 및 SSH 설치
12334정성태9/21/2020170오류 유형: 652. windbg - !py 확장 명령어 실행 시 "failed to find python interpreter"
1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...