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
정성태

... 166  167  168  169  170  171  172  173  174  175  176  177  [178]  179  180  ...
NoWriterDateCnt.TitleFile(s)
536정성태9/12/200732322.NET Framework: 97. WCF : netTcpBinding에서의 각종 Timeout 값 설명 [11]
535정성태9/11/200729800.NET Framework: 96. WCF - PerSession에서의 클라이언트 연결 관리 [5]
534정성태9/3/200725296개발 환경 구성: 29. VHD 파일 크기 줄이기
533정성태9/2/200727964개발 환경 구성: 28. CA 서비스 - 사용자 정의 템플릿 유형 추가
532정성태9/2/200730514개발 환경 구성: 27. AD CA에서 Code Signing 인증서 유형 추가 방법
531정성태9/2/200726256.NET Framework: 95. WCF에서의 DataTable 사용
530정성태9/1/200722719.NET Framework: 94. WCF 예외에 대한 시행착오
529정성태8/31/200725648.NET Framework: 93. WCF - DataContract와 KnownType 특성 [1]
528정성태8/30/200720281오류 유형: 47. VPC - 네트워크 어댑터 MAC 주소 중복 오류
527정성태8/30/200730408Team Foundation Server: 20. 잠긴 파일을 강제로 해제 [2]
526정성태8/29/200720269오류 유형: 46. VS.NET 2008 - ASP.NET 디버깅 : Strong name validation failed.
525정성태8/27/200722548VS.NET IDE: 54. VS.NET 2008 - 새롭게 도입되는 XSD Schema Designer
524정성태8/23/200740054오류 유형: 45. 요청한 작업은, 사용자가 매핑한 구역이 열려 있는...
523정성태8/16/200722655VS.NET IDE: 53. VS.NET 2008 - 서비스 참조 시 기존 데이터 컨테이너 DLL 사용
522정성태8/13/200726334VS.NET IDE: 52. VS.NET 2008 - WCF를 위한 디버깅 환경 개선
521정성태8/8/200726376.NET Framework: 92. XmlSerializer 생성자의 실행 속도를 올리는 방법 - 두 번째 이야기 [3]
520정성태8/7/200721562VS.NET IDE: 51. Visual Studio 2008 베타 2 설치
519정성태7/27/200727920오류 유형: 44. System.BadImageFormatException [2]
518정성태7/26/200728958오류 유형: 43. System.ComponentModel.LicenseException [1]
517정성태7/19/200717208개발 환경 구성: 26. VPC - 일반 사용자 계정으로 구동
516정성태7/19/200720359오류 유형: 42. TFS - Error loading menu: Index was outside the bounds of the array [2]
515정성태7/18/200728065오류 유형: 41. SSL 서버 자격 증명을 만드는 동안 심각한 오류가 발생했습니다.
514정성태7/14/200720762Team Foundation Server: 19. Orcas에서 개선되는 TFS 기능들
513정성태7/4/200731737.NET Framework: 91. Foreground Thread / Background Thread [1]
512정성태6/27/200721716오류 유형: 40. error PRJ0050: Failed to register output.
511정성태6/25/200729691.NET Framework: 90. XmlSerializer 생성자의 실행 속도를 올리는 방법 [2]
... 166  167  168  169  170  171  172  173  174  175  176  177  [178]  179  180  ...