성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Roll A Lisp In C - Reading ; https...
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>PInvoke 호출 시 참조 타입(class)을 마샬링하는 [IN], [OUT] 특성</h1> <p> 예를 하나 들어볼까요? ^^<br /> <br /> 우선, 비교를 위해 값 형식인 struct 타입을 P/Invoke로 호출해 보겠습니다. 이를 위해 다음과 같이 API 하나를 노출하는 DLL을 만들고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // C++ 헤더 struct Store { public: __int64 value1; }; extern "C" { __declspec(dllexport) void GetStore(<span style='color: blue; font-weight: bold'>Store* pValue</span>); }; </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // C++ cpp __declspec(dllexport) void GetStore(Store* pValue) { printf("Called: %I64d\n", pValue->value1); pValue->value1 = 50; } </pre> <br /> C# 측에서 다음과 같이 호출하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Runtime.InteropServices; [StructLayout(<a target='tab' href='https://www.sysnet.pe.kr/2/0/1558'>LayoutKind</a>.Sequential)] public struct StoreStruct { public long v1; public StoreStruct(long value) => v1 = value; } internal class Program { [DllImport("Dll1.dll")] static extern void GetStore(<span style='color: blue; font-weight: bold'>out StoreStruct</span> value); static void Main(string[] args) { StoreStruct value = new StoreStruct(10); <span style='color: blue; font-weight: bold'>GetStore(out value);</span> Debug.Assert(value.v1 == 50); } } /* 실행 시 출력 결과: Called: 10 */ </pre> <br /> C/C++ 측의 GetStoreStruct 함수도 잘 호출이 되었고, 거기서 반환한 v1의 값도 C# 측에서 50으로 잘 받았습니다. 여기서 재미있는 것은, C#의 out 예약어를 사용했지만 그와 무관하게 C/C++ 측의 API에서는 마치 ref와 동일하게 작용했다는 점입니다. 사실 ref와 out은 C# 언어의 구분일 뿐 내부적으로는 2개 모두 ref와 동일하게 처리되기 때문입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 저 예제를 class로 바꿔서, 즉 struct StoreStruct를 class StoreClass로 테스트하면 어떻게 될까요? 이를 위해 C# 측에서는 다음과 같은 코드를 추가한 다음,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [StructLayout(LayoutKind.Sequential)] public <span style='color: blue; font-weight: bold'>class StoreClass</span> { public long v1; public StoreClass(long value) => v1 = value; } internal class Program { [DllImport("Dll1.dll", EntryPoint = "GetStore")] static extern void GetStoreClass(<span style='color: blue; font-weight: bold'>ref StoreClass</span> value); static void Main(string[] args) { StoreClass value = new StoreClass(20); GetStoreClass(ref value); // 예외 발생 } } /* 실행 결과: C/C++ 측의 printf 출력과 함께, Called: 1809131705472 C# 측으로 넘어오는 시점에 예외 발생 System.NullReferenceException HResult=0x80004003 Message=Object reference not set to an instance of an object. Source=<Cannot evaluate the exception source> StackTrace: <Cannot evaluate the exception stack trace> */ </pre> <br /> 실행해 보면, 일단 C/C++ 함수까지는 코드가 실행된 것임을 printf 출력으로 알 수 있지만, 예외는 C# 측으로 제어를 반환하면서 발생합니다. 추적을 해보면, C/C++ 측에서 "Store *pValue"로 받은 값의 첫 번째 필드인 1809131705472 값을 포인터라고 가정하고 hex 주소로 따지만 0x1a538a71480가 됩니다. 위의 경우 디버거에서 메모리 창을 이용해 그 주소의 값을 보면 20이 들어있는 것을 확인할 수 있습니다.<br /> <br /> 따라서, 오히려 C/C++ 측에서 다음과 같이 값을 받은 거나 마찬가지입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > __declspec(dllexport) void GetStore(<span style='color: blue; font-weight: bold'>Store value</span>) { printf("Called: %I64d(%I64x)\n", value.value1); } </pre> <br /> 이유가 뭘까요? ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 사실, C#의 out/ref 인자는 마샬링에 어떠한 영향도 주지 않습니다. 단지 그것은 C#의 문법대로 변수의 값, 또는 참조 주소를 넘기는 역할만 하며 거기에 struct나 class의 차별은 없습니다.<br /> <br /> 하지만, P/Invoke에 의한 Managed <- -> Native 간의 데이터 이동은 struct 값 형식은 상관없지만 class 참조 형식의 경우에는 반드시 닷넷 런타임이 관여를 하게 됩니다. 따라서, 애당초 참조 형식이라면 C# 측에서 out이나 ref가 아닌, 대상 API에 맞춰 마샬링 방법을 지정해야 합니다.<br /> <br /> 아래의 문서가 바로 그런 내용을 설명합니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Marshalling Classes, Structures, and Unions ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/framework/interop/marshalling-classes-structures-and-unions#systime-sample'>https://learn.microsoft.com/en-us/dotnet/framework/interop/marshalling-classes-structures-and-unions#systime-sample</a> </pre> <br /> <div style='BACKGROUND-COLOR: #ccffcc; padding: 10px 10px 5px 10px; MARGIN: 0px 10px 10px 10px; FONT-FAMILY: Malgun Gothic, Consolas, Verdana; COLOR: #005555'> The parameter must be declared with the InAttribute and OutAttribute attributes because classes, which are reference types, <span style='color: blue; font-weight: bold'>are passed as In parameters by default.</span> </div><br /> <br /> 기본 동작이 class 참조 형식에 대해 "InAttribute" 역할로 전달된다고 하는데요, 이전 예제 코드에서 왜 "__declspec(dllexport) void GetStore(Store value)"처럼 값이 전달된 것인지 설명이 됩니다.<br /> <br /> 그러니까, class 참조 형식은 InAttribute, OutAttribute 특성을 지정해 닷넷 런타임 측에 마샬링을 제어할 수 있는 힌트를 줘야 하는 것입니다. 따라서, 위에서 오류가 발생했던 코드를 올바르게 동작하게 하려면 다음과 같이 호출해 주면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [DllImport("Dll1.dll", EntryPoint = "GetStore")] static extern void GetStoreClass(<span style='color: blue; font-weight: bold'>[In, Out]</span> StoreClass value); StoreClass value = new StoreClass(20); <span style='color: blue; font-weight: bold'>GetStoreClass(value);</span> Debug.Assert(value.v1 == 50); </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 사실 위의 구조체 같은 경우에는 굳이 [In, Out]을 지정하지 않아도 잘 동작합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [DllImport("Dll1.dll", EntryPoint = "GetStore")] static extern void GetStoreClass(<span style='color: blue; font-weight: bold'>StoreClass value</span>); StoreClass value = new StoreClass(20); <span style='color: blue; font-weight: bold'>GetStoreClass(value);</span> Debug.Assert(value.v1 == 50); </pre> <br /> 왜 그런 걸까요? ^^ 이유는 간단합니다. 닷넷 런타임이 해당 타입은 별도의 전/후처리를 할 필요가 없다고 판단하므로 그냥 value 인스턴스 자체의 주소를 C/C++ 측에 넘기기 때문입니다. 실제로 포인터 값을 출력해 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // C# 측 코드 { StoreClass value = new StoreClass(20); fixed (long* ptr = &value.v1) { Console.WriteLine($"<span style='color: blue; font-weight: bold'>ptr: 0x{new IntPtr(ptr):X}</span>"); GetStoreClass(value); Debug.Assert(value.v1 == 50); } } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // C/C++ 측 코드 __declspec(dllexport) void GetStore(Store* pValue) { printf("Called: %I64d - <span style='color: blue; font-weight: bold'>ptr == 0x%p</span>\n", pValue->value1, pValue); pValue->value1 = 50; } </pre> <br /> 실행해 보면 이런 출력이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ptr: 0x<span style='color: blue; font-weight: bold'>1E402415640</span> Called: 20(14) - ptr == 0x00000<span style='color: blue; font-weight: bold'>1E402415640</span> </pre> <br /> 동일하죠? ^^ 위와 같은 이유 덕분에 [In, Out]을 지정하지 않아도 상관없지만, 언제나 그런 것은 아닙니다. 즉, 닷넷 런타임이 마샬링에 관여하게 되는 경우에 문제가 발생합니다. 이를 재현하기 위해선, 마샬링이 관여하도록 일부러 MarshalAs를 사용하는 타입을 하나 넣으면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [StructLayout(LayoutKind.Sequential)] public class OrderTestClass { public int i; <span style='color: blue; font-weight: bold'>[MarshalAs(UnmanagedType.LPWStr, SizeConst = 10)]</span> public string s; } </pre> <br /> 위의 타입을 C/C++에서는 이렇게 사용하도록 코드를 추가하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // C/C++ 헤더 struct OrderTest { public: int i; <span style='color: blue; font-weight: bold'>wchar_t* string;</span> }; extern "C" { __declspec(dllexport) void GetOrderTest(OrderTest* pValue); }; </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // C/C++ 측 코드 __declspec(dllexport) void GetOrderTest(OrderTest* pValue) { printf("Called: %d, %ls - <span style='color: blue; font-weight: bold'>ptr == 0x%p</span>\n", pValue->i, pValue->string, pValue); wcscpy_s(pValue->string, 10, L"GOOD"); pValue->i = 70; } </pre> <br /> C#에서 호출하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [DllImport("Dll1.dll")] static extern void GetOrderTest(<span style='color: blue; font-weight: bold'>[In, Out]</span> OrderTestClass value); OrderTestClass value = new OrderTestClass(); value.i = 5; value.s = "TEST"; fixed (int* ptr = &value.i) { Console.WriteLine($"[GetOrderTest] <span style='color: blue; font-weight: bold'>ptr: 0x{new IntPtr(ptr):X}</span>"); GetOrderTest(value); Debug.Assert(value.i == 70); Debug.Assert(value.s == "GOOD"); } </pre> <br /> 이번에는 출력 결과에서 포인터 값이 다르게 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ptr: 0x<span style='color: blue; font-weight: bold'>15193015700</span> Called: 5, TEST - ptr == 0x000000<span style='color: blue; font-weight: bold'>3FF837E150</span> </pre> <br /> 이유는 간단합니다. C#은 value 인스턴스를 넘겼지만, 닷넷 런타임은 int 4바이트 + sizeof(wchar_t) * 10개의 연속된 메모리 공간을 별도로 할당하고 거기에 ([In] 특성을 지정했으므로) 값을 채운 다음에 그 포인터를 C/C++ 측에 넘깁니다.<br /> <br /> 그리고 C/C++ 측에서는 해당 포인터에 값을 채운 다음 제어를 닷넷 런타임으로 반환하면, ([Out] 특성을 지정했으므로) 별도로 할당된 공간에 채워졌던 그 값을 다시 C#의 value 인스턴스에 설정해 주면서 P/Invoke API 호출을 마칩니다.<br /> <br /> 따라서, 위와 같은 경우에는 [In], [Out] 특성을 모두 지정해야만 C/C++ 측의 GetOrderTest 함수가 정상적으로 동작합니다. 여기서 만약,<br /> <br /> <ol> <li>[In]을 뺀다면, 닷넷 런타임은 새로 할당한 공간에 C# 측의 value 인스턴스 값을 채우지 않고, 즉 0으로 초기화된 공간에 대한 포인터를 C/C++에 전달하므로 "OrderTest *pValue"의 i, string 값은 모두 0, null로 채워집니다.</li> <li>[Out]을 뺀다면, 닷넷 런타임은 [In]에 따라 값은 채워서 보내지만, C/C++ API 호출이 완료된 후 C# 측으로 값 복사를 해주지 않아 value 인스턴스의 값은 변하지 않게 됩니다.</li> </ol> <br /> 이 정도면... 대충 이해가 되셨죠? ^^<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=2169&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1659
(왼쪽의 숫자를 입력해야 합니다.)