
About a year ago I
wrote an article about how you can call methods of classes written in pure C ++ from any .NET program without resorting to registering COM libraries, C ++ / CLI, etc.
Today I will talk about another original and very convenient approach, and, in addition, this topic will be of interest to all habiliters who want to learn more about the wonderful
Reflection.Emit tool (in my opinion this topic is not well covered in Habré).
')
The example is based on
the same old application with a couple of C ++ 's classes and a test form.
So let's get started. First of all, let's take the old example and remove from it all the garbage one way or another connected with COM. In addition, remove the returned circle, with or without, HRESTS, GUIDs and other vermin. The code was almost twice as small at once :) Instead, add one Dispose method that will release the object.
So, our simplest C ++ class now looks like this:
class CHello
{
public :
LIBCALL Dispose()
{
delete this ;
}
LIBCALL SetName(LPWSTR AName)
{
mName = AName;
}
LIBCALL Say(HWND AParent)
{
wstring message = L "Hello, my friend " + mName;
MessageBox(AParent, message.c_str(), L "From C++" , MB_ICONINFORMATION);
}
private :
wstring mName;
};
Here LIBCALL == virtual void __stdcall
Let's move on to the C # part. The first thing we need to do is declare an interface describing the exported C ++ class (here it is important to preserve the order of function declarations):
[InvokerObject(EObjectType.Hello)]
public interface IHello : IDisposable
{
void SetName([MarshalAs(UnmanagedType.LPWStr)] string AName);
void Say( IntPtr AParent);
}
You may notice that the interface is inherited from IDisposable, so the first object method will, in essence, be the Dispose method. About the InvokerObject attribute, I'll tell you more.
Now, to use our C ++ object in a C # program, it suffices to write :
IHello hello = Invoker.Create<IHello>();
hello.SetName(txtName.Text);
hello.Say(Handle);

On the other side of the code
Now, you can go to the most interesting - how it all works from the inside. The main character of our program today is the CIL opcode
Calli . This opcode allows you to make a function call to an arbitrary machine address with a given set of arguments. On it all work of our vraper will be under construction.
All work on creating a wrapper object designed to call Unmanaged functions will be done by our Invoker Class. I will not give his full code here, but I will tell only about his idea and principles of work (if you are interested, you can download the full source code of the example at the end of the article in the archive).
The algorithm for the Invoker class is:
- Create a dynamic assembly ( AppDomain.CurrentDomain.DefineDynamicAssembly )
- Create a class inherited from the specified interface in this assembly.
- Create a constructor in the class that accepts an IntPtr - a pointer to the Unmanaged class and, in accordance with the number of methods in the interface, reads the required number of function addresses from the virtual method table
- We go around all the methods in the interface
- For each method, we create our own function that accepts the required set of arguments and calls the final method of the C ++ class using Calli
To make it easier for the hacker to understand the essence, I present here the commented code of the function for creating this class (steps 2 and 3):
// (+1 .. Dispose)
int k = InterfaceType.GetMethods().Count() + 1;
// unmanaged
typeBuilder = InvokerDynamicAssembly.Instance.Builder.DefineType(TypeName, TypeAttributes.Class | TypeAttributes.Public);
//
typeBuilder.AddInterfaceImplementation(InterfaceType);
//
ptrThis = typeBuilder.DefineField( "ptr" , typeof ( IntPtr ), FieldAttributes.Private);
methods = typeBuilder.DefineField( "methods" , typeof ( IntPtr []), FieldAttributes.Private);
vtbl = typeBuilder.DefineField( "vtbl" , typeof ( IntPtr ), FieldAttributes.Private);
// , VTBL unmanaged
ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof (System. IntPtr ) });
constructorBuilder.DefineParameter(1, ParameterAttributes.In, "Pointer" );
ILGenerator ctorGen = constructorBuilder.GetILGenerator();
//
ctorGen.Emit(OpCodes.Ldarg_0);
ctorGen.Emit(OpCodes.Call, ObjectCtorInfo);
// ptrThis
ctorGen.Emit(OpCodes.Ldarg_0);
ctorGen.Emit(OpCodes.Ldarg_1);
ctorGen.Emit(OpCodes.Stfld, ptrThis);
//
ctorGen.Emit(OpCodes.Ldarg_0);
ctorGen.Emit(OpCodes.Ldarg_1);
ctorGen.Emit(OpCodes.Call, ReadIntPtr); // Marshal.ReadIntPtr
ctorGen.Emit(OpCodes.Stfld, vtbl);
ctorGen.Emit(OpCodes.Ldarg_0);
ctorGen.Emit(OpCodes.Ldc_I4_S, k);
ctorGen.Emit(OpCodes.Newarr, typeof ( IntPtr ));
ctorGen.Emit(OpCodes.Stfld, methods);
//
for ( int i = 0; i < k; i++)
{
ctorGen.Emit(OpCodes.Ldarg_0);
ctorGen.Emit(OpCodes.Ldfld, methods);
ctorGen.Emit(OpCodes.Ldc_I4_S, i);
ctorGen.Emit(OpCodes.Ldarg_0);
ctorGen.Emit(OpCodes.Ldfld, vtbl);
ctorGen.Emit(OpCodes.Ldc_I4, i * IntPtr .Size);
ctorGen.Emit(OpCodes.Add);
ctorGen.Emit(OpCodes.Call, ReadIntPtr);
ctorGen.Emit(OpCodes.Stelem, typeof ( IntPtr ));
}
//
ctorGen.Emit(OpCodes.Ret);
//
AddMethods();
createdType = typeBuilder.CreateType();
Now let's talk about InvokerObjectAttribute. The most important feature of the Invoker class is:
/// <summary>
///
/// </summary>
/// <returns> </returns>
public static T Create<T>()
where T : class , IDisposable
{
object [] attr = typeof (T).GetCustomAttributes( true );
foreach ( var it in attr)
{
if (it is InvokerObjectAttribute)
{
var objectType = (it as InvokerObjectAttribute).ObjectType;
Invoker inv = new Invoker();
inv.InterfaceType = typeof (T);
inv.Pointer = Lib.CreateObject(objectType);
inv.InitializeType();
return inv.CreateInstance() as T;
}
}
return null ;
}
Here we will use the InvokerObjectAttribute to find out which object we need to create and with the help of the P \ Invoke Lib.CreateObject call we ask the C ++ library to create a new unmanaged object of the desired type and return us the pointer.
What is particularly interesting is that we can declare a new type inside a dynamic assembly only when we first access the interface, and later use an existing one, passing it a new pointer. It works like this:
private void InitializeType()
{
if (InvokerDynamicAssembly.Instance.HasType(TypeName))
createdType = InvokerDynamicAssembly.Instance.GetType(TypeName);
else
CreateType();
}
Obviously, the main task of Invoker is to create methods to wrap call to unmanaged functions. This is what the created wrapper for the
Say method looks like:
.method public hidebysig virtual
instance void Say (
native int ''
) cil managed
{
// Method begins at RVA 0x2164
// Code size 29 (0x1d)
.maxstack 4
IL_0000: ldarg.0
IL_0001: ldfld native int InvokerDynamicAssembly.net2c.IHello::ptr
IL_0006: ldarg.1
IL_0007: ldarg.0
IL_0008: ldfld native int [] InvokerDynamicAssembly.net2c.IHello::methods
IL_000d: ldc.i4.s 2
IL_000f: nop
IL_0010: nop
IL_0011: nop
IL_0012: ldelem.any [mscorlib]System. IntPtr
IL_0017: calli System.Void(System. IntPtr ,System. IntPtr )
IL_001c: ret
}
As you can see, he just takes the arguments passed to the input and calls the virtual method of the unmanaged object with them.

Benefits of the approach
1. It works about 1.6 times faster than COM Interop. With C ++ / CLI did not compare, anyone can test and unsubscribe.
2. Untie from MTA \ STA. COM Interop requires strict adherence to the Thread Appartament State, meanwhile, very often (almost always in my memory) instead of helping the programmer, this creates many unnecessary difficulties in working with objects in C #. This method is completely devoid of this disadvantage, because it does not implement any bindings to the stream.
3. It is good in terms of ease of use and a small amount of necessary code (not counting, of course, the Invoker class which is written once and for all).
Of the main drawbacks - I did not think of how to make the correct marshaling using the MarshalAs attribute (except for the full manual implementation option). Therefore, at this point, separate marshaling is implemented only for the string type (to provide a Unicode representation), the other types are marshaled by default. For me personally, this is not a particular problem, since in the unmanaged code I only endure algorithms that require tight optimization of performance, and as a rule, they have enough parameters of the simplest types (pointers and numbers). But if someone prompts the right decision on this issue, I would be very very grateful, because I puzzled for a long time about this task.
PS If you are programming in C #, but are still familiar with IL only by hearsay, I recommend reading a good brief introduction to the topic:
www.wasm.ru/series.php?sid=22PPS Yes, I almost forgot, as I promised, I attach the full source of all this disgrace:
public.66bit.ru/files/2011.10.07/7458c3ca96a602091fe049117974fab4/Net2C.rar