Microsoft MVP성태의 닷넷 이야기
.NET Framework: 971. UnmanagedCallersOnly 특성과 DNNE 사용 [링크 복사], [링크+제목 복사]
조회: 1243
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

UnmanagedCallersOnly 특성과 DNNE 사용

지난 글에 설명한 UnmanagedCallersOnly의 함수 export 기능은,

.NET 5 / .NET Core - UnmanagedCallersOnly 특성을 사용한 함수 내보내기
; https://www.sysnet.pe.kr/2/0/12413

전에도 한번 언급했지만, 위의 방법으로 실제 프로젝트를 진행하는 것은 'dotnet publish' 명령을 따로 해야 한다는 것으로 인해 비주얼 스튜디오 등에서의 개발 시에는 차라리 UnmanagedExports와 같은 개발 환경이 더 낫습니다.

그런데 nuget에서 배포 중인 DNNE(.NET Native Export Extension)는 유사한 기능을 수행하면서도 별도의 부가 과정 없이 export 기능을 프로젝트 빌드 시에 처리해 줍니다.

AaronRobinsonMSFT / DNNE
; https://github.com/AaronRobinsonMSFT/DNNE
; https://www.nuget.org/packages/DNNE

방법도 매우 간단한데, 기존 Microsoft.DotNet.ILCompiler 참조 대신 (혹은 그대로 놔둬도 되니) DNNE를 추가하고 소스 코드는 동일하게 작성하면 됩니다.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0lt;/TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="DNNE" Version="1.*" />
  </ItemGroup>
</Project>

공식적으로는 "net5.0"에 의존적이라고 하지만, 실제로 해보면 기존 .NET Core(위의 예제에서는 .NET Core 3.0) 프로젝트도 잘 빌드합니다.




그런데, DNNE는 Microsoft.DotNet.ILCompiler와 확연히 다른 방식으로 함수 export 기능을 수행합니다. 단적으로, Microsoft.DotNet.ILCompiler는 AOT 방식으로 빌드하지만, DNNE는 단지 실행 시에 적절한 .NET 런타임을 로드하고 닷넷 메서드로의 호출을 "대행하는 native 함수"를 export하는 식입니다.

예를 들어, 다음과 같은 코드를 담은 .NET Core 3.0 프로젝트를 빌드하면,

// .NET Core 3.0 프로젝트

using System;
using System.Runtime.InteropServices;
using DNNE;

namespace ClassLibrary
{
    public class Class1
    {
        [Export(EntryPoint = "mymethod_netcore")]
        public static void MyMethod(IntPtr ptrText)
        {
            string text = Marshal.PtrToStringUni(ptrText);
            Console.WriteLine($"[NETCORE] {DateTime.Now} {text}");
        }

        // 반드시 export할 메서드의 이름에 "Delegate" 접미사를 붙인 델리게이트를 정의해야 함
        public delegate void MyMethodDelegate(IntPtr ptrText);
    }
}

namespace DNNE
{
    internal class ExportAttribute : Attribute
    {
        public ExportAttribute() { }
        public string EntryPoint { get; set; }
    }
}

출력 파일은 다음과 같은 것들이 생성됩니다.

D:\temp\ClassLibrary2\bin\Debug\netcoreapp3.0> tree /F
Folder PATH listing for volume New Volume
Volume serial number is 8609-EAAD
D:.
    ClassLibrary2.deps.json
    ClassLibrary2.dll
    ClassLibrary2.pdb
    ClassLibrary2NE.dll
    ClassLibrary2NE.h
    ClassLibrary2NE.lib
    dnne.h

이 중에서 ClassLibrary2NE.dll은 다음의 4개 함수를 export합니다.

  • get_hostfxr_path
  • mymethod_netcore
  • preload_runtime
  • set_failure_callback

이때 mymethod_netcore가 하는 일은 런타임이 프로세스 내에 올라오지 않은 상태라면 로드를 한 후, 이어서 "ClassLibrary2.dll"을 로드해 그 안의 MyMethod를 호출하는 코드를 담고 있습니다.




DNNE로 출력한 export 함수를 실제로 Visual C++에서 사용해 볼까요? 코드는 뭐 대충 다음과 같은 식으로 호출할 텐데,

#include <iostream>
#include <Windows.h>

extern "C" void __stdcall mymethod_netcore(intptr_t text);
#pragma comment(lib, "..\\ClassLibrary2\\bin\\Debug\\netcoreapp3.0\\ClassLibrary2NE.lib") 

int main()
{
    const wchar_t* pText = L"Hello World";
    mymethod_netcore((intptr_t)pText);
}

실행해 보면 이런 오류 메시지가 발생합니다.

[디버그 실행]
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

그러니까, 어떤 런타임을 로드해야 하는지 명시하는 runtimeconfig.json 파일이 필요한 것입니다. 따라서 정상적인 실행을 하려면 ClassLibrary2.runtimeconfig.json 파일을 만들어 다음의 내용을 채워야 합니다.

{
    "runtimeOptions": {
        "tfm": "net3.0",
        "framework": {
            "name": "Microsoft.NETCore.App",
            "version": "3.0.0"
        }
    }
}

정리하면, DNNE로 만든 라이브러리를 C/C++에서 사용하려고 하면 다음의 3개 파일이 필요합니다.

  • ClassLibrary2.dll - .NET managed 메서드를 담은 어셈블리
  • ClassLibrary2.runtimeconfig.json - 닷넷 메서드를 실행하기 위한 런타임 선택
  • ClassLibrary2NE.dll - managed 메서드를 호출하는 unmanaged export 함수를 담은 DLL




그렇다면 DNNE 빌드 시에 함께 생성하는 헤더 파일은 무슨 용도일까요? 굳이 사용하려고 들자면, 위의 예제 코드에서 export 함수의 extern 선언을 ClassLibrary2NE.h로 바꿔줄 수도 있는데,

#include <iostream>
#include <Windows.h>

// extern "C" void __stdcall mymethod_netcore(intptr_t text);
#include "..\\ClassLibrary2\\bin\\Debug\\netcoreapp3.0\\ClassLibrary2NE.h"

그러면 (윈도우 환경의 Visual Studio에서) 2가지 에러에 시달리게 됩니다. 우선, 자동 생성된 "ClassLibrary2NE.h"에서 include 하고 있는 "<dnne.h>"를 찾을 수 없어 C1083 오류가 발생합니다.

Error C1083 Cannot open include file: 'dnne.h': No such file or directory

이것을 피하려면 프로젝트의 Include 경로에 dnne.h가 있는 경로를 추가하거나, 아니면 귀찮음을 감수하고 자동 생성되는 ClassLibrary2NE.h의 include 문을 다음과 같이 수정해야 합니다.

// #include <dnne.h>
#include "dnne.h"

위와 같이 하면 ClassLibrary2NE.h와 동일한 폴더에 있는 dnne.h를 포함시키기 때문에 오류가 발생하지 않습니다. 하지만, 더 귀찮은 문제가 두 번째 에러에 있습니다.

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

자동 생성해 준 코드에는 mymethod_netcore에 대한 함수 signature를 다음과 같이 선언하고 있는데,

DNNE_API void DNNE_CALLTYPE mymethod_netcore(intptr_t ptrText);

==>
__declspec(dllexport) void __stdcall mymethod_netcore(intptr_t ptrText);

extern "C"로 되어 있지 않아 x64로 빌드시 __cdecl로 호출 규약이 바뀌면서 Name mangling이 발생합니다. 반면, ClassLibrary2NE.dll이 export하고 있는 mymethod_netcore는 __stdcall로 export하고 있기 때문에 컴파일 시에 이를 찾지 못해 링크 에러가 발생하는 것입니다.

결국, ClassLibrary2NE.h 파일을 사용하기보다는 차라리 여러분들의 C++ 소스 코드에서 직접 선언을 담고 있는 것이 편합니다.

extern "C" void __stdcall mymethod_netcore(intptr_t text);

(다른 글에서 더 자세하게 설명하겠지만, 자동 생성된 ClassLibrary2NE.h, dnne.h 헤더 파일과 "#define DNNE_COMPILE_AS_SOURCE" 상수를 연결해 libnethost.lib를 적절하게 링크하면 ClassLibrary2NE.dll을 직접 생성하는 것도 가능합니다.)




DNNE는 .NET Core와 .NET 5에서의 지원이 다릅니다. 이 글에서 작성한 예제는 .NET Core를 대상으로 했기에 ExportAttribute를 사용했지만, .NET 5 대상이라면 신규 포함된 UnmanagedCallersOnlyAttribute를 사용하도록 바뀌었습니다.

따라서, .NET Core 및 5를 모두 지원하는 소스 코드로 만들려면 다음과 같은 식의 전처리 사용이 필요합니다.

using System;
using System.Runtime.InteropServices;

#if !NET5_0
using DNNE;
#endif

namespace ClassLibrary
{
    public class Class1
    {
#if NET5_0
        [UnmanagedCallersOnly(EntryPoint = "mymethod_net5")]
#else
        [Export(EntryPoint = "mymethod_netcore")]
#endif
        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

만약, .NET Core 3.1 미만 프로젝트에서 UnmanagedCallersOnly를 사용하거나, 반대로 .NET 5 이상에서 ExportAttribute를 사용하면 다음과 같은 식의 런타임 오류를 보게 됩니다.

Exception thrown at 0x00007FFF7FA4BA61 in ConsoleApplication1.exe: 0xC0000005: Access violation reading location 0xFFFFFFFFFFFFFFFF.

Unhandled exception at 0x00007FF8215F2059 (ClassLibrary2NE.dll) in ConsoleApplication1.exe: Fatal program exit requested.




참고로, .NET Core 3.0 미만의 프로젝트에 대해서도 DNNE는 (컴파일 오류 없이) 정상적으로 "...NE.dll"까지 생성해 냅니다. 하지만, 런타임은 3.0 미만으로 사용할 수 없습니다. 예를 들어, 2.0 프로젝트로 runtimeconfig.json에도 2.0 런타임을 지정해 export한 함수를 사용하면 실행 시 이런 오류 메시지가 발생합니다.

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.

대신, 3.0 미만의 프로젝트로 빌드했어도 runtimeconfig.json을 3.0 이상의 버전으로 변경하면 오류 없이 잘 실행됩니다. 따라서, 프로젝트 자체에는 버전의 제약이 없습니다.




이렇게 AOT 방식이 아닌, 런타임을 로드하는 단계만 추가한 탓에 서로 다른 런타임 설정을 갖는(runtimeconfig.json) DLL을 동시에 사용할 수 없습니다. 이런 상황을 이상적으로 처리하려면, 즉 런타임이 다른 경우 각각의 런타임을 프로세스 내에 동시에 올릴 수 있어야 합니다. 하지만 (.NET Framework과는 달리) .NET Core 계열은 2개 이상의 런타임을 하나의 프로세스에 로드하는 것이 불가능합니다. 따라서 다음과 같은 식으로 처리하면,

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();
    }
}

preload_runtime의 호출 순서에 상관없이 이런 오류가 발생합니다.

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.

(참고로, 런타임을 .NET 5만 로드하고 export시킨 함수를 get_export로 동적 로딩해서 사용해도 동일한 오류가 발생합니다. 왜냐하면, DNNE가 빌드한 export 함수 자체에 런타임을 로드하는 동작을 포함하고 있기 때문입니다.)

대신 이것도 DLL에 해당하는 runtimeconfig.json 파일을 모두 단일하게 (예를 들어 .NET 5로) 맞춰주면 가능합니다.

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




마지막으로, 아래는 DNNE를 참조했을 때 Nuget 캐시에 저장된 파일 목록입니다.

F:\nuget_root\dnne\1.0.16> tree /F
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

폴더 구조로 보면, .NET Framework 4.7.2 이상도 지원하는 듯합니다.




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

[연관 글]


donaricano-btn



[최초 등록일: ]
[최종 수정일: 11/19/2020]

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

비밀번호

댓글 쓴 사람
 




1  2  3  4  5  6  7  8  9  10  11  12  13  [14]  15  ...
NoWriterDateCnt.TitleFile(s)
12525정성태2/1/2021707개발 환경 구성: 531. Azure Devops - 파이프라인 실행 시 빌드 이벤트를 생략하는 방법
12524정성태1/31/2021845개발 환경 구성: 530. 기존 github 프로젝트를 Azure Devops의 빌드 Pipeline에 연결하는 방법 [1]
12523정성태1/31/2021830개발 환경 구성: 529. 기존 github 프로젝트를 Azure Devops의 Board에 연결하는 방법
12522정성태1/31/20211093개발 환경 구성: 528. 오라클 클라우드의 리눅스 VM - 9000 MTU Jumbo Frame 테스트
12521정성태1/31/20211188개발 환경 구성: 527. 이더넷(Ethernet) 환경의 TCP 통신에서 MSS(Maximum Segment Size) 확인
12520정성태1/30/2021723개발 환경 구성: 526. 오라클 클라우드의 VM에 ping ICMP 여는 방법
12519정성태1/30/2021890개발 환경 구성: 525. 오라클 클라우드의 VM을 외부에서 접근하기 위해 포트 여는 방법
12518정성태1/30/20212007Linux: 37. Ubuntu에 Wireshark 설치
12517정성태1/30/20211501Linux: 36. 윈도우 클라이언트에서 X2Go를 이용한 원격 리눅스의 GUI 접속 - 우분투 20.04
12516정성태1/29/2021986Windows: 188. Windows - TCP default template 설정 방법
12515정성태1/28/20211051웹: 41. Microsoft Edge - localhost에 대해 http 접근 시 무조건 https로 바뀌는 문제 [3]
12514정성태1/28/20211617.NET Framework: 1021. C# - 일렉트론 닷넷(Electron.NET) 소개 [1]파일 다운로드1
12513정성태1/28/2021800오류 유형: 698. electronize - User Profile 디렉터리에 공백 문자가 있는 경우 빌드가 실패하는 문제
12512정성태1/28/2021841오류 유형: 697. The program can't start because VCRUNTIME140.dll is missing from your computer. Try reinstalling the program to fix this problem.
12511정성태1/27/2021882Windows: 187. Windows - 도스 시절의 8.3 경로를 알아내는 방법
12510정성태1/27/20211028.NET Framework: 1020. .NET Core Kestrel 호스팅 - Razor 지원 추가 [1]파일 다운로드1
12509정성태1/27/2021999개발 환경 구성: 524. Jupyter Notebok에서 C#(F#, PowerShell) 언어 사용을 위한 환경 구성
12508정성태1/27/2021854개발 환경 구성: 523. Jupyter Notebook - Slide 플레이 버튼이 없는 경우
12507정성태1/26/2021781VS.NET IDE: 157. Visual Studio - Syntax Visualizer 메뉴가 없는 경우
12506정성태1/25/2021804.NET Framework: 1019. Microsoft.Tye 기본 사용법 소개
12505정성태1/23/20211012.NET Framework: 1018. .NET Core Kestrel 호스팅 - Web API 추가 [1]파일 다운로드1
12504정성태1/23/20211401.NET Framework: 1017. .NET 5에서의 네트워크 라이브러리 개선 (2) - HTTP/2, HTTP/3 관련 [2]
12503정성태1/21/20211128오류 유형: 696. C# - HttpClient: Requesting HTTP version 2.0 with version policy RequestVersionExact while HTTP/2 is not enabled.
12502정성태1/21/20211114.NET Framework: 1016. .NET Core HttpClient의 HTTP/2 지원파일 다운로드1
12501정성태1/21/2021993.NET Framework: 1015. .NET 5부터 HTTP/1.1, 2.0 선택을 위한 HttpVersionPolicy 동작 방식파일 다운로드1
12500정성태1/21/2021977.NET Framework: 1014. ASP.NET Core(Kestrel)의 HTTP/2 지원 여부파일 다운로드1
1  2  3  4  5  6  7  8  9  10  11  12  13  [14]  15  ...