Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 16개 있습니다.)
.NET Framework: 112. How to Interop DISPPARAMS
; https://www.sysnet.pe.kr/2/0/617

.NET Framework: 137. C#에서 Union 구조체 다루기
; https://www.sysnet.pe.kr/2/0/728

.NET Framework: 141. Win32 Interop - 크기가 정해지지 않은 배열을 C++에서 C#으로 전달하는 경우
; https://www.sysnet.pe.kr/2/0/737

.NET Framework: 168. [in,out] 배열을 C#에서 C/C++로 넘기는 방법
; https://www.sysnet.pe.kr/2/0/810

.NET Framework: 169. [in, out] 배열을 C#에서 C/C++로 넘기는 방법 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/811

.NET Framework: 183. 구조체 포인터 인자에 대한 P/Invoke 정의
; https://www.sysnet.pe.kr/2/0/912

.NET Framework: 472. C/C++과 C# 사이의 메모리 할당/해제 방법
; https://www.sysnet.pe.kr/2/0/1784

.NET Framework: 620. C#에서 C/C++ 함수로 콜백 함수를 전달하는 예제 코드
; https://www.sysnet.pe.kr/2/0/11099

.NET Framework: 627. C++로 만든 DLL을 C#에서 사용하기
; https://www.sysnet.pe.kr/2/0/11111

.NET Framework: 686. C# - string 배열을 담은 구조체를 직렬화하는 방법
; https://www.sysnet.pe.kr/2/0/11319

.NET Framework: 757. 포인터 형 매개 변수를 갖는 C++ DLL의 함수를 C#에서 호출하는 방법
; https://www.sysnet.pe.kr/2/0/11533

.NET Framework: 978. C# - GUID 타입 전용의 UnmanagedType.LPStruct
; https://www.sysnet.pe.kr/2/0/12444

C/C++: 158. Visual C++ - IDL 구문 중 "unsigned long"을 인식하지 못하는 #import
; https://www.sysnet.pe.kr/2/0/13128

.NET Framework: 2058. [in,out] 배열을 C#에서 C/C++로 넘기는 방법 - 세 번째 이야기
; https://www.sysnet.pe.kr/2/0/13141

.NET Framework: 2083. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용 (2)
; https://www.sysnet.pe.kr/2/0/13205

닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제
; https://www.sysnet.pe.kr/2/0/13429




Visual C++ - IDL 구문 중 "unsigned long"을 인식하지 못하는 #import

아래의 질문이 있었는데요,

C# dll 과 C++ 간 배열 전달. SafeArray
; https://www.sysnet.pe.kr/3/0/5726

문제를 정리해 보면, C# 측에서 다음과 같은 메서드를 하나 정의했고,

using System;
using System.Runtime.InteropServices;

namespace CsClassLibrary
{
    [ComVisible(true)]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    [Guid("EF9B11F3-AAF7-47B3-95BA-5D842F376E8E")]
    public interface IDevice
    {
        UInt32[] GetFoo();
    }

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [Guid("E0F14B0E-7749-4BA5-8304-FA61F8056D24")]
    public class CDevice : IDevice
    {
        public UInt32[] GetFoo()
        {
            UInt32[] retArray = new UInt32[] { 0, 1, 2, 3, 4 };
            return retArray;
        }
    }
}

"Register for COM interop" 옵션을 통해 COM 개체로 노출시킨 상태입니다. 그리고 이것을 Visual C++에서 다음과 같이 사용하는데,

#include <atlstr.h>
#include <atlconv.h>
#include <atlbase.h>
#include <atlsafe.h>

#import "..\\CsClassLibrary\\bin\\Debug\\CsClassLibrary.tlb" no_namespace named_guids

int main()
{
    CoInitialize(NULL);

    IDevice* mp_device;
    HRESULT hr = CoCreateInstance(CLSID_CDevice, NULL, CLSCTX_INPROC_SERVER, DIID_IDevice, reinterpret_cast<void**>(&mp_device));
    if (hr == S_OK)
    {
        SAFEARRAY* presult = mp_device->GetFoo();
        mp_device->Release();
    }

    CoUninitialize();
}

GetFoo 함수 호출에서 런타임 오류가 발생합니다.

Unhandled exception at 0x00007FFE9AB6474C in CppClient.exe: Microsoft C++ exception: _com_error at memory location 0x000000CABF78F360.

>    vcruntime140d.dll!_CxxThrowException(void * pExceptionObject, const _s__ThrowInfo * pThrowInfo) Line 75 C++ Non-user code. Symbols loaded.
    CppClient.exe!_com_raise_error(HRESULT hr, IErrorInfo * perrinfo) Line 19   C++ Symbols loaded.
    CppClient.exe!_com_dispatch_method(IDispatch * pDispatch, long dwDispID, unsigned short wFlags, unsigned short vtRet, void * pvRet, const wchar_t * pwParamInfo, ...) Line 96   C++ Symbols loaded.
    CppClient.exe!IDevice::GetFoo() Line 17 C++ Symbols loaded.
    CppClient.exe!main() Line 18    C++ Symbols loaded.
    CppClient.exe!invoke_main() Line 79 C++ Non-user code. Symbols loaded.
    CppClient.exe!__scrt_common_main_seh() Line 288 C++ Non-user code. Symbols loaded.
    CppClient.exe!__scrt_common_main() Line 331 C++ Non-user code. Symbols loaded.
    CppClient.exe!mainCRTStartup(void * __formal) Line 17   C++ Non-user code. Symbols loaded.
    kernel32.dll!BaseThreadInitThunk() Unknown Non-user code. Symbols loaded.
    ntdll.dll!RtlUserThreadStart() Unknown Non-user code. Symbols loaded.

도대체 무슨 오류가 있는 것일까요? ^^




Visual C++가 "#import ..." 구문을 이용해 tlb 파일로부터 생성한 GetFoo 함수의 호출 코드를 보면 이렇습니다.

inline SAFEARRAY * IDevice::GetFoo ( ) {
    SAFEARRAY * _result = 0;
    _com_dispatch_method(this, 0x60020000, DISPATCH_METHOD, VT_ARRAY|VT_I4, (void*)&_result, NULL);
    return _result;
}

뭔가.. 좀 이상하지 않나요? ^^ 우리가 만든 C# 메서드는 uint[]를 반환하는데 위에서 명시한 "VT_ARRAY|VT_I4" 플래그는 int[]에 해당합니다. 이 가정이 맞는지, 테스트를 위해 위의 함수를 우리가 직접 다음과 같이 만들어,

inline SAFEARRAY* GetFoo(IDevice *pDevice) {
    SAFEARRAY* _result = 0;
    _com_dispatch_method(pDevice, 0x60020000, DISPATCH_METHOD, VT_ARRAY | VT_UI4, (void*)&_result, NULL);
    return _result;
}

이렇게 호출할 수 있습니다.

SAFEARRAY* presult = GetFoo(mp_device);

호출 결과,

cs_cpp_interop_ui4_1.png

presult에도 값이 정상적으로 들어 있으니 이것으로 문제가 파악되었습니다.




그렇다면, 혹시 어느 쪽에서 잘못하고 있는 것인지 알 수 있을까요?

우선, C# 프로젝트의 "Register for COM interop" 옵션으로 생성된 TLB 파일을 oleview를 통해 확인해 보면,

[
  uuid(63722C41-D463-41DB-BCE4-8A497077448B),
  version(1.0),
  custom(90883F05-3D28-11D2-8F17-00A0C9A6186D, "CsClassLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")

]
library CsClassLibrary
{
    // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
    importlib("mscorlib.tlb");
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    dispinterface IDevice;

    [
      uuid(EF9B11F3-AAF7-47B3-95BA-5D842F376E8E),
      version(1.0),
      custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "CsClassLibrary.IDevice")    

    ]
    dispinterface IDevice {
        properties:
        methods:
            [id(0x60020000)]
            SAFEARRAY(unsigned long) GetFoo();
    };

    [
      uuid(E0F14B0E-7749-4BA5-8304-FA61F8056D24),
      version(1.0),
      custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "CsClassLibrary.CDevice")
    ]
    coclass CDevice {
        interface _Object;
        [default] dispinterface IDevice;
    };
};

정상적으로 "SAFEARRAY(unsigned long)"라고 출력을 하고 있습니다. 이것으로 C# 측에서는 정상적으로 COM 규약에 따라 메타데이터를 생성하고 있는 것입니다. 그럼, 남은 범인은 Visual C++ 측이 되는군요. ^^

그러니까, #import 구문(혹은 아마도 이 처리를 수행하는 하위 프로세스)은 IDL의 SAFEARRAY(unsigned long) 규약을 "VT_ARRAY|VT_ UI4"가 아닌 "VT_ARRAY|VT_I4"로 출력하는 버그(?)가 있는 것입니다.

일단, 어쩔 수 없습니다. 현상이 저러하니, 마이크로소프트 측에 이슈 제기를 하거나, 이 글에서처럼 별도의 함수를 만들어 직접 마샬링 타입을 지정하는 수밖에는 없습니다. (물론, 이렇게 되면 C#에서 메서드 시그니처를 변경했을 때 주의 깊게 해당 메서드의 마샬링 플래그도 직접 수정해야 하는 단점이 있습니다.)

이렇게 해결 방법은 알게 되었지만, 그나저나 런타임 오류가 발생하는 것이 좀 신기하긴 합니다. UI4나 I4나 어차피 4바이트인데... 너무 빡빡하게 ^^; 타입 체크를 해서 문제가 되는 듯합니다.




자, 그럼 이번에는 반환값이 아닌 out 인자로 들어온 경우를 테스트해볼까요?

// C# 코드

public void GetFoo2(out uint[] result)
{
    result = new UInt32[] { 0, 1, 2, 3, 4 };
}

위의 메서드를 C++에서 #import하면 이런 함수가 자동생성됩니다.

// .tli 파일에 자동생성된 코드
inline HRESULT IDevice::GetFoo2 ( SAFEARRAY * * result ) {
    return _com_dispatch_method(this, 0x60020001, DISPATCH_METHOD, VT_EMPTY, NULL, 
        L"\x6003", result);
}

그리고 이번엔 해당 메서드를 호출하면 아예 Access Violation 예외가 발생합니다.

Exception thrown at 0x00007FFE851509A2 (clr.dll) in CppClient.exe: 0xC0000005: Access violation reading location 0xFFFFFFFFFFFFFFFF.

    clr.dll!ClrSafeArrayGetVartype(struct tagSAFEARRAY *,unsigned short *)  Unknown Symbols loaded.
    clr.dll!OleVariant::CreateArrayRefForSafeArray(struct tagSAFEARRAY *,unsigned short,class MethodTable *)    Unknown Symbols loaded.
    clr.dll!DispParamArrayMarshaler::MarshalNativeToManaged(struct tagVARIANT *,class Object * *)   Unknown Symbols loaded.
    clr.dll!DispatchMemberInfo::MarshalParamNativeToManaged(int,struct tagVARIANT *,class Object * *)   Unknown Symbols loaded.
    clr.dll!DispatchInfo::InvokeMemberWorker(struct DispatchMemberInfo *,struct InvokeObjects *,int,int,int,int &,int &,long,struct tagDISPPARAMS *,struct tagVARIANT *,unsigned short,unsigned long,long *,struct tagVARIANT *,struct OBJECTHANDLE__ * *,int *,struct tagVARIANT * *)  Unknown Symbols loaded.
    clr.dll!DispatchInfo::InvokeMemberDebuggerWrapper(struct DispatchMemberInfo *,struct InvokeObjects *,int,int,int,int &,int &,long,struct tagDISPPARAMS *,struct tagVARIANT *,unsigned short,unsigned long,long *,struct tagVARIANT *,struct OBJECTHANDLE__ * *,int *,struct tagVARIANT * *,class Frame *)   Unknown Symbols loaded.
    clr.dll!DispatchInfo::InvokeMember(struct SimpleComCallWrapper *,long,unsigned long,unsigned short,struct tagDISPPARAMS *,struct tagVARIANT *,struct tagEXCEPINFO *,struct IServiceProvider *,unsigned int *)   Unknown Symbols loaded.
    clr.dll!InternalDispatchImpl_Invoke(struct IDispatch *,long,struct _GUID const &,unsigned long,unsigned short,struct tagDISPPARAMS *,struct tagVARIANT *,struct tagEXCEPINFO *,unsigned int *)  Unknown Symbols loaded.
    clr.dll!InternalDispatchImpl_Invoke_CallBack(void *)    Unknown Symbols loaded.
    clr.dll!InternalDispatchImpl_Invoke_Wrapper(struct IDispatch *,long,struct _GUID const &,unsigned long,unsigned short,struct tagDISPPARAMS *,struct tagVARIANT *,struct tagEXCEPINFO *,unsigned int *)  Unknown Symbols loaded.
    CppClient.exe!_com_invoke_helper(IDispatch * pDispatch, long dwDispID, unsigned short wFlags, unsigned short vtRet, void * pvRet, const wchar_t * pwParamInfo, char * argList, IErrorInfo * * pperrinfo) Line 324   C++ Symbols loaded.
    CppClient.exe!_com_dispatch_method(IDispatch * pDispatch, long dwDispID, unsigned short wFlags, unsigned short vtRet, void * pvRet, const wchar_t * pwParamInfo, ...) Line 84   C++ Symbols loaded.
>    CppClient.exe!GetFoo2(IDevice * pDevice, tagSAFEARRAY * * result) Line 18   C++ Symbols loaded.
    CppClient.exe!main() Line 35    C++ Symbols loaded.
    CppClient.exe!invoke_main() Line 79 C++ Non-user code. Symbols loaded.
    CppClient.exe!__scrt_common_main_seh() Line 288 C++ Non-user code. Symbols loaded.
    CppClient.exe!__scrt_common_main() Line 331 C++ Non-user code. Symbols loaded.
    CppClient.exe!mainCRTStartup(void * __formal) Line 17   C++ Non-user code. Symbols loaded.
    kernel32.dll!BaseThreadInitThunk() Unknown Non-user code. Symbols loaded.
    ntdll.dll!RtlUserThreadStart() Unknown Non-user code. Symbols loaded.

사실, 이번에도 원인은 같을 것입니다. 반환 값에 대해서 VT_ARRAY|VT_I4 조합을 했던 것처럼 인자에 대해서도 역시 VT_I4를 했을 테니까요.

실제로 인자 형식을 지정한 L"\x6003" 값은 VARENUM의 정의 중,

// wtypes.h

enum VARENUM
    {
        //...[생략]...
        VT_I4   = 3,
        //...[생략]...
        VT_UI4  = 19,
        //...[생략]...
        VT_ARRAY    = 0x2000,
        VT_BYREF    = 0x4000,
        //...[생략]...
    } ;

아래의 3개 값이 조합한 것임을 유추할 수 있습니다.

VT_ARRAY    = 0x2000,
VT_BYREF    = 0x4000,
VT_I4   = 3,

따라서, VT_I4만 VT_UI4로 바꿔서 다음과 같이 맞춰주면,

inline HRESULT GetFoo2(IDevice* pDevice, SAFEARRAY** result) {
    return _com_dispatch_method(pDevice, 0x60020001, DISPATCH_METHOD, VT_EMPTY, NULL, 
        L"\x6013", result);
}

이제 정상적으로 호출이 됩니다. ^^

SAFEARRAY *pArrayResult = nullptr;
GetFoo2(mp_device, &pArrayResult);

cs_cpp_interop_ui4_2.png




참고로, 이런 식의 문제가 발생했을 때 IDispatch를 이용한 원시적인 호출을 하면서 접근하는 것도 원인 파악에 도움이 됩니다. 가령, 위에서 구현한 GetFoo, GetFoo2에 대해서는 다음과 같이 GetIDsOfNames, Invoke를 직접 호출하는 식으로 접근할 수 있습니다.

// ...[생략]...

{
    DISPID dispId;
    LPOLESTR lpDesc = (LPOLESTR)L"GetFoo";
    HRESULT hr = mp_device->GetIDsOfNames(IID_NULL, &lpDesc, 1, 0, &dispId);
            
    CComVariant varResult;
    DISPPARAMS params = { nullptr, nullptr, 0, 0 };
    hr = mp_device->Invoke(dispId, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &params, &varResult, nullptr, nullptr);
    if (hr != S_OK)
    {
        throw 1;
    }
}


{
    DISPID dispId;
    LPOLESTR lpDesc = (LPOLESTR)L"GetFoo2";
    HRESULT hr = mp_device->GetIDsOfNames(IID_NULL, &lpDesc, 1, 0, &dispId);

    CComVariant* varParams = new CComVariant[1];

    SAFEARRAY* pArray = nullptr;
    varParams[0].pparray = &pArray;
    varParams[0].vt = VT_ARRAY | VT_BYREF | VT_UI4;
    DISPPARAMS params = { varParams, nullptr, 1, 0 };

    hr = mp_device->Invoke(dispId, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &params, nullptr, nullptr, nullptr);
    if (hr != S_OK)
    {
        throw 1;
    }
}

이 정도면.. 대충 모두 설명한 것 같군요. ^^

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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 11/13/2023]

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

비밀번호

댓글 작성자
 



2024-08-30 08시37분
On the strange status of wchar_t in classic MIDL
; https://devblogs.microsoft.com/oldnewthing/20240828-00/?p=110188
정성태

... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1780정성태10/15/201424159오류 유형: 249. The application-specific permission settings do not grant Local Activation permission for the COM Server application with CLSID
1779정성태10/15/201419666오류 유형: 248. Active Directory에서 OU가 지워지지 않는 경우
1778정성태10/10/201418110오류 유형: 247. The Netlogon service could not create server share C:\Windows\SYSVOL\sysvol\[도메인명]\SCRIPTS.
1777정성태10/10/201421211오류 유형: 246. The processing of Group Policy failed. Windows attempted to read the file \\[도메인]\sysvol\[도메인]\Policies\{...GUID...}\gpt.ini
1776정성태10/10/201418240오류 유형: 245. 이벤트 로그 - Name resolution for the name _ldap._tcp.dc._msdcs.[도메인명]. timed out after none of the configured DNS servers responded.
1775정성태10/9/201419377오류 유형: 244. Visual Studio 디버깅 (2) - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
1774정성태10/9/201426577개발 환경 구성: 246. IIS 작업자 프로세스의 20분 자동 재생(Recycle)을 끄는 방법
1773정성태10/8/201429755.NET Framework: 471. 웹 브라우저로 다운로드가 되는 파일을 왜 C# 코드로 하면 안되는 걸까요? [1]
1772정성태10/3/201418526.NET Framework: 470. C# 3.0의 기본 인자(default parameter)가 .NET 1.1/2.0에서도 실행될까? [3]
1771정성태10/2/201428045개발 환경 구성: 245. 실행된 프로세스(EXE)의 명령행 인자를 확인하고 싶다면 - Sysmon [4]
1770정성태10/2/201421672개발 환경 구성: 244. 매크로 정의를 이용해 파일 하나로 C++과 C#에서 공유하는 방법 [1]파일 다운로드1
1769정성태10/1/201424086개발 환경 구성: 243. Scala 개발 환경 구성(JVM, 닷넷) [1]
1768정성태10/1/201419509개발 환경 구성: 242. 배치 파일에서 Thread.Sleep 효과를 주는 방법 [5]
1767정성태10/1/201424612VS.NET IDE: 94. Visual Studio 2012/2013에서의 매크로 구현 - Visual Commander [2]
1766정성태10/1/201422428개발 환경 구성: 241. 책 "프로그래밍 클로저: Lisp"을 읽고 나서. [1]
1765정성태9/30/201426012.NET Framework: 469. Unity3d에서 transform을 변수에 할당해 사용하는 특별한 이유가 있을까요?
1764정성태9/30/201422259오류 유형: 243. 파일 삭제가 안 되는 경우 - The action can't be comleted because the file is open in System
1763정성태9/30/201423844.NET Framework: 468. PDB 파일을 연동해 소스 코드 라인 정보를 알아내는 방법파일 다운로드1
1762정성태9/30/201424541.NET Framework: 467. 닷넷에서 EIP/RIP 레지스터 값을 구하는 방법 [1]파일 다운로드1
1761정성태9/29/201421544.NET Framework: 466. 윈도우 운영체제의 보안 그룹 이름 및 설명 문자열을 바꾸는 방법파일 다운로드1
1760정성태9/28/201419810.NET Framework: 465. ICorProfilerInfo::GetILToNativeMapping 메서드가 0x80131358을 반환하는 경우
1759정성태9/27/201430965개발 환경 구성: 240. Visual C++ / x64 환경에서 inline-assembly를 매크로 어셈블리로 대체하는 방법파일 다운로드1
1758정성태9/23/201437828개발 환경 구성: 239. 원격 데스크톱 접속(RDP)을 기존의 콘솔 모드처럼 사용하는 방법 [1]
1757정성태9/23/201418383오류 유형: 242. Lync로 모임 참여 시 소리만 들리지 않는 경우 - 두 번째 이야기
1756정성태9/23/201427380기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434180오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...