성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
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'>UnmanagedCallersOnly 특성과 DNNE 사용</h1> <p> 지난 글에 설명한 UnmanagedCallersOnly의 함수 export 기능은,<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 /> 전에도 한번 언급했지만, 위의 방법으로 실제 프로젝트를 진행하는 것은 'dotnet publish' 명령을 따로 해야 한다는 것으로 인해 비주얼 스튜디오 등에서의 개발 시에는 차라리 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12118'>UnmanagedExports</a>와 같은 개발 환경이 더 낫습니다.<br /> <br /> 그런데 nuget에서 배포 중인 DNNE(.NET Native Export Extension)는 유사한 기능을 수행하면서도 별도의 부가 과정 없이 export 기능을 프로젝트 빌드 시에 처리해 줍니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > AaronRobinsonMSFT / DNNE ; <a target='tab' href='https://github.com/AaronRobinsonMSFT/DNNE'>https://github.com/AaronRobinsonMSFT/DNNE</a> ; <a target='tab' href='https://www.nuget.org/packages/DNNE'>https://www.nuget.org/packages/DNNE</a> </pre> <br /> 방법도 매우 간단한데, 기존 Microsoft.DotNet.ILCompiler 참조 대신 (혹은 그대로 놔둬도 되니) DNNE를 추가하고 소스 코드는 동일하게 작성하면 됩니다.<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>netcoreapp3.0lt;/TargetFramework> </PropertyGroup> <ItemGroup> <span style='color: blue; font-weight: bold'><PackageReference Include="DNNE" Version="1.*" /></span> </ItemGroup> </Project> </pre> <br /> 공식적으로는 "net5.0"에 의존적이라고 하지만, 실제로 해보면 기존 .NET Core(위의 예제에서는 .NET Core 3.0) 프로젝트도 잘 빌드합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, DNNE는 Microsoft.DotNet.ILCompiler와 확연히 다른 방식으로 함수 export 기능을 수행합니다. 단적으로, Microsoft.DotNet.ILCompiler는 AOT 방식으로 빌드하지만, DNNE는 단지 실행 시에 적절한 .NET 런타임을 로드하고 닷넷 메서드로의 호출을 "대행하는 native 함수"를 export하는 식입니다.<br /> <br /> 예를 들어, 다음과 같은 코드를 담은 .NET Core 3.0 프로젝트를 빌드하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // .NET Core 3.0 프로젝트 using System; using System.Runtime.InteropServices; using DNNE; namespace ClassLibrary { public class Class1 { <span style='color: blue; font-weight: bold'>[Export(EntryPoint = "mymethod_netcore")]</span> public static void MyMethod(IntPtr ptrText) { string text = Marshal.PtrToStringUni(ptrText); Console.WriteLine($"[NETCORE] {DateTime.Now} {text}"); } // 반드시 export할 메서드의 이름에 "Delegate" 접미사를 붙인 델리게이트를 정의해야 함 <span style='color: blue; font-weight: bold'>public delegate void MyMethodDelegate(IntPtr ptrText);</span> } } namespace DNNE { internal class ExportAttribute : Attribute { public ExportAttribute() { } public string EntryPoint { get; set; } } } </pre> <a name='dnne_output'></a> <br /> 출력 파일은 다음과 같은 것들이 생성됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > D:\temp\ClassLibrary2\bin\Debug\netcoreapp3.0> <span style='color: blue; font-weight: bold'>tree /F</span> Folder PATH listing for volume New Volume Volume serial number is 8609-EAAD D:. ClassLibrary2.deps.json ClassLibrary2.dll ClassLibrary2.pdb <span style='color: blue; font-weight: bold'>ClassLibrary2NE.dll ClassLibrary2NE.h ClassLibrary2NE.lib dnne.h</span> </pre> <br /> 이 중에서 ClassLibrary2NE.dll은 다음의 4개 함수를 export합니다.<br /> <br /> <ul> <li>get_hostfxr_path</li> <li><span style='color: blue; font-weight: bold'>mymethod_netcore</span></li> <li>preload_runtime</li> <li>set_failure_callback</li> </ul> <br /> 이때 mymethod_netcore가 하는 일은 런타임이 프로세스 내에 올라오지 않은 상태라면 로드를 한 후, 이어서 "ClassLibrary2.dll"을 로드해 그 안의 MyMethod를 호출하는 코드를 담고 있습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> DNNE로 출력한 export 함수를 실제로 Visual C++에서 사용해 볼까요? 코드는 뭐 대충 다음과 같은 식으로 호출할 텐데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #include <iostream> #include <Windows.h> <span style='color: blue; font-weight: bold'>extern "C" void __stdcall mymethod_netcore(intptr_t text);</span> <span style='color: blue; font-weight: bold'>#pragma comment(lib, "..\\ClassLibrary2\\bin\\Debug\\netcoreapp3.0\\ClassLibrary2NE.lib") </span> int main() { const wchar_t* pText = L"Hello World"; <span style='color: blue; font-weight: bold'>mymethod_netcore((intptr_t)pText);</span> } </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;' > [디버그 실행] Unhandled exception at 0x00007FF82AC02069 (ClassLibrary2NE.dll) in ConsoleApplication1.exe: Fatal program exit requested. [실행] The specified runtimeconfig.json [D:\temp\x64\Debug\ClassLibrary2.runtimeconfig.json] does not exist </pre> <br /> 그러니까, 어떤 런타임을 로드해야 하는지 명시하는 <a target='tab' href='https://docs.microsoft.com/en-us/dotnet/core/run-time-config/#runtimeconfigjson'>runtimeconfig.json</a> 파일이 필요한 것입니다. 따라서 정상적인 실행을 하려면 ClassLibrary2.runtimeconfig.json 파일을 만들어 다음의 내용을 채워야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > { "runtimeOptions": { "tfm": "net3.0", "framework": { "name": "Microsoft.NETCore.App", "version": "3.0.0" } } } </pre> <br /> 정리하면, DNNE로 만든 라이브러리를 C/C++에서 사용하려고 하면 다음의 3개 파일이 필요합니다.<br /> <br /> <ul> <li>ClassLibrary2.dll - .NET managed 메서드를 담은 어셈블리</li> <li>ClassLibrary2.runtimeconfig.json - 닷넷 메서드를 실행하기 위한 런타임 선택</li> <li>ClassLibrary2NE.dll - managed 메서드를 호출하는 unmanaged export 함수를 담은 DLL</li> </ul> <br /> <hr style='width: 50%' /><br /> <br /> 그렇다면 DNNE 빌드 시에 함께 생성하는 헤더 파일은 무슨 용도일까요? 굳이 사용하려고 들자면, 위의 예제 코드에서 export 함수의 extern 선언을 ClassLibrary2NE.h로 바꿔줄 수도 있는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #include <iostream> #include <Windows.h> <span style='color: blue; font-weight: bold'>// extern "C" void __stdcall mymethod_netcore(intptr_t text); #include "..\\ClassLibrary2\\bin\\Debug\\netcoreapp3.0\\ClassLibrary2NE.h"</span> </pre> <a name='dnne_header'></a><br /> 그러면 (윈도우 환경의 Visual Studio에서) 2가지 에러에 시달리게 됩니다. 우선, 자동 생성된 "ClassLibrary2NE.h"에서 include 하고 있는 "<dnne.h>"를 찾을 수 없어 C1083 오류가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Error C1083 Cannot open include file: 'dnne.h': No such file or directory </pre> <br /> 이것을 피하려면 프로젝트의 Include 경로에 dnne.h가 있는 경로를 추가하거나, 아니면 귀찮음을 감수하고 자동 생성되는 ClassLibrary2NE.h의 include 문을 다음과 같이 수정해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // #include <dnne.h> #include "dnne.h" </pre> <br /> 위와 같이 하면 ClassLibrary2NE.h와 동일한 폴더에 있는 dnne.h를 포함시키기 때문에 오류가 발생하지 않습니다. 하지만, 더 귀찮은 문제가 두 번째 에러에 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 1>ConsoleApplication1.obj : error LNK2019: unresolved external symbol "void __cdecl mymethod_netcore(__int64)" (?mymethod_netcore@@YAX_J@Z) referenced in function main 1>D:\temp\x64\Debug\ConsoleApplication1.exe : fatal error LNK1120: 1 unresolved externals </pre> <br /> 자동 생성해 준 코드에는 mymethod_netcore에 대한 함수 signature를 다음과 같이 선언하고 있는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DNNE_API void DNNE_CALLTYPE mymethod_netcore(intptr_t ptrText); ==> __declspec(dllexport) void __stdcall mymethod_netcore(intptr_t ptrText); </pre> <br /> <a target='tab' href='https://www.sysnet.pe.kr/2/0/12414'>extern "C"로 되어 있지 않아 x64로 빌드시 __cdecl로 호출 규약이 바뀌면서</a> Name mangling이 발생합니다. 반면, ClassLibrary2NE.dll이 export하고 있는 mymethod_netcore는 __stdcall로 export하고 있기 때문에 컴파일 시에 이를 찾지 못해 링크 에러가 발생하는 것입니다.<br /> <br /> 결국, ClassLibrary2NE.h 파일을 사용하기보다는 차라리 여러분들의 C++ 소스 코드에서 직접 선언을 담고 있는 것이 편합니다.<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'>extern "C"</span> void __stdcall mymethod_netcore(intptr_t text); </pre> <br /> (다른 글에서 더 자세하게 설명하겠지만, 자동 생성된 ClassLibrary2NE.h, dnne.h 헤더 파일과 "#define DNNE_COMPILE_AS_SOURCE" 상수를 연결해 libnethost.lib를 적절하게 링크하면 ClassLibrary2NE.dll을 직접 생성하는 것도 가능합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> DNNE는 .NET Core와 .NET 5에서의 지원이 다릅니다. 이 글에서 작성한 예제는 .NET Core를 대상으로 했기에 ExportAttribute를 사용했지만, .NET 5 대상이라면 신규 포함된 <a target='tab' href='https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute'>UnmanagedCallersOnlyAttribute</a>를 사용하도록 바뀌었습니다.<br /> <br /> 따라서, .NET Core 및 5를 모두 지원하는 소스 코드로 만들려면 다음과 같은 식의 전처리 사용이 필요합니다.<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; using System.Runtime.InteropServices; #if !NET5_0 using DNNE; #endif namespace ClassLibrary { public class Class1 { <span style='color: blue; font-weight: bold'>#if NET5_0 [UnmanagedCallersOnly(EntryPoint = "mymethod_net5")] #else [Export(EntryPoint = "mymethod_netcore")] #endif</span> public static void MyMethod(IntPtr ptrText) { string text = Marshal.PtrToStringUni(ptrText); #if NET5_0 Console.WriteLine($"[NET5] {DateTime.Now} {text}"); #else Console.WriteLine($"[NETCORE] {DateTime.Now} {text}"); #endif } public delegate void MyMethodDelegate(IntPtr ptrText); } } #if NET5_0 #else namespace DNNE { internal class ExportAttribute : Attribute { public ExportAttribute() { } public string EntryPoint { get; set; } } } #endif </pre> <br /> 만약, .NET Core 3.1 미만 프로젝트에서 UnmanagedCallersOnly를 사용하거나, 반대로 .NET 5 이상에서 ExportAttribute를 사용하면 다음과 같은 식의 런타임 오류를 보게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Exception thrown at 0x00007FFF7FA4BA61 in ConsoleApplication1.exe: 0xC0000005: Access violation reading location 0xFFFFFFFFFFFFFFFF. </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Unhandled exception at 0x00007FF8215F2059 (ClassLibrary2NE.dll) in ConsoleApplication1.exe: Fatal program exit requested. </pre> <br /> <hr style='width: 50%' /><br /> <br /> 참고로, .NET Core 3.0 미만의 프로젝트에 대해서도 DNNE는 (컴파일 오류 없이) 정상적으로 "...NE.dll"까지 생성해 냅니다. 하지만, 런타임은 3.0 미만으로 사용할 수 없습니다. 예를 들어, 2.0 프로젝트로 runtimeconfig.json에도 2.0 런타임을 지정해 export한 함수를 사용하면 실행 시 이런 오류 메시지가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > This component must target .NET Core 3.0 or a higher version. Failed to initialize context for config: D:\temp\x64\Debug\ClassLibrary2.runtimeconfig.json. Error code: 0x800080a2 ConsoleApplication1.exe (process 20684) exited with code -1073740791. </pre> <br /> 대신, 3.0 미만의 프로젝트로 빌드했어도 runtimeconfig.json을 3.0 이상의 버전으로 변경하면 오류 없이 잘 실행됩니다. 따라서, 프로젝트 자체에는 버전의 제약이 없습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이렇게 AOT 방식이 아닌, 런타임을 로드하는 단계만 추가한 탓에 서로 다른 런타임 설정을 갖는(runtimeconfig.json) DLL을 동시에 사용할 수 없습니다. 이런 상황을 이상적으로 처리하려면, 즉 런타임이 다른 경우 각각의 런타임을 프로세스 내에 동시에 올릴 수 있어야 합니다. 하지만 (.NET Framework과는 달리) <a target='tab' href='https://www.sysnet.pe.kr/2/0/12268'>.NET Core 계열은 2개 이상의 런타임을 하나의 프로세스에 로드하는 것이 불가능</a>합니다. 따라서 다음과 같은 식으로 처리하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > int main() { void* mod_net5 = load_library("ClassLibrary3NE.dll"); void* mod_netcore = load_library("ClassLibrary2NE.dll"); { preload_runtime_t preload = (preload_runtime_t)get_export(mod_net5, "preload_runtime"); preload(); } { preload_runtime_t preload = (preload_runtime_t)get_export(mod_netcore, "preload_runtime"); preload(); } } </pre> <br /> preload_runtime의 호출 순서에 상관없이 이런 오류가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > The specified framework 'Microsoft.NETCore.App', version '3.0.0', apply_patches=1, version_compatibility_range=minor is incompatible with the previously loaded version '5.0.0'. ConsoleApplication1.exe (process 35928) exited with code -1073740791. </pre> <br /> (참고로, 런타임을 .NET 5만 로드하고 export시킨 함수를 get_export로 동적 로딩해서 사용해도 동일한 오류가 발생합니다. 왜냐하면, DNNE가 빌드한 export 함수 자체에 런타임을 로드하는 동작을 포함하고 있기 때문입니다.)<br /> <br /> 대신 이것도 DLL에 해당하는 runtimeconfig.json 파일을 모두 단일하게 (예를 들어 .NET 5로) 맞춰주면 가능합니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1661&boardid=331301885'>첨부 파일은 이 글의 예제를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 마지막으로, 아래는 DNNE를 참조했을 때 Nuget 캐시에 저장된 파일 목록입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > F:\nuget_root\dnne\1.0.16> <span style='color: blue; font-weight: bold'>tree /F</span> Folder PATH listing for volume New Volume Volume serial number is 524C-A5D1 F:. │ .nupkg.metadata │ .signature.p7s │ dnne.1.0.16.nupkg │ dnne.1.0.16.nupkg.sha512 │ dnne.nuspec │ ├───build │ │ DNNE.props │ │ DNNE.targets │ │ │ ├───net472 │ │ DNNE.BuildTasks.dll │ │ DNNE.BuildTasks.pdb │ │ Microsoft.Build.Framework.dll │ │ Microsoft.Build.Utilities.Core.dll │ │ Microsoft.VisualStudio.Setup.Configuration.Interop.dll │ │ System.Collections.Immutable.dll │ │ │ └───netstandard2.1 │ DNNE.BuildTasks.deps.json │ DNNE.BuildTasks.dll │ DNNE.BuildTasks.pdb │ └───tools │ dnne-gen.dll │ dnne-gen.runtimeconfig.json │ └───platform dnne.h platform.c </pre> <br /> 폴더 구조로 보면, .NET Framework 4.7.2 이상도 지원하는 듯합니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
4272
(왼쪽의 숫자를 입력해야 합니다.)