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

... 106  107  108  109  110  [111]  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11149정성태2/21/201722894오류 유형: 378. A 64-bit test cannot run in a 32-bit process. Specify platform as X64 to force test run in X64 mode on X64 machine.
11148정성태2/20/201721895.NET Framework: 644. AppDomain에 대한 단위 테스트 시 알아야 할 사항
11147정성태2/19/201721121오류 유형: 377. Windows 10에서 Fake 어셈블리를 생성하는 경우 빌드 시 The type or namespace name '...' does not exist in the namespace 컴파일 오류 발생
11146정성태2/19/201719741오류 유형: 376. Error VSP1033: The file '...' does not contain a recognized executable image. [2]
11145정성태2/16/201721236.NET Framework: 643. 작업자 프로세스(w3wp.exe)가 재시작되는 시점을 알 수 있는 방법 - 두 번째 이야기 [4]파일 다운로드1
11144정성태2/6/201724588.NET Framework: 642. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (부록 1) - CallingConvention.StdCall, CallingConvention.Cdecl에 상관없이 왜 호출이 잘 될까요?파일 다운로드1
11143정성태2/5/201722023.NET Framework: 641. [Out] 형식의 int * 인자를 가진 함수에 대한 P/Invoke 호출 방법파일 다운로드1
11142정성태2/5/201730012.NET Framework: 640. 닷넷 - 배열 크기의 한계 [2]파일 다운로드1
11141정성태1/31/201724278.NET Framework: 639. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (4) - CLR JIT 컴파일러의 P/Invoke 호출 규약 [1]파일 다운로드1
11140정성태1/27/201720054.NET Framework: 638. RSAParameters와 RSA파일 다운로드1
11139정성태1/22/201722732.NET Framework: 637. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (3) - x64 환경의 __fastcall과 Name mangling [1]파일 다운로드1
11138정성태1/20/201721013VS.NET IDE: 113. 프로젝트 생성 시부터 "Enable the Visual Studio hosting process" 옵션을 끄는 방법 - 두 번째 이야기 [3]
11137정성태1/20/201719731Windows: 135. AD에 참여한 컴퓨터로 RDP 연결 시 배경 화면을 못 바꾸는 정책
11136정성태1/20/201718888오류 유형: 375. Hyper-V 내에 구성한 Active Directory 환경의 시간 구성 방법 - 두 번째 이야기
11135정성태1/20/201719922Windows: 134. Windows Server 2016의 작업 표시줄에 있는 시계가 사라졌다면? [1]
11134정성태1/20/201727354.NET Framework: 636. System.Threading.Timer를 이용해 타이머 작업을 할 때 유의할 점 [5]파일 다운로드1
11133정성태1/20/201723495.NET Framework: 635. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (2) - x86 환경의 __fastcall [1]파일 다운로드1
11132정성태1/19/201734974.NET Framework: 634. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (1) - x86 환경에서의 __cdecl, __stdcall에 대한 Name mangling [1]파일 다운로드1
11131정성태1/13/201723875.NET Framework: 633. C# - IL 코드 분석을 위한 팁 [2]
11130정성태1/11/201724408.NET Framework: 632. x86 실행 환경에서 SECURITY_ATTRIBUTES 구조체를 CreateEvent에 전달할 때 예외 발생파일 다운로드1
11129정성태1/11/201728766.NET Framework: 631. async/await에 대한 "There Is No Thread" 글의 부가 설명 [9]파일 다운로드1
11128정성태1/9/201723190.NET Framework: 630. C# - Interlocked.CompareExchange 사용 예제 [3]파일 다운로드1
11127정성태1/8/201722725기타: 63. (개발자를 위한) Visual Studio의 "with MSDN" 라이선스 설명
11126정성태1/7/201727486기타: 62. Edge 웹 브라우저의 즐겨찾기(Favorites)를 편집/백업/복원하는 방법 [1]파일 다운로드1
11125정성태1/7/201724302개발 환경 구성: 310. IIS - appcmd.exe를 이용해 특정 페이지에 클라이언트 측 인증서를 제출하도록 설정하는 방법
11124정성태1/4/201727767개발 환경 구성: 309. 3년짜리 유효 기간을 제공하는 StartSSL [2]
... 106  107  108  109  110  [111]  112  113  114  115  116  117  118  119  120  ...