Using Native Types in Managed Code

Even though the C++/CLI compiler silently maps all usages of native types in your source code to functioning IL code, it makes sense to have a closer look at the internals. Let's begin with a simple function:

Since all numeric primitives—like int and double—have counterparts with the same binary layout in the managed type system, the C++/CLI compiler simply considers equivalent managed and native primitives as one type. To the C++/CLI compiler, the following declaration is equivalent to the preceding one:

System::Int32 f(System::Double d);

However, the Visual C++ compiler supports two 32-bit signed integer types: int and long. As a C++ programmer, you can write the following two overloads of the function f:

void

f(int

)

{ /*

... */

}

void

f(long

l)

{ /*

... */

}

Both int and long should be mapped to System::Int32. However, there must be an option to differentiate both functions. To achieve this, the compiler uses a signature modifier. The signature for the function with the int argument just uses the IL keyword for System::Int32, which is int32:

.method assembly static void f(int32 i) cil managed

The method with the long argument has an int32 argument with a signature modifier to express that the parameter type should be mapped to the native type long instead of int:

.method assembly static void f(

int32 modopt([mscorlib]System.Runtime.CompilerServices.IsLong) l ) cil managed

At first glance, signature modifiers have a lot of similarities to .NET attributes that are applied to a parameter. Both represent metadata that provides extra information about an argument of a function. However, in contrast to attributes, signature modifiers are part of the method's signature and therefore part of the method's identity. The runtime can differentiate between both overloads when they have different signature modifiers, even if the rest of the functions are the same.

Managed code can also use functions that have native pointer types as arguments, like the function shown here:

The C++/CLI compiler translates this function into the following IL method: .method assembly static void f(int32* pi) cil managed

Most .NET languages do not use this feature, but for C++/CLI interoperability, this feature is essential. IL supports pointers of any type and any level. For example, the C++ type double*** would be mapped to the IL type float64***. To perform read and write operations via a pointer, the IL instruction set has special instructions. To understand these instructions, assume that f is implemented as follows:

To map this C++ source code to IL, the C++/CLI compiler generates the following code:

// Push the first argument (int* pi) on the stack // Push 42 as a 4-byte integer value on the stack // Pop the top two elements of the stack,

// store the value in the top element in the address specified by // the top - 1 element on the stack

The instruction stind.i4 (store a 4-byte integer indirectly) allows managed code to operate on the virtual memory of the running process. Whenever a 4-byte integer value needs to be stored at an arbitrary address in virtual memory, this IL instruction is used. The next code sample combines a read and a write operation:

To map this C++ source code to IL, the C++/CLI compiler generates the following code:

dup ldind.i4

ldc.i4 add stind.i4

// Push the first argument (pi) on the stack (it will be needed // by the stind.i4 instruction at the end of this code sample) // Push it again

// (it will also be needed by the following instruction)

// Consider the top of the stack to be a virtual address to a

// 4-byte integer. Replace the top of the stack with

// the value that the address refers to

// Push the 4-byte integer value 42 on the stack

// Consider the top two elements of the stack to be two 4-byte

// integer values. Replace these two elements with one element

// Store the value on the top of the stack at the address specified // by the top - 1 stack element (which is the element pushed in the // first instruction of this code sample)

In addition to the stind.i4 instruction, this code contains the ldind.i4 instruction (load a 4-byte integer indirectly). This instruction can be used to read a 4-byte integer value at a given virtual memory address.

To completely support the C++ type system, C++/CLI must also be able to map C++ reference arguments and arguments with const modifiers to IL code. The following function shows an example of a function with a reference argument:

Since a C++ reference has the same binary layout as a native pointer (both simply store addresses), the C++/CLI compiler maps an int& to the IL type int32*. To differentiate the C++ types int& and int*, a signature modifier is used again:

.method assembly static void f(int32*

modopt([mscorlib]System.Runtime.CompilerServices.IsImplicitlyDereferenced) i) cil managed

Signature modifiers are also used to differentiate between the functions void f(int&) and void f(const int&):

.method assembly static void f(int32 modopt([mscorlib]System.Runtime.CompilerServices.IsConst)*

modopt([mscorlib]System.Runtime.CompilerServices.IsImplicitlyDereferenced) i) cil managed

Notice that pointers, even though they are supported by the IL, are not first-class managed types, because they do not fit into the picture of a single rooted type system with System::Object as the ultimate root. For example, the following code does not compile:

using namespace System;

Console::WriteLine("pi = {0}", pi); // this line will cause a compiler error

The C++/CLI compiler will refuse to compile this code because no matching overload of Console::WriteLine could be found. One might expect that the overload Console::WriteLine( StringA formatString, ObjectA arg ) should match, because the first argument passed matches StringA and the type of the second argument is ObjectA, which is known as the ultimate root. However, there is no standard conversion from int* to ObjectA. Trying to use a cast to ObjectA will also fail:

Console::WriteLine("pi = {0}", (ObjectA)pi); // error: cannot convert from int* to ObjectA

As discussed in Chapter 2, a variable of type ObjectA is a reference to a managed object on the GC heap. Apart from a few rare cases, native pointers refer to native memory, not into the managed heap. Therefore, it makes sense that a conversion cannot be done. If a native pointer were a value type, it would be boxed so that an ObjectA could refer to a new managed object that contains the native pointer value. Native pointers are not treated like value types, but there is a special managed value type that encapsulates native pointers: System::IntPtr.

To pass a pointer to WriteLine, you can wrap it in System::IntPtr, as shown in the following code:

Console::WriteLine("pi = {0}", IntPtr(pi)); // this code works

Since System::IntPtr is a managed value type, it can be boxed to be passed as an Objects Just as the IL uses the keyword int32 for System::Int32, the keyword native int is used for the type System::IntPtr.

Was this article helpful?

0 0

Post a comment