In my practice, I have several times faced the case of integrating and communicating with low-level languages (C/C++) and low-level APIs like Windows APIs.
This tutorial will simplify my practice of using low-level languages and APIs and will simply demonstrate how to write and integrate a simple C library to your C# application along with Windows API integration.
This topic has a special name called Platform Invocation(P/Invoke). NET.
- P/Invoke (Platform Invocation) is a powerful mechanism in C# that allows you to interact with unmanaged code libraries (typically DLLs) from your managed .NET applications. This enables you to leverage existing C or C++ codebases or access system-level functionalities that aren't directly exposed in the .NET framework.
- P/Invoke is essential in C# for bridging the gap between the managed world of .NET and the unmanaged world of native code (typically C or C++)
- Many organizations have substantial investments in C or C++ code. P/Invoke allows you to leverage this existing functionality without rewriting it in C#. It's often more efficient to integrate existing code than to recreate it from scratch.
- We usually work with managed code in .Net, but sometimes we need to integrate, for instance, some devices that don’t have a C# driver/wrapper. Most of the device integrations are written in low-level languages like C/C++.
- P/Invoke provides access to system hardware and devices that might not be directly exposed in the .NET framework. For tasks demanding maximum performance, such as real-time processing or high-throughput applications, direct interaction with the operating system can be beneficial.
- Some functionalities are only available through platform-specific APIs, which can be accessed using P/Invoke. If a required library is only available in native form, P/Invoke is the way to utilize it.
- My first experience with Platform Invocation was back in 2013 when we planned to integrate a smartcard reader into our C# application. The driver for the reader was written in C.
Exposing Windows API through P/Invoke
In this part, we will focus on integrating user32.dll’s functionality into our C# application.
User32.dll is a critical component of the Windows operating system responsible for managing the user interface (UI). It provides the core functions for creating, manipulating, and displaying windows, menus, dialog boxes, and other graphical elements.
Here are the key Functions.
- It allows you to manage Windows: Creating, moving, resizing, and destroying windows.
- It has input handling possibility: Processing keyboard and mouse events.
- It also allows to handling Messages: Managing the message loop for window-based applications.
- You can draw something: Interacting with the GDI32.dll to render graphics and text.
- Possible to do clipboard operations: Providing functions for copying and pasting data.
Most Windows applications, whether written in C++, C#, or other languages, rely on User32.dll for their UI components. When you interact with a window, button, or menu, the application makes calls to functions within User32.dll to handle the corresponding actions.
User32.dll lives inside C:/windows/SysWow64 (for 64bit) and C:/Windows/system32 (32 bit) folders.
Let's create a new Console application with the name User32ConsoleApp.
We plan to use the MessageBox function of user32.dll. This is one of the most popular functions in user32.dll. As you might guess, .NET already uses it in WinForms and WPF but a few of us know that it is just a wrapper over user32.dll’s MessageBox function.
When working with Platform Invocation we simply delegate the functionality to the source (i.e. user32.dll) Think of it like invoking some function remotely. You should just declare the signature of the called function with some additional attribute.
MessageBox is a fundamental function in the User32.dll library that displays a modal dialog box containing a system icon, a set of push buttons, and an application-specific message. It's commonly used to provide information, warnings, or errors to the user.
int WINAPI MessageBox(
_In_opt_ HWND hWnd,
_In_ LPCTSTR lpText,
_In_ LPCTSTR lpCaption,
_In_ UINT uType
);
Parameters
- hWnd: A handle to the owner window of the message box. If NULL, the message box has no owner window.
- lpText: The text to be displayed in the message box.
- lpCaption: The text is to be displayed in the title bar of the message box.
- uType: An integer that specifies the contents and behavior of the message box.
Return Value
The return value indicates which button the user pressed.
In order to call MessageBox from user32.dll in our C# application, we should declare a signature of the function in C#. It is a bit similar to when invoking functions from EF core. You just declare it but under the hood system will automatically bind it to the appropriate function. Here is what it looks like in C#:
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(
IntPtr owner,
string message,
string title,
uint type
);
This C# code above declares a method named MessageBox that allows you to interact with the unmanaged MessageBox function in the user32.dll library. Let's break down the components.
Attributes
[DllImport("user32.dll")]: This attribute specifies that the following method is an external function imported from the "user32.dll" library. This DLL contains Windows API functions for user interface elements. There is no need to specify the full path to the DLL. The system will automatically scan the required folders and find the appropriate library to bind.
CharSet = CharSet.Unicode: This attribute indicates that the character set used for string parameters should be Unicode. This is important for handling international characters correctly.
Method Declaration
- private: The method is private, meaning it can only be accessed within the same assembly.
- static: The method is static, meaning it can be called without creating an instance of the class.
- extern: The method's implementation is located in an external library (user32.dll).
Parameters
- IntPtr owner: A handle to the owner window of the message box.
- string message: The text to be displayed in the message box.
- string title: The text to be displayed in the title bar of the message box.
- uint type: An unsigned integer that specifies the contents and behavior of the message box.
This code provides a managed wrapper around the unmanaged MessageBox function. It allows us to call the MessageBox function from your C# code without directly dealing with unmanaged code. The CharSet.Unicode attribute ensures correct handling of text in different languages.
By using this declaration, we can easily display message boxes in our C# applications by calling the MessageBox method with the appropriate parameters.
Here is how we call it.
static void Main(string[] args)
{
MessageBox(IntPtr.Zero, "Hello from messagebox", "messagebox title", 0);
}
Here is the result.
Well, that is not all. We can provide different arguments for our fourth parameter.
static void Main(string[] args)
{
const int MS_OK = 0;
const int OK_CANCEL = 1;
const int MS_STOP = 16;
MessageBox(IntPtr.Zero, "Hello from messagebox", "messagebox title", OK_CANCEL);
}
Here is the result.
Writing and exposing the C library from .NET.
In this section, we will focus on writing and calling C library from .NET using platform invocation. We will follow approximately the same flow that we did for Windows API integration.
First, let's create a new C library that multiplies 2 numbers.
Open a new text editor ( you can use Notepad also) , paste the following code, and save it with the .c extension (it is file.c in our case).
int multiply(int a, int b) {
return a * b;
}
This C code defines a function named multiply that takes two integer arguments, a and b. The function calculates the product of a and b using the multiplication operator (*). The result, which is also an integer, is then returned from the function.
Now let us compile it. There are many C compilers out there, and you can select any of them. After installing just navigate to the folder where you have file.c file, and from the command line, type the following.
gcc -shared -o file.dll file.c
This command instructs the GCC compiler to create a dynamic link library (DLL) file named "file.dll" from the C code contained in "file.c".
Here's a breakdown of the command.
- gcc: This is the GNU Compiler Collection, a powerful toolchain used for compiling various programming languages.
- shared: This flag tells GCC to create a shared library, which is essentially a DLL.
- o file.dll: This specifies the output file name as "file.dll".
- file.c: This is the source code file containing the C code to be compiled into the DLL.
Long story short, this command takes the C code in "file.c", compiles it into a DLL format, and saves the resulting library as "file.dll". This DLL can then be used by other programs that require the functions defined within it.
The integration part to .NET is really simple. We have the same approach as we had in Windows API integration. In the same class, let's add the following lines.
[DllImport("file.dll")]
private static extern int multiply(int a, int b);
Here is our Main method.
static void Main(string[] args)
{
int firstNumber = 67;
int secondNumber = 90;
Console.WriteLine($"{nameof(firstNumber)} * {nameof(secondNumber)} = {multiply(firstNumber, secondNumber)}");
}
The prolog
Platform Invocation, while often perceived as a complex mechanism, is an indispensable tool for C# developers seeking to extend the capabilities of their applications beyond the .NET framework. By carefully considering its strengths and limitations, developers can effectively leverage P/Invoke to integrate legacy code, access system-level functionalities, and optimize performance-critical operations. As technology continues to evolve, P/Invoke remains a valuable asset, empowering developers to build robust and efficient software solutions.