← Back to list

Working with Native Libraries in .NET

Sometimes .NET developers have to work with native libraries in the form of .dll (Windows) or .so (Linux) files. And this causes some…

Serhiy Krasovskyy · 2025-05-28 06:50 · 0 claps · 2.8 min read
#c-sharp-programming #c-libraries #dot-net-framework
Open on Medium ↗
Wiki topics: 💻 · Programming 🔓 · Open Source

Working with Native Libraries in .NET

Sometimes .NET developers have to work with native libraries in the form of .dll (Windows) or .so (Linux) files. And this causes some difficulties with their usage. Because you can’t add them statically like we do with .NET libraries. You have to use them dynamically.

I had trouble with this too, the first and the last time — surprisingly. Luckily, I have some experience with C/C++ — that helped a lot.

First of all, you always need to keep in mind that in native libraries, all data types except for simple ones are represented as pointers. In .NET this is mapped as IntPtr.

OK, let’s begin. Hopefully, the native library has some documentation with usage examples and method signatures. Because without that, it would be very painful. I won’t say it’s impossible, but it becomes much harder. We won’t cover that case here.

Let’s multiply two integers. For this, we just add a static method to a class:

[DllImport("TestLib", EntryPoint = "multiply",
       CallingConvention = CallingConvention.Cdecl)]
public static extern int Multiply(int x, int y);

Console.WriteLine("Multiply values 3 and 9");
var result = NativeLibFunctions.Multiply(3, 9);
Console.WriteLine($"Result: {result}");

That’s it, now we can use it in our app like a regular method.

Working with strings is a bit more interesting. We didn’t forget that a string is a pointer, right? Technically, in .NET a string is also a pointer, but not the same type 😊 So we can’t work with it directly like in the last example. We need to convert the input string into a C pointer, call the native function, and convert the result back to a string. Simple:

[DllImport("TestLib", EntryPoint = "modify_input",
       CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern IntPtr NativeModifyInputAnsi(IntPtr input);

public static string? ModifyInputAnsi(string input)
{
    var inputPointer = Marshal.StringToHGlobalAnsi(input);
    try
    {
        var modifiedInputPointer = NativeModifyInputAnsi(inputPointer);
        return modifiedInputPointer != IntPtr.Zero
            ? Marshal.PtrToStringAnsi(modifiedInputPointer)
            : null;
    }
    finally
    {
        Marshal.FreeHGlobal(inputPointer);
    }
}

But not so simple. We have at least two types of string encoding: ANSI and Unicode. So we need to explicitly set what encoding we use in the function signature. Here’s an example for Unicode strings:

 [DllImport("TestLib", EntryPoint = "modify_input_uni",
        CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
 private static extern IntPtr NativeModifyInputUnicode(IntPtr input);

 public static string? ModifyInputUnicode(string input)
 {
     var inputPointer = Marshal.StringToHGlobalUni(input);
     try
     {
         var modifiedInputPointer = NativeModifyInputUnicode(inputPointer);
         return modifiedInputPointer != IntPtr.Zero
             ? Marshal.PtrToStringUni(modifiedInputPointer)
             : null;
     }
     finally
     {
         Marshal.FreeHGlobal(inputPointer);
     }
 }

However, we typically use native libraries to work with more complex types. Actually, it’s quite simple — we handle objects like we did with strings:

public struct SaleResponse
{
    public int SaleId { get; set; }
    public double Amount { get; set; }
};

public struct SaleRequest
{
    public int Value { get; set; }
};
[DllImport("TestLib", EntryPoint = "test_sale",
       CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern IntPtr NativeTestSale(IntPtr saleRequest);

public static SaleResponse TestSale(SaleRequest saleRequest)
{
    var saleRequestPointer = Marshal.AllocHGlobal(Marshal.SizeOf(saleRequest));
    Marshal.StructureToPtr(saleRequest, saleRequestPointer, false);
    try
    {
        var saleResponsePointer = NativeTestSale(saleRequestPointer);
        return Marshal.PtrToStructure<SaleResponse>(saleResponsePointer)!;
    }
    finally
    {
        Marshal.FreeHGlobal(saleRequestPointer);
    }
}

And now we’ve reached the most complicated part. The thing that stopped me the last time I used native libraries was working with callbacks. The method signature in the documentation looks like this:

typedef void(__cdecl* TraceCallback)(int level, const char* message);

extern "C" __declspec(dllexport)
void __cdecl test_log_callback(int traceLevel, TraceCallback traceCallback)

But how do we call this kind of function from .NET? I was thinking for a long time until I realized two things: — We are working with pointers — The equivalent of a callback in C# is a delegate

And it all made sense — we follow the same logic as we did with strings and objects. Just add working with delegates. And here’s the result:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void IntTraceCallbackDelegate(int traceLevel, IntPtr traceMessagePointer);

public delegate void TraceCallback(int traceLevel, string traceMessage);

private static IntTraceCallbackDelegate _callbackKeeper;

[DllImport("TestLib", EntryPoint = "test_log_callback",
       CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern void NativeTestLogCallback(int traceLevel, IntPtr traceCallback);

public static void TestLogCallback(int traceLevel, TraceCallback traceCallback)
{
    _callbackKeeper = (level, ptr) =>
    {
        string? msg = ptr != IntPtr.Zero
            ? Marshal.PtrToStringAnsi(ptr)
            : null;
        traceCallback(level, msg!);
    };

    var traceDelegatePointer = Marshal
        .GetFunctionPointerForDelegate(_callbackKeeper);
    NativeTestLogCallback(traceLevel, traceDelegatePointer);
}

And its usage:

//Test the external library functions with callback
NativeLibFunctions.TestLogCallback(1, (int traceLevel, string traceMessage) =>
{
    var traceLevelStr = traceLevel switch
    {
        0 => "Error",
        1 => "Warning",
        2 => "Info",
        _ => "Unknown"
    };
    Console.WriteLine($"[{traceLevelStr.ToUpper()}] {traceMessage}");
});

The full code of working with native functions and the C code for the native library can be found on my GitHub: https://github.com/XHunter74/ExternalCalls


메타데이터
post_id
7959bf594d6b
slug
working-with-native-libraries-in-net-7959bf594d6b
url
https://medium.com/@serhiy-krasovskyy/working-with-native-libraries-in-net-7959bf594d6b
canonical_url
https://medium.com/@serhiy-krasovskyy/working-with-native-libraries-in-net-7959bf594d6b
author_url
https://medium.com/@serhiy-krasovskyy
status
ok
fetched_at
2026-07-14 19:14:01