Skip to content

Interop in C# — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Interop in C#. We cover key concepts, practical examples, and best practices to help you master this topic.

Hook

Not everything is written in C#. Sometimes you need to call operating system APIs, use native C++ libraries, or interoperate with COM components. .NET provides several mechanisms for interoperability: P/Invoke for C-style APIs, COM interop for Windows components, and unsafe code for direct memory manipulation.

Learning Path

graph LR
  A[Interop] --> B[P/Invoke]
  A --> C[Unsafe Code]
  A --> D[COM Interop]
  B --> E[DllImport]
  B --> F[Marshalling]
  C --> G[Pointers]
  D --> H[RCW]
  style A fill:#4a90d9,color:#fff
  style B fill:#4a90d9,color:#fff
  style C fill:#4a90d9,color:#fff
  style D fill:#4a90d9,color:#fff
  style E fill:#4a90d9,color:#fff
  style F fill:#4a90d9,color:#fff
  style G fill:#4a90d9,color:#fff
  style H fill:#4a90d9,color:#fff

Platform Invocation (P/Invoke)

P/Invoke lets you call functions from native DLLs.

using System;
using System.Runtime.InteropServices;

public static class NativeMethods
{
    // Windows API
    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    public static extern int MessageBox(
        IntPtr hWnd,
        string lpText,
        string lpCaption,
        uint uType);

    // Linux/POSIX
    [DllImport("libc", SetLastError = true)]
    public static extern int getpid();

    // macOS
    [DllImport("libSystem.dylib")]
    public static extern int getpid_mac();
}

// Usage
// NativeMethods.MessageBox(IntPtr.Zero, "Hello from C#!", "P/Invoke", 0);
Console.WriteLine($"Process ID: {NativeMethods.getpid()}");

Struct Marshalling

Pass complex data structures to native code.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SystemInfo
{
    public ushort wProcessorArchitecture;
    public ushort wReserved;
    public uint dwPageSize;
    public IntPtr lpMinimumApplicationAddress;
    public IntPtr lpMaximumApplicationAddress;
    public IntPtr dwActiveProcessorMask;
    public uint dwNumberOfProcessors;
    public uint dwProcessorType;
    public uint dwAllocationGranularity;
    public ushort wProcessorLevel;
    public ushort wProcessorRevision;
}

[DllImport("kernel32.dll")]
public static extern void GetSystemInfo(out SystemInfo lpSystemInfo);

// Usage
GetSystemInfo(out var info);
Console.WriteLine($"Processors: {info.dwNumberOfProcessors}");
Console.WriteLine($"Page size: {info.dwPageSize} bytes");

Source-Generated P/Invoke (.NET 7+)

Use source generators for better performance.

// Source-generated P/Invoke (.NET 7+)
[LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)]
public static partial int MessageBoxW(IntPtr hWnd, string text, string caption, uint type);

// No marshalling code at runtime - generated at compile time

Unsafe Code and Pointers

Work directly with memory using unsafe blocks.

public static unsafe class UnsafeOperations
{
    public static void PointerDemo()
    {
        int value = 42;
        int* ptr = &value;

        Console.WriteLine($"Value: {value}");
        Console.WriteLine($"Pointer: {(nint)ptr:X}");
        Console.WriteLine($"Dereferenced: {*ptr}");

        *ptr = 100;
        Console.WriteLine($"Modified value: {value}");
    }

    public static void ArrayPointer()
    {
        int[] numbers = { 10, 20, 30, 40, 50 };

        fixed (int* ptr = numbers)
        {
            for (int i = 0; i < numbers.Length; i++)
            {
                Console.WriteLine($"numbers[{i}] = {*(ptr + i)}");
            }
        }
    }

    public static void StackAlloc()
    {
        // Allocate on stack (no GC pressure)
        int* buffer = stackalloc int[256];
        for (int i = 0; i < 256; i++)
            buffer[i] = i * i;

        Console.WriteLine($"buffer[100] = {buffer[100]}");
    }

    public static void MemoryCopy(byte* source, byte* destination, int length)
    {
        for (int i = 0; i < length; i++)
            destination[i] = source[i];
    }
}

// Must compile with AllowUnsafeBlocks
// <AllowUnsafeBlocks>true</AllowUnsafeBlocks> in .csproj

COM Interop

Interoperate with COM components on Windows.

// Early binding (requires COM reference)
// Add reference to Microsoft Excel Object Library
using Microsoft.Office.Interop.Excel;

public class ExcelExporter
{
    public void ExportToExcel(string[,] data)
    {
        var excel = new Application { Visible = true };
        var workbook = excel.Workbooks.Add();
        var worksheet = (Worksheet)workbook.Sheets[1];

        for (int row = 0; row < data.GetLength(0); row++)
            for (int col = 0; col < data.GetLength(1); col++)
                worksheet.Cells[row + 1, col + 1] = data[row, col];

        // Release COM objects
        Marshal.ReleaseComObject(worksheet);
        Marshal.ReleaseComObject(workbook);
        Marshal.ReleaseComObject(excel);
    }
}

// Late binding (no COM reference needed)
Type? excelType = Type.GetTypeFromProgID("Excel.Application");
object? excel = Activator.CreateInstance(excelType!);
excelType?.InvokeMember("Visible", BindingFlags.SetProperty, null, excel, new object[] { true });

Native Memory Management

Allocate and free native memory manually.

public static class NativeMemoryDemo
{
    public static IntPtr AllocateAndUse(int size)
    {
        // Allocate unmanaged memory
        IntPtr ptr = Marshal.AllocHGlobal(size);

        // Write data
        for (int i = 0; i < size; i++)
            Marshal.WriteByte(ptr, i, (byte)i);

        // Read data
        byte first = Marshal.ReadByte(ptr);
        Console.WriteLine($"First byte: {first}");

        return ptr; // Caller must free
    }

    public static void Free(IntPtr ptr)
    {
        Marshal.FreeHGlobal(ptr);
    }

    // SafeHandle for automatic cleanup
    public class NativeBuffer : SafeHandle
    {
        public NativeBuffer(int size)
            : base(IntPtr.Zero, ownsHandle: true)
        {
            SetHandle(Marshal.AllocHGlobal(size));
        }

        public override bool IsInvalid => handle == IntPtr.Zero;

        protected override bool ReleaseHandle()
        {
            Marshal.FreeHGlobal(handle);
            return true;
        }
    }
}

NativeAOT

Compile .NET to native code for scenarios where the runtime is not available.

<!-- .csproj -->
<PropertyGroup>
  <PublishAot>true</PublishAot>
  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
# Publish as native binary
dotnet publish -r win-x64 -c Release
# Produces a single native executable with no .NET runtime dependency

Common Mistakes

  1. Incorrect marshalling of strings: Use CharSet = CharSet.Unicode and MarshalAs attributes to match native string encoding.

  2. Memory leaks with native resources: Always free native allocations. Use SafeHandle for automatic cleanup.

  3. P/Invoke stack imbalance: Ensure calling convention matches (CallingConvention.Cdecl vs CallingConvention.StdCall).

  4. Forgetting to pin managed objects: Use fixed statement or GCHandle.Alloc when passing managed arrays to native code.

  5. Unsafe code without proper validation: Validate all pointer operations to avoid buffer overflows and memory corruption.

Practice Questions

  1. Write a P/Invoke wrapper for the clock_gettime system call on Linux to measure high-resolution time.

  2. Create a library that uses unsafe code to perform fast byte array comparison (like memcmp).

  3. Build a COM-based automation tool that controls Microsoft Word from C#.

  4. Challenge: Implement a memory-mapped file reader using P/Invoke to CreateFileMapping and MapViewOfFile.

FAQ

What is the performance cost of P/Invoke?

Each P/Invoke call has overhead for marshalling and security checks. Batch calls when possible to minimize transitions.

Can I call C++ classes from C#?

Not directly. Use C-style wrapper functions (extern C) around C++ classes, or use C++/CLI for managed/unmanaged bridging.

Is unsafe code safe to use?

Unsafe code is not verifiable by the CLR. Use it only when necessary and validate all pointer operations carefully.

What is the difference between COM and P/Invoke?

P/Invoke calls individual C functions from DLLs. COM is an object-oriented component model with lifetime management through reference counting.

What is NativeAOT?

NativeAOT compiles .NET code to a native binary with no JIT or runtime dependency. Startup is instantaneous but some reflection features are limited.

Mini Project: System Monitor

Use P/Invoke to display system information across platforms.

using System;
using System.Runtime.InteropServices;

public static class SystemMonitor
{
    // Windows
    [DllImport("kernel32.dll")]
    private static extern bool GlobalMemoryStatusEx(out MEMORYSTATUSEX lpBuffer);

    [StructLayout(LayoutKind.Sequential)]
    private struct MEMORYSTATUSEX
    {
        public uint dwLength;
        public uint dwMemoryLoad;
        public ulong ullTotalPhys;
        public ulong ullAvailPhys;
        public ulong ullTotalPageFile;
        public ulong ullAvailPageFile;
        public ulong ullTotalVirtual;
        public ulong ullAvailVirtual;
        public ulong ullAvailExtendedVirtual;
    }

    // Linux
    [DllImport("libc", SetLastError = true)]
    private static extern int sysinfo(out SysInfo info);

    private struct SysInfo
    {
        public long uptime;
        public ulong[] loads;
        public ulong totalram;
        public ulong freeram;
        public ulong sharedram;
        public ulong bufferram;
        public ulong totalswap;
        public ulong freeswap;
        public ushort procs;
        public ulong totalhigh;
        public ulong freehigh;
        public uint mem_unit;
    }

    public static void ShowSystemInfo()
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            GlobalMemoryStatusEx(out var mem);
            Console.WriteLine("Windows System Info:");
            Console.WriteLine($"  Memory load: {mem.dwMemoryLoad}%");
            Console.WriteLine($"  Total RAM: {mem.ullTotalPhys / (1024 * 1024 * 1024)} GB");
            Console.WriteLine($"  Available: {mem.ullAvailPhys / (1024 * 1024 * 1024)} GB");
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            sysinfo(out var info);
            Console.WriteLine("Linux System Info:");
            Console.WriteLine($"  Uptime: {info.uptime / 3600} hours");
            Console.WriteLine($"  Total RAM: {info.totalram / (1024 * 1024 * 1024)} GB");
            Console.WriteLine($"  Free RAM: {info.freeram / (1024 * 1024 * 1024)} GB");
            Console.WriteLine($"  Processes: {info.procs}");
        }
        else
        {
            Console.WriteLine($"OS: {RuntimeInformation.OSDescription}");
            Console.WriteLine($"Arch: {RuntimeInformation.OSArchitecture}");
        }
    }
}

SystemMonitor.ShowSystemInfo();

Interop capabilities make C# uniquely powerful -- you can leverage existing native libraries, access operating system APIs, and interoperate with other technologies. The .NET platform's interop features give you the best of both managed and native worlds.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro