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