Interoperation in the .NET framework
Lesson 1: Using COM objects
Prior to .NET COM was primary framework for Windows developers to interact with operating system.
Importing Type Libraries
Mechanism that servers as proxy so that .NET runtime can communicate with COM component is known as a Runtime Callable Wrapper (RCW). THE RCW handles most of the work in communication - including type marshalling, handling event and interfaces.
Registered COM components can be imported to the .NET framework via the Type Library Importer tool (TlbImp.exe) or via the COM tab of the "Add Reference" dialogue in the visual studio IDE.
Using TlbImp to Import a Type
- Open command prompt
- Navigate to DLL to import
- Type
tlbimp <dllname>
. This creates .NET assembly with same name as original DLL, if want to use another name use the /out: argument.
Add reference to generated assembly in same way as any other .NET assembly.
Note, C# (unlike VB) does not support optional parameters whilst COM parameters are passed by reference meaning they must have a value supplied. This means that for C# applications all arguments to a COM call must be specified which can lead to unnecessary and confusing code. An attempt to minimise this problem is provided with the Type.Missing field which means that dummy object variables do not need to be created, e.g.
Application newExcelApp = new Application();
newExcelApp.Worksheets.Add(Type.Missing, Type.Missing, Type.Missing, Type.Missing);
Tools used by COM interop
Name | Use | Executable |
---|---|---|
Type Library Importer | Imports .NET assembly based on COM component | TlbImp.exe |
Type Library Exporter | Creates COM type library that can be consumed by COM application | TlpExp.exe |
Registry Editor | Permits editing of the registry | Regedit.exe |
Intermediate Language Disassembler | View visual representation of .NET Intermediate Language (IL) | Ildasm.exe |
Assembly Registration Tool | Add and remove .NET assemblies from system registration database | Regasm.exe |
Using COM objects in code
Virtually identical to native .NET assembly. From your codes perspective there is no difference as the code generated by the type library importer is a .NET assembly.
Handling COM Interop Exceptions
Exception handling changed drastically between COM and .NET 2.0
Previous versions had System.Exception at the root of the hierarchy. By capturing System.Exception all CLS compliant exceptions would be caught - unfortunately COM is not CLS compliant and so would not appear.
.NET 2.0 introduces System.Runtime.CompilerServices.RuntimeWrapperException which inherits most of its behaviour from System.Exception but adds a WrappedException property that provides access to the runtime exception. When non-CLS compliant exception is thrown the CLR creates an instance of this class and sets WrappedException to the object that was thrown. This allows your code to catch non-CLS compliant exceptions when System.Exception is caught.
COM Interop Limitations
- Static/shared members - not supported by COM
- Parametrised constructors - not supported by COM
- Inheritance - COM objects place limitations on inheritance chain. Members that shadow members in a base class are not recognisable and so can't be called.
- Portability - O/S other than Windows do not have registries and so can't support COM objects.
Lesson 2: Exposing .NET components to COM
Building
One additional step required over building normal .NET components
From the Project Properties dialogue box, select the build tab and ensure "Register For Com Interop" in Output section is ticked.
Visibility
Need to consider what should be visible to COM, control using ComVisible attribute.
To hide entire assembly apply
[assembly: ComVisible(false)]
Can make classes and members individually visible / invisible by applying the attribute.
Deployment
- All classes must use default constructor with no parameters
- Any exposed type must be public
- And exposed member must be public
- Abstract classes will not be consumed
After building use type library exporter to generate tlb:
tlbexp ComVisiblePerson.dll /out:ComVisiblePersonlib.tlb
Create a resource script containing the following statement:
IDR_TYPELIB1 typelib "ComVisiblePersonlib.tlb"
Generate a resource file from the script
rc ComVisiblePersonLib.res
Recompile the assembly with the type library embedded as a Win32 resource file
Lesson 3: Using unmanaged code
Provides access to areas of code (including portions of the Win32 API) that do not have .NET wrappers in place.
Calling Platform Invoke
Used to call unmanaged code.
Managed through the System.Runtime.InteropServices namespace.
To use P/Invoke:
- Create static external method with name of function to be called
- Decorate with DllImport attribute specifying the library it should call
- Call method from code
e.g.
class WindowsExample
{
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
public GetScreen()
{
IntPtr DemoHandle = GetForegroundWindow();
}
}
Note when using P/Invoke use StringBuilder instead of String for parameters. StringBuilder is a pure reference type with no atypical behaviour (Strings are not pure references).
Encapsulating DLL Functions
P/Invoke calls are not elegant. Frequently they are encapsulated within a class that wraps their functionality within a more elegant visage. Much of the .NET framework follows this pattern. Advantages:
- Consumers will not know this code is different to any other
- Developers do not have to remember API calls and their parameters once encapsulated within a method call.
- Reduces errors. Slight typing differences can cause P/Invoke calls to fail.
Converting Data Types
For managed code specify conversion functionality using the TypeConverter class. Need to take different approach for unmanaged.
First mechanism is the MarshalAs attribute. Can be applied to property or parameters. Create code, decorate it and specify type it should be converted to
class MarshalAsDemo
{
[MarshalAs(UnmanagedType.LPStr)]
public string FirstName;
public string LastName([MarshalAs(UnmanagedType.LPStr)] String firstName {};
[MarshalAs(UnmangedType.Bool)]
public Boolean IsCurrentlyWorking;
}.
Structures commonly used by Windows API.
Performance is major objective of CLR and it will optimise performance wherever possible. Types provide a good illustration. When type created developer lays out members as they see fit. By default the CLR will decide how to arrange classes members - which may be different to that specified by developer. Can manually direct CLR to handle (or not) the layout of a type via the StructLayoutAttribute.
The constructor to StructLayoutAttribute takes enumeration
- LayoutKind.Auto - Developer relinquishes control over layout to CLR
- LayoutKind.Sequential - CLR preserves layout specified by developer
- LayoutKind.Explicit - CLR users layout explicitly specified by developer via memory offsets (these must be specified for each field)
e.g.
[StructLayout(LayoutKind.Sequential)]
class OSVersionInfo
{
public Int32 dwOSVersionInfoSize;
public Int32 dwMajorVersion;
public Int32 dwMinorVersion;
public Int32 dwBuidlNumber;
public Int32 dwPlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst128)]
public string szCSDVersion;;
}
[StructLayout(LayoutKind.Explicitl)]
class OSVersionInfo
{
[FieldOffset(0)]
public Int32 dwOSVersionInfoSize;
[FieldOffset(4)]
public Int32 dwMajorVersion;
[FieldOffset(8)]
public Int32 dwMinorVersion;
[FieldOffset(12)]
public Int32 dwBuidlNumber;
[FieldOffset(16)]
public Int32 dwPlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst128)]
[FieldOffset(20)]
public string szCSDVersion;;
}
Callbacks with Unmanaged Code
Traditionally implemented with pointers. Tremendous power, but not type safe. To address this .NET framework introduces Delegate objects.
To use delegates:
- Create Delegate with same signature as callback
- Substitute Delegate for callback and make call
public class UnmangedCallbackDemo
{
public delegate Boolean DemoCallback(IntPtr hWnd, Int32 lParam);
[DllImport(UserReference)]
public static extern In32 EnumWindows(DemoCallback callback, Int32 param);
public static Boolean DisplayWindowInfo(IntPtr hWnd, Int32 lParam)
{
...
}
public void RunDemo()
{
EnumWindows(DisplayWindowInfo, 0);
}
}
Exceptions in Managed Code
In COM the last error was made available via GetLastError function. To make this error code available in .NET code need to apply SetLastError parameter to DllImport attribute:
[DllImport("user32.dll", SetLastError = true)]
private static extern Int32 MessageBox(IntPtr hWnd, String pText, String pCaption, Int32 uType);
...
MessageBox(IntPtr)(-100), "Error", "error", 0);
Console.WriteLine(ErrorCode.ToString());
Limitations of Unmanaged Code
- Performance - Code not managed by runtime will typically perform faster. Marshalling information between environments takes time. Consequently calling unmanaged code can be slower than calling equivalent managed code.
- Type safety - unmanaged code need not be type safe. Also no guarantee that type library definitions are accurate.
- Code security - Framework features such as declarative security not available to unmanaged code.
- Versioning - Unmanaged code does not support versioning in same way as .NET framework. Therefore side-by-side execution might not be available when using unmanaged code.