Introduction
Welcome to my second article on .NET programming, specifically about C++/CLI. Shortly after I wrote the previous article, the C++/CLI was becoming more in use, thus rendering MC++ obsolete. If you've read the previous MC++ article, then reading this one is easier and will only update your knowledge.
The goal of this article is to present you with most of the information needed for you to start using C++/CLI in a short amount of time (given that you come from a C++ and .NET background). The information will be presented in a "how-to" manner, and you will be provided with an example for every topic introduced.
Before you proceed, it is recommended that you warm-up your neurons and take a look at these two articles [1] & [8].
You may build each example through the following command line (or what is equivalent to it):
"C:\Program Files\Microsoft Visual Studio 8\Common7\Tools\vsvars32.bat"
Setup the environment (do this once), then compile with the CL tool as:
cl your_file.cpp /clr
Table of contents
- What's C++/CLI
- Handles and Pointers
- Hello World
- Classes and UDTs
- Arrays
- Parameter Array
- Properties
- Wrapping Around a Native C++ Class
- Wrapping Around C Callbacks
- The Other Way Round: From Managed to C Callbacks
C++, as you already know, is a high-level language that is mainly considered a superset of the C language, which adds many features such as OOP and templates, but what's CLI?
CLI stands for Common Language Infrastructure. It is explained thoroughly all over the web, but in short: It is an open specification that describes the executable code and runtime environment that allows multiple high-level languages to be used on different computer platforms without being rewritten for specific architectures. [2]
Now, C++/CLI is the means to program .NET in C++, similarly like C# or VB.NET are used.
Handles and Pointers
You may have seen the punctuator "^" symbol in C++/CLI code and wondered about it. As you know, in C++, we have the "*
" to denote a pointer, and in C++/CLI, we have the ^
to denote a handle. Now, a "*
" designates a native pointer which resides on the CRT heap, whereas the handles designate "safe pointers" and reside on the managed heap. The handles can be thought of as references, and unlike native pointers, they won't cause memory leaks if they are not properly deleted, since the GC will take care of that, and they don't have a fixed memory address and thus will be moved around during execution.
To create a new reference of a certain class or value type, we have to allocate it with the "gcnew
" keyword; for example:
System::Object ^x = gcnew System::Object();
It is worthwhile noting that the "nullptr
" keyword designates a null reference. In addition to the punctuator "^
" symbol, we have the percent "%
" which stands for a tracking reference; I would like to quote the ECMA-372:
N* pn = new N; N& rn = *pn; R^ hr = gcnew R; R% rr = *hr;
In general, the punctuator %
is to ^
as the punctuator &
is to *
.
Let us get started: Hello World
In this section, you will learn how to create a simple skeleton C++/CLI program. To start, you need to know how to define a correct "main
". As you will notice, both prototypes (C's main
and C++/CLI's main
) require an array of strings to be passed to them.
#using <mscorlib.dll>
using namespace System;
int main(array<System::String ^> ^args)
{
System::Console::WriteLine("Hello world");
return 0;
}
Classes and UDTs
Classes
In this example, we will illustrate how to create classes and user defined types. To create a managed class, all you have to do is prefix the class definition with a protection modifier followed by "ref
", thus:
public ref class MyClass
{
private:
public:
MyClass()
{
}
}
And to create a native class, you simply create it the way you know. Now, you may wonder about destructors in C++/CLI and if they still behave the same, and the answer is yes, destructors (which are deterministic) are still used the same way they are used in C++; however, the compiler will translate the destructor call into a Dispose()
call after transparently implementing the IDisposable
interface for you. In addition to that, there is the so called finalizer (non-deterministic) that is called by the GC, and it is defined like this: "!MyClass()
". In the finalizer, you may want to check if the destructor was called, and if not, you may call it.
#using <mscorlib.dll>
using namespace System;
public ref class MyNamesSplitterClass
{
private:
System::String ^_FName, ^_LName;
public:
MyNamesSplitterClass(System::String ^FullName)
{
int pos = FullName->IndexOf(" ");
if (pos < 0)
throw gcnew System::Exception("Invalid full name!");
_FName = FullName->Substring(0, pos);
_LName = FullName->Substring(pos+1, FullName->Length - pos -1);
}
void Print()
{
Console::WriteLine("First name: {0}\nLastName: {1}", _FName, _LName);
}
};
int main(array<System::String ^> ^args)
{
MyNamesSplitterClass s("John Doe");
s.Print();
MyNamesSplitterClass ^ms = gcnew MyNamesSplitterClass("Managed C++");
ms->Print();
return 0;
}
Value types
Value types are a means to allow the user to create new types beyond the primitive types; all value types derive from System::ValueType
. The value types can be stored on the stack, and can be assigned using the equal operator.
public value struct MyPoint
{
int x, y, z, time;
MyPoint(int x, int y, int z, int t)
{
this->x = x;
this->y = y;
this->z = z;
this->time = t;
}
};
Enums
Similarly, you can create enums with the following syntax:
public enum class SomeColors { Red, Yellow, Blue};
Or even specify the type of the elements, as:
public enum class SomeColors: char { Red, Yellow, Blue};
Arrays
Array creation cannot be simpler, this example will get you started:
cli::array<int> ^a = gcnew cli::array<int> {1, 2, 3};
which will create an array of three integers, whereas:
array<int> ^a = gcnew array<int>(100) {1, 2, 3};
will create an array of 100 elements, with the first three elements initialized. To walk in an array, you can use the Length
attribute and the index as if it were a normal array, and you use the foreach
:
for each (int v in a)
{
Console::WriteLine("value={0}", v);
}
To create a multi-dimensional array, 3D in this case, like a 4x5x2, all initialized to zero:
array<int, 3> ^threed = gcnew array<int, 3>(4,5,2);
Console::WriteLine(threed[0,0,0]);
An array of classes of strings can be done like this:
array<String ^> ^strs = gcnew array<String ^> {"Hello", "World"}
An array of strings initialized in a for
loop [3]:
array<String ^> ^strs = gcnew array<String ^>(5);
int cnt = 0;
for each (String ^%s in strs)
{
s = gcnew String( (cnt++).ToString() );
}
For more reference about cli::array
, check the System::Array
class, and if you want to add/remove elements, check the ArrayList
class.
Parameter Array
This is equivalent to the variable parameters in C++. The variable parameter must be one in a function and the last parameter. Define it by putting "...", followed by the array of the desired type:
using namespace System;
void avg(String ^msg, ... array<int> ^values)
{
int tot = 0;
for each (int v in values)
tot += v;
Console::WriteLine("{0} {1}", msg, tot / values->Length);
}
int main(array<String ^> ^args)
{
avg("The avg is:", 1,2,3,4,5);
return 0;
}
Properties
public ref class Xyz
{
private:
int _x, _y;
String ^_name;
public:
property int X
{
int get()
{
return _x;
}
void set(int x)
{
_x = x;
}
}
property String ^Name
{
void set(String ^N)
{
_name = N;
}
String ^get()
{
return _name;
}
}
};
Wrapping Around a Native C++ Class
In this section, we will illustrate how to create a C++/CLI wrapper for a native C++ class. Consider the following native class:
class Student
{
private:
char *_fullname;
double _gpa;
public:
Student(char *name, double gpa)
{
_fullname = new char [ strlen(name+1) ];
strcpy(_fullname, name);
_gpa = gpa;
}
~Student()
{
delete [] _fullname;
}
double getGpa()
{
return _gpa;
}
char *getName()
{
return _fullname;
}
};
Now, to wrap it, we follow this simple guideline:
- Create the managed class, and let it have a member variable pointing to the native class.
- In the constructor or somewhere else you find appropriate, construct the native class on the native heap (using "
new
"). - Pass the arguments to the constructor as needed; some types you need to marshal as you pass from managed to unmanaged.
- Create stubs for all the functions you want to expose from your managed class.
- Make sure you delete the native pointer in the destructor of the managed class.
Here's our managed wrapper for the Student
class:
ref class StudentWrapper
{
private:
Student *_stu;
public:
StudentWrapper(String ^fullname, double gpa)
{
_stu = new Student((char *)
System::Runtime::InteropServices::Marshal::StringToHGlobalAnsi(
fullname).ToPointer(),
gpa);
}
~StudentWrapper()
{
delete _stu;
_stu = 0;
}
property String ^Name
{
String ^get()
{
return gcnew String(_stu->getName());
}
}
property double Gpa
{
double get()
{
return _stu->getGpa();
}
}
};
Wrapping Around C Callbacks
In this section, we will demonstrate how to have C callbacks call .NET. For illustration purposes, we will wrap the EnumWindows()
API. This is the outline of the code below:
- Create a managed class with delegates or a function to be called when the native callback is reached.
- Create a native class that has a reference to our managed class. We can accomplish this by using the
gcroot_auto
from the vcclr.h header. - Create the native C callback procedure, and pass it as a context parameter (in this case, the
lParam
) a pointer to the native class. - Now, inside the native callback, and having the context which is the native class, we can get the reference to the managed class and call the desired method.
A short example is provided below:
using namespace System;
#include <vcclr.h>
public ref class MyClass
{
public:
delegate bool delOnEnum(int h);
event delOnEnum ^OnEnum;
bool handler(int h)
{
System::Console::WriteLine("Found a new window {0}", h);
return true;
}
MyClass()
{
OnEnum = gcnew delOnEnum(this, &MyClass::handler);
}
};
The native class created for the purpose of holding a reference to our managed class and for hosting the native callback procedure:
class EnumWindowsProcThunk
{
private:
msclr::auto_gcroot<MyClass^> m_clr;
public:
static BOOL CALLBACK fwd(
HWND hwnd,
LPARAM lParam)
{
return static_cast<EnumWindowsProcThunk *>(
(void *)lParam)->m_clr->OnEnum((int)hwnd) ? TRUE : FALSE;
}
EnumWindowsProcThunk(MyClass ^clr)
{
m_clr = clr;
}
};
Putting it all together:
int main(array<System::String ^> ^args)
{
MyClass ^mc = gcnew MyClass();
EnumWindowsProcThunk t(mc);
::EnumWindows(&EnumWindowsProcThunk::fwd, (LPARAM)&t);
return 0;
}
The Other Way Round: From Managed to C Callbacks
Now, this problem is even easier, since we can have a pointer to a native class within the managed class. The solution can be described as:
- Create a managed class with the desired delegates that should trigger the native callback.
- Create a managed class that will bind between the native class (containing the callback) and the previous managed class (the event generator).
- Create a native class containing the given callback.
For demonstration purpose, we created a "TickGenerator
" managed class which will generate an OnTick
event every while, then an INativeHandler
class (interface) that should be called by the managed class TickGeneratorThunk
. A MyNativeHandler
class is a simple implementation of the INativeHandler
to show you how to set your own handler.
The tick generator delegate:
public delegate void delOnTick(int tickCount);
The managed tick generator class:
ref class TickGenerator
{
private:
System::Threading::Thread ^_tickThread;
int _tickCounts;
int _tickFrequency;
bool _bStop;
void ThreadProc()
{
while (!_bStop)
{
_tickCounts++;
OnTick(_tickCounts);
System::Threading::Thread::Sleep(_tickFrequency);
}
}
public:
event delOnTick ^OnTick;
TickGenerator()
{
_tickThread = nullptr;
}
void Start(int tickFrequency)
{
if (_tickThread != nullptr)
return;
_tickCounts = 0;
_bStop = false;
_tickFrequency = tickFrequency;
System::Threading::ThreadStart ^ts =
gcnew System::Threading::ThreadStart(this, &TickGenerator::ThreadProc);
_tickThread = gcnew System::Threading::Thread(ts);
_tickThread->Start();
}
~TickGenerator()
{
Stop();
}
void Stop()
{
if (_tickThread == nullptr)
return;
_bStop = true;
_tickThread->Join();
_tickThread = nullptr;
}
};
Now, the unmanaged tick handler interface:
#pragma unmanaged
class INativeOnTickHandler
{
public:
virtual void OnTick(int tickCount) = 0;
};
A simple implementation:
class MyNativeHandler: public INativeOnTickHandler
{
public:
virtual void OnTick(int tickCount)
{
printf("MyNativeHandler: called with %d\n", tickCount);
}
};
Now, back to managed to create the thunk, bridging between managed and unmanaged:
#pragma managed
ref class TickGeneratorThunk
{
private:
INativeOnTickHandler *_handler;
public:
TickGeneratorThunk(INativeOnTickHandler *handler)
{
_handler = handler;
}
void OnTick(int tickCount)
{
_handler->OnTick(tickCount);
}
};
Putting it all together:
int main(array<System::String ^> ^args)
{
MyNativeHandler NativeHandler;
TickGenerator ^tg = gcnew TickGenerator();
TickGeneratorThunk ^thunk = gcnew TickGeneratorThunk(&NativeHandler);
tg->OnTick += gcnew delOnTick(thunk, &TickGeneratorThunk::OnTick);
tg->Start(1000);
Console::ReadLine();
tg->Stop();
return 0;
}
Conclusion
I hope you learned and enjoyed while reading this article. It should be enough to get you started in little time, the rest is up to you. Make sure you skim through the list of references provided in this article.
- Pure C++: Hello, C++/CLI
- Common Language Infrastructure - Wikipedia
- C++/CLI - Wikipedia
- Pro Visual C++/CLI
- Applied Microsoft .NET Framework Programming
- ECMA 372 - C++/CLI Specification
- A first look at C++/CLI
- Managed C++ - Learn by Example - Part 1
- MSDN