Microsoft MVP성태의 닷넷 이야기
.NET Framework: 971. UnmanagedCallersOnly 특성과 DNNE 사용 [링크 복사], [링크+제목 복사]
조회: 1246
글쓴 사람
정성태 (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)
12675정성태6/16/2021360Java: 20. maven package 명령어 결과물로 (war가 아닌) jar 생성 방법
12674정성태6/15/2021387VC++: 142. DEFINE_GUID 사용법
12673정성태6/15/2021523Java: 19. IntelliJ - 자바(Java)로 만드는 Web App을 Tomcat에서 실행하는 방법
12672정성태6/15/2021476오류 유형: 725. IntelliJ에서 Java webapp 실행 시 "Address localhost:1099 is already in use" 오류
12671정성태6/15/2021745오류 유형: 724. Tomcat 실행 시 Failed to initialize connector [Connector[HTTP/1.1-8080]] 오류
12670정성태6/13/2021380.NET Framework: 1071. DLL Surrogate를 이용한 Out-of-process COM 개체에서의 CoInitializeSecurity 문제파일 다운로드1
12669정성태6/11/2021542.NET Framework: 1070. 사용자 정의 GetHashCode 메서드 구현은 C# 9.0의 record 또는 리팩터링에 맡기세요.
12668정성태6/11/2021685.NET Framework: 1069. C# - DLL Surrogate를 이용한 Out-of-process COM 개체 제작파일 다운로드2
12667정성태6/10/2021508.NET Framework: 1068. COM+ 서버 응용 프로그램을 이용해 CoInitializeSecurity 제약 해결파일 다운로드1
12666정성태6/10/2021482.NET Framework: 1067. 별도 DLL에 포함된 타입을 STAThread Main 메서드에서 사용하는 경우 CoInitializeSecurity 자동 호출파일 다운로드1
12665정성태6/9/2021494.NET Framework: 1066. Wslhub.Sdk 사용으로 알아보는 CoInitializeSecurity 사용 제약파일 다운로드1
12664정성태6/9/2021413오류 유형: 723. COM+ PIA 참조 시 "This operation failed because the QueryInterface call on the COM component" 오류
12663정성태6/9/2021476.NET Framework: 1065. Windows Forms - 속성 창의 디자인 설정 지원: 문자열 목록 내에서 항목을 선택하는 TypeConverter 제작파일 다운로드1
12662정성태6/8/2021533.NET Framework: 1064. C# COM 개체를 PIA(Primary Interop Assembly)로써 "Embed Interop Types" 참조하는 방법파일 다운로드1
12661정성태6/4/20211670.NET Framework: 1063. C# - MQTT를 이용한 클라이언트/서버(Broker) 통신 예제 [3]파일 다운로드1
12660정성태6/3/2021623.NET Framework: 1062. Windows Forms - 폼 내에서 발생하는 마우스 이벤트를 자식 컨트롤 영역에 상관없이 수신하는 방법 [1]파일 다운로드1
12659정성태6/2/2021609Linux: 40. 우분투 설치 후 MBR 디스크 드라이브 여유 공간이 인식되지 않은 경우 - Logical Volume Management
12658정성태6/2/2021645Windows: 194. Microsoft Store에 있는 구글의 공식 Youtube App
12657정성태6/2/2021883Windows: 193. 윈도우 패키지 관리자 - winget 설치
12656정성태6/1/2021416.NET Framework: 1061. 서버 유형의 COM+에 적용할 수 없는 Server GC
12655정성태6/1/2021458오류 유형: 722. windbg/sos - savemodule - Fail to read memory
12654정성태5/31/2021449오류 유형: 721. Hyper-V - Saved 상태의 VM을 시작 시 오류 발생
12653정성태5/31/2021707.NET Framework: 1060. 닷넷 GC에 새롭게 구현되는 DPAD(Dynamic Promotion And Demotion for GC)
12652정성태5/31/2021412VS.NET IDE: 164. Visual Studio - Web Deploy로 Publish 시 암호창이 매번 뜨는 문제
12651정성태5/31/2021593오류 유형: 720. PostgreSQL - ERROR: 22P02: malformed array literal: "..."
12650정성태5/17/2021468기타: 82. OpenTabletDriver의 버튼에 더블 클릭을 매핑 및 게임에서의 지원 방법
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...