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 특성이 사라지므로 다중 스레드에서 동시 호출을 하는 경우가 있다면 공용 저장소에 대한 동기화 처리는 개발자가 직접 해줘야 합니다.

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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 8/31/2018]

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

비밀번호

댓글 작성자
 




... 61  62  63  64  65  [66]  67  68  69  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
11997정성태7/30/201913446.NET Framework: 850. C# - Excel(을 비롯해 Office 제품군) COM 객체를 제어 후 Excel.exe 프로세스가 남아 있는 문제 [2]파일 다운로드1
11996정성태7/25/201915863.NET Framework: 849. C# - Socket의 TIME_WAIT 상태를 없애는 방법파일 다운로드1
11995정성태7/23/201918944.NET Framework: 848. C# - smtp.daum.net 서비스(Implicit SSL)를 이용해 메일 보내는 방법 [2]
11994정성태7/22/201914440개발 환경 구성: 454. Azure 가상 머신(VM)에서 SMTP 메일 전송하는 방법파일 다운로드1
11993정성태7/22/20199882오류 유형: 561. Dism.exe 수행 시 "Error: 2 - The system cannot find the file specified." 오류 발생
11992정성태7/22/201911668오류 유형: 560. 서비스 관리자 실행 시 "Windows was unable to open service control manager database on [...]. Error 5: Access is denied." 오류 발생
11991정성태7/18/20199179디버깅 기술: 128. windbg - x64 환경에서 닷넷 예외가 발생한 경우 인자를 확인할 수 없었던 사례
11990정성태7/18/201911381오류 유형: 559. Settings / Update & Security 화면 진입 시 프로그램 종료
11989정성태7/18/201910295Windows: 162. Windows Server 2019 빌드 17763부터 Alt + F4 입력시 곧바로 로그아웃하는 현상
11988정성태7/18/201911739개발 환경 구성: 453. 마이크로소프트가 지정한 모든 Root 인증서를 설치하는 방법
11987정성태7/17/201916718오류 유형: 558. 윈도우 - KMODE_EXCEPTION_NOT_HANDLED 블루스크린(BSOD) 문제 [1]
11986정성태7/17/20199511오류 유형: 557. 드라이브 문자를 할당하지 않은 파티션을 탐색기에서 드라이브 문자와 함께 보여주는 문제
11985정성태7/17/20199638개발 환경 구성: 452. msbuild - csproj에 환경 변수 조건 사용 [1]
11984정성태7/9/201917838개발 환경 구성: 451. Microsoft Edge (Chromium)을 대상으로 한 Selenium WebDriver 사용법 [1]
11983정성태7/8/20198896오류 유형: 556. nodemon - 'mocha' is not recognized as an internal or external command, operable program or batch file.
11982정성태7/8/20198894오류 유형: 555. Visual Studio 빌드 오류 - result: unexpected exception occured (-1002 - 0xfffffc16)
11981정성태7/7/201911076Math: 64. C# - 3층 구조의 신경망(분류)파일 다운로드1
11980정성태7/7/201921515개발 환경 구성: 450. Visual Studio Code의 Java 확장을 이용한 간단한 프로젝트 구축파일 다운로드1
11979정성태7/7/201911048개발 환경 구성: 449. TFS에서 gitlab/github등의 git 서버로 마이그레이션하는 방법
11978정성태7/6/201910401Windows: 161. 계정 정보가 동일하지 않은 PC 간의 인증을 수행하는 방법 [1]
11977정성태7/6/201914964오류 유형: 554. git push - error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413 Request Entity Too Large
11976정성태7/4/20199324오류 유형: 553. (잘못 인증 한 후) 원격 git repo 재인증 시 "remote: HTTP Basic: Access denied" 오류 발생
11975정성태7/4/201917832개발 환경 구성: 448. Visual Studio Code에서 콘솔 응용 프로그램 개발 시 "입력"받는 방법
11974정성태7/4/201913190Linux: 22. "Visual Studio Code + Remote Development"로 윈도우 환경에서 리눅스(CentOS 7) C/C++ 개발
11973정성태7/4/201912401Linux: 21. 리눅스에서 공유 라이브러리가 로드되지 않는다면?
11972정성태7/3/201915282.NET Framework: 847. JAVA와 .NET 간의 AES 암호화 연동 [1]파일 다운로드1
... 61  62  63  64  65  [66]  67  68  69  70  71  72  73  74  75  ...