0Pricing
C# Academy · 강의

LibraryImport와 소스 생성 P/Invoke

[LibraryImport](C# 11+)를 사용해 AOT와 호환되고 DllImport보다 뛰어난 성능을 내는 소스 생성 마샬링을 구현합니다.

LibraryImport와 소스 생성 P/Invoke은(는) CoddyKit의 무료 C# Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 C# Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. C# Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

LibraryImport를 사용하는 이유

[LibraryImport]는 .NET 7(C# 11)에서 소스 생성 방식이며 AOT와 호환되는 [DllImport]의 대체 수단으로 도입되었습니다. 기존의 DllImport는 리플렉션을 통한 런타임 마샬링에 의존하므로 Native AOT에 문제가 될 수 있습니다. LibraryImport는 모든 마샬링 코드를 컴파일 시점에 생성합니다.

LibraryImport 메서드 선언하기

static partial 메서드에 [LibraryImport]를 지정합니다. 소스 생성기가 구현을 채웁니다. 메서드를 포함하는 클래스에도 partial을 지정해야 합니다.

using System.Runtime.InteropServices;

internal static partial class NativeMethods
{
    // Source generator creates the P/Invoke body at compile time
    [LibraryImport("mylib", EntryPoint = "add_integers")]
    internal static partial int AddIntegers(int a, int b);

    // String marshaling must be explicit in LibraryImport
    [LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
    internal static partial int ProcessString(string text);
}

LibraryImport에서 문자열 마샬링하기

DllImport와 달리 LibraryImport에서는 문자열 마샬링을 명시적으로 지정해야 합니다. StringMarshalling 열거형 또는 MarshalAs 특성을 사용하면 마샬링 비용을 명확히 확인하고 제어할 수 있습니다.

// Option 1: StringMarshalling enum (all strings in method)
[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16)]
static partial bool CreateDirectory(string lpPathName, nint lpSecurityAttributes);

// Option 2: Per-parameter MarshalAs
[LibraryImport("libc")]
static partial int Open(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string path,
    int flags);

// Option 3: MarshalUsing for custom marshalers
[LibraryImport("mylib")]
static partial void Process(
    [MarshalUsing(typeof(Utf8StringMarshaller))] string name);

구조체 마샬링

구조체의 경우 [NativeMarshalling]을 추가하여 관리형 형식을 네이티브 표현으로 매핑하는 방법을 정의합니다. 소스 생성기는 마샬러 형식을 사용하여 안전하면서 할당을 최소화하는 코드를 생성합니다.

[NativeMarshalling(typeof(PointMarshaller))]
public struct Point
{
    public int X;
    public int Y;
}

[CustomMarshaller(typeof(Point), MarshalMode.Default, typeof(PointMarshaller))]
public static class PointMarshaller
{
    public static Point ConvertToManaged(NativePoint native)
        => new Point { X = native.X, Y = native.Y };
    public static NativePoint ConvertToUnmanaged(Point managed)
        => new NativePoint { X = managed.X, Y = managed.Y };

    [StructLayout(LayoutKind.Sequential)]
    public struct NativePoint { public int X, Y; }
}

DllImport와 LibraryImport 비교

DllImport는 런타임에 해석되므로 시작이 느리고 리플렉션을 기반으로 하며 AOT 트리밍과 호환되지 않습니다. LibraryImport는 빌드 시점에 최적화된 C# 코드를 생성하므로 런타임 리플렉션이 필요 없고 트리밍에 안전하며 벤치마크에서 측정할 수 있을 정도로 더 빠릅니다.

// Old — DllImport (still works, but avoid for new AOT code)
[DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool MoveFile(string src, string dst);

// New — LibraryImport (AOT-safe, source-generated)
[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16,
                SetLastError = true)]
static partial bool MoveFile(string src, string dst);
// The compiler generates the actual P/Invoke wrapper body

SetLastError 및 오류 처리

[LibraryImport]에서 SetLastError = true로 설정하여 OS 오류 코드를 캡처합니다. 호출 후 Marshal.GetLastPInvokeError()를 사용하는 것이 권장되며, Marshal.GetLastWin32Error()를 사용해도 오류 코드를 가져올 수 있습니다.

[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16,
                SetLastError = true)]
static partial bool CreateDirectory(string path, nint secAttr);

bool ok = CreateDirectory(@"C:\Temp\NewDir", 0);
if (!ok)
{
    int err = Marshal.GetLastPInvokeError();
    // err is ERROR_ALREADY_EXISTS (183) if folder exists
    throw new Win32Exception(err);
}

Span<T> 및 메모리 마샬링

소스 생성 P/Invoke의 장점 중 하나는 Span<T>를 일급으로 지원한다는 점입니다. ReadOnlySpan<byte>를 전달하면 배열을 사용하는 DllImport에서 필요한 고정과 할당을 피할 수 있습니다.

[LibraryImport("mylib")]
static partial int ProcessBuffer(
    ReadOnlySpan<byte> data,
    int length);

// Usage — no fixed or GCHandle needed
byte[] buffer = Encoding.UTF8.GetBytes("hello");
int result = ProcessBuffer(buffer, buffer.Length);

// For output buffers use Span<byte>
[LibraryImport("mylib")]
static partial int FillBuffer(Span<byte> output, int maxLen);

소스 생성 활성화하기

.NET 7 이상을 대상으로 하는 프로젝트에서 System.Runtime.InteropServices 네임스페이스를 참조하면 소스 생성이 자동으로 활성화됩니다. 이전 대상 프레임워크에서는 Microsoft.Interop.SourceGeneration 분석기 패키지가 필요합니다.

<!-- In your .csproj — no extra package needed on .NET 7+ -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <!-- Enable Roslyn analyzers and source generators -->
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <!-- AllowUnsafeBlocks may be needed for some marshalers -->
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
</Project>

생성된 코드 확인하기

csproj에 <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>를 추가하면 생성된 파일이 obj/에 기록됩니다. 이를 통해 소스 생성기가 정확히 무엇을 생성하는지 확인할 수 있어 훌륭한 학습 연습이 됩니다.

// The generator emits something like this for a LibraryImport method:
// (simplified view of generated stub)

internal static partial int AddIntegers(int a, int b)
{
    // Direct call — no reflection, no boxing, fully inlineable
    return __PInvoke(a, b);

    [System.Runtime.InteropServices.DllImportAttribute(
        "mylib",
        EntryPoint = "add_integers",
        ExactSpelling = true)]
    static extern int __PInvoke(int a, int b);
}

실전: C 라이브러리 래핑하기

일반적인 패턴은 모든 LibraryImport 선언을 포함하는 정적 래퍼 클래스를 정의한 다음, 그 위에 높은 수준의 안전한 API를 노출하는 것입니다. partial 선언은 내부 또는 비공개로 유지하고, 안전한 래퍼만 공개적으로 노출하십시오.

internal static partial class LibSodiumNative
{
    private const string LibName = "libsodium";

    [LibraryImport(LibName, EntryPoint = "crypto_secretbox_keybytes")]
    internal static partial nuint KeyBytes();

    [LibraryImport(LibName, EntryPoint = "crypto_secretbox_easy")]
    internal static partial int SecretBoxEasy(
        Span<byte> ciphertext,
        ReadOnlySpan<byte> message,
        ulong mlen,
        ReadOnlySpan<byte> nonce,
        ReadOnlySpan<byte> key);
}

// Public safe wrapper hides the native signature
public static byte[] Encrypt(byte[] message, byte[] key, byte[] nonce)
{
    var cipher = new byte[message.Length + 16];
    LibSodiumNative.SecretBoxEasy(cipher, message, (ulong)message.Length, nonce, key);
    return cipher;
}

빠른 확인

[DllImport]보다 [LibraryImport]가 갖는 가장 큰 장점은 무엇인가요?

복습: LibraryImport 및 소스 생성 P/Invoke

핵심 요점:

  • AOT에 안전한 P/Invoke를 위해 [LibraryImport]가 [DllImport]를 대체합니다
  • 마샬링이 컴파일 시점에 생성되므로 런타임 리플렉션이 필요하지 않습니다
  • static partial 메서드와 partial 클래스가 필요합니다
  • 문자열 마샬링은 StringMarshalling 또는 MarshalAs를 통해 명시적으로 지정해야 합니다
  • 고정에 따른 오버헤드 없이 Span<T>를 일급으로 지원합니다
  • EmitCompilerGeneratedFiles를 사용하여 생성된 코드를 확인할 수 있습니다

자주 묻는 질문

“LibraryImport와 소스 생성 P/Invoke” 강의는 무료인가요?

네 — “LibraryImport와 소스 생성 P/Invoke” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 C# Academy 강의 전체를 잠금 해제할 수 있습니다. C# Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“LibraryImport와 소스 생성 P/Invoke”에서 뭘 배우나요?

[LibraryImport](C# 11+)를 사용해 AOT와 호환되고 DllImport보다 뛰어난 성능을 내는 소스 생성 마샬링을 구현합니다. 브라우저에서 직접 실행하는 실습 코드로 C# Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

C# Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 C# Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“LibraryImport와 소스 생성 P/Invoke” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 C# Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 C# Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. P/Invoke 기초
  2. LibraryImport와 소스 생성 P/Invoke
  3. 안전하지 않은 코드, 포인터 및 고정 버퍼
  4. COM 상호 운용 및 런타임 호출 가능 래퍼
← C# Academy(으)로 돌아가기