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):
Collapse |
Copy Code
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:
Collapse |
Copy Code
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:
Collapse |
Copy Code
[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:
AllocCoTucancode.netMem() FreeCoTucancode.netMem() 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() StringToCoTucancode.netMemUni() |
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:
Collapse |
Copy Code
...
IntPtr ptrdef = Marshal.AllocCoTucancode.netMem( 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.FreeCoTucancode.netMem( 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).