DllImport 대신 LibraryImport 사용

.NET 7에 LibraryImport 특성이 추가되었습니다. 기존 DllImport 특성으로 다음의 DllImport 장식을

    [DllImport("Kernel32.dll")]
    public extern static bool Beep(uint freq, uint duration);

다음의 LibraryImport 특성으로 장식할 수 있습니다.

    [LibraryImport("Kernel32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static partial bool Beep(uint freq, uint duration);

LibraryImport가 새로 생겨난 이유는 DllImport가 마샬링을 런타임에서 수행해서 IL 코드를 emit한다고 하는데 NativeAOT 등 동적으로 IL 코드를 생성할 수 없는 환경에서 쓸 수 없기 때문입니다.

LibraryImport는 소스 생성기 기능을 이용해 컴파일 시점에서 마샬링 코드를 삽입합니다.

obj/Debug/net7.0/generated/Microsoft.Interop.LibraryImportGenerator/Microsoft.Interop.LibraryImportGenerator/LibraryImports.g.cs
(csproj에 EmitCompilerGeneratedFiles 설정을 true로 줘야 해당 경로에 파일로 생성되어 확인할 수 있습니다.)

// <auto-generated/>
static unsafe partial class ExternDll
{
    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.6.42610")]
    [System.Runtime.CompilerServices.SkipLocalsInitAttribute]
    public static partial bool Beep(uint freq, uint duration)
    {
        bool __retVal;
        int __retVal_native;
        {
            __retVal_native = __PInvoke(freq, duration);
        }

        // Unmarshal - Convert native data to managed data.
        __retVal = __retVal_native != 0;
        return __retVal;
        // Local P/Invoke
        [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "Beep", ExactSpelling = true)]
        static extern unsafe int __PInvoke(uint freq, uint duration);
    }
}

뭔가 코드가 삽입됐고 심지어 최종 다시 DllImport를 사용하는군요? 결국에 LibraryImport 특성으로 인해 생성된 코드는 마샬링 관련 코드를 삽입하는 역할만 딱 하고 기존 DllImport를 사용합니다.

        // Unmarshal - Convert native data to managed data.
        __retVal = __retVal_native != 0;

정말로 마샬링 처리를 하는지 다른 예제로 확인해 봤습니다. LZ4 DLL을 다음과 같이 LibraryImport 특성을 사용해

static partial class ExternDll
{
    [LibraryImport("msys-lz4-1.dll")]
    public static partial int LZ4_compressBound(int inputSize);

    [LibraryImport("msys-lz4-1.dll")]
    public static partial int LZ4_compress_default(byte[] src, byte[] dst, int srcSize, int dstCapacity);

    [LibraryImport("msys-lz4-1.dll")]
    public static partial int LZ4_decompress_safe(byte[] src, byte[] dst, int compressedSize, int dstCapacity);
}

byte[] 은 관리 힙 메모리에 위치하므로 비관리 모듈에 그대로 전달하면 안됩니다. LibraryImport 장식에 의해 생성된 코드를 보면,

static unsafe partial class ExternDll
{
    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.6.42610")]
    [System.Runtime.CompilerServices.SkipLocalsInitAttribute]
    public static partial int LZ4_compress_default(byte[] src, byte[] dst, int srcSize, int dstCapacity)
    {
        int __retVal;
        // Pin - Pin data in preparation for calling the P/Invoke.
        fixed (void* __src_native = &global::System.Runtime.InteropServices.Marshalling.ArrayMarshaller<byte, byte>.ManagedToUnmanagedIn.GetPinnableReference(src))
        fixed (void* __dst_native = &global::System.Runtime.InteropServices.Marshalling.ArrayMarshaller<byte, byte>.ManagedToUnmanagedIn.GetPinnableReference(dst))
        {
            __retVal = __PInvoke((byte*)__src_native, (byte*)__dst_native, srcSize, dstCapacity);
        }

        return __retVal;
        // Local P/Invoke
        [System.Runtime.InteropServices.DllImportAttribute("msys-lz4-1.dll", EntryPoint = "LZ4_compress_default", ExactSpelling = true)]
        static extern unsafe int __PInvoke(byte* src, byte* dst, int srcSize, int dstCapacity);
    }
}

위와 같이 마샬링을 하는 코드가 삽입됨을 볼 수 있습니다.

간단하게 테스트 코드를 만든 후,

using System.Runtime.InteropServices;
using System.Text;

// TestBeep()
TestLZ4Compress();


static void TestBeep()
{
    for (uint i = 0; i < 100; i++)
    {
        ExternDll.Beep(i * 100, 50);
    }
}

static void TestLZ4Compress()
{
    var sourceText = """
        닷넷 코어는 ASP.NET Core 웹 응용 프로그램, 명령줄 응용 프로그램, 라이브러리 및 유니버셜 윈도우 플랫폼 앱, 응용 프로그램 등 총 4가지로 크로스 플랫폼 시나리오를 지원한다. 다만, 현재 윈도우의 데스크톱 소프트웨어용 표준 GUI를 렌더링하는 윈도우 폼 또는 WPF는 구현되어 있지 않다.[3][4] 이에 마이크로소프트는 닷넷 코어3에서 윈도우 폼, WPF을 유니버셜 윈도우 플랫폼 앱과 함께 지원할 방침이다.[5] 여기에 닷넷 코어는 NuGet 패키지의 사용을 지원한다. 윈도우 버전의 닷넷 프레임워크와는 달리 업데이트는 윈도우 업데이트에서만 주로 이루어지만, 닷넷 코어는 업데이트를 패키지 관리자 형식으로 업데이트를 하는 장점이 있다.[3][4]

        닷넷 코어는 공통 언어 런타임(CLR)의 완전한 런타임환경을 구현시킨 CoreCLR로 구성되어있다. 이 런타임은 닷넷 프로그램 실행 관리를 위한 가상 컴퓨터로 마이크로소프트에서 시작하여, RyuJIT라는 JIT 컴파일을 포함한다.[6] 또한, AOT 컴파일 된 원시 바이너리에 통합되도록 최적화 된 닷넷 원시 런타임인 CoreRT를 포함한다.

        닷넷 코어는 닷넷 프레임워크의 표준 라이브러리의 일부 포크인 CoreFX도 포함되어 있으며,[7] 닷넷 코어의 API의 일부분은 닷넷 프레임워크의 API과 동일한 부분도 있으나, 닷넷 프레임워크와는 전혀 다른 전용 API을 사용한다. 그리고 닷넷 코어의 라이브러리를 변형시켜 UWP의 개발에 활용할 수 있다.[8]

        닷넷 코어의 명령 줄 인터페이스는 운영 체제에 대한 실행 진입 점을 제공하고 컴파일 및 패키지 관리와 같은 개발자 서비스를 제공한다.[9]
        """;

    Console.WriteLine(sourceText);
    Console.WriteLine("------");

    var source = Encoding.Default.GetBytes(sourceText);

    var maxDstSize = ExternDll.LZ4_compressBound(source.Length);
    //Console.WriteLine(maxDstSize);
    var target = new byte[maxDstSize];

    var compressedSize = ExternDll.LZ4_compress_default(source, target, source.Length, maxDstSize);
    Console.WriteLine($"압축 (LZ4 기본)");
    Console.WriteLine($"소스 크기 = {source.Length}");
    Console.WriteLine($"압축된 크기 = {compressedSize}");
    Console.WriteLine($"압축(%) = {(1 - (float)compressedSize / source.Length) * 100} %");
    Console.WriteLine("------");

    var decompressedTarget = new byte[source.Length];
    var decompressedSize = ExternDll.LZ4_decompress_safe(target, decompressedTarget, compressedSize, decompressedTarget.Length);

    Console.WriteLine($"압축 해제 (LZ4 기본)");
    Console.WriteLine($"압축 해제 크기 = {decompressedSize}");

    Console.WriteLine("------");

    var decompressedText = Encoding.Default.GetString(decompressedTarget);
    Console.WriteLine(decompressedText);
}

NativeAOT로 다음의 프로젝트 설정으로 게시하여,

...
	  <PublishAot>true</PublishAot>
	  <InvariantGlobalization>true</InvariantGlobalization>
	  <UseSystemResourceKeys>true</UseSystemResourceKeys>

	  <IlcOptimizationPreference>Size</IlcOptimizationPreference>
	  <IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>

	  <DebuggerSupport>false</DebuggerSupport>
	  <EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
	  <EventSourceSupport>false</EventSourceSupport>
	  <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
	  <MetadataUpdaterSupport>false</MetadataUpdaterSupport>

	  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
	  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>     
...

2M 사이즈 .NET 런타임 의존성 없는 실행파일과 500K 사이즈의 LZ4 DLL을 볼 수 있고,

$ .\No19.LibraryImportAttributeTest.exe
닷넷 코어는 ASP.NET Core 웹 응용 프로그램, 명령줄 응용 프로그램...
------
압축 (LZ4 기본)
소스 크기 = 1857
압축된 크기 = 1216
압축(%) = 34.51804 %
------
압축 해제 (LZ4 기본)
압축 해제 크기 = 1857
------
닷넷 코어는 ASP.NET Core 웹 응용 프로그램, 명령줄 응용 프로그램...

잘 동작하는 것을 확인할 수 있었습니다.

https://github.com/dimohy/csharp-check/tree/main/No19.LibraryImportAttributeTest

9개의 좋아요