Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)
(시리즈 글이 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 서버가 제공하는 COM 이벤트를 C++에서 받는 방법

다음과 같은 질문이 있군요. ^^

c# dll을 C++에서 사용 시 event 호출
; https://www.sysnet.pe.kr/3/0/5056

사실, ^^; 개인적으로 더 이상 COM과 엮이고 싶지 않습니다. 뭐랄까, 마이크로소프트가 만든 COM 객체를 사용하는 정도의 끈만 잡고 있을 뿐 더 이상 깊게 관여하고 싶지 않은 것이 제 솔직한 심정입니다.

그런데, 재현 예제를 너무 잘 만들어 주셔서 이렇게 별도로 정리해 봅니다. 일단, C#으로 COM 객체를 다음과 같이 구현합니다.

[ComVisible(true)]
[Guid("c7452557-b191-3d04-bbba-cf90fa7c7141")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IHehServerEvents
{
    void FireEvent();
}

[ComVisible(true)]
[Guid("05fe466f-d00f-39bf-b4a0-04fb53438a4a")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IHehServer
{
    void TriggerEventOnThisThread();
    void AddEvent(IHehServerEvents evt);
}

[ComVisible(true)]
[Guid("c9f685ea-3388-3ff5-958a-3234d08587c1")]
[ClassInterface(ClassInterfaceType.None)]
public class HehServer : IHehServer
{
    Thread thCall;

    public HehServer()
    {
        thCall = new Thread(threadFunc);
        thCall.Start();
    }

    void threadFunc()
    {
        while (true)
        {
            TriggerAllEvent();
            Thread.Sleep(1000);
        }
    }

    void TriggerAllEvent()
    {
        foreach (IHehServerEvents evt in m_listeners)
        {
            evt.FireEvent();
        }
    }

    public List<IHehServerEvents> m_listeners =
        new List<IHehServerEvents>();

    public void TriggerEventOnThisThread()
    {
        TriggerAllEvent();
    }

    public void AddEvent(IHehServerEvents evt)
    {
        m_listeners.Add(evt);
    }
}

구현은 간단합니다. 위의 C# 객체를 사용하는 C++ 측에서 AddEvent로 자신의 callback 객체를 등록하면 threadFunc에서는 주기적으로 그 이벤트를 호출해 주는 것입니다. 질문에서는 위의 C# COM 객체를 사용하기 위해 C++ 측에서 다음과 같은 식으로 코딩하고 있습니다.

#include "stdafx.h"
#include "ClientTest.h"

int _tmain(int argc, _TCHAR* argv[])
{
    HRESULT hr = CoInitialize(0); // STA로 초기화하고,

    {
        CClientTest test;
        test.TestEvent();
    }

    CoUninitialize();
    return 0;
}

#include "StdAfx.h"
#include <stdio.h>

#include "ClientTest.h"

CClientTest::CClientTest(void)
{
    m_server.CreateInstance(__uuidof(HehServer));
}

void CClientTest::TestEvent(void)
{
    HRESULT hr = S_OK;

    m_server->AddEvent(this);
    printf("TestEvent - called\n");

    system("pause"); // C# 스레드에서 이벤트 호출하는 시간을 벌기 위해.
}

HRESULT CClientTest::QueryInterface(const IID & iid, void ** pp)
{
    if (iid == __uuidof(IHehServerEvents) ||
        iid == __uuidof(IUnknown))
    {
        *pp = this;
        AddRef();
        return S_OK;
    }
    return E_NOINTERFACE;
}

HRESULT CClientTest::FireEvent(void)
{
    printf("FireEvent - called\n");
    return S_OK;
}

그런데, 이것을 실행하면 이벤트가 받아지지 않습니다. 대신 이벤트를 C#의 별도 스레드가 아닌, C# 메서드를 호출하는 스레드에서 실행하면,

void CClientTest::TestEvent(void)
{
    HRESULT hr = S_OK;

    m_server->AddEvent(this);
    printf("TestEvent - called\n");

    m_server->TriggerEventOnThisThread();

    system("pause"); // C# 스레드에서 이벤트 호출하는 시간을 벌기 위해.
}

정상적으로 callback 이벤트가 수신되는 것을 확인할 수 있습니다. 왜 그럴까요?




우선, C# COM 객체는 기본적으로 COM Apartment 유형이 MTA입니다. 이 때문에 STA에서 MTA COM 객체를 활성화한 경우 스레드가 달라지면 마샬링을 하게 됩니다. 이때의 마샬링이란 콜백 이벤트의 호출을 직렬화하기 위해 Win32 이벤트를 사용한다는 것입니다.

즉, C# MTA COM 객체는 콜백 이벤트를 정상적으로 호출했는데 문제는 그것이 Window 이벤트 큐에 쌓이고 있는 것입니다. 따라서, 정상적으로 콜백 호출이 되려면 다음과 같이 이벤트 루프를 만들어줘야 합니다.

void CClientTest::TestEvent(void)
{
    HRESULT hr = S_OK;

    m_server->AddEvent(this);
    printf("TestEvent - called\n");

    // system("pause"); // 메시지 루프가 있으므로 주석 처리
}

int _tmain(int argc, _TCHAR* argv[])
{
    HRESULT hr = S_OK;

    hr = CoInitialize(0);
    {
        CClientTest test;
        test.TestEvent();

        BOOL bRet;
        MSG msg;
        while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0)
        {
            if (bRet != -1)
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }
    }

    CoUninitialize();
    return 0;
}

위와 같이 해 주면 이제 정상적으로 이벤트를 받게 됩니다.




또 다른 해결책도 있습니다.

결국 위의 문제는 MTA 객체를 STA로 초기화한 환경에서 활성화했기 때문에 저렇게 스레드가 달라지는 경우 마샬링이 필요했던 것입니다. 따라서 MTA 객체를 MTA로 초기화한 환경에서 실행하면 마샬링이 발생하지 않으므로 코드를 다음과 같이 변경하는 것도 가능합니다.

void CClientTest::TestEvent(void)
{
    HRESULT hr = S_OK;

    m_server->AddEvent(this);
    printf("TestEvent - called\n");

    system("pause"); // C# 스레드에서 이벤트 호출하는 시간을 벌기 위해.
}

int _tmain(int argc, _TCHAR* argv[])
{
    HRESULT hr = S_OK;

    hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
    {
        CClientTest test;
        test.TestEvent();
    }

    CoUninitialize();
    return 0;
}

복잡한 메시지 루프도 필요 없고, MTA COM을 MTA 환경에서 활성화시켰으므로 마샬링 없이 곧바로 호출합니다. 물론 이런 경우 STA 특유의 직렬화로 인한 thread-safe 특성이 사라지므로 다중 스레드에서 동시 호출을 하는 경우가 있다면 공용 저장소에 대한 동기화 처리는 개발자가 직접 해줘야 합니다.

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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/14/2025]

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

비밀번호

댓글 작성자
 



2025-02-14 10시03분
A very brief introduction to patterns for implementing a COM object that hands out references to itself
; https://devblogs.microsoft.com/oldnewthing/20211025-00/?p=105828

A sample implementation of the weak reference pattern for COM callbacks
; https://devblogs.microsoft.com/oldnewthing/20250213-00/?p=110865
정성태

... 181  182  183  184  185  186  187  188  189  190  191  [192]  193  194  195  ...
NoWriterDateCnt.TitleFile(s)
142정성태4/14/200517865    답변글 VS.NET IDE: 36.1. 메모리 구성에 대한 추가 내용
137정성태3/31/200522183.NET Framework: 35. XP SP2 팝업 뚫은 소스
135정성태3/31/200520926VS.NET IDE: 26. SQL Server 2000구성이 실패
134정성태3/31/200518132COM 개체 관련: 16. Microsoft.XMLHTTP 개체에서 Microsoft.XMLDOM 개체를 전송할 때 charset 지정 문제? [2]
128정성태3/30/200516476.NET Framework: 34. VC++에서 Managed 타입의 메서드에 BSTR을 넘기는 경우의 오류(!)
129정성태3/30/200518593    답변글 .NET Framework: 34.1. 위의 질문에 대한 답변으로 나온 것입니다.
130정성태3/30/200515909        답변글 .NET Framework: 34.2. 다시... 제가 질문한 내용입니다. ^^
131정성태3/30/200516420            답변글 .NET Framework: 34.3. 다시... 정봉겸님이 하신... 명확한 답변입니다.
126정성태3/26/200516271.NET Framework: 33. Proxy 환경에서의 Smart Client 업데이트 문제 [1]
133정성태3/31/200517417    답변글 .NET Framework: 33.1. [추가]: Proxy 환경에서의 Smart Client 업데이트 문제 [2]
125정성태3/26/200516390VC++: 15. VC++ Keyword
124정성태3/25/200516910.NET Framework: 32. 네트워크 공유 없이 상대 컴퓨터에 프로그램 설치
119정성태3/21/200516487.NET Framework: 31. 소스세이프 오류현상: 웹 프로젝트를 열수 없습니다.
120정성태3/21/200517803    답변글 .NET Framework: 31.1. 소스세이프 오류현상: PDB 파일이 잠기는 문제
121정성태3/21/200517853    답변글 .NET Framework: 31.2. 소스세이프 오류현상: VS.NET 2003 IDE 와 연동되는 소스세이프 버전 문제
122정성태3/21/200516543    답변글 .NET Framework: 31.3. 소스세이프 관련 사이트
160정성태11/14/200519433    답변글 VS.NET IDE: 31.4. [추가]: 웹 애플리케이션 로드시 "_1"을 붙여서 묻는 경우. [1]
196이문석12/23/200516255        답변글 .NET Framework: 31.8. [답변]: [추가]: 웹 애플리케이션 로드시 "_1" 을 붙여서 묻는 경우.
167정성태10/10/200515825    답변글 .NET Framework: 31.5. [추가]: 삭제한 웹 가상 디렉터리에 대해 동일한 이름으로 웹 공유를 설정할 때 - 이미 있다고 오류발생
190정성태12/11/200515121    답변글 VC++: 31.6. ASP.NET 소스세이프 오류현상: 다른 사람이 체크아웃 한 것을 또 다른 사람이 체크아웃 가능!
191정성태12/11/200517571    답변글 VC++: 31.7. 소스 세이프 사용 시, 특정 프로젝트의 빌드 체크가 솔루션 로드할 때마다 해제되는 경우
118정성태3/30/200623353VC++: 14. TCP through HTTP tunneling: 기업 내 Proxy 서버 제한에서 벗어나는 방법 [2]
117정성태3/19/200524475.NET Framework: 30. Process.Start에서의 인자 길이 제한 [4]
116정성태3/14/200516986.NET Framework: 29. [.NET WebService] 자동생성되는 WSDL 을 막는 방법.
115정성태3/13/200517525VS.NET IDE: 25. [IIS 서버] ODBC 로그 남기기 [1]
195정성태12/21/200516764    답변글 VC++: 25.1. ODBC 로그를 못 남길 때의 오류 화면
... 181  182  183  184  185  186  187  188  189  190  191  [192]  193  194  195  ...