성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] VT sequences to "CONOUT$" vs. STD_O...
[정성태] NetCoreDbg is a managed code debugg...
[정성태] Evaluating tail call elimination in...
[정성태] What’s new in System.Text.Json in ....
[정성태] What's new in .NET 9: Cryptography ...
[정성태] 아... 제시해 주신 "https://akrzemi1.wordp...
[정성태] 다시 질문을 정리할 필요가 있을 것 같습니다. 제가 본문에...
[이승준] 완전히 잘못 짚었습니다. 댓글 지우고 싶네요. 검색을 해보...
[정성태] 우선 답글 감사합니다. ^^ 그런데, 사실 저 예제는 (g...
[이승준] 수정이 안되어서... byteArray는 BYTE* 타입입니다...
글쓰기
제목
이름
암호
전자우편
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# - .NET 7부터 UnmanagedCallersOnly 함수 export 기능을 AOT 빌드에 통합</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;' > .NET 5 / .NET Core - UnmanagedCallersOnly 특성을 사용한 함수 내보내기 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12413'>https://www.sysnet.pe.kr/2/0/12413</a> </pre> <br /> UnmanagedCallersOnly 특성을 이용해 .NET DLL에서 Win32 API와 같은 식의 export 기능을 소개한 적이 있습니다. 그런데, 이 과정이 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13162'>.NET 7의 "PublishAot" 옵션</a>과 만나면서 더 쉽게 바뀌었습니다.<br /> <br /> 그래서 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12413'>예전 글의 예제</a>를, 단순히 다음과 같이 구현해 주고,<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; namespace ClassLibrary2; public class Class1 { <span style='color: blue; font-weight: bold'>[UnmanagedCallersOnly(EntryPoint = "mymethod")]</span> public static void MyMethod(nint ptrText) { if (ptrText == IntPtr.Zero) { return; } string? text = Marshal.PtrToStringUni(ptrText); Console.WriteLine($"{DateTime.Now} {text}"); } } </pre> <br /> csproj에 PublishAot, RuntimeIdentifier 옵션만 추가한 다음,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <span style='color: blue; font-weight: bold'><RuntimeIdentifier>win-x64</RuntimeIdentifier> <PublishAot>true</PublishAot></span> </PropertyGroup> </Project> </pre> <br /> 명령행에서 "dotnet publish" 명령만 수행하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C:\temp\ClassLibrary2> <span style='color: blue; font-weight: bold'>dotnet publish</span> MSBuild version 17.8.3+195e7f5a3 for .NET Determining projects to restore... All projects are up-to-date for restore. ClassLibrary2 -> C:\temp\ClassLibrary2\bin\Debug\net7.0\win-x64\ClassLibrary2.dll <span style='color: blue; font-weight: bold'>Generating native code</span> Creating library bin\Debug\net7.0\win-x64\native\ClassLibrary2.lib and object bin\Debug\net7.0\win-x64\native\ClassLibrary2.exp ClassLibrary2 -> C:\temp\ClassLibrary2\bin\Debug\net7.0\win-x64\publish\ </pre> <br /> 그럼, 소스 코드 내에서의 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot#build-native-libraries'>UnmanagedCallersOnly + EntryPoint 값이 부여</a>된 것에 대해 자동으로 Win32 EXPORT 함수로 등록해 줍니다.<br /> <br /> 이후 사용하는 측에서는, 저렇게 생성한 (AOT로 빌드된) ClassLibrary2.dll 파일을 EXE 측에 복사해 둔 다음 DllImport를 연결한 코드를 수행하면 됩니다.<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; namespace ConsoleApp1; internal class Program { // 받는 측에서 text 인자를 Unicode로 취급하므로 반드시 <a target='tab' href='https://www.sysnet.pe.kr/2/0/11099#tchar'>MarshalAs로 LPWStr 값을 지정 (기본값은 ANSI)</a> <span style='color: blue; font-weight: bold'>[DllImport(@"ClassLibrary2.dll")] private static extern int mymethod([MarshalAs(UnmanagedType.LPWStr)] string text);</span> static void Main(string[] args) { mymethod("TEST"); } } </pre> <br /> <hr style='width: 50%' /><br /> <a name='copy2dir'></a> <br /> 개발을 좀 편리하게 하려면, (AOT로 publish된) ClassLibrary2.dll 출력 파일을 EXE 프로젝트 측에 연결하는 것이 좋습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <span style='color: blue; font-weight: bold'><ItemGroup> <None Include="..\ClassLibrary2\bin\$(Configuration)\net7.0\win-x64\publish\*"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup></span> </Project> </pre> <br /> 비록 여전히 ClassLibrary2 프로젝트의 소스 코드가 변경되면 명령행에서 "dotnet publish"를 수행해야 하지만 그래도 일단 배포가 되면 CopyToOutputDirectory까지 연결이 되므로 그 외의 수작업은 줄어듭니다.<br /> <br /> 게다가 PDB 파일까지 함께 배포되므로, ClassLibrary2의 코드를 디버깅할 수도 있는데요, 단지 AOT로 빌드된 네이티브 코드로 다뤄지기 때문에 "Launch Profiles" 설정에서 "Enable native code debugging" 옵션은 켜야 합니다.<br /> <br /> <img onclick='toggle_img(this)' class='imgView' alt='unmanaged_export_debug_1.png' src='/SysWebRes/bbs/unmanaged_export_debug_1.png' /><br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Enable debugging for managed and native code together, also known as mixed-mode debugging. </pre> <br /> <hr style='width: 50%' /><br /> <br /> 하는 김에, 할당한 메모리를 반환값으로 사용하는 경우 호출 측에서의 메모리 해제를 다뤄볼까요? ^^ 이를 위해 다음과 같은 export 함수를 만들고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>[UnmanagedCallersOnly(EntryPoint = "Concat")]</span> public static <span style='color: blue; font-weight: bold'>char*</span> Concat(<span style='color: blue; font-weight: bold'>char* text1, char* text2</span>) { int len1 = GetTextLen(text1); int len2 = GetTextLen(text2); int dstSize = (len1 + len2 + 1) * 2; <span style='color: blue; font-weight: bold'>nint pBuffer = Marshal.AllocHGlobal(dstSize);</span> System.Buffer.MemoryCopy(text1, (void*)pBuffer, dstSize, len1 * 2); System.Buffer.MemoryCopy(text2, (void*)(pBuffer + (len1 * 2)), dstSize - (len1 * 2), len2 * 2); *((char*)(pBuffer + dstSize - 2)) = '\0'; <span style='color: blue; font-weight: bold'>return (char*)pBuffer;</span> } private static int GetTextLen(char* ptr) { int len = 0; while (*ptr != 0) { len++; ptr++; } return len; } </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;' > using System.Runtime.InteropServices; namespace ConsoleApp1; internal partial class Program { [DllImport(@"ClassLibrary2.dll")] <span style='color: blue; font-weight: bold'>[return: MarshalAs(UnmanagedType.LPWStr)]</span> public static extern string Concat([MarshalAs(UnmanagedType.LPWStr)] string text1, [MarshalAs(UnmanagedType.LPWStr)] string text2); static void Main(string[] args) { while (true) { Console.WriteLine($"Concat-Output: {<span style='color: blue; font-weight: bold'>Concat("test is ", "good")</span>}"); } } } </pre> <br /> 그렇다면, 저 코드는 Marshal.AllocHGlobal을 무한 루프로 실행하기 때문에 메모리 누수가 발생할까요? 의외로, 발생하지 않습니다. 왜냐하면, string으로 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12065'>마샬링을 처리하는 런타임 생성 코드</a>에서 자동으로 Marshal.FreeHGlobal을 호출해 주기 때문입니다.<br /> <br /> 물론, string이 아닌, 포인터로 직접 받도록 signature를 지정하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [DllImport(@"ClassLibrary2.dll")] public static extern <span style='color: blue; font-weight: bold'>nint</span> Concat([MarshalAs(UnmanagedType.LPWStr)] string text1, [MarshalAs(UnmanagedType.LPWStr)] string text2); static void Main(string[] args) { mymethod("TEST"); while (true) { <span style='color: blue; font-weight: bold'>nint result</span> = Concat("test is ", "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;' > nint result = Concat("test is ", "good"); Marshal.FreeHGlobal(result); // 호출 측에서 명시적인 메모리 해제 </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=2112&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1069
(왼쪽의 숫자를 입력해야 합니다.)