
Abstract
In industrial automation, OPC (OLE for Process Control, see www.opcfoundation.org) is the primary COM component interface used to connect devices from different manufactures. The OPC standard is defined at 'two different layers' of COM/DCOM. First, as a collection of COM custom interfaces, and secondly as COM-automation compliant components/wrappers. For some reasons and applications, it is preferable to use the custom interface directly.
We will show you in this article how to access OPC servers with custom interfaces and how to write an OPC client in .NET.
The problem
The new Microsoft .NET Framework will provide some interoperability layers and tools to reuse a large part of the existing COM/ActiveX/OCX components, but with some strong limitations. While automation compliant COM objects will be nicely imported (as referenced COM objects inside Visual Studio .NET, or with the TLBIMP tool), pure custom COM interfaces will not work.
To understand the issues with COM custom interfaces and the .NET framework, we must first analyze, why automation components can be used immediately. Visual Studio .NET relies on the information found in a type-library for every imported COM component (e.g. the library generated by MIDL-compiler, named *.TLB). The problem is now, type libraries can only contain automation compliant information. So if we compile a custom-interface IDL file, the generated TLB misses very important type descriptions, especially the method call parameter size (e.g. of arrays). At the time of .NET Beta2, there's unfortunately no tool (like 'IDLIMP') to import custom interface IDL files.
Example: Since the MIDL compiler does not propagate size_is
information to the type library, the marshaler doesn't know an array length and translates int[]
to ref int
.
One first solution would be to edit the assembly produced by TLBIMP (using ILDASM), and replace ref int
with int[]
, and then compile IL back again with ILASM.
But if we look at some very custom IDL files, we also find methods with parameters like foo( int **arraybyref )
where arrays are passed by reference (caller allocates the memory)! Hand editing IL code won't work here, there's no marshaling signature for this. Currently, we must use one of two different workarounds:
- Write a custom marshaler (e.g. in Managed C++)
- Or the way we used, write a marshaling helper class (in C#)
The solution, first step
We have to rewrite our custom IDL file in a managed language code, here C#. Note this can be a very time consuming work! every method of all interfaces have to be coded in C#, and the critical (custom) parameters must be declared completely different. We found there's often no way around the use of the special IntPtr
type.
Let's look at a sample method (from an OPC custom interface IDL):
HRESULT AddItems(
[in] DWORD dwCount,
[in, size_is( dwCount)] OPCITEMDEF * pItemArray,
[out, size_is(,dwCount)] OPCITEMRESULT ** ppAddResults,
[out, size_is(,dwCount)] HRESULT ** ppErrors );
Redefined in C#, looks now like this:
int AddItems(
[In] int dwCount,
[In] IntPtr pItemArray,
[Out] out IntPtr ppAddResults,
[Out] out IntPtr ppErrors );
As you can see, we loose many type information by declaring parameters as IntPtr
!
Another point is the default exception mapping of .NET: as custom interface methods return HRESULT
values, failed calls will be converted by the .NET marshaller to exceptions of type COMException
. Further, some COM methods will also return other success codes besides S_OK
, mainly S_FALSE
. With the default mapping, this hint return value will be lost.
To bypass exception mapping, declare the interface with special signature attributes. See at the code below for the head of the final interface declaration:
[ComVisible(true), ComImport,
Guid("39c13a54-011e-11d0-9675-0020afd8adb3"),
InterfaceType( ComInterfaceType.InterfaceIsIUnknown )]
internal interface IOPCItemMgt
{
[PreserveSig]
int AddItems(
[In] int dwCount,
[In] IntPtr pItemArray,
[Out] out IntPtr ppAddResults,
[Out] out IntPtr ppErrors );
...
The solution, second step
To use the interfaces we declared as above, it is recommended to write some wrapper classes. But more important, this wrapper now has to do all the custom marshaling, e.g. for all IntPtr
parameters. So the wrapper must reconstruct the information we lost. Managed marshaling code makes use of the framework services provided in the System.Runtime.InteropServices
namespace, especially the Marshal
class. We found the following methods as useful for this:
AllocCoTaskMem() FreeCoTaskMem() SizeOf() |
manage COM native memory (as pointed to by IntPtr ) |
StructureToPtr() PtrToStructure() DestroyStructure() |
marshaling of simple structures |
ReadInt32() WriteInt32() Copy() |
read/write to native memory (also -Byte /Int16 /Int64 ) |
PtrToStringUni() StringToCoTaskMemUni() |
string marshaling |
GetObjectForNativeVariant() GetNativeVariantForObject() |
conversions between VARIANT and Object |
ThrowExceptionForHR() |
map HRESULT to exception and throw |
ReleaseComObject() |
finally, release COM object |
To get the idea, see a simplified excerpt for the sample method AddItems()
we declared above - here we allocate native memory and marshal an array of structures into it:
...
IntPtr ptrdef = Marshal.AllocCoTaskMem( count * sizedefinition );
int rundef = (int) ptrdef;
for( int i = 0; i < count; i++ )
{
Marshal.StructureToPtr( definitions[i], (IntPtr) rundef, false );
rundef += sizedefinition;
}
int hresult = itemsinterface.AddItems( count, ptrdef, ... );
...
int rundef = (int) ptrdef;
for( int i = 0; i < count; i++ )
{
Marshal.DestroyStructure( (IntPtr) rundef, typedefinition );
rundef += sizedefinition;
}
Marshal.FreeCoTaskMem( ptrdef );
...
Download
In the download package, you will find the complete interface declarations and a sample client application showing how to use them.
Please note:
- First read the included whitepaper
- To run this OPC client, you must have any OPC-DA 2.0 servers installed!
Useful links
OPC, the OPC logo, and OPC Foundation are trademarks of the OPC Foundation. .NET, the .NET logo, and Microsoft .NET are trademarks of the Microsoft Corporation.
Disclaimer
The information in this article & source code are published in accordance with the Beta2 bits of the .NET Framework SDK).