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

(시리즈 글이 7개 있습니다.)
.NET Framework: 969. .NET Framework 및 .NET 5 - UnmanagedCallersOnly 특성 사용
; https://www.sysnet.pe.kr/2/0/12412

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

.NET Framework: 971. UnmanagedCallersOnly 특성과 DNNE 사용
; https://www.sysnet.pe.kr/2/0/12415

.NET Framework: 972. DNNE가 출력한 NE DLL을 직접 생성하는 방법
; https://www.sysnet.pe.kr/2/0/12421

.NET Framework: 973. .NET 5, .NET Framework에서만 허용하는 UnmanagedCallersOnly 사용예
; https://www.sysnet.pe.kr/2/0/12422

.NET Framework: 976. UnmanagedCallersOnly + C# 9.0 함수 포인터 사용 시 x86 빌드에서 오동작하는 문제
; https://www.sysnet.pe.kr/2/0/12431

닷넷: 2174. C# - .NET 7부터 UnmanagedCallersOnly 함수 export 기능을 AOT 빌드에 통합
; https://www.sysnet.pe.kr/2/0/13464




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 이상도 지원하는 듯합니다.




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







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

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

비밀번호

댓글 작성자
 




... 16  17  [18]  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13171정성태11/25/20224680Windows: 214. 윈도우 - 스레드 스택의 "red zone"
13170정성태11/24/20224986Windows: 213. 윈도우 - 싱글 스레드는 컨텍스트 스위칭이 없을까요?
13169정성태11/23/20225604Windows: 212. 윈도우의 Protected Process (Light) 보안 [1]파일 다운로드2
13168정성태11/22/20224876제니퍼 .NET: 31. 제니퍼 닷넷 적용 사례 (9) - DB 서비스에 부하가 걸렸다?!
13167정성태11/21/20224916.NET Framework: 2070. .NET 7 - Console.ReadKey와 리눅스의 터미널 타입
13166정성태11/20/20224637개발 환경 구성: 651. Windows 사용자 경험으로 WSL 환경에 dotnet 런타임/SDK 설치 방법
13165정성태11/18/20224544개발 환경 구성: 650. Azure - "scm" 프로세스와 엮인 서비스 모음
13164정성태11/18/20225462개발 환경 구성: 649. Azure - 비주얼 스튜디오를 이용한 AppService 원격 디버그 방법
13163정성태11/17/20225386개발 환경 구성: 648. 비주얼 스튜디오에서 안드로이드 기기 인식하는 방법
13162정성태11/15/20226461.NET Framework: 2069. .NET 7 - AOT(ahead-of-time) 컴파일
13161정성태11/14/20225702.NET Framework: 2068. C# - PublishSingleFile로 배포한 이미지의 역어셈블 가능 여부 (난독화 필요성) [4]
13160정성태11/11/20225631.NET Framework: 2067. C# - PublishSingleFile 적용 시 native/managed 모듈 통합 옵션
13159정성태11/10/20228803.NET Framework: 2066. C# - PublishSingleFile과 관련된 옵션 [3]
13158정성태11/9/20225112오류 유형: 826. Workload definition 'wasm-tools' in manifest 'microsoft.net.workload.mono.toolchain' [...] conflicts with manifest 'microsoft.net.workload.mono.toolchain.net7'
13157정성태11/8/20225765.NET Framework: 2065. C# - Mutex의 비동기 버전파일 다운로드1
13156정성태11/7/20226678.NET Framework: 2064. C# - Mutex와 Semaphore/SemaphoreSlim 차이점파일 다운로드1
13155정성태11/4/20226187디버깅 기술: 183. TCP 동시 접속 (연결이 아닌) 시도를 1개로 제한한 서버
13154정성태11/3/20225656.NET Framework: 2063. .NET 5+부터 지원되는 GC.GetGCMemoryInfo파일 다운로드1
13153정성태11/2/20226933.NET Framework: 2062. C# - 코드로 재현하는 소켓 상태(SYN_SENT, SYN_RECV)
13152정성태11/1/20225561.NET Framework: 2061. ASP.NET Core - DI로 추가한 클래스의 초기화 방법 [1]
13151정성태10/31/20225676C/C++: 161. Windows 11 환경에서 raw socket 테스트하는 방법파일 다운로드1
13150정성태10/30/20225713C/C++: 160. Visual Studio 2022로 빌드한 C++ 프로그램을 위한 다른 PC에서 실행하는 방법
13149정성태10/27/20225643오류 유형: 825. C# - CLR ETW 이벤트 수신이 GCHeapStats_V1/V2에 대해 안 되는 문제파일 다운로드1
13148정성태10/26/20225637오류 유형: 824. msbuild 에러 - error NETSDK1005: Assets file '...\project.assets.json' doesn't have a target for 'net5.0'. Ensure that restore has run and that you have included 'net5.0' in the TargetFramew
13147정성태10/25/20224760오류 유형: 823. Visual Studio 2022 - Unable to attach to CoreCLR. The debugger's protocol is incompatible with the debuggee.
13146정성태10/24/20225588.NET Framework: 2060. C# - Java의 Xmx와 유사한 힙 메모리 최댓값 제어 옵션 HeapHardLimit
... 16  17  [18]  19  20  21  22  23  24  25  26  27  28  29  30  ...