Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 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# 개발자를 위한 C++ COM 객체의 기본 구현 방식 설명

순수 C# 개발자들이라면, 아마도 C++ COM 객체는 잘 이해가 안 되는 개념일 수 있습니다. 물론, C++을 공부해 COM 객체를 이해하면 가장 좋겠지만 녹록지 않은 것도 현실입니다. 그래도 다행히 C#을 이용해서도 어느 정도는 전체적인 개념을 잡는 것이 가능합니다. 왜냐하면, C#으로도 C++의 COM 객체를 구현하는 것과 거의 동일한 절차를 밟을 수 있기 때문입니다.

이미 C# 클래스 자체를 regasm.exe를 이용해 COM 객체로 만드는 방법을 알고 있는 분도 있을 것입니다.

regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (1) - .NET 2.0 + x86/x64/AnyCPU
; https://www.sysnet.pe.kr/2/0/1286

하지만 이 글에서는 위의 방법이 아닌, 말 그대로 C++의 COM 객체 구현과 유사한 방식을 따름으로써 COM의 개요를 이해할 수 있도록 할 예정입니다.

사실, 이 방법들은 다음의 글들을 통해 어느 정도 이미 나와 있기도 합니다. ^^

Implementing COM OutOfProc Servers in C# .NET !!! 
; http://developerexperience.blogspot.kr/2006/04/implementing-com-outofproc-servers-in.html

C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법
; https://www.sysnet.pe.kr/2/0/11052

그래도, step-by-step 식으로 하나씩 설명해 보는 것도 좋을 것 같아 이렇게 글을 쓰게 되었습니다. 자~~~~, 그럼 이제부터 재미 삼아 한번 만들어 볼까요? ^^




우선, 대개의 COM DLL 파일들이 export하고 있는 다음의 4개 함수를 C# DLL에서도 구현해야 합니다.


각각의 함수들이 어떤 역할을 하는지 하나씩 살펴볼 텐데요.

이를 위해 가장 먼저 C++ 측에서 COM 객체를 어떻게 생성할 수 있는지를 봐야 합니다. 원칙적으로 C++은 CoGetClassObject Win32 API를 통해,

CoGetClassObject function
; https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cogetclassobject

다음과 같이 "원하는 COM 객체를 생성할 줄 아는 Factory 클래스"를 먼저 구해옵니다.

int main()
{
    CoInitialize(nullptr);
    {
        CLSID refclsId = { 0, };

        // {C0000000-71EB-43A6-9BD1-5770A3737617} == 원하는 Factory Class의 식별자
        //                                           개발자가 임의로 지정해서 약속으로 만듦
        CLSIDFromString(L"{C0000000-71EB-43A6-9BD1-5770A3737617}", &refclsId);
        LPVOID ppvFactoryObject = nullptr;

        CoGetClassObject(refclsId, CLSCTX_INPROC_SERVER, nullptr, IID_IClassFactory, &ppvFactoryObject);
    }
    CoUninitialize();
    return 0;
}

CoGetClassObject의 첫 번째 인자로 들어가는 refclsId 인자가 바로 "Factory 클래스"를 식별하는 역할을 합니다. 그리고, 그 Factory 클래스를 구현하고 있는 실행 파일을 찾기 위해 레지스트리를 이용합니다. 즉, 윈도우에서 제공하는 CoGetClassObject는 레지스트리를 검색해 DLL/EXE 실행 파일을 찾고, 그 파일로부터 "Factory 클래스"를 얻어 반환하는 역할을 하는 것입니다. 이때의 레지스트리 경로는 다음과 같습니다.

HKEY_CLASSES_ROOT\CLSID\{C0000000-71EB-43A6-9BD1-5770A3737617}

만약 저 경로가 존재한다면, 그 하위의 InProcServer32 키를 조사하고, 그 키도 있다면 (Default) 값으로 설정된 구현 파일의 경로를 구할 수 있어 결국 해당 바이너리를 메모리에 로드하게 됩니다.

물론, 기본 설치된 윈도우 운영체제에는 {C0000000-71EB-43A6-9BD1-5770A3737617} 키 파일의 경로가 레지스트리에 등록되어 있지 않습니다. 따라서 CoGetClassObject API가 정상적으로 동작하려면 레지스트리에 CLSID 경로가 등록되어야 하는데, 대개의 경우 그 역할을 해당 COM 객체를 구현한 DLL 파일이 담당하게 됩니다. 그리고 바로 그 기능을 구현해야 할 의무가 있는 것이 DllRegisterServer 함수입니다. 이렇게 해서, 우리가 첫 번째로 구현하는 C# COM DLL의 DllRegisterServer 함수는 다음과 같게 됩니다.

using Microsoft.Win32;
using RGiesecke.DllExport;
using System;
using System.Runtime.InteropServices;

namespace MyCOMObj
{
    public class Register
    {
        const string CLSID_KEY = "{C0000000-71EB-43A6-9BD1-5770A3737617}";

        [DllExport("DllRegisterServer", CallingConvention = CallingConvention.StdCall)]
        public static int DllRegisterServer()
        {
            try
            {
                if (IntPtr.Size == 4)
                {
                    RegisterDLL(RegistryView.Registry32);
                }
                else
                {
                    RegisterDLL(RegistryView.Registry64);
                }
            }
            catch (Exception e)
            {
                return Marshal.GetHRForException(e);
            }

            return 0;
        }

        private static void RegisterDLL(RegistryView regView)
        {
            using (RegistryKey clsRoot = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, regView))
            {
                using (RegistryKey clsKey = clsRoot.OpenSubKey("CLSID", true))
                using (RegistryKey clsIdKey = clsKey.CreateSubKey(CLSID_KEY))
                {
                    using (RegistryKey inprocKey = clsIdKey.CreateSubKey("InProcServer32"))
                    {
                        inprocKey.SetValue(null, typeof(Register).Assembly.Location);
                        inprocKey.SetValue("ThreadingModel", "Apartment");
                    }
                }
            }
        }
    }
}

위와 같이 구현하고 빌드한 후, 생성된 DLL을 대상으로 regsvr32.exe를 실행시켜 주면,

C:\ClassLibrary1\bin\x86\Debug>regsvr32 MyCOMObj.dll

RegisterDLL 메서드에 지정했던 레지스트리 키들이 만들어진 것을 확인할 수 있습니다.

cs_comobj_1.png

DllRegisterServer를 이렇게 구현했으니, 당연히 그 반대의 역할을 하는 DllUnregisterServer도 구현할 수 있습니다.

[DllExport("DllUnregisterServer", CallingConvention = CallingConvention.StdCall)]
public static int DllUnregisterServer()
{
    try
    {
        if (IntPtr.Size == 4)
        {
            UnregisterDLL(RegistryView.Registry32);
        }
        else
        {
            UnregisterDLL(RegistryView.Registry64);
        }
    }
    catch (Exception e)
    {
        return Marshal.GetHRForException(e);
    }

    return 0;
}

private static void UnregisterDLL(RegistryView regView)
{
    using (RegistryKey clsRoot = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, regView))
    {
        using (RegistryKey clsKey = clsRoot.OpenSubKey("CLSID", true))
        {
            clsKey.DeleteSubKeyTree(CLSID_KEY, false);
        }
    }
}

이제는 다음의 명령어로 레지스트리로부터 COM DLL 관련 정보를 제거할 수 있습니다.

C:\ClassLibrary1\bin\x86\Debug>regsvr32 /u MyCOMObj.dll




여기까지 마치고, 다시 C++의 CoGetClassObject API를 호출해도 역시 오류가 발생합니다.

HRESULT hr = CoGetClassObject(refclsId, CLSCTX_INPROC_SERVER, nullptr, IID_IClassFactory, &ppvFactoryObject);
if (hr == S_OK)
{
    // ...[성공]...
}
else
{
    // ...[실패]...
    // 0x800401f9 - Error in the DLL
}

레지스트리를 통해 refclsId에 해당하는 DLL 파일은 찾았지만, 그 DLL을 로딩한 후 DllGetClassObject 함수를 호출해 Factory Class를 가져와야 하는데 그것이 구현되지 않은 상태이기 때문입니다.

이를 해결하기 위해 다음과 같이 구현을 추가해 주면 됩니다.

[DllExport("DllGetClassObject", CallingConvention = CallingConvention.StdCall)]
public static int DllGetClassObject(ref Guid rclsid, ref Guid riid, out IntPtr pUnk)
{
    pUnk = IntPtr.Zero;

    // COM DLL 내에는 여러 개의 Class Factory를 제공할 수 있기 때문에,
    // 그중에서도 (C0000000-71EB-43A6-9BD1-5770A3737617로 약속했던) MyClassFactory를 요구하는지 확인
    if (rclsid.ToString().ToUpper() == "C0000000-71EB-43A6-9BD1-5770A3737617") 
    {
        // MyClassFactory 객체의 IClassFactory 인터페이스를 요구하는 것인지 확인
        if (riid.ToString().ToUpper() == "00000001-0000-0000-C000-000000000046") // IClassFactory
        {
            pUnk = Marshal.GetComInterfaceForObject(new MyClassFactory(), typeof(IClassFactory));
            return 0; // S_OK
        }
    }

    return -1; // S_FALSE
}

이때의 MyClassFactory 클래스는 IClassFactory 인터페이스를 구현하기만 하면 됩니다.

using COM;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace COM
{
    static class Guids
    {
        public const string IClassFactory = "00000001-0000-0000-C000-000000000046";
        public const string IUnknown = "00000000-0000-0000-C000-000000000046";
    }

    // http://developerexperience.blogspot.kr/2006/04/implementing-com-outofproc-servers-in.html
    /// 
    /// IClassFactory declaration
    /// 
    [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid(COM.Guids.IClassFactory)]
    public interface IClassFactory
    {
        [PreserveSig]
        int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject);
        [PreserveSig]
        int LockServer(bool fLock);
    }
}

namespace MyCOMObj
{
    [Guid(MyClassFactory.CLSID)]
    public class MyClassFactory : IClassFactory
    {
        public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject)
        {
            ppvObject = IntPtr.Zero;

            return 0; // S_OK;
        }

        public int LockServer(bool fLock)
        {
            return 0; // S_OK
        }
    }
}

일단은, MyClassFactory가 구현한 CreateInstance는 아무것도 생성하지 않습니다. 그렇긴 해도 C++ 측의 CoGetClassObject는 IClassFactory 인터페이스를 구현한 객체까지 구해오는 것이 전부이므로 다음의 코드가 성공하게 됩니다.

int main()
{
    CoInitialize(nullptr);
    {
        CLSID refclsId = { 0, };

        CLSIDFromString(L"{C0000000-71EB-43A6-9BD1-5770A3737617}", &refclsId);
        LPVOID ppvFactoryObject = nullptr;

        HRESULT hr = CoGetClassObject(refclsId, CLSCTX_INPROC_SERVER, nullptr, IID_IClassFactory, &ppvFactoryObject);
        if (hr == S_OK)
        {
            printf("Created\n"); // ppvFactoryObject에는 IClassFactory를 구현한 C# 측의 COM 객체를 받아옴
        }
        else
        {
            printf("Failed\n");
        }
    }
    CoUninitialize();
    return 0;
}




"원하는 COM 객체를 만들 줄 아는 Factory 객체"를 구했으니, 이제 그 Factory를 통해서 "원하는 COM" 객체를 생성할 수 있습니다. C++ 측에서는 다음과 같이 코딩할 수 있습니다.

IClassFactory *pFactory = (IClassFactory *)ppvFactoryObject;

IID iid = { 0, };
IIDFromString(L"{82996B14-75F2-41FC-86A3-14C13DDD7A2C}", &iid);

// Factory 클래스가 만들어야 할 COM 객체를 IID 식별자로 요구
// 82996B14-75F2-41FC-86A3-14C13DDD7A2C 식별자는 개발자가 임의로 정해 약속으로 고정
LPVOID ppvObject = nullptr;
hr = pFactory->CreateInstance(nullptr, iid, &ppvObject);
if (hr == S_OK)
{
    printf("Created\n");
}
else
{
    printf("Failed\n");
}

위의 코드를 실행하면 ppvObject는 nullptr로 반환되지만 호출 자체는 hr == S_OK로 성공하게 됩니다. 왜냐하면, C# 측의 MyClassFactory.CreateInstance 메서드가 return 0으로 성공을 반환했기 때문입니다.

public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject)
{
    ppvObject = IntPtr.Zero;

    return 0; // S_OK;
}

자, 그럼 원하는 COM 객체를 반환하도록 구현을 해보겠습니다. 우선 CreateInstance는 다음과 같이 변경할 수 있습니다.

public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject)
{
    ppvObject = IntPtr.Zero;

    if (pUnkOuter != IntPtr.Zero)
    {
        return -2147221232; // 2147221232 == 0x80040110 == CLASS_E_NOAGGREGATION;
    }

    string riidText = riid.ToString().ToUpper();

    switch (riidText)
    {
        case "82996B14-75F2-41FC-86A3-14C13DDD7A2C": // 생성해야 할 COM 객체의 식별자 GUID
        case "00020400-0000-0000-C000-000000000046": // IDispatch
        case "00000000-0000-0000-C000-000000000046": // IUnknown
            ppvObject = Marshal.GetComInterfaceForObject(new MySimpleObject(), typeof(IMySimpleObject));
            break;

        default:
            return -2147467262; // -2147467262 == 0x80004002 == E_NOINTERFACE
    }            

    return 0;
}

그리고 MySimpleObject와 IMySimpleObject는 (예를 들어) 이렇게 구현하면 됩니다.

using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace MyCOMObj
{
    [ComVisible(true)]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface IMySimpleObject
    {
        [DispId(1)]
        void ShowWinFormMessage([MarshalAs(UnmanagedType.LPWStr)] string text);
    }

    [ClassInterface(ClassInterfaceType.None)]
    public class MySimpleObject : IMySimpleObject
    {
        public void ShowWinFormMessage(string text)
        {
            MessageBox.Show(text);
        }
    }
}

이제 다시 실행해 보면, C++ 소스 코드 측의 CreateInstance 호출은 정상적으로 ppvObject에 값을 반환받게 됩니다. 그런데, C++ 측에서 ppvObject를 어떻게 사용해야 할까요?

어차피, COM 객체는 vtable을 기반으로 약속된 함수들의 집합이기 때문에 C++ 측에서도 동일한 규칙의 클래스를 명시한 후 형 변환해서 맞춰주면 됩니다.

interface IMySimpleObject : IDispatch
{
public:
    virtual HRESULT __stdcall ShowWinFormMessage(wchar_t *text) = 0;
};

int main()
{
    CoInitialize(nullptr);
    {
        // ...[생략]...

        LPVOID ppvObject = nullptr;
        hr = pFactory->CreateInstance(nullptr, iid, &ppvObject);
        if (hr == S_OK)
        {
            printf("COM: Created\n");

            IMySimpleObject *pSimple = (IMySimpleObject *)ppvObject;
            pSimple->ShowWinFormMessage(L"TEST IS GOOD");
        }
        else
        {
            printf("COM: Failed\n");
        }
    }

    CoUninitialize();
    return 0;
}

실행해 보면, C# 측의 ShowWinFormMessage가 호출되는 것을 확인할 수 있습니다. 이렇게까지 구현이 되었으면, 이제 CoGetClassObject + IClassFactory::CreateInstance의 2단계가 아닌, 다음과 같이 1단계로 끝낼 수 있습니다.

IMySimpleObject *pSimple2;
hr = CoCreateInstance(refclsId, nullptr, CLSCTX_INPROC_SERVER, iid, (LPVOID *)&pSimple2);
if (hr == S_OK)
{
    pSimple2->ShowWinFormMessage(L"TEST IS GOOD2");
}

당연히, C#에서도 위에서 만든 COM DLL을 호출할 수 있습니다. 다음과 같이!

Guid guid = new Guid("C0000000-71EB-43A6-9BD1-5770A3737617");
Type type = Type.GetTypeFromCLSID(guid);

object comObject = Activator.CreateInstance(type);

Type objType = comObject.GetType();

objType.InvokeMember("ShowWinFormMessage", 
    System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public
    | System.Reflection.BindingFlags.InvokeMethod,
    null, comObject, new object[] { "TEST IS GOOD3" });




마지막으로 남은 함수 하나! 닷넷의 특성상 DLL을 내리는 것이 불가능하므로 별다른 선택의 여지없이 -1을 반환하는 DllCanUnloadNow 코드.

[DllExport("DllCanUnloadNow", CallingConvention = CallingConvention.StdCall)]
public static int DllCanUnloadNow()
{
    return -1; // S_FALSE
}




C#으로 설명하긴 했지만, 써 놓고 보니 결국 C++의 배경 지식이 있어야 잘 이해될 수 있는 곳들이 많군요. ^^;

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




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







[최초 등록일: ]
[최종 수정일: 4/28/2025]

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

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1755정성태9/22/201434169오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
1754정성태9/22/201424517VC++: 80. 내 컴퓨터에서 C++ AMP 코드가 실행이 될까요? [1]
1753정성태9/22/201420492오류 유형: 240. Lync로 세미나 참여 시 소리만 들리지 않는 경우 [1]
1752정성태9/21/201440970Windows: 100. 윈도우 8 - RDP 연결을 이용해 VNC처럼 사용자 로그온 화면을 공유하는 방법 [5]
1751정성태9/20/201438840.NET Framework: 464. 프로세스 간 통신 시 소켓 필요 없이 간단하게 Pipe를 열어 통신하는 방법 [1]파일 다운로드1
1750정성태9/20/201423757.NET Framework: 463. PInvoke 호출을 이용한 비동기 파일 작업파일 다운로드1
1749정성태9/20/201423680.NET Framework: 462. 커널 객체를 위한 null DACL 생성 방법파일 다운로드1
1748정성태9/19/201425307개발 환경 구성: 238. [Synergy] 여러 컴퓨터에서 키보드, 마우스 공유
1747정성태9/19/201428345오류 유형: 239. psexec 실행 오류 - The system cannot find the file specified.
1746정성태9/18/201425977.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기 [6]파일 다운로드1
1745정성태9/17/201422925개발 환경 구성: 237. 리눅스 Integration Services 버전 업그레이드 하는 방법 [1]
1744정성태9/17/201430956.NET Framework: 460. GetTickCount / GetTickCount64와 0x7FFE0000 주솟값 [4]파일 다운로드1
1743정성태9/16/201420907오류 유형: 238. 설치 오류 - Failed to get size of pseudo bundle
1742정성태8/27/201426883개발 환경 구성: 236. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 [2]
1741정성태8/26/201421256.NET Framework: 459. GetModuleHandleEx로 알아보는 .NET 메서드의 DLL 모듈 관계파일 다운로드1
1740정성태8/25/201432430.NET Framework: 458. 닷넷 GC가 순환 참조를 해제할 수 있을까요? [2]파일 다운로드1
1739정성태8/24/201426447.NET Framework: 457. 교착상태(Dead-lock) 해결 방법 - Lock Leveling [2]파일 다운로드1
1738정성태8/23/201421964.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스파일 다운로드1
1737정성태8/20/201419673VS.NET IDE: 93. Visual Studio 2013 동기화 문제
1736정성태8/19/201425526VC++: 79. [부연] CAS Lock 알고리즘은 과연 빠른가? [2]파일 다운로드1
1735정성태8/19/201418119.NET Framework: 455. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 두 번째 이야기
1734정성태8/13/201419773오류 유형: 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/201426235.NET Framework: 454. EmptyWorkingSet Win32 API를 사용하는 C# 예제파일 다운로드1
1732정성태8/13/201434364Windows: 99. INetCache 폴더가 다르게 보이는 이유
1731정성태8/11/201426943개발 환경 구성: 235. 점(.)으로 시작하는 파일명을 탐색기에서 만드는 방법
1730정성태8/11/201422049개발 환경 구성: 234. Royal TS의 터미널(Terminal) 연결에서 한글이 깨지는 현상 해결 방법
... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...