Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 13개 있습니다.)
.NET Framework: 397. C# - OCX 컨트롤에 구현된 메서드에 배열을 in, out으로 전달하는 방법
; https://www.sysnet.pe.kr/2/0/1547

.NET Framework: 652. C# 개발자를 위한 C++ COM 객체의 기본 구현 방식 설명
; https://www.sysnet.pe.kr/2/0/11175

.NET Framework: 792. C# COM 서버가 제공하는 COM 이벤트를 C++에서 받는 방법
; https://www.sysnet.pe.kr/2/0/11679

.NET Framework: 907. C# DLL로부터 TLB 및 C/C++ 헤더 파일(TLH)을 생성하는 방법
; https://www.sysnet.pe.kr/2/0/12220

.NET Framework: 977. C# PInvoke - C++의 매개변수에 대한 마샬링을 tlbexp.exe를 이용해 확인하는 방법
; https://www.sysnet.pe.kr/2/0/12443

.NET Framework: 1008. 배열을 반환하는 C# COM 개체의 메서드를 C++에서 사용 시 메모리 누수 현상
; https://www.sysnet.pe.kr/2/0/12491

.NET Framework: 1064. C# COM 개체를 PIA(Primary Interop Assembly)로써 "Embed Interop Types" 참조하는 방법
; https://www.sysnet.pe.kr/2/0/12662

.NET Framework: 1069. C# - DLL Surrogate를 이용한 Out-of-process COM 개체 제작
; https://www.sysnet.pe.kr/2/0/12668

.NET Framework: 1095. C# COM 개체를 C++에서 사용하는 예제
; https://www.sysnet.pe.kr/2/0/12791

.NET Framework: 2003. C# - COM 개체의 이벤트 핸들러에서 발생하는 예외에 대한 CLR의 특별 대우
; https://www.sysnet.pe.kr/2/0/13050

닷넷: 2177. C# - (Interop DLL 없이) CoClass를 이용한 COM 개체 생성 방법
; https://www.sysnet.pe.kr/2/0/13469

닷넷: 2248. C# - 인터페이스 타입의 다중 포인터를 인자로 갖는 C/C++ 함수 연동
; https://www.sysnet.pe.kr/2/0/13607

닷넷: 2254. C# - COM 인터페이스의 상속 시 중복으로 메서드를 선언
; https://www.sysnet.pe.kr/2/0/13614




배열을 반환하는 C# COM 개체의 메서드를 C++에서 사용 시 메모리 누수 현상

아래와 같은 질문이 있군요. ^^

C# dll 파일을 C++에서 사용 시 memory leak 문제
; https://www.sysnet.pe.kr/3/0/5453

질문하신 분이 예제 정리를 잘해주셨지만, ^^ 그래도 조금 더 욕심을 내 재현 프로젝트를 간략화시켜 보겠습니다. 우선 C# 프로젝트는 다음의 코드만 작성할 수 있습니다.

using System;
using System.Runtime.InteropServices;

namespace ClassLibrary1
{
    [ComVisible(true)]
    [Guid("8EA9EAA8-CA3D-4584-B1E0-7B9561757CA4")]
    public interface ICSharpLibrary
    {
        int[] GetData();
        string GetName();
    }

    [ComVisible(true)]
    [Guid("B62A2B51-621D-41AA-8F4F-021E404B593C")]
    public class CSharpLibrary : ICSharpLibrary
    {
        public int[] GetData()
        {
            return new int[1024];
        }

        public string GetName()
        {
            return new string('x', 1024);
        }
    }
}

그런 다음 프로젝트 속성의 "Build" / "Output" 범주에 있는 "Register for COM interop" 옵션을 체크해 둡니다. (이 옵션을 체크하면 비주얼 스튜디오를 "관리자 권한"으로 실행해야 대상 프로젝트를 정상적으로 빌드할 수 있습니다.)

그리고 C++ 프로젝트와의 빌드 연동을 위해 DLL/PDB/TLB 파일을 다음의 빌드 이벤트 스크립트로 솔루션 디렉터리 하위의 Lib에 모아두도록 합니다.

robocopy $(TargetDir) $(SolutionDir)Lib $(TargetName).dll $(TargetName).pdb $(TargetName).tlb
exit 0

그럼, C++ 예제 코드는 이렇게 간단하게 구성할 수 있습니다.

#include <iostream>
#include <Windows.h>

#import "./../Lib/ClassLibrary1.tlb" no_namespace named_guids

int main()
{
    HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    if (!SUCCEEDED(hr))
    {
        return false;
    }

    ICSharpLibrary* lib = nullptr;

    do
    {
        hr = CoCreateInstance(CLSID_CSharpLibrary, NULL, CLSCTX_INPROC_SERVER, IID_ICSharpLibrary, reinterpret_cast<void**>(&lib));

        if (!SUCCEEDED(hr))
        {
            break;
        }

        while (true)
        {
            SAFEARRAY* array = lib->GetData();
            _bstr_t bstrName = lib->GetName();
        }

    } while (false);

    if (lib != nullptr)
    {
        lib->Release();
        lib = nullptr;
    }

    CoUninitialize();
}

tlb 파일에 대한 #import 구문으로 생성한 C++ TLH, TLI 파일을 보면 위와 같이 C# 라이브러리를 사용한 이유를 알 수 있습니다.

// classlibrary1.tlh

#pragma once
#pragma pack(push, 8)

#include <comdef.h>

struct __declspec(uuid("af9d8dd7-d8ae-427b-b1e7-77b7feeb343c")) /* LIBID */ __ClassLibrary1;
struct __declspec(uuid("8ea9eaa8-ca3d-4584-b1e0-7b9561757ca4")) /* dual interface */ ICSharpLibrary;
struct /* coclass */ CSharpLibrary;
struct __declspec(uuid("d6c19716-755b-3d95-8cbc-40d68a3dbfea")) /* dual interface */ _CSharpLibrary;

_COM_SMARTPTR_TYPEDEF(ICSharpLibrary, __uuidof(ICSharpLibrary));
_COM_SMARTPTR_TYPEDEF(_CSharpLibrary, __uuidof(_CSharpLibrary));

struct __declspec(uuid("8ea9eaa8-ca3d-4584-b1e0-7b9561757ca4"))
ICSharpLibrary : IDispatch
{
    SAFEARRAY * GetData ( );
    _bstr_t GetName ( );

      virtual HRESULT __stdcall raw_GetData (
        /*[out,retval]*/ SAFEARRAY * * pRetVal ) = 0;
      virtual HRESULT __stdcall raw_GetName (
        /*[out,retval]*/ BSTR * pRetVal ) = 0;
};

struct __declspec(uuid("b62a2b51-621d-41aa-8f4f-021e404b593c"))
CSharpLibrary;

struct __declspec(uuid("d6c19716-755b-3d95-8cbc-40d68a3dbfea"))
_CSharpLibrary : IDispatch {};

extern "C" const GUID __declspec(selectany) LIBID_ClassLibrary1 = {0xaf9d8dd7,0xd8ae,0x427b,{0xb1,0xe7,0x77,0xb7,0xfe,0xeb,0x34,0x3c}};
extern "C" const GUID __declspec(selectany) IID_ICSharpLibrary = {0x8ea9eaa8,0xca3d,0x4584,{0xb1,0xe0,0x7b,0x95,0x61,0x75,0x7c,0xa4}};
extern "C" const GUID __declspec(selectany) CLSID_CSharpLibrary = {0xb62a2b51,0x621d,0x41aa,{0x8f,0x4f,0x02,0x1e,0x40,0x4b,0x59,0x3c}};
extern "C" const GUID __declspec(selectany) IID__CSharpLibrary = {0xd6c19716,0x755b,0x3d95,{0x8c,0xbc,0x40,0xd6,0x8a,0x3d,0xbf,0xea}};

// ...[생략]...

#pragma pack(pop)

// classlibrary1.tli

inline SAFEARRAY * ICSharpLibrary::GetData ( ) {
    SAFEARRAY * _result = 0;
    HRESULT _hr = raw_GetData(&_result);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _result;
}

inline _bstr_t ICSharpLibrary::GetName ( ) {
    BSTR _result = 0;
    HRESULT _hr = raw_GetName(&_result);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _bstr_t(_result, false);
}

마지막으로, 빌드 완료 후 실행하면 메모리 누수가 발생하고!




반면, GetData 호출을 제거하면,

while (true)
{
    // SAFEARRAY* array = lib->GetData();
    _bstr_t bstrName = lib->GetName();
}

이번엔 메모리 누수가 없습니다. 그럼 대충 짐작이 가시겠죠? ^^ 실제로 classlibrary1.tli 파일을 보면, GetName의 경우 raw_GetName 메서드를 통해 BSTR을 받아옵니다.

inline _bstr_t ICSharpLibrary::GetName ( ) {
    BSTR _result = 0;
    HRESULT _hr = raw_GetName(&_result);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _bstr_t(_result, false);
}

사실, C# 측에서는 COM 개체로 호출하는 측에 반환할 문자열을 GC Heap에 있는 것을 그냥 전달할 수 없습니다. 따라서 COM 개체 간의 interop으로 약속된 BSTR 메모리를 할당해서 그것을 반환하게 되는데, 이에 대해 호출 측에서는 BSTR 메모리를 사용 후 반드시 해제해야 합니다. GetName 함수에서는 _bstr_t로 C# 측에서 반환한 BSTR을 래핑하는데,

_bstr_t Class
; https://learn.microsoft.com/en-us/cpp/cpp/bstr-t-class

내부적으로 _bstr_t 측에서 참조 카운트를 이용해 자원 해제까지 담당하므로 SysFreeString으로 BSTR 텍스트 자원이 해제가 됩니다. 실제로 GetName에서 어떻게 참조 카운트가 변하는지 다음과 같이 유추할 수 있습니다.

inline _bstr_t ICSharpLibrary::GetName ( ) {
    BSTR _result = 0;
    HRESULT _hr = raw_GetName(&_result); // C# 측에서 BSTR 자원을 SysAllocString으로 할당 후 반환
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _bstr_t(_result, false); // _bstr_t 인스턴스가 반환되지만 2번째 인자의 false로 인해 원본 BSTR을 가리키면서 참조 카운트 = 1 상태 유지
}

그리고 저 메서드를 사용하는 측에서 보면,

while (true)
{
    _bstr_t bstrName = lib->GetName(); // 반환 순간 _bstr_t가 담고 있는 참조 카운트는 1

    // block scope을 벗어나는 순간 _bstr_t의 파괴자에 의해 참조 카운트는 -1
    // 따라서 참조 카운트가 0이므로 BSTR 문자열 메모리에 대해 SysFreeString을 호출해 자원 해제
}

결국 깨끗하게 자원 정리가 되는 것을 볼 수 있습니다.




그런데, GetData의 동작 방식은 다릅니다.

inline SAFEARRAY * ICSharpLibrary::GetData ( ) {
    SAFEARRAY * _result = 0;
    HRESULT _hr = raw_GetData(&_result);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _result;
}

raw_GetData의 호출로 C# 측에서는 int 배열을 담고 있는 SAFEARRAY를 SafeArrayCreate 함수를 이용해 할당 후 반환합니다. 그런데, GetName과는 다르게 SAFEARRAY에 대한 참조 카운트를 관리하는 별도의 래퍼 타입을 반환하지 않고, 직접 SAFEARRAY 인스턴스를 반환하고 있습니다.

따라서, 호출 측에서는 반드시 SafeArrayDestroy를 호출해야만,

while (true)
{
    SAFEARRAY* data = lib->GetData();
    // ...[data 사용]...
    SafeArrayDestroy(data);
}

정상적인 자원 정리가 됩니다.

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




참고로, 본문에서 "Register for COM interop" 옵션을 체크해 두어야 한다고 했지만 개인적으로는 "관리자 권한"으로 비주얼 스튜디오를 실행하는 것을 선호하지 않기 때문에 다음과 같은 식으로,

C# DLL로부터 TLB 및 C/C++ 헤더 파일(TLH)을 생성하는 방법
; https://www.sysnet.pe.kr/2/0/12220

C# DLL에 정의한 (메서드 내부의 코드가 아닌) 메서드 signature의 변경이나 추가/삭제 시에만 관리자 권한의 cmd.exe 창을 띄워 regasm.exe로 등록하는 방식으로 개발합니다.




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

[연관 글]






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

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

비밀번호

댓글 작성자
 



2021-01-18 09시38분
[안녕하세요] 글 잘 읽었습니다.

C# dll wrapping을 처음 하다보니 디버깅을 제대로 못 한 것 같네요.. 잘 배워갑니다 :)

제 경우에는 SAFEARRAY에서 SafeArrayAccessData 함수로 데이터를 받아 써서 이후에 SafeArrayUnaccessData 함수를 써야 제대로 메모리가 해제되더라고요 ㅎㅎ

커피 맛있게 드세요 ^^
[guest]

... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1756정성태9/23/201427486기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434280오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
1754정성태9/22/201424657VC++: 80. 내 컴퓨터에서 C++ AMP 코드가 실행이 될까요? [1]
1753정성태9/22/201420611오류 유형: 240. Lync로 세미나 참여 시 소리만 들리지 않는 경우 [1]
1752정성태9/21/201441071Windows: 100. 윈도우 8 - RDP 연결을 이용해 VNC처럼 사용자 로그온 화면을 공유하는 방법 [5]
1751정성태9/20/201438948.NET Framework: 464. 프로세스 간 통신 시 소켓 필요 없이 간단하게 Pipe를 열어 통신하는 방법 [1]파일 다운로드1
1750정성태9/20/201423832.NET Framework: 463. PInvoke 호출을 이용한 비동기 파일 작업파일 다운로드1
1749정성태9/20/201423732.NET Framework: 462. 커널 객체를 위한 null DACL 생성 방법파일 다운로드1
1748정성태9/19/201425385개발 환경 구성: 238. [Synergy] 여러 컴퓨터에서 키보드, 마우스 공유
1747정성태9/19/201428486오류 유형: 239. psexec 실행 오류 - The system cannot find the file specified.
1746정성태9/18/201426106.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기 [6]파일 다운로드1
1745정성태9/17/201423037개발 환경 구성: 237. 리눅스 Integration Services 버전 업그레이드 하는 방법 [1]
1744정성태9/17/201431064.NET Framework: 460. GetTickCount / GetTickCount64와 0x7FFE0000 주솟값 [4]파일 다운로드1
1743정성태9/16/201420985오류 유형: 238. 설치 오류 - Failed to get size of pseudo bundle
1742정성태8/27/201426973개발 환경 구성: 236. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 [2]
1741정성태8/26/201421334.NET Framework: 459. GetModuleHandleEx로 알아보는 .NET 메서드의 DLL 모듈 관계파일 다운로드1
1740정성태8/25/201432522.NET Framework: 458. 닷넷 GC가 순환 참조를 해제할 수 있을까요? [2]파일 다운로드1
1739정성태8/24/201426551.NET Framework: 457. 교착상태(Dead-lock) 해결 방법 - Lock Leveling [2]파일 다운로드1
1738정성태8/23/201422049.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스파일 다운로드1
1737정성태8/20/201419765VS.NET IDE: 93. Visual Studio 2013 동기화 문제
1736정성태8/19/201425579VC++: 79. [부연] CAS Lock 알고리즘은 과연 빠른가? [2]파일 다운로드1
1735정성태8/19/201418221.NET Framework: 455. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 두 번째 이야기
1734정성태8/13/201419879오류 유형: 237. Windows Media Player cannot access the file. The file might be in use, you might not have access to the computer where the file is stored, or your proxy settings might not be correct.
1733정성태8/13/201426362.NET Framework: 454. EmptyWorkingSet Win32 API를 사용하는 C# 예제파일 다운로드1
1732정성태8/13/201434480Windows: 99. INetCache 폴더가 다르게 보이는 이유
1731정성태8/11/201427084개발 환경 구성: 235. 점(.)으로 시작하는 파일명을 탐색기에서 만드는 방법
... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...