0Pricing
C# Academy · Lesson

LibraryImport & Source-Generated P/Invoke

Use [LibraryImport] (C# 11+) for AOT-compatible, source-generated marshaling that outperforms DllImport.

LibraryImport & Source-Generated P/Invoke is a free C# Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why LibraryImport?

[LibraryImport] was introduced in .NET 7 (C# 11) as a source-generated, AOT-compatible replacement for [DllImport]. The classic DllImport relies on runtime marshaling via reflection — problematic for Native AOT. LibraryImport emits all marshaling code at compile time.

Declaring a LibraryImport Method

Mark a static partial method with [LibraryImport]. The source generator fills in the implementation. You must also mark the containing class 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);
}

String Marshaling in LibraryImport

Unlike DllImport, LibraryImport requires you to explicitly specify string marshaling. You can use StringMarshalling enum or MarshalAs attributes, making the marshaling cost visible and controllable.

// 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);

Struct Marshaling

For structs, add [NativeMarshalling] to define how the managed type maps to its native representation. The source generator uses the marshaller type to emit safe, allocation-minimizing code.

[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; }
}

Comparing DllImport vs LibraryImport

DllImport is interpreted at runtime — slow startup, reflection-based, incompatible with AOT trimming. LibraryImport generates optimized C# code at build time: zero runtime reflection, trim-safe, and measurably faster in benchmarks.

// 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 & Error Handling

Set SetLastError = true in [LibraryImport] to capture the OS error code. Use Marshal.GetLastPInvokeError() (preferred) or Marshal.GetLastWin32Error() to retrieve it after the call.

[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> and Memory Marshaling

One of the advantages of source-generated P/Invoke is first-class Span<T> support. Passing a ReadOnlySpan<byte> avoids pinning and allocation that DllImport with arrays would require.

[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);

Enabling Source Generation

Source generation is enabled automatically when you reference the System.Runtime.InteropServices namespace in a project targeting .NET 7+. For older targets you need the Microsoft.Interop.SourceGeneration analyzer package.

<!-- 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>

Inspecting Generated Code

Add <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> to your csproj to write generated files to obj/. This lets you inspect exactly what the source generator produces — a great learning exercise.

// 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);
}

Real-World: Wrapping a C Library

A common pattern is to define a static wrapper class with all LibraryImport declarations, then expose a high-level safe API on top. Keep the partial declarations internal/private and expose only the safe wrappers publicly.

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;
}

Quick Check

What is the primary advantage of [LibraryImport] over [DllImport]?

Recap: LibraryImport & Source-Generated P/Invoke

Key takeaways:

  • [LibraryImport] replaces [DllImport] for AOT-safe P/Invoke
  • Marshaling is generated at compile time — no runtime reflection
  • Requires static partial method and partial class
  • String marshaling must be explicit via StringMarshalling or MarshalAs
  • First-class Span<T> support without pinning overhead
  • Inspect generated code with EmitCompilerGeneratedFiles

Frequently asked questions

Is the “LibraryImport & Source-Generated P/Invoke” lesson free?

Yes — the full text of “LibraryImport & Source-Generated P/Invoke” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.

What will I learn in “LibraryImport & Source-Generated P/Invoke”?

Use [LibraryImport] (C# 11+) for AOT-compatible, source-generated marshaling that outperforms DllImport. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C# Academy?

No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “LibraryImport & Source-Generated P/Invoke” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C# Academy lesson?

Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. P/Invoke Fundamentals
  2. LibraryImport & Source-Generated P/Invoke
  3. Unsafe Code, Pointers & Fixed Buffers
  4. COM Interop & Runtime Callable Wrappers
← Back to C# Academy