Calling a C# library from C

There are many articles on the web that talk about how to call native libraries from C#, but what about the other way around? 

In this article I'll describe the steps that needs to be taken, to call a C# library from a C program. Personally I am a big fan of Serilog and decided to see if it was possible to use this logging framework from a C program. Turns out, it is!

If you prefer just to check out the source code, it is available at my Github page.

C# related

To support compiling C# to a native binary, you will need to add the following NuGet package to the project: Microsoft.DotNet.ILCompiler 7.0.0 which is currently out in preview.

In the current preview version, publishing from within Visual Studio does not seem to pick up the IL compiler properly. Use the command prompt to publish and ensure that it is published as self contained. The full command used in this example is:

dotnet publish /p:NativeLib=Shared --self-contained -r win-x64 -c release

Exporting a C# method 

Use the UnmanagedCallersOnly attribute to mark a method as native.  There are several restrictions on the methods:

  • Method must be marked static.
  • Must not be called from managed code.
  • Must only have blittable arguments.
  • Must not have generic type parameters or be contained within a generic class.

In this example, I'll highlight the LogMessage method that I want to call from C.

The log message should be passed as a pointer, since we're in C# we use an IntPtr.  To convert the pointer to a string, PtrToStringAnsi  is used.

[UnmanagedCallersOnly(EntryPoint = "log_message")]
public static void LogMessage(int level, IntPtr message)
{
	// Parse strings from the passed pointers
	string logMessage = Marshal.PtrToStringAnsi(message) ?? string.Empty;

	if (_log == null)
	{
		Console.WriteLine($"Unable to load Serilog. Message received was: {logMessage}");
		return;
	}
	
	_log.LogMessage(level, logMessage);
}

 

C related

In this sample the native C# library will be loaded in during runtime. The following header adds the support that's needed.

#ifdef _WIN64
#include <windows.h>
#include <direct.h> // _getcwd
#define symLoad GetProcAddress
#else
#include dlfcn.h
#include unistd.h
#define symLoad dlsym
#endif

Defining the C# prototype in C.

typedef void (cLogMessage)(int, char*);
cLogMessage* logMessage;

Loading the native C# DLL and getting a reference to the log_message method.

#ifdef _WIN64
  HINSTANCE logCHandle = LoadLibraryA("SampleLog.dll");
#else
  void logCHandle = dlopen("SampleLog.so", RTLD_LAZY);
#endif

logMessage = (cLogMessage*) symLoad(logCHandle, "log_message");

 

After the steps above , you are able to invoke the log_message method just like any other C method.

logMessage(0, "Verbose message");

 

The initialization of Serilog is left out from this article, but can be viewed in the Github repository.  In the demo there is also the option to use a configuration file for Serilog.

There are some other caveats I ran into while testing this demo. In C# I often use the following line to get the directory of the executable. This does not return the proper value anymore when I call it from C.

Directory.GetParent(Assembly.GetExecutingAssembly().Location)?.FullName;

To workout around this, I simply fetch the current directory from C and then pass it to C#.

The C demo app should produce the following output:

[00:20:54 VRB] Verbose message
[00:20:54 DBG] Debug message
[00:20:54 INF] Informational message
[00:20:54 WRN] Warning message
[00:20:54 ERR] Error message
[00:20:54 FTL] Fatal message

 



Profile picture
Gideon Bakx
Updated on: Friday, October 6, 2023 8:24:52 PM
C# certified software engineer living in Calgary, Canada.
C C# Serilog Native