The Easy Way: MKTYPLIB and the Object Description Language

If you think about the amount of information you might want to describe for, say, one object and four interfaces, with each interface having about six member functions, you can quickly see that calling ICreateTypeLib and ICreateTypeInfo functions directly would get tedious beyond human patience. You can also see that much of what goes into a type library is similar to what you'd put in a header file. You would figure it makes sense to have some sort of tool that would parse headerlike information and call the appropriate interfaces and member functions internally to create type information out of that header. Just such a tool exists: MKTYPLIB.EXE—literally, "Make Type Library." This small compiler is more or less a C++ preprocessor that uses a file written in ODL as input. The parser recognizes ODL keywords and syntax and in turn calls CreateTypeLib and the other interfaces' member functions to turn the ODL information into a real type library. MKTYPLIB can also create a C/C++ header file containing interface and GUID definitions to simplify your implementation of the interfaces and classes defined in the library. This is quite convenient, and we'll see examples of this in later chapters.

To ship type information with an object, you can either ship the type library file stand-alone, integrate it into the resources of an EXE or a DLL, or place it inside a compound file. In a later section, we'll look at deployment and use of a type library. First, however, let's look at the format of an ODL file in more detail.

The keywords you can use and the syntax of an ODL file allow you to basically describe any structures, interfaces, dispinterfaces, modules, and objects that you want, using whatever types you want. An ODL file is simply a text file that has the following general syntax structure, in which keywords are indicated with boldface:


[uuid(<GUID>), <helpinfo>, <attributes>]
library <name>
{
importlib(<path to another type library>)

typedef [<attributes>] <basename | struct | enum | union>
{
§
} <type>;

[attributes] module <name>
{
<element list>
};

[uuid(<GUID>), <helpinfo>, <attributes>]
interface <name>
{
[<attributes>] <return type> [calling convention]
<function name>(<arguments>);
§
}

[uuid(<GUID>), <helpinfo>, <attributes>]
dispinterface <name>
{
Properties:
[<attributes>] <type> <name>;
§
Methods:
[<attributes>] <return type> <name>(<arguments>);
§
}

[uuid(<GUID>), <helpinfo>, <attributes>]
dispinterface <name>
{
interface <name>;
}

[uuid(<GUID>), <helpinfo>, <attributes>]
coclass
{
[<attributes>] dispinterface <dispinterface name>;
§
[<attributes>] interface <interface name>;
}
};

Although the OLE Programmer's Reference has a complete description of ODL (it's too lengthy to repeat here), let's nonetheless take a look at this structure and point out the most important concepts.

You define the type library with the library keyword, assigning to it a LIBID (a GUID, the uuid), help information such as a descriptive string, and other attributes, all of which are specified between the square brackets preceding library. The attributes can include version information, the library LCID, and whether the entire library is hidden or restricted.

The body of a type library is contained between the braces following the library statement and can contain any number of each of the statements listed in Table 3-5. The most interesting statements to us so far are coclass and interface


Head-Banging MKTYPLIB Errors

MKTYPLIB is not necessarily the friendliest tool in the world, and the error messages that it generates can be the epitome of ambiguity. Here are a few I've seen:

First, it might complain, "Error generating type library: MKTYPLIB cannot preprocess the file." This is provoked either by a failure to find STDOLE.TLB as referenced in your importlib statement or by a failure to find CL.EXE, the C-compiler preprocessor, both of which have to be on your path. To avoid the preprocessing step, you can use the /nocpp switch to bypass it without consequence.

The second strange error message is MKTYPLIB complaining of "unknown types," especially about things like boolean and BSTR which are definitely known types. This problem is caused by mismatched OLE DLLs on your system—MKTYPLIB uses the OLE DLLs themselves, where the implementation of CreateTypeLib and ICreateType[Lib- | Info] are found. If, for example, you have debug OLE DLLs in your path, you'll end up with an OLENLS.DLL that is not enabled for DBCS characters and ends up for a variety of reasons making types line BSTR unrecognizable.

Finally, you need to be very careful when creating a dispinterface or one marked as oleautomation (automation compatible). MKTYPLIB itself can accept any type you want to put in an interface, including your own structs, enums, and unions. However, as we've seen, these are not accepted in an automation-compatible interface which includes by design any dispinterface. Furthermore, unsigned is not accepted for automation-compatible interfaces, although you can use it anywhere else. The inconsistencies that arise from not understanding what automation compatible means can really drive you batty.


(and dispinterface). Through these statements, you describe the core of an object: its interfaces and the methods and properties of those interfaces. All of the <type> entries shown earlier can be a predefined standard type such as int, long, double, or char or any user-defined type created with a typedef statement.

Statement

Description

importlib

Brings in all the information contained in another type library and makes it part of this library. The statement requires the filename of the TLB file. If a full pathname is not specified, this statement looks for the file in directories specified by the system path. All ODL files must have the statement importlib ("STDOLE.TLB") to pull in standard types.

coclass

Describes a component class in which uuid is the object's CLSID. The object can have any number of interfaces and dispinterfaces listed in its body, specifying the full set of interfaces that the object implements, both incoming and outgoing.

interface

Describes a vtable interface with the given name in which the body contains only member functions as in any other interface declaration. The uuid attribute is the IID.

dispinterface

Describes a dispinterface with the given name in which the body contains the list of properties and methods; properties are listed as data members in a C++ class and methods as C++ member functions. An alternative syntax can be used to distinguish a dispinterface from a vtable interface. The uuid attribute is the IID.

typedef

Defines a new type as an alias for an existing type or as a struct, enum, or union. The syntax is identical to the C typedef syntax. To be included in the final type library, a typedef must have at least one attribute, the most common of which is public, making it a public type. All typedefs can also have help information, a UUID, and a version number.

module

Describes exported functions and constants from a DLL just like an import library (LIB), which can be used for linking without an import library at all.


Table 3-5.

Statements that can appear in a type library.

The coclass statement

If you'll remember back to Chapter 2, there was the question about why IUnknown didn't have a way to ask an object to enumerate all of its incoming interfaces short of calling QueryInterface for everything you knew. The answer was type information, and the coclass statement provides just such a listing of the interfaces an object supports. The object itself is identified with its CLSID, given in the uuid attribute. A client that knows this CLSID can look through the type information, with or without instantiating the object, to easily determine an object's full set of incoming and outgoing interfaces. Without type information, QueryInterface is the only way to learn about the incoming interfaces; IConnectionPointContainer::EnumConnectionPoints is the only way to know an object's outgoing interfaces, and then you have to make sense of the enumerated IIDs for which you need type information anyway.

One very important attribute for coclass is called licensed. This attribute identifies the object's server as supporting an interface named IClassFactory2, which means the object (component) is licensed. This makes a difference with respect to how an object of the class must be instantiated, as we'll see in Chapter 5.

Each interface or dispinterface listed in a coclass can have only two optional attributes: default and source. If an interface or dispinterface is marked source, it is an outgoing interface; otherwise, it's an incoming interface. The default attribute can be used for one incoming and one outgoing interface or dispinterface. The default incoming interface is the primary interface for the object. If the interface is a dispinterface, a client can obtain its pointer with QueryInterface(IID_IDispatch); all other dispinterfaces must be queried for by using their specific IIDs. The default source interface is called the primary event set for the object, but the object can have any number of event sets or other outgoing interfaces. Marking interfaces and dispinterfaces as default makes it convenient for a client to locate the object's most important interfaces.

The interface statement

If you want to describe a vtable interface, such as IUnknown, the interface statement lets you list all the member functions of that interface with the syntax shown earlier. Each function has optional attributes, a return type, an optional calling convention (cdecl, pascal, or stdcall, with or without a leading _ or __ ), the name of the member function, and its list of arguments, which can be void or any sequence of [attributes] <type> <name> entries separated by a comma. Yes, this is pretty rich stuff.

The attributes you can specify on an interface include its IID, help information, version information, whether it is hidden or restricted, whether the interface is what is called a dual interface in OLE Automation, and whether the interface uses only oleautomation-compatible argument types (which are restricted, as we'll see in Chapter 14). You must also always include the odl attribute on every interface to distinguish it from an IDL definition.5 The attributes on each function in the interface include whether the function is a property get, property put, or property put-by-reference operation (propget, propput, propputref); whether it has variable arguments; and whether it is hidden. A leading underscore on a member name, as in _NewEnum, is equivalent to including the hidden attribute. In addition, the id(xx) attribute assigns some identifier to the member (useful for OLE Automation, especially dual interfaces). Finally, arguments themselves each have attributes such as in, out, and optional, which specify the behavior of the argument.

The dispinterface statement

You might have noticed that dispinterface has two forms. The first form allows you to describe methods and properties as if you were declaring a C++ class, using the keywords Properties or Methods as you would use public or private. Each property can have an id attribute to specify its dispatch identifier (dispID) as well as help information, whether it's hidden, and whether this particular property is read-only. Each method and argument can have the same attributes as interface.

The second form of the dispinterface statement basically picks up the functions in some interface and makes a dispinterface out of it. Whichever members in the interface had propget, propput, or propputref on them become properties in the dispinterface; other members become methods. In all cases, any id in the interface is preserved.

Having two forms for dispinterface was necessary before the idea of dual interfaces became available. Dual interfaces are now recommended over the definition of a dispinterface from an interface. We'll see dual interfaces in Chapter 14.

The previous discussion of the most important ODL statements mentioned a fair number of attributes, but not all of them. The complete list as of the time of writing this chapter appears in Table 3-6. Entries marked with an asterisk (*) are available under Win16 with OLE 2.02 or later, and those marked with a double asterisk (**) are available only under Win16 if you have the MKTYPLIB.EXE file from the OLE Custom Controls Development Kit. Because not all of these attributes can be used with every statement or other field in an ODL file, the allowable uses of each attribute are cross-referenced in Figure 3-2 on page 168.

Attribute

Description

appobject

Identifies a coclass as an application object, which is associated with a full EXE application.

bindable**

Identifies a property as supporting data binding through the IPropertyNotifySink::OnChanged function.

control**

Identifies a coclass as an OLE control.

default**

Specifies a default incoming or outgoing interface in a coclass.

defaultbind**

Identifies a bindable property that best represents the object as a whole.

displaybind**

Indicates that the binding capability of a property can be displayed in the user interface of a controller.

dllname

Identifies the name of the DLL described in a module.

dual*

Marks an interface as a dual interface.

entry

Specifies an exported function or constant in a module.

helpcontext

Specifies a context ID within a help file that can be used to view information about this element.

helpfile

Specifies the filename (no path) of the help file that contains information about this element. Your installation program must save the path of the help file in the registry under TypeLib\HELPDIR; you do not want to hard code such paths into your type library.

helpstring

Contains a short description of the element, usually suitable for a status line or a similar user interface.

hidden*

Indicates that the element should never be displayed in a browser in a programming environment.

id

Assigns a DISPID to a function, method, or property.

in

Identifies an argument as an in-parameter.

lcid

Identifies the locale for a library or an argument.

licensed**

Indicates that a coclass must be instantiated using IClassFactory2.

nonextensible*

Indicates that an interface or dispinterface includes only those methods and properties listed and cannot be extended with additional members at run time.

odl

Required for all interface statements to identify the description as an ODL description as opposed to IDL.

oleautomation*

Indicates that the interface or dispinterface is OLE Automation compatible.

optional

Marks an argument as optional.

out

Marks an argument as an out-parameter.

propget

Used in an interface to indicate that the named function performs a property get on a property of the same name.

propput

Used in an interface to indicate that the named function performs a property put on a property of the same name.

propputref

Same as propput but indicates that the put value can be passed by reference.

public

Includes a typedef in the type library so it can be browsed later; otherwise, treats it as a #define.

readonly

Marks a property as read-only.

requestedit**

Identifies a property that will send IPropertyNotifySink::OnRequestEdit before changing.

restricted

Indicates that a library or members in a module or interface or dispinterface cannot be called from arbitrary clients. Usually this means that some other form of access is required to call the function.

retval*

Marks a parameter in a dual interface as the actual return value for a method when the method returns HRESULT.

source**

Indicates that an interface or dispinterface listed in a coclass is outgoing.

string

Declares a string for compatibility with IDL.

uuid

Assigns a GUID to the element.

vararg

Indicates that the function takes variable arguments.

version

Specifies a version number for the element.


Table 3-6.

Listing of ODL attributes.

Figure 3-2.

Attributes and their corresponding ODL statements.

Keep in mind that you can use C-language comments, /*...*/ and //, in an ODL file. The various samples that accompany this chapter show different ODL files that use many of these attributes in real working pieces of code.


Other Type Library Generation Tools

Since MKTYPLIB is really little more than a preprocessor that ties into an OLE service, you can create other tools that generate type libraries without involving MKTYPLIB at all. You can, in fact, even bypass ODL files completely since such a file is simply input to MKTYPLIB. You can easily imagine a tool that would provide a way to specify interfaces and object classes faster and easier than writing an ODL file from scratch. You can also imagine a tool that would actually write the ODL file for you. (The ClassWizard in Microsoft Visual C++ does this.) The point is that MKTYPLIB and ODL files should not be considered the final word in creating type libraries—they're just tools.


5 The Interface Definition Language is used to describe interfaces for marshaling purposes as we'll see in Chapter 6. ODL and IDL are very similar and have their differences but Microsoft is currently looking at ways to merge the two. Interface descriptions in ODL without the odl will simply fail to compile.