|
This section describes how .NET entities, including interfaces, classes, and methods , are converted when exported to a type library.
A .NET assembly is converted into a single type library. It is a one-to-one mapping, and you cannot split an assembly over more than one type library. You will always get the same type library regardless of the way it has been produced from the assembly (for example, using the TlbExp.exe tool or the ConvertAssemblyToTypeLib class).
Note | Assembly names often contain periods (for example, System.Runtime.InteropServices ). Because the assembly name is often used as the base name for the type library and periods are not allowed in type library names, any periods in the assembly name will be converted to underscores during export. |
Assemblies are fully identified by a strong name, which consists of the assembly name, version number, public key, and locale information. Type libraries, on the other hand, are identified by three items:
A GUID, called the library ID (or LIBID)
An optional locale identifier (or LCID)
A version number
The LIBID is constructed from the assembly name and public key information. You must use the public key because there might be two assemblies with the same name, signed with different keys, that need to be mapped onto different type libraries. A given assembly name and public key always yields the same LIBID. If you want to use a specific LIBID, you can override the default by using the Guid attribute on the assembly.
Version information is passed from the assembly to the type library, but because COM supports only a two-part version number (in contrast to an assemblys four-part version number), only the major and minor parts of the version are preserved. For example, an assembly with version number 1.22.5.123 will produce a type library with version 1.22.
The assembly locale information is converted to an LCID; if there is no locale information, an LCID of zero will be used in the type library.
If an assembly has the AssemblyDescription attribute, the description will be used to provide a HelpString for the type library.
.NET types will lose their namespace information when exported. For example, the class MyNamespace.MyClass will be exported simply as MyClass . This can lead to problems if two classes have the same name but live in different namespaces, as you see here:
namespaceOuter{ //EnuminOuternamespace publicenumSeasons { Spring,Summer,Autumn,Winter } namespaceInner{ //EnuminOuter.Innernamespace publicenumSeasons { Spring,Summer,Autumn,Winter } } }
In this case, the export process will prefix the generated typenames with namespace information so that the two enums will be called Outer_Seasons and Outer_Inner_Seasons . Although this transformation will make the code work correctly when called from COM, the type names wont match those in the original .NET code, thus leading to possible confusion for COM programmers.
Note | Namespace prefixes are not used if there is no name clash . |
By default, every class in an assembly is converted into a coclass. The ComVisible attribute can be used to prevent .NET classes from being exported to the type library, as shown in the following C# sample code:
//Classesareexportedbydefault publicclassExportedToCOM { ... } //Thisclasswillnotbeexported [ComVisible(false)] publicclassNotExportedToCom { ... }
The generated coclass will be given an automatically generated GUID unless you specify one using the Guid attribute:
//SpecifyaGUIDforthecoclass [Guid(4a79399a-bf0f-4208-9512-aa3a438bd67a)] publicclassMyComClass { ... }
Abstract classes and interfaces will have their definitions in the type library marked with the noncreatable attribute. Concrete classes that dont have a default constructor will also be marked noncreatable because COM needs to be able to call a default constructor to create instances.
Inheritance disappears when the type library is generated because there is no inheritance of coclasses . A coclass will expose its own interfaces, plus all the interfaces exposed by its base classes, as shown in the following Visual C# example:
//BaseinheritsfromObjectbydefault,andimplementsinterfaceIOne classBase:IOne { } //DerivedderivesfromBase,andexposesadual //classinterface [ClassInterface(ClassInterfaceType.AutoDual)] publicclassDerived:Base { }
The coclass for the Derived class will look like this:
coclassDerived { interfaceIOne; interface_Object; }
The coclass exposes the IOne interface from the base class. The _Object interface represents an interface to the Object class; this interface will be generated for all .NET types, since everything inherits from Object eventually.
COM interfaces derived from .NET interfaces will contain the same methods and properties as the original .NET interface.
As with classes, a GUID will be generated automatically to represent the COM interface ID (IID) during the export process. You can override the default IID generation by using the Guid attribute, as the following C# code fragment shows:
[Guid(db568ac0-0c40-40ca-b8dc-9badf68588fc)] interfaceIMyInterface { ... }
Note | The automatically generated interface ID is derived from the interface name and the complete signatures of all the methods defined by the interface. If you alter any method signatures, or reorder the methods in the interface definition, youll get a different IID generated. The names of individual methods are not used, so you can change method names without affecting the IID. |
When exporting .NET interfaces, COM dual interfaces are generated by default, as they provide the greatest flexibility for COM clients . You can control the type of interface produced by using the InterfaceType attribute. This attribute takes one of the members of the ComInterfaceType enumeration, listed in Table 4-6, as a parameter:
Member | Description |
---|---|
InterfaceIsDual | The .NET interface is exposed to COM as a dual interface. This is the default value. |
InterfaceIsIDispatch | The .NET interface is exposed to COM as a dispinterface . Such interfaces can be used only via late binding. |
InterfaceIsIUnknown | The .NET interface is exposed to COM as a custom interface. Such interfaces can be used only via early binding. |
The following Visual C# example shows how to use the InterfaceType attribute:
'ACOMdispinterfacewillbegeneratedwhenthisinterfaceis 'exportedtoatypelibrary [ InterfaceType(ComInterfaceType.InterfaceIsIDispatch), Guid("ACDA141C-A24C-4499-B16E-A95A48A24484") ] publicinterfaceIBase { voidBaseMethod(); }
Interfaces can, and often do, form an inheritance hierarchy, as shown in the following C# example, where IDerived inherits from IBase . Any class that implements IDerived will have to implement the cumulative interface defined by both IDerived and IBase :
namespaceInterfaceInherit { //Abaseinterface,whichwillbeexportedasadualinterface //bydefault.AGUIDhasbeenspecifiedusingtheGuidattribute. [Guid("ACDA141C-A24C-4499-B16E-A95A48A24484")] publicinterfaceIBase { voidBaseMethod(); } //IDerivedderivesfromIBase [Guid("5DE99C74-D1F1-451d-A509-F8ACCD54BB2E")] publicinterfaceIDerived:IBase { voidDerivedMethod(); longSquare(intval); } //TheclasshastoimplementthemethodsofbothIBaseand //IDerived.SinceallthemethodsIwanttoexposetoCOMare //implementedviainterfaces,aclassinterfaceisnotneeded [ ClassInterface(ClassInterfaceType.None), Guid("682C25B2-5FC8-4b50-903B-2F8B2972F1DC") ] publicclassTheClass:IDerived { publicTheClass() { } //IDerivedMembers publicvoidDerivedMethod() { } publiclongSquare(intval) { returnval*val; } //IBaseMembers publicvoidBaseMethod() { } } }
When such a hierarchy is exported to a type library, the inheritance hierarchy is necessarily flattened because COM isnt object oriented and doesnt support interface inheritance at run time. A dual interface might appear to be derived from IDispatch in Interface Definition Language (IDL), but this means only that it contains the IDispatch methods in its vtable. It doesnt mean a dual interface is IDispatch in the object-oriented sense.
For the example above, you end up with the following items when the interfaces and class are exported to a type library:
An IBase interface that derives from IDispatch
An IDerived interface that derives from IDispatch
A coclass that implements both interfaces
The IDL that follows shows how this appears in the type library. You can see how the relationship between the two interfaces has disappeared, and how they are regarded as completely separate:
[ odl,uuid(ACDA141C-A24C-4499-B16E-A95A48A24484), version(1.0),dual,oleautomation, custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, InterfaceInherit.IBase) ] interfaceIBase:IDispatch{ [id(0x60020000)]HRESULTBaseMethod(); }; [ odl,uuid(5DE99C74-D1F1-451D-A509-F8ACCD54BB2E), version(1.0),dual,oleautomation, custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, InterfaceInherit.IDerived) ] interfaceIDerived:IDispatch{ [id(0x60020000)]HRESULTDerivedMethod(); [id(0x60020001)]HRESULTSquare([in]longval, [out,retval]int64*pRetVal); }; [ uuid(682C25B2-5FC8-4B50-903B-2F8B2972F1DC),version(1.0), custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, InterfaceInherit.TheClass) ] coclassTheClass{ interface_Object; [default]interfaceIDerived; interfaceIBase; };
If an exported class has a class interface, it will be made the coclasss default interface. If a class doesnt have a class interface, the default interface will be the first one implemented by the class. In this case, the default interface is IDerived .
COM interface method parameters have directional attributes that inform the marshaling code in which direction parameters need to be marshaled. The following bullet points summarize how parameters of .NET methods are represented in COM interface methods:
Reference types that are passed by value are marked as [in] parameters.
Types that are passed by reference are marked as [in,out] parameters.
Pointers (as used in C# unsafe code and managed C++ code) are also marked as [in,out] parameters.
You can apply the In and Out attributes to .NET parameters, and this will affect how they are represented when the type is exported. As you might expect, .NET In parameters are marshaled as COM [in] parameters, Out parameters are marshaled as [out] , and parameters that have both In and Out attributes are marshaled as [in,out] .
COM methods return HRESULT s, so .NET methods are converted in such a manner that function return values are represented as [out, retval] parameters, and the return type is converted to an HRESULT . The following example shows how a Visual C# function would be represented in a type library:
//Functionthatreturnsalong publiclongSquare(intn) //ResultingIDL HRESULTSquare([in]longn,[out,retval]int64*pRetVal);
Note | The PreserveSig attribute can be used to prevent this transformation from taking place so that the signature of the COM method matches that of the .NET method. |
If .NET methods dont use HRESULTs , what values get returned when .NET methods are executed by COM clients? If no error occurs, the runtime will return S_OK . If an exception is thrown by the .NET code, the runtime will return an HRESULT that depends on the exception thrown. A large number of possible HRESULT s can be returned (over 60 in total), and the most common ones are summarized in Table 4-7. For a full list, consult the .NET Framework Developers Guide in the online help, under the topic HRESULTs and Exceptions. Note that some HRESULT values can be mapped onto more than one .NET exception class; the table shows only the most common .NET exception classes for each HRESULT .
Exception Type | HRESULT | Numeric Value |
---|---|---|
ArgumentException , | COR_E_ARGUMENT | 0x80070057 |
ArgumentNullException | COR_E_NULLREFERENCE | 0x80004003 |
ArgumentOutOfRangeException | COR_E_ARGUMENTOUTOFRANGE | 0x80131502 |
ArithmeticException | COR_E_ARITHMETIC or ERROR_ARITHMETIC_OVERFLOW | 0x80070216 |
ArrayTypeMismatchException | COR_E_ARRAYTYPEMISMATCH | 0x80131503 |
DivideByZeroException | COR_E_DIVIDEBYZERO | 0x80020012 |
Exception | COR_E_EXCEPTION | 0x80131500 |
FileNotFoundException | COR_E_FILENOTFOUND or ERROR_FILE_NOT_FOUND | 0x80070002 |
FormatException , | COR_E_FORMAT | 0x80131537 |
IndexOutOfRangeException | COR_E_INDEXOUTOFRANGE | 0x80131508 |
InvalidCastException | COR_E_INVALIDCAST | 0x80004002 |
IOException | COR_E_IO | 0x80131620 |
MissingMemberException | COR_E_MISSINGMEMBER | 0x80131512 |
NotImplementedException | E_NOTIMPL | 0x80004001 |
NotSupportedException | COR_E_NOTSUPPORTED | 0x80131515 |
OutOfMemoryException | COR_E_OUTOFMEMORY or E_OUTOFMEMORY | 0x8007000E |
SecurityException | COR_E_SECURITY | 0x8013150A |
SystemException , | COR_E_SYSTEM | 0x80131501 |
Overloaded methods in .NET classes pose a problem because COM doesnt support the concept of overloading of interface methods. To overcome this, the exporter will generate unique method names by adding a suffix to each method name, consisting of an underscore and a numeral. The numeral starts at two and is incremented by one for each additional overloaded method, as shown in the following Visual C# example:
//C#overloadedmethods intMyMethod(intval); intMyMethod(intval1,intval2); intMyMethod(doubled); //COMinterfacemethods HRESULTMyMethod([in]longval,[out,retval]long*pRetVal); HRESULTMyMethod_2([in]longval1,[in]longval2, [out,retval]long*pRetVal); HRESULTMyMethod_3([in]doubleval,[out,retval]long*pRetVal);
Note that these names are generated automatically when the type is exported, and methods arent guaranteed to retain the same numerical suffixes in subsequent type library generations.
Properties, with their get and set methods, are a fundamental feature of .NET languages and can be defined in both classes and interfaces. COM also supports properties, using the [propget] and [propput] attributes on interface methods. Heres how the type library exporter handles exporting properties:
Property get methods become interface methods with the [propget] attribute.
Property set methods become interface methods with the [propput] attribute.
Properties without a get or a set method are ignored.
If the property type is a class or interface, the property set method will become an interface method with the [propputref] attribute, giving it an added level of indirection.
To show you how this works, here is a Visual C# class that implements two properties. One of them is of type double , while the other is of class type:
namespaceprops { [ClassInterface(ClassInterfaceType.AutoDual)] publicclassWorker { publicWorker(Workerbs) { theBoss=bs; } privateWorkertheBoss; privatedoublethePittance; publicWorkerBoss { get { returntheBoss; } set { theBoss=value; } } publicdoubleSalary { get { returnthePittance; } set { thePittance=value; } } } }
When exported to a type library, the resulting interface looks like this:
[ odl,uuid(2A33C1A5-B38A-36DB-B370-5C3F7AAA0E5F), hidden,dual,nonextensible,oleautomation, custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,props.Worker) ] interface_Worker:IDispatch{ //MethodsinheritedfromObjecthavebeenomitted [id(0x60020004),propget] HRESULTBoss([out,retval]_Worker**pRetVal); [id(0x60020004),propputref] HRESULTBoss([in]_Worker*pRetVal); [id(0x60020006),propget] HRESULTSalary([out,retval]double*pRetVal); [id(0x60020006),propput] HRESULTSalary([in]doublepRetVal); };
The get methods have been converted into [propget] methods, the set method for the double property has been converted to a [propput] method, and the set method for the Worker property has been converted to a [propputref] property.
Any public fields exposed by .NET types are also converted into [propput] and [propget] methods when the type is exported. Read-only fields are represented by a [propget] method.
The type library exporter will convert .NET types to suitable unmanaged types during the export process. This is more complex than importing data types when using COM components in .NET because.NET components can use a greater range of data types than are available for COM interfaces.
Table 4-8 shows the corresponding .NET, COM IDL, and Visual Basic types for some of the most common data types.
.NET Type | IDL Type | VB6 Type | Remarks |
---|---|---|---|
System.Boolean (when used as a parameter) | VARIANT_BOOL | Boolean | |
System.Boolean (when used as a field in a structure) | long | Long | |
System.Byte | unsigned char | Byte | |
System.Int16 | short | Integer | |
System.Int32 | long | Long | Can also be marshaled as an IDL HRESULT by using the MarshalAsAttribute . |
System.Int64 | int64 | n/a | |
System.IntPtr | long | Long | |
System.UInt16 | unsigned short | n/a | Unsigned types arent |
System.UInt32 | unsigned long | n/a | Can also be marshaled as an IDL HRESULT by using the MarshalAs attribute. |
System.UInt64 | uint64 | n/a | |
System.Single | single | Single | |
System.Double | double | Double | |
System.String (used as a parameter) | BSTR | String | |
System.String (used as a field in a structure) | LPSTR | String | |
System.DateTime | DATE | Date | |
System.Guid | GUID | n/a | |
System.Decimal | DECIMAL | n/a | Can be marshaled to Visual Basic as a Currency by using the MarshalAs attribute. |
System.Object (used as a parameter) | VARIANT | Variant | |
System.Object (used as a field in a structure) | IUnknown* | n/a | |
Arrays of a given type | SAFEARRAY(type) | type() |
Note that for some types the marshaled type will be different depending on whether theyre used as parameters or as fields in structs. You can also vary the way some types are marshaled by using the MarshalAs attribute. This advanced interop feature is discussed in Chapter 13, Advanced Interaction.
.NET value types are exported as IDL structs. Youll need to use the StructLayout attribute with the LayoutKind.Sequential argument to fix the layout of the members so that it isnt altered by .NET. Here is an example struct coded in C#:
[StructLayout(LayoutKind.Sequential)] publicstructMyStruct { publicintvalOne; publicintvalTwo; publicvoidMethod1(inta) { valOne=valTwo=a; } }
The corresponding COM type in the type library looks like this:
typedef[uuid(4D469648-1406-3683-BADA-580CE600EE2E),version(1.0), custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,ExportTest.MyStruct) ] structtagMyStruct{ longvalOne; longvalTwo; }MyStruct;
You can see how the value type has been represented by an IDL struct. Note also that the generated struct contains only data members; methods will not be exported.
Note | The custom IDL attribute was introduced in Chapter 3. It provides a way to specify the namespace that will be used for this COM type if it is imported back into .NET using the TlbImp.exe tool. |
Enumerations are converted into COM enumerations in the generated type library. Since it is a COM requirement that names of enumeration members be unique, the exporter will generate unique names by adding the name of each member of the enumeration as a prefix to the enumeration name itself. For example, consider the following .NET enumeration coded in C#:
publicenumCompassPoint { North=0, East=90, South=180, West=270 }
The generated COM enum will look like this:
typedef[uuid(9371DAC5-C4FA-3B40-8822-90CE107AD8F9),version(1.0), custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9,ExportTest.CompassPoint) ] enum{ CompassPoint_North=0, CompassPoint_East=90, CompassPoint_South=180, CompassPoint_West=270 }CompassPoint;
You can see that each name has been prefixed with CompassPoint_ to generate a unique identifier.
|