Microsoft MVP성태의 닷넷 이야기
닷넷: 2175. C# - DllImport 메서드의 AOT 지원을 위한 LibraryImport 옵션 [링크 복사], [링크+제목 복사],
조회: 10820
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 7개 있습니다.)
개발 환경 구성: 386. .NET Framework Native compiler 프리뷰 버전 사용법
; https://www.sysnet.pe.kr/2/0/11563

.NET Framework: 2069. .NET 7 - AOT(ahead-of-time) 컴파일
; https://www.sysnet.pe.kr/2/0/13162

닷넷: 2175. C# - DllImport 메서드의 AOT 지원을 위한 LibraryImport 옵션
; https://www.sysnet.pe.kr/2/0/13466

닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법
; https://www.sysnet.pe.kr/2/0/13483

개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행
; https://www.sysnet.pe.kr/2/0/13487

닷넷: 2202. C# - PublishAot의 glibc에 대한 정적 링킹하는 방법
; https://www.sysnet.pe.kr/2/0/13529

오류 유형: 913. C# - AOT StaticExecutable 정적 링킹 시 빌드 오류
; https://www.sysnet.pe.kr/2/0/13669




C# - DllImport 메서드의 AOT 지원을 위한 LibraryImport 옵션

P/Invoke는 DllImport 호출 시 .NET 런타임 측에서 동적으로 코드를 생성해 연결하는 식으로 동작합니다. 이에 대해서는 전에 한번 다룬 적이 있습니다.

windbg - C# PInvoke 호출 시 마샬링을 담당하는 함수 분석
; https://www.sysnet.pe.kr/2/0/12065

이러한 동적 코드 생성이 AOT 시에는 문제가 된다고 하는데요,

Source generation for platform invokes
; https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke-source-generation

The IL stub handles marshalling of parameters and return values and calling the unmanaged code while respecting settings on DllImportAttribute that affect how the unmanaged code should be invoked (for example, SetLastError). Since this IL stub is generated at run time, it isn't available for ahead-of-time (AOT) compiler or IL trimming scenarios


그래서 그 해법으로 .NET 7부터 LibraryImport를 이용한 방법을 소개하고 있습니다. 간단하게, LibraryImport 속성이 어떻게 동작하는지 한번 파헤쳐 볼까요? ^^

LibraryImportAttribute
; https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.libraryimportattribute

예를 들어, 기존에 다음과 같이 DllImport를 사용하던 경우라면,

public class Program
{
    [DllImport(@"ClassLibrary2.dll")]
    public static extern void mymethod([MarshalAs(UnmanagedType.LPWStr)] string text);
}

이것을 LibraryImport로 바꾸기 위해서는 class를 partial로 만들고,

public partial class Program
{
    // ...[생략]...
}

DllImport를 적용했던 메서드를 partial과 함께 LibraryImport 특성으로 대체한 다음, 마샬링이 필요한 참조 형식의 인자에 대해서는 어떻게 처리해야 하는지를 알려주는 옵션을 적용하면 됩니다. (string 형식에 대해서만 지원합니다.)

// "string text" 인자가 참조형이므로 StringMarshalling 옵션을 이용해 Utf16으로 하도록 명시

[LibraryImport(@"ClassLibrary2.dll", StringMarshalling = StringMarshalling.Utf16)]
public static partial void mymethod(string text);

이렇게 바꾸면 C# 컴파일러는 LibraryImport 특성이 있는 partial 메서드에 대해 다음과 같은 식으로 partial class + partial method를 적용해 소스 코드를 생성해 줍니다.

// unsafe가 들어가기 때문에, LibraryImport를 적용한 경우 프로젝트 옵션에 AllowUnsafeBlocks 값을 true로 설정해야 함

// <auto-generated/>
namespace ConsoleApp1
{
    internal unsafe partial class Program
    {
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "8.0.9.3103")]
        [global::System.Runtime.CompilerServices.SkipLocalsInitAttribute]
        private static partial void mymethod(string text)
        {
            // Pin - Pin data in preparation for calling the P/Invoke.
            fixed (void* __text_native = &global::System.Runtime.InteropServices.Marshalling.Utf16StringMarshaller.GetPinnableReference(text))
            {
                __PInvoke((ushort*)__text_native);
            }

            // Local P/Invoke
            [global::System.Runtime.InteropServices.DllImportAttribute("ClassLibrary2.dll", EntryPoint = "mymethod", ExactSpelling = true)]
            static extern unsafe void __PInvoke(ushort* __text_native);
        }
    }
}

그러니까, 참조형 타입에 대해 미리 pinning 등의 작업을 한 후, 원래의 DllImport를 감싼 __PInvoke를 호출하는 식으로 바뀝니다. 결국, DllImport를 다음과 같이 사용하던 것과 별반 다를 게 없습니다.

[DllImport(@"ClassLibrary2.dll")]
public static extern void mymethod(char* text);

fixed (char* ptr = text)
{
    mymethod(ptr);
}




보는 바와 같이 LibraryImport는 간단한 래퍼에 불과한 것으로, DllImport를 사용하는 유형이긴 한데, 전달되는 인자에 대해 부가적인 마샬링 코드를 런타임 측에서 동적으로 생성할 필요가 없도록 강제하는 효과를 갖는 것입니다.

일례로, (마샬링이 필요한) 참조 형식을 전달하려 하면,

[StructLayout(LayoutKind.Sequential)]
public class MyType
{
    public int Age { get; set; }
}

[LibraryImport(@"ClassLibrary2.dll")]
public static partial void InitMyType(MyType instance);

SYSLIB1051 컴파일 오류를 발생시킵니다.

SYSLIB1051: The type 'ConsoleApp1.MyType' is not supported by source-generated P/Invokes. The generated source will not handle marshalling of parameter 'instance'. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1051)


반면 마샬링이 필요 없도록 struct로 바꾸면,

public struct MyType
{
    public int Age { get; set; }
}

/*
// 참고로, 이때 자동 생성하는 partial 메서드는, DllImport로 유형만 바뀐 채로 처리됩니다.
public unsafe partial class Program
{
    [global::System.Runtime.InteropServices.DllImportAttribute("ClassLibrary2.dll", EntryPoint = "InitMyType", ExactSpelling = true)]
    public static extern partial void InitMyType(global::ConsoleApp1.MyType instance);
}
*/

다시 AOT 빌드까지 잘 되지만, 만약 저 struct의 필드에 참조 형식을 포함하게 되면,

public struct MyType
{
    public int Age { get; set; }
    public string Text { get; set; } // 참조 형식
}

재차 SYSLIB1051 오류가 발생합니다. 메서드의 인자에 string이 있는 경우에는 "StringMarshalling = StringMarshalling.Utf16" 옵션을 적용해 LibraryImportGenerator 소스 코드 생성기로 하여금 마샬링 코드를 컴파일 시점에 생성하도록 만들 수 있지만, 저렇게 사용자 타입에 들어가는 유형은 (아직인지는 모르겠지만) LibraryImportGenerator에서 지원하지 않습니다.

검색해 보면, MarshalUsing을 이용해 Custom marshaler까지 제공하면 된다고 하는데요,

Marshalling Function Pointers with .NET 7 LibraryImport
; https://stackoverflow.com/questions/75304403/marshalling-function-pointers-with-net-7-libraryimport

정리하자면, LibraryImport를 사용함으로써 인자의 마샬링 작업만큼은 PInvoke 호출 전에, 즉 컴파일 시점에 끝내겠다는 ^^ 확고한 의지를 보여주는 것과 같다고 하겠습니다. (덤으로 C# 컴파일러로 하여금 유효성 체크를 받을 수도 있고!)

결국 마샬링이 필요 없는 경우라면 DllImport를 기존과 다름없이 사용하셔도 됩니다. 심지어, 문서에서 예를 든 ToLower 메서드도,

[DllImport(
    "nativelib",
    EntryPoint = "to_lower",
    CharSet = CharSet.Unicode)]
internal static extern string ToLower(string str);

// string lower = ToLower("StringToConvert");

PublishAot로 빌드할 때 아무런 문제 없이 잘 실행이 됩니다. 사실, 저 경우는 string에 대해 CharSet 및 pinning 처리를 수반하는 코드가 생성될 텐데도, ... string 정도는 감안하고 특별히 AOT 컴파일러 측에서 처리를 해주는 것인지는 알 수 없지만, 어쨌든 빌드 및 실행까지 됩니다.




돌이켜 보면, P/Inovke 관련해서는 정말 많은 변화가 있었군요. ^^

가령, Microsoft.Windows.CsWin32 패키지를 통한 DllImport를 자동 생성해 주는 것도 있었고,

C# - Win32 API에 대한 P/Invoke를 대신하는 Microsoft.Windows.CsWin32 패키지
; https://www.sysnet.pe.kr/2/0/12540

SuppressGCTransition을 이용한 속도 향상을 꾀하기도 했습니다.

.NET 5+ 환경에서 P/Invoke의 성능을 높이기 위한 SuppressGCTransition 특성
; https://www.sysnet.pe.kr/2/0/13401




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 11/29/2023]

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

비밀번호

댓글 작성자
 




... 181  182  [183]  184  185  186  187  188  189  190  191  192  193  194  195  ...
NoWriterDateCnt.TitleFile(s)
375정성태10/25/200624413    답변글 .NET Framework: 75.1. 개인 발행 카드에 대한 Microsoft 예제 실습(이 글의 내용은 재작성되어질 예정입니다.)
376정성태10/27/200624074    답변글 .NET Framework: 75.2. "Windows CardSpace"와 "인증서 서비스"의 만남(이 글의 내용은 재작성되어질 예정입니다.)
377정성태10/26/200623698    답변글 .NET Framework: 75.3. Managed Card 발행에 대한 Microsoft 예제 실습 (1) - CardWriter (이 글의 내용은 재작성되어질 예정입니다.)
385정성태11/6/200626327    답변글 .NET Framework: 75.4. Managed Card 발행에 대한 Microsoft 예제 실습 (2) - STS 구현 (이 글의 내용은 재작성되어질 예정입니다.) [7]
387정성태11/2/200627128    답변글 .NET Framework: 75.5. Windows CardSpace와 SYSNET 사이트의 만남 (이 글의 내용은 재작성되어질 예정입니다.) [1]
397정성태11/11/200624655    답변글 .NET Framework: 75.6. CardWriter.csproj와 함께 알아보는 인증서 식별 방법(이 글의 내용은 재작성되어질 예정입니다.)
398정성태11/12/200623147    답변글 .NET Framework: 75.7. 카드에 암호 거는 방법(이 글의 내용은 재작성되어질 예정입니다.)
399정성태11/12/200625435    답변글 .NET Framework: 75.8. 인증서/스마트 카드에 기반한 Managed Card - STS 구현(이 글의 내용은 재작성되어질 예정입니다.) [5]
369정성태10/22/200620936오류 유형: 15. 자동 업데이트 실패
367정성태10/22/200636705Windows: 3. IIS 7.0 다중 바인딩 설정하는 방법 [1]
365정성태10/21/200620460Windows: 2. 서버(build 5600)에 IIS 7.0 서비스와 .NET 3.0 설치 방법
359정성태10/17/200616544오류 유형: 14. VS.NET 빌드 오류 - FxCopCmd.exe returned error code 65.
358정성태10/17/200621705오류 유형: 13. WSE 3.0 서비스 관련 WSE101 오류 / Destination Unreachable
357정성태12/1/200623976.NET Framework: 74. WCF 이야기 [4]
378정성태10/28/200628807    답변글 .NET Framework: 74.1. WCF와 WSE 3.0의 활용 [4]파일 다운로드1
379정성태11/3/200627746    답변글 .NET Framework: 74.2. WCF로 구현하는 .NET Remoting [4]파일 다운로드1
380정성태10/28/200626642    답변글 .NET Framework: 74.3. 웹 서비스와 닷넷 리모팅으로써의 WCF 구현파일 다운로드1
381정성태10/28/200629027    답변글 .NET Framework: 74.4. WCF 서비스 참조 추가 메뉴 [2]
382정성태10/28/200635099    답변글 .NET Framework: 74.5. WCF 서비스를 IIS에서 호스팅하는 방법파일 다운로드1
383정성태10/28/200629918    답변글 .NET Framework: 74.6. IIS 6.0: 다중 Endpoint 제공파일 다운로드1
384정성태10/28/200626762    답변글 .NET Framework: 74.7. IIS 7.0: 다중 Endpoint 제공
389정성태11/11/200629632    답변글 .NET Framework: 74.8. WCF에 SSL 적용 (1) - Httpcfg.exe 도구를 이용한 SSL 설정
390정성태11/6/200626737    답변글 .NET Framework: 74.9. WCF에 SSL 적용 (2) - 서비스 제작파일 다운로드1
356정성태10/7/200622306COM 개체 관련: 19. COM의 Apartment를 이해해 보자. [8]
386light10/30/200617268    답변글 COM 개체 관련: 19.1. [답변]: COM 객체를 글로벌마샬으로 만든후, 사용한다.
355정성태10/9/200625002개발 환경 구성: 19. Internet_Zone 하위에 새로운 코드 그룹을 추가하는 예제 [4]파일 다운로드2
... 181  182  [183]  184  185  186  187  188  189  190  191  192  193  194  195  ...