Microsoft MVP성태의 닷넷 이야기
.NET Framework: 971. UnmanagedCallersOnly 특성과 DNNE 사용 [링크 복사], [링크+제목 복사],
조회: 9956
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  52  [53]  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12314정성태9/7/202010882오류 유형: 645. IIS HTTPERR - Timer_MinBytesPerSecond, Timer_ConnectionIdle 로그
12313정성태9/6/202012091개발 환경 구성: 509. Logstash - 사용자 정의 grok 패턴 추가를 이용한 IIS 로그 처리
12312정성태9/5/202015978개발 환경 구성: 508. Logstash 기본 사용법 [2]
12311정성태9/4/202011196.NET Framework: 937. C# - 간단하게 만들어 보는 리눅스의 nc(netcat), json_pp 프로그램 [1]
12310정성태9/3/202010479오류 유형: 644. Windows could not start the Elasticsearch 7.9.0 (elasticsearch-service-x64) service on Local Computer.
12309정성태9/3/202010221개발 환경 구성: 507. Elasticsearch 6.6부터 기본 추가된 한글 형태소 분석기 노리(nori) 사용법
12308정성태9/2/202011493개발 환경 구성: 506. Windows - 단일 머신에서 단일 바이너리로 여러 개의 ElasticSearch 노드를 실행하는 방법
12307정성태9/2/202012239오류 유형: 643. curl - json_parse_exception / Invalid UTF-8 start byte
12306정성태9/1/202010408오류 유형: 642. SQL Server 시작 오류 - error code 10013
12305정성태9/1/202011280Windows: 172. "Administered port exclusions"이 아닌 포트 범위 항목을 삭제하는 방법
12304정성태8/31/202010243개발 환경 구성: 505. 윈도우 - (네트워크 어댑터의 우선순위로 인한) 열거되는 IP 주소 순서를 조정하는 방법
12303정성태8/30/202010428개발 환경 구성: 504. ETW - 닷넷 프레임워크 기반의 응용 프로그램을 위한 명령행 도구 etrace 소개
12302정성태8/30/202010286.NET Framework: 936. C# - ETW 관련 Win32 API 사용 예제 코드 (5) - Private Logger파일 다운로드1
12301정성태8/30/202010625오류 유형: 641. error MSB4044: The "Fody.WeavingTask" task was not given a value for the required parameter "IntermediateDir".
12300정성태8/29/202010008.NET Framework: 935. C# - ETW 관련 Win32 API 사용 예제 코드 (4) CLR ETW Consumer파일 다운로드1
12299정성태8/27/202010951.NET Framework: 934. C# - ETW 관련 Win32 API 사용 예제 코드 (3) ETW Consumer 구현파일 다운로드1
12298정성태8/27/202010699오류 유형: 640. livekd - Could not resolve symbols for ntoskrnl.exe: MmPfnDatabase
12297정성태8/25/20209917개발 환경 구성: 503. SHA256 테스트 인증서 생성 방법
12296정성태8/24/202010304.NET Framework: 933. C# - ETW 관련 Win32 API 사용 예제 코드 (2) NT Kernel Logger파일 다운로드1
12295정성태8/24/20209728오류 유형: 639. Bitvise - Address is already in use; bind() in ListeningSocket::StartListening() failed: Windows error 10013: An attempt was made to access a socket ,,,
12293정성태8/24/202011055Windows: 171. "Administered port exclusions" 설명
12292정성태8/20/202012392.NET Framework: 932. C# - ETW 관련 Win32 API 사용 예제 코드 (1)파일 다운로드2
12291정성태8/15/202011350오류 유형: 638. error 1297: Device driver does not install on any devices, use primitive driver if this is intended.
12290정성태8/11/202012004.NET Framework: 931. C# - IP 주소에 따른 국가별 위치 확인 [8]파일 다운로드1
12289정성태8/6/20209489개발 환경 구성: 502. Portainer에 윈도우 컨테이너를 등록하는 방법
12288정성태8/5/20209537오류 유형: 637. WCF - The protocol 'net.tcp' does not have an implementation of HostedTransportConfiguration type registered.
... 46  47  48  49  50  51  52  [53]  54  55  56  57  58  59  60  ...