성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
[정성태] Modules 창(Ctrl+Shift+U)을 띄워서, 해당 Op...
[정성태] 만드실 수 있습니다. 단지, Unity 엔진 내의 스크립트와 W...
글쓰기
제목
이름
암호
전자우편
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'>C# - (LSA_UNICODE_STRING 예제로) CustomMarshaler 사용법</h1> <p> 지난 글을 통해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# Interop 예제 - (LSA_UNICODE_STRING 예제로) 구조체를 C++에 전달하는 방법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13203'>https://www.sysnet.pe.kr/2/0/13203</a> </pre> <br /> C/C++ 측의 구조체를 Interop하는 방법을 살펴봤는데요, 그런데 해당 예제를 다르게 접근하는 것도 가능합니다. 사실, LSA_UNICODE_STRING 구조체는 엄밀히 구분하면 string의 표현 방식에 불과합니다. 따라서, 보다 더 C# 언어의 방식처럼 다룬다면 더 좋지 않을까요?<br /> <br /> 예를 들어, C#에서 LSA_UNICODE_STRING 구조체를 직접 다루기보다는 그냥 DllImprt 함수를 다음과 같은 식으로 쓰고 싶은 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > internal class Program { // <a target='tab' href='https://www.sysnet.pe.kr/2/0/13203#cpp_func'>지난 글에 작성한 C/C++ Struct_TestFunc 예제 함수</a> [DllImport("Dll1.dll")] static extern int Struct_TestFunc(<span style='color: blue; font-weight: bold'>string value</span>); static void Main(string[] args) { <span style='color: blue; font-weight: bold'>Struct_TestFunc("test is"); } } </pre> <br /> 저런 식의 표현이 가능하려면 뭐가 필요할까요? 당연히, string 타입을 LSA_UNICODE_STRING 구조체에 맞게 상호 변환할 수 있는 코드가 있어야 합니다. 하지만 string이 C/C++ 측에서 구현한 문자열의 다양한 표현 방식을 닷넷 런타임이 어떻게 알고 맞춰줄 수 있을까요?<br /> <br /> 물론, 기본적으로는 해당 변환을 닷넷 런타임은 하지 못합니다. 대신 닷넷 런타임은 인자를 넘기고/받을 때 사용자의 "변환 코드"를 수행할 수 있는 확장 모드를 제공합니다. 바로 그것이 이번 글의 주제인 CustomMarshaler입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ICustomMarshaler Interface ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.icustommarshaler'>https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.icustommarshaler</a> </pre> <br /> <hr style='width: 50%' /><br /> <br /> CustomMarshaler의 구현은, 왠지 어려운 듯하지만 역할 자체가 간단하므로 직관적으로 풀어나갈 수 있습니다. 즉, 닷넷에서 네이티브로 인자를 넘길 때 직렬화하는 코드와, 네이티브에서 다시 닷넷으로 넘어갈 때의 직렬화를 적절하게 처리해 주면 되는 것입니다.<br /> <br /> 가령, 이 글에서 예를 든 LSA_UNICODE_STRING과 string 타입의 변환은 다음과 같은 사용자 정의 마샬러 코드를 작성하는 것으로 해결할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // <a target='tab' href='https://www.pinvoke.net/default.aspx/advapi32/lsaopenpolicy.html'>https://www.pinvoke.net/default.aspx/advapi32/lsaopenpolicy.html</a> public class LSAStringMarshaler : <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.icustommarshaler'>ICustomMarshaler</a> { Hashtable myAllocated = new Hashtable(); int _itemSize; public static ICustomMarshaler GetInstance(string cookie) { return new LSAStringMarshaler { _itemSize = Marshal.SizeOf(typeof(LSA_UNICODE_STRING)) }; } #nullable disable // C/C++에서 C# 측으로 인자가 반환될 때 호출되는 함수 (따라서 out, ref인 경우 호출됨) public object <span style='color: blue; font-weight: bold'>MarshalNativeToManaged</span>(System.IntPtr pNativeData) { if (pNativeData != IntPtr.Zero) { LSA_UNICODE_STRING lus = (LSA_UNICODE_STRING)Marshal.PtrToStructure(pNativeData, typeof(LSA_UNICODE_STRING)); return lus.ToString(); } return null; } #nullable restore // C#에서 C/C++로 인자를 넘길 때 호출되는 함수 (따라서 out을 제외하고는 ref와 일반 인자 호출에서 사용) public System.IntPtr <span style='color: blue; font-weight: bold'>MarshalManagedToNative</span>(object ManagedObj) { LSA_UNICODE_STRING lus = new LSA_UNICODE_STRING((string)ManagedObj); IntPtr memory = Marshal.AllocHGlobal(_itemSize); myAllocated[memory] = memory; Marshal.StructureToPtr(lus, memory, true); return memory; } public void CleanUpManagedData(object ManagedObj) { } public int GetNativeDataSize() { return _itemSize; } public void CleanUpNativeData(System.IntPtr pNativeData) { if (myAllocated.ContainsKey(pNativeData)) { myAllocated.Remove(pNativeData); LSA_UNICODE_STRING? lus = (LSA_UNICODE_STRING?)Marshal.PtrToStructure(pNativeData, typeof(LSA_UNICODE_STRING)); lus?.Dispose(); Marshal.FreeHGlobal(pNativeData); } } } </pre> <br /> 이렇게 작성한 CustomMarshaler가 있으면, 이제 DllImport 메서드에 string을 LSAStringMarshaler를 이용해 직렬화하라는 힌트를 추가합니다.,<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 int Struct_TestFunc(<span style='color: blue; font-weight: bold'>[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(LSAStringMarshaler))]</span> string value); </pre> <br /> 끝입니다. 이제 앞으로는 C#의 string 타입으로 호출하는 것이 가능합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Struct_TestFunc("test is"); </pre> <br /> 오~~~ 멋지죠? ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 혹시 ref 지정도 가능할까요?<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 int PPStruct_TestFunc( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(LSAStringMarshaler))] <span style='color: blue; font-weight: bold'>ref</span> string value); </pre> <br /> 이럴 때는 원래의 C#에서 제공하는 ref처럼 동작하는 것을 가정해야 합니다. 즉, 피-호출 측에서 value에 넘겨주는 값 자체를 변경하는 것이 가능하기 때문에 위와 같은 형식을 C/C++ 언어에서 받기 위해서는 2중 포인터를 사용한 함수를 정의해야 합니다.<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) int PPStruct_TestFunc(<span style='color: blue; font-weight: bold'>PLSA_UNICODE_STRING*</span> ptr); </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // CPP __declspec(dllexport) int PPStruct_TestFunc(<span style='color: blue; font-weight: bold'>LSA_UNICODE_STRING**</span> ptr) { <span style='color: blue; font-weight: bold'>LSA_UNICODE_STRING* pItem = *ptr;</span> printf("Text: %S, Len: %d, Max: %d\n", <span style='color: blue; font-weight: bold'>pItem->Buffer, pItem->Length, pItem->MaximumLength</span>); return 0; } </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;' > string text = "good"; PPStruct_TestFunc(<span style='color: blue; font-weight: bold'>ref</span> text); </pre> <br /> "ref"의 적용으로 인해 C/C++ 측의 함수 반환 후 관리 코드로 넘어오는 단계에서 CustomMarshaler 코드의 ICustomMarshaler.MarshalNativeToManaged 메서드가 호출돼 변화된 값을 호출 측의 변수에 적용하는 것이 가능해집니다.<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) int PPStruct_TestFunc(LSA_UNICODE_STRING** ptr) { LSA_UNICODE_STRING* pItem = *ptr; printf("Text: %S, Len: %d, Max: %d\n", pItem->Buffer, pItem->Length, pItem->MaximumLength); <span style='color: blue; font-weight: bold'>pItem->Buffer[0] = 't';</span> return 0; } </pre> <br /> 호출 측의 "text" 변숫값이 바뀌게 됩니다. 완벽하게 C# 본연의 ref 역할을 하게 된 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > string text = "<span style='color: blue; font-weight: bold'>good</span>"; PPStruct_TestFunc(<span style='color: blue; font-weight: bold'>ref</span> text); Console.WriteLine(text); // 출력 결과: <span style='color: blue; font-weight: bold'>tood</span> </pre> <br /> (사실 위의 예제에서라면, <a target='tab' href='https://www.sysnet.pe.kr/2/0/13203#ptr_field'>어차피 Buffer가 포인터이기 때문에 ref를 사용하지 않아도 값은 바뀝</a>니다.)<br /> <br /> <hr style='width: 50%' /><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;' > // <a target='tab' href='https://www.sysnet.pe.kr/2/0/13203#cpp_func2'>지난 글에 작성한 C/C++ Struct_TestFunc 예제 함수</a> [DllImport("Dll1.dll")] static extern int PStructWithLen_TestFunc( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(LSAStringMarshaler))] <span style='color: blue; font-weight: bold'>string[] value</span>, int length); </pre> <br /> 실제로 해보면, ICustomMarshaler.MarshalManagedToNative에서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 인자로 전달된 ManagedObj의 타입은 string[] public System.IntPtr MarshalManagedToNative(<span style='color: blue; font-weight: bold'>object ManagedObj</span>) { LSA_UNICODE_STRING lus = new LSA_UNICODE_STRING(<span style='color: blue; font-weight: bold'>(string)</span>ManagedObj); // 이 단계에서 System.InvalidCastException 예외 발생 IntPtr memory = Marshal.AllocHGlobal(_itemSize); myAllocated[memory] = memory; Marshal.StructureToPtr(lus, memory, true); return memory; } </pre> <br /> 예외가 발생합니다. 이 오류를 해결할 수 있는 방법은 대충 2가지 정도로 나뉩니다. 인자인 ManagedObj가 배열인지 판정해서 그것에 맞게 마샬링을 하거나, 아니면 배열인 경우를 위해 아예 독립적인 CustomMarshaler를 만드는 것입니다. 아래의 코드는 아예 새롭게 만든 예이고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class LSAStringArrayMarshaler : ICustomMarshaler { Hashtable myAllocated = new Hashtable(); int _arraySize = 0; int _itemSize = 0; public static ICustomMarshaler GetInstance(string cookie) { return new LSAStringArrayMarshaler { _itemSize = Marshal.SizeOf(typeof(LSA_UNICODE_STRING)) }; } #nullable disable public object MarshalNativeToManaged(System.IntPtr pNativeData) { if (pNativeData != IntPtr.Zero) { string[] texts = new string[_arraySize]; for (int i = 0; i < _arraySize; i++) { LSA_UNICODE_STRING lus = (LSA_UNICODE_STRING)Marshal.PtrToStructure(pNativeData, typeof(LSA_UNICODE_STRING)); texts[i] = lus.ToString(); pNativeData = IntPtr.Add(pNativeData, _itemSize); } return texts; } return null; } #nullable restore public System.IntPtr MarshalManagedToNative(object ManagedObj) { string[] items = (string[])ManagedObj; this._arraySize = items.Length; IntPtr memory = Marshal.AllocHGlobal(this._itemSize * this._arraySize); myAllocated[memory] = memory; IntPtr ptr = memory; foreach (string item in items) { LSA_UNICODE_STRING lus = new LSA_UNICODE_STRING(item); Marshal.StructureToPtr(lus, ptr, false); ptr = IntPtr.Add(ptr, this._itemSize); } return memory; } public void CleanUpManagedData(object ManagedObj) { } public int GetNativeDataSize() { return this._itemSize; } public void CleanUpNativeData(System.IntPtr pNativeData) { if (myAllocated.ContainsKey(pNativeData)) { myAllocated.Remove(pNativeData); IntPtr ptr = pNativeData; for (int i = 0; i < this._arraySize; i ++) { LSA_UNICODE_STRING? lus = (LSA_UNICODE_STRING?)Marshal.PtrToStructure(ptr, typeof(LSA_UNICODE_STRING)); lus?.Dispose(); ptr = IntPtr.Add(ptr, this._itemSize); } Marshal.FreeHGlobal(pNativeData); } } } </pre> <br /> 따라서 배열에 대해 LSAStringArrayMarshaler를 사용하도록 지정하면,<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 int PStructWithLen_TestFunc( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(<span style='color: blue; font-weight: bold'>LSAStringArrayMarshaler</span>))] <span style='color: blue; font-weight: bold'>string[]</span> value, int length); </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;' > string[] texts = new string[] { "test is", "good", }; <span style='color: blue; font-weight: bold'>PStructWithLen_TestFunc(texts, 2);</span> </pre> <br /> 당연히 배열도 ref로 전달할 수 있고,<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 int PPStructWithLen_TestFunc( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(LSAStringArrayMarshaler))] <span style='color: blue; font-weight: bold'>ref</span> string[] value, int length); { string[] texts = new string[] { "test is", "good", }; PPStructWithLen_TestFunc(<span style='color: blue; font-weight: bold'>ref</span> texts, 2); Console.WriteLine(texts[0]); Console.WriteLine(texts[1]); } </pre> <br /> 이를 위해서는 C/C++ 측에서도 2중 포인터를 이용한 함수를 제공해야 합니다.<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) int PPStructWithLen_TestFunc(<span style='color: blue; font-weight: bold'>PLSA_UNICODE_STRING* value</span>, int length); </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // CPP __declspec(dllexport) int PPStructWithLen_TestFunc(<span style='color: blue; font-weight: bold'>LSA_UNICODE_STRING** value</span>, int length) { PLSA_UNICODE_STRING ptr = *value; for (int i = 0; i < length; i++) { printf("Text: %S, Len: %d, Max: %d\n", ptr->Buffer, ptr->Length, ptr->MaximumLength); ptr++; } return 0; } </pre> <br /> 그런데 2중 포인터를 사용한 C/C++ 함수에 대해 ref 호출을 한 경우, CustomMarshaler를 이용하는 상황에서 한 가지 아쉬운 점이 있습니다. 일례로, ref이기 때문에 C/C++ 측에서 아예 새로운 개수의 LSA_UNICODE_STRING 배열을 반환하는 것도 가능할 텐데요, 문제는 ICustomMarshaler.MarshalNativeToManaged에서 그 배열의 개수를 가져올 수 있는 방법이 없다는 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public object MarshalNativeToManaged(<span style='color: blue; font-weight: bold'>System.IntPtr pNativeData</span>) { if (pNativeData != IntPtr.Zero) { string[] texts = new string[<span style='color: blue; font-weight: bold'>_arraySize</span>]; for (int i = 0; i &lt; _arraySize; i++) { LSA_UNICODE_STRING lus = (LSA_UNICODE_STRING)Marshal.PtrToStructure(pNativeData, typeof(LSA_UNICODE_STRING)); texts[i] = lus.ToString(); pNativeData = IntPtr.Add(pNativeData, _itemSize); } return texts; } return null; } </pre> <br /> 위의 코드에 보면, _arraySize를 사용해 배열 크기를 판단하고 있는데 이 값은 관리 코드에서 넘겨준 원본 texts 배열의 크기에 해당합니다. 게다가 MarshalNativeToManaged 메서드 자체에는 IntPtr 포인터만 넘어올 뿐, 비관리 코드에서 넘겨준 배열의 크기에 대한 정보 자체가 없습니다.<br /> <br /> 따라서, 만약 그런 경우를 원한다면 C/C++ 측과 협의를 해야 합니다. 흔히들 이런 경우 C/C++ 시절에는 배열의 마지막 요소는 null로 채워서 반환해 주는 방식을 썼는데요, 어떤 식으로든 서로 합의만 할 수 있다면 그에 따라 코딩하시면 됩니다. ^^<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=2000&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <a name='limit'></a> <br /> CustomMarshaler는 오직 P/Invoke의 인자에만 적용할 수 있습니다. 만약 struct 등의 필드에 적용했다면 Marshal.PtrToStructure 등의 호출에서 이런 예외를 만나게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > System.TypeLoadException: 'Cannot marshal field '...' of type '...': Custom marshalers cannot be used on fields of structures.' </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
2384
(왼쪽의 숫자를 입력해야 합니다.)