Author avatar

Dániel Szabó

Invoking the Unmanaged

Dániel Szabó

  • Aug 23, 2019
  • 11 Min read
  • 32 Views
  • Aug 23, 2019
  • 11 Min read
  • 32 Views
Languages Frameworks and Tools
C#

Introduction

Every code that we write that relies on the .NET framework is called managed code. It is managed code as there are facilities provided by the framework which provide security and garbage collection. However, C# provides us with the possibility to call unmanaged code which is either hidden in a DLL or written entirely in another programming language like C or C++. COM components and the Win32 API also fall into the unmanaged code category.

When the .NET framework came to life, it was a requirement to provide support for calling and integrating unmanaged code into C# applications. This was not solely for C# but every language that relies on the .NET framework.

This support comes in the form of two main services. The first one is InteropServices and the second is Platform Invoke Services.

There are four criteria, one of which you need to meet to consume unmanaged code.

  1. Identify functions in DLLs (specify the function name and the DLL which holds it)
  2. Create a class to holds/group DLLs
  3. Create prototypes in managed code
  4. Call a DLL function.

Simple messagebox

The messagebox will come from the user32.dll. It allows you to show a small message box with a caption and a message. This DLL provides facilities and functions that are far more capable than the messagebox but, for this demonstration, it should be fine.

In order to use the DLL, we need to start our application with the following line.

1
using System.Runtime.InteropServices;
csharp

This gives us the DllImport function that is able to load a specified DLL.

1
[DllImport("user32.dll")]
csharp

This statement loads the DLL to our disposal. our namespace is almost ready to manipulate the messagebox and show our messages.

We need to define the structure of the messagebox which needs to be exactly the same as in the DLL.

1
2
static extern int MessageBoxA(int hWnd, string strMsg,
                              string strCaption,uint iType);
csharp

The hWnd is going to give us a handle for the owner of the window. The strMsg will be the message we want to show in our window; it's of the type LPCTSTR that comes from the DLL. The strCaption is the caption of our message box which is of the same type as the strMsg. Finally, we have the iType which is an unsigned integer, this controls the contents and behavior of our dialog box.

Some examples of iType variants are:

  1. 0x00000002L -> gives us Abort,Retry,Ignore buttons.
  2. 0x00000006L -> gives us Cancel, Try Again, Ignore buttons.
  3. 0x00000004L -> gives us Yes, No buttons.

After this is done, all that is left to do is to complete our Main function and call the external code.

1
2
3
4
5
6
7
static void Main(string[] args)
{
	MessageBoxA(0,
				"Hello, Pluralsight!",
				"Welcome to the written guides!",
				Convert.ToUInt32(0x00000002L));
}
csharp

The result looks like this.

The maps

As you recall from the enlist, the value 0x00000002L results in three buttons: the Abort the Retry, and the Ignore. Since this is a demonstration we don't really care about what happens at these button clicks. But you need to know that you should handle each event appropriately, otherwise it can result in an application crash. The messagebox returns an integer when a button is clicked and this is how you handle the associated events based on the clicked button. The Convert.ToUInt32() function is necessary because the argument type is unit but the documentation holds the sample values in long datatype, which are not directly convertible.

Let's look at an example of the return value of these buttons. We need to modify the code to look like this:

1
2
3
4
5
6
int buttonPushed = MessageBoxA(0,
	"Hello, Pluralsight!",
	"Welcome to the written guides!",
	Convert.ToUInt32(0x00000002L));
    
    Console.WriteLine($"The button push resulted in value: {buttonPushed}");
csharp

Pushing the Abort button gives us the following output.

1
The button push resulted in value: 3
bash

Pushing the Retry button gives us the following output.

1
The button push resulted in value: 4
bash

Last, but not least, the Ignore gives us the following output.

1
The button push resulted in value: 5
bash

Note a common mistake that developers make: they don't read the API documentation and they pass either incompatible or not implicitly compatible arguments to the functions or classes coming from unmanaged code. This results in application crashes and lots of headaches. Save yourself some time and ALWAYS read the documentation.

C++ Unmanaged Code Wrapper

This part of the guide will show you how you can invoke unmanaged C++ code and build a project around it. You will need to have the CLI package installed for C++ in Visual Studio and, please note, this example was only tested Visual Studio 2017.

We need to create a new project called Core; this should be an empty Visual C++ project. Once the project is generated, you need to right-click on the project go to properties. On the General tab, change the Configuration Type to Static Library (.lib). Then, under the C/C++ - Precompiled Headers, set the Precompiled Header to Not Using Precompiled Headers.

Now, right-click on the Core in the solution explorer and add a new header file called Worker.h. This is, basically, a worker with a salary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma once
namespace Core
{
	class Worker
	{
	public:
		const char* m_Name;
	private:
		float m_Salary;
	public:
		Worker(const char* name, float salary);

		void raiseSalary(float amount);
		inline float GetSalary() const { return m_Salary; };
	};
}
cpp

We need another header file called Core.h. This is for further extensibility and you can skip creating this file without repercussions. But, if you do create it, make sure you include the following two lines in it.

1
2
#pragma once
#include "Worker.h"
cpp

Last, but not least, we need real code to power our app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "Worker.h"
#include <iostream>
namespace Core
{
	Worker::Worker(const char* name, float salary)
		: m_Name(name), m_Salary(salary)
	{
		std::cout << "Successfully created Worker object!" << std::endl;
	}
	void Worker::raiseSalary(float amount)
	{
		m_Salary *= amount;
		std::cout << "Raised salary of Worker" << m_Name << " to " << m_Salary << std::endl;
	}
}
cpp

Now we need a new project. Right-Click on our project and add a new project called Wrapper. This should be a Class Library under Visual C++ - CLR.

First, we define our Worker.h header file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#pragma once
#include "ManagedObject.h"
#include "../Core/Core.h"
using namespace System;
namespace CLI
{
	public ref class Worker : public ManagedObject<Core::Worker>
	{
	public:

		Worker(String^ name, float salary);
		void raiseSalary(float amount);
		property float Salary
		{
		public:
			float get()
			{
				return m_Salary->GetSalary();
			}
		private:
			void set(float value)
			{
			}
		}
	};
}
cpp

Our ManagedObject.h gives us the flexibility to further extend the app. It also provides a means for the destructor to the Garbage Collector to clean up after the app execution. This is a template that can be found on the internet, as it is very common to use it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#pragma once
using namespace System;
namespace CLI {

	template<class T>
	public ref class ManagedObject
	{
	protected:
		T* m_Instance;
	public:
		ManagedObject(T* instance)
			: m_Instance(instance)
		{
		}
		virtual ~ManagedObject()
		{
			if (m_Instance != nullptr)
			{
				delete m_Instance;
			}
		}
		!ManagedObject()
		{
			if (m_Instance != nullptr)
			{
				delete m_Instance;
			}
		}
		T* GetInstance()
		{
			return m_Instance;
		}
	};
}

using namespace System::Runtime::InteropServices;
static const char* string_to_char_array(String^ string)
{
	const char* str = (const char*)(Marshal::StringToHGlobalAnsi(string)).ToPointer();
	return str;
}
cpp

In order to link the Core application with the Wrapper, we need to add it as a reference. We need to right-click onto the Wrapper project, then Add/Refernce, select the Core, and then click apply.

The last thing we need to do is to create an application in C# which will invoke the unmanaged code.

We need to create our HR project, which is a C# project, from the (.NET Framework) type.

This is all the code we need to have in there:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using CLI;
namespace HR
{
    class Program
    {
        static void Main(string[] args)
        {
            Worker daniel = new Worker("Dániel Szabó", 100000);
            Console.WriteLine($"The worker: {daniel.Name} salary before the raise is: {daniel.Salary}");
            daniel.raiseSalary(1.1);
            Console.WriteLine($"The worker: {daniel.Name} salary after the raise is: {daniel.Salary}");
            
            Console.Read();
        }
    }
}
csharp

Before we can run this application, we need to build the Wrapper project. This is done by right-clicking on the project in the solution and then clicking Build. Now, the last thing we need to do is to set HR to be the default startup project. This is done in the solution explorer too. Just right-click on the project and then click the Set as StartUp Project.

Now we can start our app and, if the instructions were followed thoroughly, we should see the following:

1
2
3
4
Successfully created Worker object!
The worker: Dániel Szabó salary before the raise is: 100000
Raised salary of Worker Dániel Szabó to 110000
The worker: Dániel Szabó salary after the raise is: 110000
bash

Conclusion

As there are as many programming languages as there are stars in the sky, it's hard to tell you a general rule of thumb which answers the question, "How to call unmanaged code from C#?" The internet is a wonderful thing, full of examples, and I encourage you to try and find the solution for your questions. The two examples I have shown you will only get you started.

As you saw, calling existing DLLs and integrating them to your app is easier than building your own DLLs and mapping them to your C# to be called as unmanaged code. The beauty of the latter is the control it gives you over the application. Since you provide the DLLs, you are able to shape them to your needs. I honestly hope you found what you were looking for and the time you spent reading this was something one might call worthwhile!

0