Microsoft MVP성태의 닷넷 이야기
.NET Framework: 971. UnmanagedCallersOnly 특성과 DNNE 사용 [링크 복사], [링크+제목 복사]
조회: 9586
글쓴 사람
정성태 (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)
13099정성태7/14/20227799.NET Framework: 2031. C# 11 - 사용자 정의 checked 연산자파일 다운로드1
13098정성태7/13/20226083개발 환경 구성: 647. Azure - scale-out 상태의 App Service에서 특정 인스턴스에 요청을 보내는 방법 [1]
13097정성태7/12/20225491오류 유형: 817. Golang - binary.Read: invalid type int32
13096정성태7/8/20228246.NET Framework: 2030. C# 11 - UTF-8 문자열 리터럴
13095정성태7/7/20226324Windows: 208. AD 도메인에 참여하지 않은 컴퓨터에서 Kerberos 인증을 사용하는 방법
13094정성태7/6/20226025오류 유형: 816. Golang - "short write" 오류 원인
13093정성태7/5/20226951.NET Framework: 2029. C# - HttpWebRequest로 localhost 접속 시 2초 이상 지연
13092정성태7/3/20227888.NET Framework: 2028. C# - HttpWebRequest의 POST 동작 방식파일 다운로드1
13091정성태7/3/20226709.NET Framework: 2027. C# - IPv4, IPv6를 모두 지원하는 서버 소켓 생성 방법
13090정성태6/29/20225843오류 유형: 815. PyPI에 업로드한 패키지가 반영이 안 되는 경우
13089정성태6/28/20226322개발 환경 구성: 646. HOSTS 파일 변경 시 Edge 브라우저에 반영하는 방법
13088정성태6/27/20225442개발 환경 구성: 645. "Developer Command Prompt for VS 2022" 명령행 환경의 폰트를 바꾸는 방법
13087정성태6/23/20228399스크립트: 41. 파이썬 - FastAPI / uvicorn 호스팅 환경에서 asyncio 사용하는 방법 [1]
13086정성태6/22/20227816.NET Framework: 2026. C# 11 - 문자열 보간 개선 2가지파일 다운로드1
13085정성태6/22/20227881.NET Framework: 2025. C# 11 - 원시 문자열 리터럴(raw string literals)파일 다운로드1
13084정성태6/21/20226520개발 환경 구성: 644. Windows - 파이썬 2.7을 msi 설치 없이 구성하는 방법
13083정성태6/20/20227095.NET Framework: 2024. .NET 7에 도입된 GC의 메모리 해제에 대한 segment와 region의 차이점 [2]
13082정성태6/19/20226136.NET Framework: 2023. C# - Process의 I/O 사용량을 보여주는 GetProcessIoCounters Win32 API파일 다운로드1
13081정성태6/17/20226211.NET Framework: 2022. C# - .NET 7 Preview 5 신규 기능 - System.IO.Stream ReadExactly / ReadAtLeast파일 다운로드1
13080정성태6/17/20226832개발 환경 구성: 643. Visual Studio 2022 17.2 버전에서 C# 11 또는 .NET 7.0 preview 적용
13079정성태6/17/20224570오류 유형: 814. 파이썬 - Error: The file/path provided (...) does not appear to exist
13078정성태6/16/20226590.NET Framework: 2021. WPF - UI Thread와 Render Thread파일 다운로드1
13077정성태6/15/20226925스크립트: 40. 파이썬 - PostgreSQL 환경 구성
13075정성태6/15/20225884Linux: 50. Linux - apt와 apt-get의 차이 [2]
13074정성태6/13/20226185.NET Framework: 2020. C# - NTFS 파일에 사용자 정의 속성값 추가하는 방법파일 다운로드1
13073정성태6/12/20226392Windows: 207. Windows Server 2022에 도입된 WSL 2
... 16  17  18  19  20  [21]  22  23  24  25  26  27  28  29  30  ...