C++에서 CSharp 호출

CSharp( 닷넷 DLL)으로 DLL을 만들고 네이티브 C++에서 닷넷의 DLL으로 호출 하는 것을 구현 해 본다.

C#으로 두값을 더하는 DLL을 만들고 네이티브 C++에서 DLL에 두값을 넘겨 줄 것이다.

int DoSum( int a, int b ) 내부는 크게 3단계로 진행 된다.

1. 어셈블리(DLL) 이름으로 DLL을 로딩해서 클래스 이름으로 인스턴스를 생성한다.
DotNet::Open( )

2. 메쏘드를 호출해서 실행한다.
Dotnet::CallFunction( )

3. 로딩 했던 어셈블리를 닫는다.
dotnet.Close();

 

1. DotNet::Open()

닷넷 런타임 시작:

닷넷 런타임을 시작하는 ICorRuntimeHost::Start() 메쏘드를 사용하기 위해 ICorRuntimeHost 포인터를 얻어 오기 위해 CorBindToRuntimeEx() 함수를 실행한다.
CorBindToRuntimeEx 함수는 mscorlib.tlb 라이브러리에 포함되어 있는 함수이다.

        hr = CorBindToRuntimeEx(NULL,   

                                L"wks",

                                STARTUP_LOADER_SAFEMODE | STARTUP_CONCURRENT_GC,

                                CLSID_CorRuntimeHost,

                                IID_ICorRuntimeHost,

                                (void**)&m_pRuntimeHost);

        if(FAILED(hr))

            throw hr;

 

        //Start the CLR

        hr = m_pRuntimeHost->Start();

        if( FAILED(hr) )

            throw hr;

닷넷 어셈블리 로딩

닷넷 런타임 호스트를 실행시 디폴트 AddDomain으로 닷넷 어셈블리가 로드 되고 관리 코드가 실행 된다. ICorRuntimeHost::GetDefaultDomain 메쏘드로 디폴트 AppDomain 포인터를 얻어 올수 있다.

        CComPtr<IUnknown> pUnknown;

 

        CComPtr<mscorlib::_AppDomain>    pAppDomain;

        hr = m_pRuntimeHost->GetDefaultDomain( &pUnknown );

        if( FAILED(hr) )

            throw hr;

 

        hr = pUnknown->QueryInterface( &pAppDomain.p );

        if(FAILED(hr))

            throw hr;

클래스의 인스턴스 생성

_AppDomain::CreateInstance()로 지정된 어셈블리의 클래스로 인스턴스를 만든다.

        _bstr_t _bstrAssemblyName( szAsseblyName );

        _bstr_t _bstrszClassNameWithNamespace( classname );

 

        CComPtr<mscorlib::_ObjectHandle>    pObjectHandle;

        //Creates an instance of the Assembly

        hr = pAppDomain->CreateInstance( _bstrAssemblyName,

                                            _bstrszClassNameWithNamespace,

                                            &pObjectHandle);

        if(FAILED(hr)) 

            throw hr;

IDispatch 인터페이스 구하기

메서드를 동적으로 호출하기 위해  IDispatch 인터페이스를 얻어온다.

        CComVariant VntUnwrapped;

        hr = pObjectHandle->Unwrap( &VntUnwrapped );

        if(FAILED(hr))

            throw hr;

 

        if( VntUnwrapped.vt != VT_DISPATCH )   

        {

            throw E_FAIL;

        }

        m_pDispatch = VntUnwrapped.pdispVal;

 

2. Dotnet::CallFunction( )

메서드를 참조하는 COM DISPID 구하기

COM DISPID( 디스패치 ID)에 의해 메서드를 참조 할 수 있다. GetIDsOfNames()에 의해 디스패치 ID를 얻어온다. ( 단 이 방법은 메서드를 모두 검색해서 찾는 방법이라 속도가 느리다고 함).

http://blog.daum.net/vacuum70/13  IDispatch에 대해서 자세히 나와 있음

        hr = m_pDispatch->GetIDsOfNames(IID_NULL,

                                    &szMethodName,

                                    1,

                                    LOCALE_SYSTEM_DEFAULT,

                                    &dispid);

        if(FAILED(hr))

            throw hr;

메서드 실행

IDispatch::Invoke()를 COM DISPID에 의해 메서드나 프로퍼티를 호출 한다.

        hr = m_pDispatch->Invoke(dispid,

                            IID_NULL,

                            LOCALE_SYSTEM_DEFAULT,

                            DISPATCH_METHOD,

                            &dispparamsArgs,

                            pvRet,

                            NULL,

                            NULL);

        if(FAILED(hr))

            throw hr;

 

3. dotnet.Close();

ICorRuntimeHost::Stop()으로  CLR(공용 언어 런타임)을 중지한다.
스마트 포인터이므로 NULL을 대입한다.

void DotNet::Close()

{

    if( m_pRuntimeHost )

        m_pRuntimeHost->Stop();

 

    m_pDispatch       = NULL;

    m_pRuntimeHost = NULL;

}

 

클라이언트 실행:

이제 모든 프로그램  작성이 끝났다. 실행해 보자..
실행이 안된다. T.T  .... DotNet::Open() 메쏘드안의  if( VntUnwrapped.vt != VT_DISPATCH )에서 자꾸 에러가 난다. 코드에 버그가 있는지, 프로젝트 셋팅이 이상한지 한참 찾아봤지만 찾았다.

        CComVariant VntUnwrapped;

        hr = pObjectHandle->Unwrap( &VntUnwrapped );

        if(FAILED(hr))

            throw hr;

 

        if( VntUnwrapped.vt != VT_DISPATCH )   

        {

            throw E_FAIL;

        }

ActiveX dll이란 것을 명시해 주어야 하기 때문에 이를 위한 코드를 변경해야 한다.
기본적으로 dll은 Assembly의 형태로 관리되기 때문에 다음 파일을 열어서 코드를 변경한다.    
C#의 DLL을 네이티브 C++에서 사용할려면 C#으로 ActiveX DLL 만들때와 마찬가지로 AssemblyInfo.cs 파일을 열어서 ComVisible 값을 false에서 true로 바꿔 준다.

// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에

// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면

// 해당 형식에 대해 ComVisible 특성을 true로 설정하십시오.

[assembly: ComVisible(true)]

이제 COM 인터페이스에 접근 할수 있다.  이제 프로그램이 잘돌아 갈것이다.

 

------------------------ 레프런스 --------------------------------

< ICorRuntimeHost  >

ICorRuntimeHost은 호스트에서 CLR(공용 언어 런타임)을 명시적으로 시작하거나 중지하고, 응용 프로그램 도메인을 만들거나 구성하고, 기본 도메인에 액세스하고, 프로세스에서 실행 중인 모든 도메인을 열거하는 데 사용할 수 있는 메서드를 제공한다( 쉽게 비유하자면 로딩한 DLL 포인터를 가지고 있다고 보면 된다.)

COM 스마트 포인터를 이용해서 클라이언트에서는 다음과 같이 선언한다.

CComPtr<ICorRuntimeHost> m_pRuntimeHost;

ICorRuntimeHost::Start
CLR(공용 언어 런타임)을 시작한다.

ICorRuntimeHost::GetDefaultDomain
현재 프로세스의 기본 도메인을 나타내는 System._AppDomain 형식의 인터페이스 포인터를 가져온다.

ICorRuntimeHost::Stop
런타임에 현재 프로세스의 코드 실행을 중지한다.

< CorBindToRuntimeEx 함수 >
관리되지 않는 호스트에서 CLR(공용 언어 런타임)을 프로세스로 로드할 수 있도록 한다라고 문서에 적혀있다.
쉽게 이야기 하자면 어셈블리(DLL) 파일의 클래스를 로딩해준다.

HRESULT CorBindToRuntimeEx (

        [in]  LPWSTR    pwszVersion,

        [in]  LPWSTR    pwszBuildFlavor,

        [in]  DWORD    startupFlags,

        [in]  REFCLSID  rclsid,

        [in]  REFIID    riid,

        [out] LPVOID*   ppv

);

설명 링크: http://msdn.microsoft.com/ko-kr/library/99sz37yh.aspx

pwszVersion :  로드할 CLR의 버전을 설명하는 문자열이다.

pwszBuildFlavor : CLR의 서버 빌드를 로드할지 워크스테이션 빌드를 로드할지를 지정하는 문자열이다.
null로 설정되면 워크스테이션 빌드가 로드된다.

startupFlags : STARTUP_FLAGS 열거형의 값 조합이다. 이러한 플래그는 동시 가비지 수집, 도메인에 중립적인 코드 및 pwszVersion 매개 변수의 동작을 제어한다. 플래그가 설정되어 있지 않은 경우 기본값은 단일 도메인이다.

rclsid : ICorRuntimeHost 인터페이스를 구현하는 coclass의 CLSID이다. 지원되는 값은 CLSID_CorRuntimeHost또는 CLSID_CLRRuntimeHost이다.

riid : rclsid에서 요청된 인터페이스의 IID이다. 지원되는 값은 IID_ICorRuntimeHost 또는 IID_ICLRRuntimeHost이다.

ppv : riid에 대한 반환된 인터페이스 포인터이다.

< mscorlib::_AppDomain >
개체에 의해 실행 되는 관리 코드를 실행하기 위한 로드, 언로드 및 보안 경계를 제공한다.  _AppDomain::CreateInstance() 지정된 어셈블리에 정의되어 있는 지정된 형식의 새 인스턴스를 만든다

< IDispatch >
IDispatch 인터페이스를 이용해서 메서드를 동적으로 호출 할 수 잇다.
IDispatch::GetIDsOfNames   클라이언트로부터 메서드 이름 목록을 받아서 dispid 목록을 리턴
IDispatch::Invoke   dispid를 이용하여 원하는 메서드나 프로퍼티를 호출한다.

프로젝트: csharp_call.zip