Interoperation - commonly referred to in .NET as interop - allows you to access components written for platforms other than .NET from your .NET components. There are three forms of interop: It Just Works is provided for C++, Platform Invoke and COM Interop are provided for all developers.
Before continuing, we need to distinguish between: managed code, native code, managed data, and unmanaged data: When you compile C# or VB.NET you will get IL code and the instances of your types will be managed by the garbage collector. When you compile C++ code with the /clr switch, most of the code will compile to IL. Types marked with __gc or __value will be compiled to IL and managed by the garbage collector. Types marked with __nogc (or without a modifier), will still be compiled to IL but will be unmanaged. Finally, types linked in from static libraries will be native and hence unmanaged. C++ is the only language that allows you to mix managed and unmanaged types, IL, and native code.
Note that C++ is not the only language that allows you access to native code. You can access native code form C# and VB.NET as long as that code is packaged in a DLL or a COM object.
The C++ compiler allows you to call native code linked in from static libraries:
// C++ compiled with /clr
#using <mscorlib.dll>
#include <stdio.h>
class Hello
{
public:
int x;
void SayHello() { puts( "hello world from C++.NET"
); }
};
void main()
{
Hello* pH = new Hello;
pH->x = 0;
pH->SayHello();
}
The interesting point is that the above code will compile to IL. The class Hello is not modified by __gc so it will not be garbage collected. The heap that is used is the unmanaged C++ heap, and the operator is the unmanaged new operator. This means you have the responsibility of calling the unmanaged delete operator. Note that SayHello() which has been compiled to IL is calling puts() which is linked in from a static library that contains native code.
Note that IL does not call the native code directly. Instead, it calls through thunk code generated by the compiler. The IL will read and write data in non garbage-collected types using direct memory access. Also not that you can use the VS.NET debugger to single-step through IL code and seamlessly stop into the native code. And if you have the CRT source installed, you will be able to single-step through the C source code as well. This is important: The VS.NET debugger allows you to move between then managed and unmanaged worlds as if you were merely stepping into a function.
Native method calls will have parameters and return values, and a thunk will give the IL equivalents to the types of these. For example, if the return value is the native int, IL's equivalent will be int32. But note that there is no equivalent for const char*, so the compiler uses int8* and the modifiers NoSignSpecifiedModifier and IsConstModifier.
You can call native code in static linked libraries from IL code. What about calling DLL functions through an export library? No problem, because the export library has type information that the C++ compiler can read and use to generated the appropriate thunks:
// C++ code compiled with cl /clr
/LD
#using <mscorlib.dll>
#include <windows.h>
#include <string.h>
#include <stdlib.h>
#pragma comment( lib, "user32.lib")
public __gc class MyMsgBox
{
public:
static void SayHello()
{
wchar_t* name = _wgetenv(""USERNAME);
MessageBoxW(NULL, name,
"Test", MB_OK);
}
}
This code can be called by C# or VB.NET because the MyMsgBox is a public __gc class. The library assembly will have a Win32 import table for MessageBoxW(), just as this code would if it had been compiled as an unmanaged DLL.
Note that this code does not use the Framework Class Library. If it did, one would be tempted to add a using namespace statement so that .NET names can be used without fully qualified names. However, this approach can cause many problems because many of the Framework classes have members with names similar to those of functions in Win32. Therefore, be very careful about mixing unmanaged C++ with code that uses .NET types. Try to keep your unmanaged C++ code in different translation units from your managed c++ code.
IJW (It Just Works) makes C++ the ideal way to use existing code in .NET applications. However, not all developers use C++. To allow non-C++ developers to use existing code, .NET provided Platform Invoke through attributes.
The major issue with calling DLL functions is that usually there is no type information. Without type information, .NET knows nothing about the parameters and return type of a function, and it does not know how to set up the stack when calling the exported function. If you export a C++ function from a DLL, the name will be C++ mangled with the parameters and return type, and the correct compiler can read this and link to it. However, C++ name mangling is compiler-specific, so other languages have the problem of importing functions with C++ names. As a consequence, DLL developers take the lowest-common-denominator approach and export unmangled names - in C++ this means using extern "C".
To provide the required type information for an exported DLL function to .NET, you must convert it into metadata. Therefore, you need to define the function prototype in managed code and mark it with the [DllImport] attribute. Defining the method as a managed function has the added advantage of allowing you to provide a description of the method using .NET types, and as long as you provide conversion information, the runtime will convert managed type to the appropriate unmanaged types. Therefore, to use Win32 interop, you effectively have two method signatures, the managed signature and the unmanaged signature. Platform Invoke performs the conversion between the two, which involves conversion of parameter types and sometimes extra actions, like reading error values and throwing managed exceptions.
The following class defines a static function called MsgBox() that is used to call the Win32 MessageBox() API. Note that the user32.lib import library is not used. Instead, the DLL is truly dynamically linked. It is loaded at runtime using the information provided by the [DllImport] attribute.
// C++
#using <mscorlib.dll>
using namespace System;
using namespace System::Runtime::InteropServices;
public __value enum MBFlags : unsigned int
{
MB_OK;
}
public __gc class MyMessageBox
{
[DllImport("user32.dll", CharSet=CharSet::Auto, EntryPoint="MessageBox")]
static int MsgBox(int hWnd, Stirng* strCaption, String* strTitle, unsing int
flags);
};
The equivalent code in C# is:
// C#
using namespace System;
using namespace System.Runtime.InteropServices;
public enum MBFlags : uint
{
MB_OK;
}
public class MyMessageBox
{
[DllImport("user32.dll", CharSet=CharSet.Auto, EntryPoint="MessageBox")]
public static extern int MsgBox(int hWnd, string strCaption, string strTitle, uint flags);
}
The two most important members of [DllImport] attribute are the name of the DLL (the attribute class's constructor parameter) and the name of the imported function (the EntryPoint field) because they are required by pinvokeimpl() function. The DLL name is mandatory because without the function cannot be imported. The function name (an attribute field) is optional, because you have to give a name for your .NET code to call, and you can use the same name as the exported function. Because the examples above chose to use a different name for the imported method, the EntryPoint field was used. EntryPoint field can also be used to import a function by ordinal rather than by name.
Consider the following code in a DLL file called print.dll, which allows you to print an ASCII or UNICODE string depending on the suffix to PrintString():
// C++ compiled with cl /LD
#include <stdioh>
extern "C" __declspec(dllexport) void PrintStringA(char* str)
{ printf("ANSI: %s", str); }
extern "C" __declspec(dllexport) void PrintStringW(wchar_t* str)
{ printf("UNICODE: %s", str); }
extern "C" __declspec(dllexport) void PrintANSI(char* str)
{ printf("ANSI: %s", str); }
You can call the above code with C#:
// C#
[DllImport("print.dll", CharSet=CharSet.Auto, EntryPoint="PrintString")]
public static extern void PrintString( string s );
Note that the name given to EntryPoint does not have a suffix. Instead, CharSet is given a value that Platform Invoke uses to choose the suffix. In this case, CharSet.Auto means that the runtime will choose the character set appropriate for the platform. For 32-bit systems like NT, 2000, XP and CE, this will be UNICODE.
Note that whether PrintStringA or PrintStringW is called, the appropriate conversion of the data in the managed string parameter will be performed. If you prefer, you can explicitly set the character set to use by specifying CharSet.Ansi or CharSet.Unicode, in which case, the runtime will add the specified suffix and perform the necessary conversion.
For ANSI methods, the Platform Invoke first tries to call the method name without adding the A suffix. If the call to GetProcAddress() fails for the raw method name, Platform Invoke then tries to access the method with the A suffix. For UNICODE methods, the algorithm works in the opposite way: Platform Invoke first tries the name with the W suffix, and if this fails it attempts to access the method without the suffix.
In general, make the signature of the managed function as similar as possible to the unmanaged version. It is your responsibility to provide the managed equivalents to the unmanaged data types. In this process you do not have to worry about C++ niceties such as const, which is actually just an instruction to the compiler to do some checking for you. You do have to take special care about pointers and structures. Note that the C concept of varargs is not yet supported.
.NET has complete support for exceptions and developers are actively discouraged from writing code that uses return status codes in preference to exceptions.
Most Win32 methods take one of three approaches:
// C++. In file commethods.dll
extern "C" __declspec(dllexport) HRESULT COMMethod( int* val1, int*
val2);
// C#
[DllImport("commethods.dll", EntryPoint="COMMethod",
PreserveSig=false)]
public static extern int COMMethod( out int i );
If the unmanaged method returns a success HRESULT, the calling code will get val2 returned from the method and val1 through i. If the unmanaged method returns a failure HRESULT, there will be no return value because execution will be passed to the exception handler, but i will still be set to the value set by the method. If an exception is thrown you can access the failure code by calling Marshal.GetHRForException().
There is a gotcha here that is serious enough not to use this approach: The HRESULT returned by a method can contain success codes as well as failure codes. but if you use PreserveSig=false, the HRESULT is accessible only if an exception is thrown, which clearly will not happen if the method succeeded. If the method's documentation does not mention success codes, you can be assured that you will need to know the return code only when the method fails. If the method's documentation does mention success codes, you have no option but to use PreserverSig=true. In this case, you can manually throw an exception for failed HRESULT values by calling Marshal.ThrowExceptionForHR(). This method does the right thing - it will only through an exception for a failure HRESULT, and it will ignore the call for a success HRESULT.
The interop runtime must know the direction of the data flow to marshal parameters correctly. Primitive types that are passed by value (i.e., parameters not passed through a pointer), will always be in parameters. The runtime assumes that all parameters passed by reference (through pointers on the unmanaged signature) will be in/out parameters. To mention the data direction flow explicitly, you can use the [In] and [Out] attributes, although these are not mandatory and in most cases, the default will be satisfactory.
Reference types, of course, are always passed by pointers, which means that a reference type passed by reference will be accessed in an unmanaged function through a pointer to a pointer.
In .NET, structures are value types, but there is no guarantee that the individual members will be in the order given or that they will be consecutive. .NET decides these details not the programmer. If you want to create a structure in .NET and pass it to unmanaged code, you must ensure that the memory layout is as the unmanaged code expects. To do this, you have to use the [StructLayout] attribute. Here is an example is C# where the data members mimic the SYSTEMTIME Win32 structure. Note also that two methods have been added, one to convert it into a string, and the other to fill it using the Win32 GetLocalTime() API.
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEMTIME
{
public short wYear;
public short wMonth;
...
public short wSecond;
public short wMilliSecond
public override string ToString()
{
// Format data into a string
...
}
[DllImport("kernel32.dll")]
public static extern void GetLocalTime( out SYSTEMTIME st);
}
The structure is used like this:
// C#
SYSTEMTIME st;
SYSTEMTIME.GetLocalTime( out st );
Console.WriteLine( st );
The LayoutKind enum has three members: Auto where the runtime chooses the memory layout, Sequential, where the memory layout will be as declared, and Explicit, where you use the FieldOffset attribute to give the offset in bytes of the member from the beginning of the structure.
Note also that you can manually marshal structures using methods in the Marshal class. In this case, your managed structure should be a reference type rather than a value type.
Arrays are buffers of memory that contain one or more items of the same type. If the type is a primitive type (int, double, etc, ), the buffer will contain the type. If the type is a complex type (a struct) or a buffer (like string), typically the array will be an array of pointers to that type.
Note that in C++ there is not distinction between a pointer to a single item and an array. When the data is marshaled from the managed world to the unmanaged world, the marshaler must know whether the pointer points to a single item or indicates the start of an array, so that it knows how much data to marshal.
To determine the size and type of data that will be marshaled, you use the [MarshalAs] attribute in the declaration of the thunk. This attribute specifies that the parameter is an array and identifies the size the of the array. For the following managed structure:
// C#
[StructLayout(LayoutKind.Sequential)]
public struct Person
{
public Person(string s, int a) { name = s; age = a; }
public string name;
public int age;
}
Access the unmanaged code is defined as follows:
// C#
public class PersonMethods
{
[DllImport("exp.dll")]
public static extern void PrintPerson( ref Person p);
[DllImport("exp.dll")]
public static extern void PrintPeople
(
[MarhsalAs(UnmanagedType.LPArray,
SizeParamIndex=1, ArraySubType=UnmanagedType.LPStruct)] Person[] p,
int size
);
}
The mandatory constructor parameter UnmanagedType.LPArray indicates the type of the item being marshaled - in this case an array. The size of the array is determined by another parameter, the SizeParamIndex field. Still, the marshaler needs to know what data is held by the array so that it knows to what type to convert the data. The information about the type of the data is indicated by the last parameter, ArraySubType field.
Platform Invoke uses a marshaler to pass data between the managed and unmanaged worlds. By default, the marshaler assumes that parameters passed by reference are in/out parameters, and that other parameters are in parameters. You can indicate the direction of data flow by using the [In] and [Out] attributes; however, these are just suggestions, and the runtime can choose to ignore them.
You use the fields of [DllImport] and [MarshalAs] to give information to the marshaler about the data type on the unmanaged side. The marshaler can read the metadata of the method signature to get information about the data type on the managed side.
Data types can be isomorphic or non-isomorphic. Isomorphic types are the same in managed and unmanaged worlds; the documentation calls them blittable because such types passed by value can be copied bit by bit. Isomorphic types are the integer types (both signed and unsigned), float, double, and void. In addition, single-dimensional arrays of isomorphic types and "formatted" structures that contain only isomorphic types - i.e., those that use [StructLayout] are also isomorphic. All other types are non-isomorphic and require conversion when being passed between the managed and unmanaged worlds.
There is a problem when an object reference is passed to unmanaged code: The object in the managed world is subject to the garbage collector's determination of whether are any references on the object. The garbage collector makes this determination by checking whether the object is reachable, and it can do this only with references held in the managed world. References passed to the unmanaged world are beyond the reach of the garbage collector. Further, even if the object is reachable in the managed world, when garbage collection occurs, the object could b moved to compact memory. Because the garbage collector cannot change the pointer passed to the unmanaged world, this pointer will become invalid. To avoid this problem, the marshaler uses two approaches: Copying and pinning.
When a managed object is pinned, the garbage collector is told that the object is locked in memory. It cannot be moved or garbage-collected. When a non-isomorphic formatted type is marshaled, a converted copy is made. If the parameter is passed by reference, a pointer to this copy is passed to the unmanaged code. Using the [In] attribute on the parameter indicates to the runtime that the copy is initialized before the parameter is passed to the callee. Using the [Out] attribute, indicates that data in the copy will be used to initialize the object when the call returns.
By default, when a parameter is passed by reference, copies are made in both directions, and using the [In] or [Out] attribute can suggest to the runtime the intended data flow, and hence optimize the copies that are made.
What happens if an array is not passed with an explicit size, but instead, the array size is determined by iteration through each array item until the item is zero? Consider the following code which takes an array of strings (i.e., an array of pointers to char). To indicate the end of the array, a NULL pointer is used for the string, so this code iterates through all strings and if the pointer is NULL, it knows it has reached the end of the array:
// C++ - printlist.dll
extern "C" __declspec(dllexport) void PrintList( const char** str )
{
// Loop until we have a NULL pointer
while (*str)
{
puts( *str );
str++;
}
}
.NET does not have support for marshaling an array like this, so you have to write your own marshaler. This is not as difficult as it sounds. The first thing to do is to use the [MarshalAs] attribute to specify the custom marshaler:
// C#
[DllImport("printlist.dll")]
static extern void PrintList( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalType="MyMarshaler")] string[] p);
The UnmanageType.CustomMarshaler indicates that the type indicated by the MarshalType field should be used. MyMarshaler is a class that implements the ICustomMarshaler interface. The marshaler object is created by Platform Invoke through a call to a static method called GetInstance() that returns the instance of the class:
// C#
class MyMarshaler : ICustomMarshaler
{
public static ICustomMarshaler GetInstance( string pstrCookie )
{
return new MyMarshaler();
}
public InPtr MarshalManagedToNative( object ManagedObj );
public void CleanUpNativeData( InPtr pNativeData );
public object MarshalNativeToManaged( InPtr pNativeData );
public void CleanUpManagedData( object ManagedObj );
publit int GetNativeDataSize();
}
Because this class will be used to marshal data from the managed to the unmanaged world, the last three methods can have empty implementations. MarshalManagedToNative() takes a reference to the managed object that should be marshaled, and it should create a native buffer and copy the data into that buffer. For our example at the beginning of this section, the managed object will be a string array, and the native buffer should contain pointers to strings (allocated on the native heap). Then when the native method returns, the data allocated in MarshalManagedToNative() should be released. This should be implemented in CleanUpNativeData().
Strings need special handling. First, they are arrays of characters, so they need allocating. Second, the characters used in a string could be single-byte or double-byte characters, so the marshaler must know the sizes of the characters for the method that is being called.
The System.String type is immutable - you can never change the contents of a String object. If you call one of the string manipulation functions, the manipulated string will be a new string rather than an alteration of the original string. To accommodate changing the contents of the String object, the runtime makes a copy of the String (converting it if necessary) and passes to the unmanaged code a pointer to the copy. The unmanaged code can then write to the buffer. The runtime will use this altered buffer to initialize a String parameter, and a reference to this parameter will replace the reference passed to the unmanaged code. For the following unmanaged code:
// Unmanaged C++ - test.dll
extern "C" __declspec(dllexport) void SayHello( char**s)
{
const char* str="Hello";
strcpy( *s, str );
}
the managed code could look like,
// C#
class Test
{
[DllImport("test.dll", CharSet=CharSet.Ansi)
static extern void SayHello( ref String s );
static void Main()
{
String s = "";
SayHello( s );
Console.WriteLine("You said
" + s);
}
}
Note the following points:
In the previous example, it is assumed that the runtime will create a buffer of the correct size. However, there are several ways to allocate a buffer for the string, and passing a StringBuilder reference is the most straightforward. In the following unmanaged code, the parameter has a single level of inidrection:
// Unmanaged C++ - test.dll
extern "C" __declspec(dllexport) void SayHello2(char*s, int
size)
{
const char* str="Hello";
int len = strlen( str );
if (size > len)
strcpy(s, str);
else
return 0;
return len;
}
The managed code looks like:
// C#
class Test
{
[DllImport("test.dll", CharSet=CharSet.Ansi)
static extern void SayHello2( StringBuilder s );
static void Main()
{
StringBuilder sb = new
StringBuilder();
/* Allocate a
buffer. It is our responsbility to allocate a big-enough buffer and that the
unmanaged
code does not overrun this buffer.
This is the reason for the size parameter in SayHello2 */
sb.EnsureCapacity( 6 );
/* The
parameter to SayHello2 is a StringBuilder reference passed by value. Note that
.NET treats
parameters passed by value as in
parameters. StringBuilder is a special case in .NET: Passing a
StringBuilder object by value
actually passes a pointer to the internal buffer of that object */
SayHello2( sb, sb.Capacity );
Console.WriteLine("You said
" + sb.ToString());
}
}
The Marshal class in the System.Runtime.InteropServices namespace has methods to allow you to allocate buffers, read and write data in unmanaged buffers, access COM objects, and access exception and error values from unmanaged code. This section covers unmanaged buffers.
The Marshal class allows you to allocate buffers using two general-purpose allocators: the Win32 HGLOBAL allocator, and the COM IMalloc allocator. In addition, it allows you to allocate BSTR values, which use the BSTR allocator. Using these allocators is straightforward.
// C++ compiled with cl /clr
#using <mscorlib.dll>
#include <windows.h>
using namespace System;
using namespace System::Runtime::InteropServices;
#pragma comment(lib, "user32.lib")
void main()
{
// Use StringBuilder to create the
string to be displayed in message box
Text::StringBuilder* sb = new StringBuilder();
sb->Append(L"Hello");
// Convert the managed string to an
unmanaged one. The IntPtr returned by StringToCoTaskMemUni is cast
// to a void*
LPCWSTR str = (LPCWSTR)(void*)Marshal::StringToCoTaskMemUni( sb->ToString());
MessageBoxW( NULL, str, L"Test", MB_OK);
// Done with string. Free its memory
Marshal::FreeCoTaskMem( (int)str );
}
Note that the call MessageBoxW(): Here we use IJW and not [DllImport] which means we have to call the function using unmanaged data types. However, to construct the string we are using the managed StringBuilder class, which means we have to convert the string from the managed type to an unmanaged string. To do this, we call StringToCoTaskMemUni(), and when we're finished with the string we free it with FreeCoTaskMem().All of these methods return an instance of IntPtr, which is the .NET structure that holds a native pointer. IntPtr can be converted to a 32-bit number, a 64-bit number, or a void* pointer, depending on the current operating system.
// C++ compiled with cl /clr
#using <mscorlib.dll>
#include <stdlib.h>
using namespace System;
using namespace System::Runtime::InteropServices;
void main()
{
// Get a wchar_t* and convert it into a
managed string
wchar_t* p = _wgetenv( L"USERNAME" );
String* s = Marshal::PtrToStringUni( (IntPtr)p );
Console.WriteLine( s );
}
Note that because the conversion is into a managed type, you do not have to release the buffer that is created - the GC does that, but you do have to specify the character set used by the unmanaged string. The available methods are PtrToStringAnsi, PtrToStringAuto, PtrToStringBSTR, and PtrToStringUni.
// C++
// Allocate an unmanaged buffer
IntPtr p = Marshal::AllocHGlobal( 27 );
// Allocate a managed buffer the copy data into it
char __gc c[] = new char __gc [27];
for (char x = 'a'; x <'a' + 26; x++)
c[x - 'a'] = x;
// Now copy data from the managed buffer to the unmanaged buffer
Marshal::Copy( c, 0, p, 27);
puts( (char*)p.ToPointer());
// Free memory
Marshal::FreeHGlobal( p );
// C#
[StructLayout(LayoutKind.Sequential)]
public struct Person
{
public Person(string s, int a) { name = s; age = a; }
public string name;
public int age;
}
Instead of using Platform Invoke to do the marshaling, we will do it explicitly,
// C#
public class PersonMethods2
{
[DllImport("exp.dll"]
public static extern void PrintPerson( IntPtr p );
public void SendPerson( string name, int age )
{
// Allocate
memory for the structure. SizeOf() is used to get the size of the memory needed
for
// the structure
IntPtr p = Marshal.AllocCoTaskMem( Marshal.SizeOf( typeof(Person)) );
// Allocate
memory for the string that will be sent
IntPtr n = Marshal.StringToCoTaskMemUni( "Yazan" );
// Initialize
structure with values - a string (name) and a number (age)
Marshal.WriteIntPtr( p, 0, n );
Marshal.WriteInt32( p, Marshal.SizeOf(typeof(IntPtr)), 33 );
// Call method.
Note that PrintPerson() takes an IntPtr instance which is essentially a void*
PrintPerson( p );
// Free Memory
Marshal.FreeCoTaskMem( n );
Marshal.FreeCoTaskMem( p );
}
}
// C#
public class PersonMethods2
{
[DllImport("exp.dll"]
public static extern void PrintPerson( IntPtr p );
public void SendPerson( string name, int age )
{
// Allocate memory for the structure. SizeOf() is used to get the size of the memory needed for
// the structure
IntPtr p = Marshal.AllocCoTaskMem( Marshal.SizeOf( typeof(Person)) );
// Initialize structure with values - a string (name) and a number (age)
Person person = new Person("Yazan", 33);
// Call method. Note that PrintPerson() takes an IntPtr instance which is essentially a void*
Marshal.StructureToPtr( person, p, false);
PrintPerson( p );
// Free Memory
Marshal.DestroyStructure( p, typeof(Person) );
Marshal.FreeCoTaskMem( p );
}
}
When managed code calls unmanaged code, any exception thrown by the unmanaged code will be caught by the runtime which will attempt to convert it into a type it understands:
// Native C++: this code will throw
System.NullReferenceException in the managed code that calls this code
int *pNumber = NULL;
*pNumber = 10;
// Oops! No memory allocated. Attempting to access invalid
memory
In some cases, the runtime cannot convert the exception into a type it understands, so instead it throws System.Runtime.InteropServices.SEHException in the managed code. These SEH exceptions have an associated exception code that can be obtained with Marshal.GetExceptionCode. Note that this method can only be called inside a catch block. For the code above, GetExceptionCode will return 0x80004005 which represents EXCEPTION_ACCESS_VIOLATION.
As mentioned previously, if the [DllImport] attribute metadata has the SetLastError field to true, you can call Marshal.GetLastWin32Error() to get the Win32 error code that the native function may have set with SetLastError(). You cal also call Marshal.GetHRForLastWin32Error() to have this status code translated to an HRESULT. Once you have an HRESULT, you can throw it as a .NET exception with ThrowExceptionForHR().
If the native method is a COM-style method, it makes sense to set the [DllImport] PreserveSig field to false to indicate that the runtime should throw the exception for you.
The idea of unsafe code is that you can access memory through C-style pointers and manipulate them in a C-like way. However, because this means that you are working outside the runtime's safety net, you have to mark the method with the unsafe keyword and compile with the /unsafe switch. The net effect is to indicate that the code is not verifiable and hence should not be verified, thus the code cannot be downloaded.
Note that if the memory buffer is unmanaged but has been allocated from the managed heap, you should pin first before accessing it with pointers. The reason for this is that during the time you are using an unsafe pointer, the Garbage Collector may move the memory to which your pointers point, and unlike references, the garbage collector cannot change the value in an unsafe pointer. For pinning the buffer, C# provides the fixed keyword:
/* C#. This method copies one array
to another using unsafe pointers. The fixed statement ensures that the arrays
are fixed in memory during the scope indicated by the braces. Note that these
fixed pointers are read-only, so they cannot be incremented. That is why an
extra pair of temporary pointers (ps and pd) were used */
static unsafe void (byte[] src, byte[] dest)
{
if (dest.length < src.length)
throw new Exception("Destination
size is smaller the Source size");
// Create fixed pointers
fixed (byte* pdest = dest, psrc = src)
{
byte* pd = pdest;
byte* ps = psrc;
for (int x = 0; x <src.Length;
x++, pd++, ps++)
*pd = *ps;
}
}
Note: COM+ is described in detail in Transactional COM+ sections
Key statement: COM+ component services are .NET component services.
When a .NET component needs transaction support, loosely coupled events, role-based security, or activity-based synchronization, it gets what it needs using COM+ component services. In general, the component indicates the support that it requires through attributes that the Framework provides through the System.EnterpriseServices namespace.
Note the following key points:
ServicedComponent class implements IDisposable, which means that it can be used with the C# using pattern. However, it makes more sense to use serviced components with just-in-time activation in which case the object is activated/deactivated during the lifetime of a method call.
Note that if you want non-.NET clients to use your .NET serviced components, you should provide the [ClassInterface] attribute - or better - provide a separate interface that can be exported as a COM interface.
There is a one-to-one mapping between .NET assemblies and COM+ applications. As assembly must be registered with COM+ as an application, which means that the application must have a strong name to make the assembly unique, and it must have an application ID - for example, as follows:
// C#
using System;
using System.Reflection;
[assembly: AssemblyKeyfile("strongname.snk")]
To register the assembly as a COM+ application you should use the regsvcs tool. This tool registers all the public types in the assembly just as regasm does, then it scripts the COM+ catalog and adds entries to those components. Finally, it generates a type library with the types it has added to the COM+ application. The regsvcs tool will generate an application ID from the public key, and the application name from the assembly name. While you can pass the tool the application name that it should generate, it is far better to generate those as part of the assembly:
// C#
using System.EnterpriseServices;
[assembly: ApplicationID("12345678-BAAD-F00D-BAADBBADF00D")]
[assembly: ApplicationName("My Test Application")]
If you decide that the COM+ application should be activated as a server application, you must add the assembly to the GAC for Fusion to find it. The regsvcs tool can also be told to update an application. While developing a COM+ application, you can add the following as a build step
regsvcs /reconfig $TARGET
With COM+ application, it is always best if all required information for deployment and configuration are kept in the assembly so that the information is kept with the types it describes. Typically, the most important piece of information is the type of the COM+ application (server or library). This is determined by the [ApplicationActivation] attribute:
// C#
[assembly: Application(ActivationOption.Server)]
COM+ allows you to run a component under the context of a transaction, and this can be applied to a .NET component using the [Transaction] attribute:
// C#
[ GuidAttribute("12345678-BAAD-F00D-BAADBBADF00D"), Transaction(TransactionOption.Required)]
public class Account : ServiceComponent, IAccount
{ ... }
Recall that if a serviced component is to run under a COM+ transaction, it should run in an activity and it must be activated via COM+ Just-In-Time-Activation (JITA). The [Transaction] attribute has many fields that can configure the transaction such as Timeout and TransactionIsolationLevel.
COM+ configured components are run in a COM+ context where the required runtime services (pooling, activation, etc.) are applied. You can get information about the context using the ContextUtil class:
// C#: This code return a GUID that
represents the current COM+ context
string strCtxInfo = "The component is running in the following context :
" + ContextUtil.ContextId;
If a component is part of a COM+ library application, it can be marked with the [MustRunInClientContext] attribute to ensure that the component runs in its creator's context ( or is not created if the contexts are incompatible). If you want to get information about a component, you can mark it with the [EventTrackEnabled] attribute which indicates that the component supports events and statistics collection during its execution. This information will be shown in the Component Services snap-in.
The [JustInTimeActivattion] attribute indicates that the component is just-in-time activated, which means that when the client creates an instance of the component, it is not activated, instead, the component is activated only when the client makes a method call. JITA is required for transactional components because it ensures transactional isolation.
A component that has [JustInTimeActivation] attribute will be activated only when a method is called on the component, at which point, its Activate() method will be called. The object's context will have been created before Activate() is called, so you can perform context-specific initializations in Activate(). The requested method will then be called. The method will need to decide whether the component has completed its task - typically, JITA items are designed so that each method is a complete work item and no state should remain after the end of the method. The object indicates that it has completed its task by setting ContextUtil.DeactivateOnReturn to true. If this property is set to false, the object will not be deactivated and its state will survive until a subsequent method call.
When a component is deactivated, its Deactivate() method is called. After Deactivate() is called, the object is destroyed, but the infrastructure of connecting to the object - the client proxy, the stub object, and the object's context - will remain. The means that the client can make a new call through the proxy at which point the object will be activated in the same context that was used before.
Note that the object exists between Activate() and Deactivate() calls, and the context exists until the client proxy is finally released. You can use the Activate() method to initialize the object's state from persistent storage, and the Deactivate() method to persist that state before the object is deactivated. Between method invocations, the state will exist in persistent storage rather than in the object. This is the so-called stateless object scenario.
If you know that a component should always be deactivated when a method completes, you can mark the method with the [AutoComplete] attribute. And if the component runs under a transaction, this attribute ensures that the transaction has completed before the component is deactivated.
Object pooling allows you to create a pool of objects before they are needed, thereby allowing you to initialize objects that would otherwise require lengthy initializations. The [ObjectPooling] attribute allows you to set the minimum and maximum pool size and any required timeouts. When a client finishes using a pooled objects, the object will not be destroyed but it will be returned to the pool. This means that a single object may be used by many clients over its lifetime (but not at the same time).
The context of the object is determined when it is executed. While the object is in a pool, it is in a default context. Only when the object is taken out of the pool will the context be fully created (because only then will COM+ know the full details of the client's context). This means that over its lifetime, a pooled object may live in many contexts. The process of moving from the object's default context to its running context is called activation.
When a pooled object is created (its constructor is called), the object will be in the default context, and therefore, you should not perform any context-specific initializations. When a client requests a pooled object, COM+ checks to see if an object is available, and if so, that object will be activated (i.e., moved from its default context to the context where it will be used). COM+ will then call the object's Activate() method to allow you to perform any context-specific initializations before the reference is returned to the client. When the client is finished with the object, it will be deactivated by calling its Deactivate() method to allow the object to perform any context-specific cleanup. COM+ will then finally call CanBePooled() on the object to determine if the object is happy to be returned back to the pool. If the deactivation was not completed successfully, the object should not be returned back to the pool.
One rule-of-thumb to use when deciding whether an object should be pooled is how long the context-specific initialization is compared to context-neutral initialization. If it takes longer to perform the context-neutral initialization, the object is a good candidate for pooling. If the context-specific initialization takes longer, the object is not a good candidate for pooling unless if you specifically need to control the number of objects that should be created, in which case you use the MaxPoolSize property in [ObjectPooling] attribute.
Of course, a .NET component will be deactivated only when the garbage collector does a collection., and you have no guarantee how frequently / infrequently this will happen. If the time between garbage collection is long, the object will not be placed in the pool and the whole point of object pooling is negated. This is why ServicedComponent derives from IDisposable: You are actively encouraged to call Dispose() when a client is finished with an object. When Dispose() is called, the object is deactivated and returned to the pool; the implementation calls it the static ServicedComponent.DisposeObject().
Construction strings for serviced components in .NET can be used by giving the object the [ConstructionEnabled] attribute, and implementing the ServicedComponent.Construct() method.
One final point: Pooled objects can be marked as using JITA, in which case the object can be activated just for a single call and then placed back into the pool when the method completes. In this case, the client still holds a reference to the object ( a proxy), and the context will still remain between method invocations. Activate() and Deactivate() will be called just before and after the method is invoked, respectively.
The most convenient of the COM+ component services is transactions. COM+ allows you to specify that a component must run in a transaction - either its own or inheriting a transactions from its creator, and any resource managers will be enlisted in the transaction before any work is done.
If your component is transactional, it must use JITA to ensure that no transactional state survives longer than the transaction. The component's methods can influence the transaction state by changing ContextUtil.MyTransactionVote to a value of TransactionVote.Commit which will indicate that the transaction should be committed. A value of TransactionVote.Abort or the method throwing an exception indicates that the transaction should abort. The MyTransactionVote property is independent of the DeactivateOnReturn property, so committing the transaction does not necessarily deactivate the object. To do this in one call (commit and deactivate), you can call ContextUtil.SetCompelete() or mark the component's method with the [AutoComplete] attribute.
.NET allows you to define roles and to have access to role-based access checks. The top-level attribute is the assembly attribute [ApplicationAccessControl], which is used to turn on or off access checks on the application. In addition, it is used to give the authentication and impersonation levels. The ApplicationAccessControl.AccessChecksLevel property indicates whether the access checks are performed on the process level or on both the process and component levels. The ApplicationAccessControl.Authentication property is used to configure the authentication that is required to access components in the application. The client call must be made at the level set in the property or at a higher level.
You can determine whether access checks are made on the objects of a class by using the [ComponentAccessControl] attribute:
/* C#. ComponentAccessCheck
attribute indicates that when calls are made into the component on any
interface, an access check should be performed. The access check consists of 1)
determining the role membership of the caller and 2)whether the caller's role is
one of the roles allowed to call the component */
[ComponentAccessCheck(true)]
public class Account : SerivcedComponent, IAccount { ... }
COM+ roles are essentially named groups of users. The contents of a role are an administration issue which should be performed when the application is deployed. The developer knows nothing about the users that will be added to the role, consequently, you cannot determine role membership with attributes. To create a role, you use the the assembly attribute [SecurityRole]:
// C#
[assembly: SecurityRole("Managers")]
[assembly: SecurityRole("Clerks")]
[assembly: SecurityRole("AllEmployees")]
One you have defined roles, you can perform access checks. There are two ways to do this - programmatically or through attributes. Using attributes, the [SecurityRole] attribute mentioned previously is also used to specify the roles that are granted access so that these roles can execute code in a component. The attribute can be applied on the component, an interface or a method:
/* C#. In the following code,
members of the "Managers" role can run anything on the Account object.
But members of the "Clerks" role can only call methods in the IAccount
interface */
[SecurityRole("Clerks")]
interface IAcccount { ... }
[ComponentAccessCheck(true), SecurityRole("Managers")]
public class Account : ServicedComponent, IAccount { ... }
Attributes allow you to specify access checks on components, interfaces, and individual methods. A finer-grained level of access checks can be performed with programmatic access checks. To achieve this, your code calls SecurityCallContext object which has methods to perform access checks and properties to get additional information.