|
Posted on
2013-01-31 17:39
bw_0927
阅读( 347)
评论()
收藏
举报
http://www.microsoft.com/msj/0898/idl/idl.aspx
http://www.codeproject.com/Articles/19605/Basics-of-an-IDL-file
Understanding Interface Definition Language: A Developer's Survival Guide
|
Bill Hludzinski
IDL is the preferred way to describe your interfaces. However, many developers only have a rudimentary knowledge of IDL. Knowing IDL will help you think about your interfaces in a more explicit manner, which is especially useful now that you spend so much time exposing interfaces. |
This article assumes you're familiar with C++, COM, and marshaling. |
The age of IDL (Interface Definition Language) is upon us. IDL is the preferred way to describe your interfaces. However, many developers only have a rudimentary knowledge of IDL. They rely on the Visual C++ wizards to generate their IDL files, but don't have any idea what the code does. On the other hand, who wants to browse the MIDL Programmer's Guide in their spare time, trying to learn a new language with over 140 keywords? To meet your IDL needs, I am going to provide a basic survival guide that will show you what IDL is, when you need it, and the basics of using it. You'll find that knowing IDL will help you think about your interfaces in a more explicit manner, which is especially useful now that you spend so much of your time exposing interfaces.
Review
IDL
was originally part of the Open Software Foundation's Distributed
Computing Environment (DCE). It described function interfaces for Remote
Procedure Calls (RPCs), so that a compiler could generate proxy and
stub code that marshaled parameters between machines. MIDL is
Microsoft's IDL compiler. In addition, Microsoft developed its own
Object Definition Language (ODL), which included the dispinterface
keyword for specifying IDispatch's logical interfaces. ODL scripts could
be compiled by MKTYPLIB into type libraries (.TLB files), which could
be used by Automation clients for early binding.
With
Windows NT 4.0, Microsoft released MIDL 3.0, which merged the two
languages by extending IDL (now called COM IDL) to include ODL's
features. This enabled the MIDL compiler to generate type libraries as
well as proxy and stub code for marshaling. For a discussion of the
differences between the older ODL and the MIDL 3.0 COM IDL, see "Introducing Distributed COM and the New OLE Features in Windows NT 4.0" by Don Box (MSJ,
May 1996). For a discussion of marshaling, IDL, and the tradeoffs
between custom, dispatch, and dual interfaces, I highly recommend Don
Box's December 1995 OLE Q&A column. Just remember that the article was written before Microsoft merged MIDL and MKTYPLIB.
So
let's look at what MIDL does. By default, if you declare an interface
in your IDL file, MIDL will take the declared functions and generate all
of the files for an RPC interface. This includes a client proxy, a
server stub, and a header file. Now, if you specify the [object]
attribute in your interface attribute list, MIDL will instead generate
all of the proxy and stub files for a COM interface. This includes the
interface proxy file (name_p.c), the type definition header file
(name.h), the interface GUID file (name_i.c), and the proxy DLL file
(dlldata.c). And finally, for each library block that you define within
the IDL file, MIDL will generate a type library (see Figure 1).
|
 |
Figure 1 MIDL-generated Files |
Before
proceeding with a discussion of IDL's makeup, let's take a look at just
where and when you need to use IDL. Even though it is always good
practice to know how to specify your interfaces in IDL, different
projects may not have a programmatic need for IDL.
The
first reason you might need IDL is so that MIDL can generate the
proxy/stub DLL for marshaling data across apartment boundaries. When you
create an inproc server that matches the threading model of the
creator's thread, you are not crossing any apartment boundaries so no
marshaling will occur. In all other cases, such as out-of-process
servers or inproc servers that do not have the same threading model as
the caller, you need to marshal the
data, thus you will always need a proxy or stub. However, you only need
MIDL to generate your proxy and stub when you are exposing a custom
interface via standard marshaling. You can't use MIDL with custom
marshaling because you must implement your own proxy and stub from
scratch, and you don't need MIDL with standard interfaces
because the proxy and stub already exist as part of the standard
marshaler.
The
second reason you need IDL is to generate type libraries, which you
will need when exposing dispinterfaces. Now even if you go through this
exercise and realize that you need IDL, you still may not have to write
any IDL because of the Visual C++ 5.0 wizards. The ATL wizards generate
the IDL for your proxy, stub, and type library, though in MFC you are
not so fortunate. If you create an MFC EXE Automation Server, it
generates the ODL file for you to generate your type library (note that
MIDL 3.0 can compile legacy ODL files with the /mktyplib203 option), but
it will not generate the IDL file needed for a custom interface. Simply
put, use ATL for custom and dual interfaces and save yourself a load of
time. Figure 2 summarizes this information for out-of-process servers.
With
the more general issues explained, I'll look at the most common
keywords in IDL from the ground up, starting with the stuff that makes
up basic RPC. I'll work my way back to the very COM-specific IDL that's
generated for you by the ATL wizards.
Just Like C++!
It's
easiest to understand IDL if you look at it from a historical
perspective. For many years, interfaces between statically linked
modules have been declared in C header files. These header files would
include function declarations, as well as any supporting data type
declarations. So when developers first thought of generating marshaling
code for RPCs automatically, they made a code generator that could read
these ubiquitous C header files. In other words, the IDL compiler acts
like a C compiler that reads C-style data and function declarations, but
instead of generating object code, it generates C source code for
marshaling the data types across process boundaries. For example, given
the following C declaration
|
long AddValues(long val1,
long val2);
|
the corresponding IDL file would look like this: |
long AddValues([in] long val1, [in] long val2);
|
So IDL is rooted in the data type declaration portion of C and C++. It supports all of the standard C++ data types as well as the data definition keywords that you've grown to know and love. More importantly, IDL's data types and definitions are both language-neutral and platform-neutral.
Some
of the base data types supported by IDL are int, Boolean, byte, char,
double, float, long, short, and void *. IDL also supports the signed and
unsigned qualifiers, the const type modifier, and the wchar_t
predefined type. As far as data definition keywords go, IDL supports
enum, typedef, union, and struct. IDL even supports the comment tokens
and the use of preprocessor directives like #define and include (which
doesn't use the # prefix). When writing IDL for an API, developers will
just take their header files, comment them out, include them with the
IDL file, and start uncommenting the functions. IDL and C/C++ really are
that close.
Code Generation
After
running an IDL file through MIDL and generating proxy/stub source, the
IDL file and the resulting source will look similar in some ways. But
the MIDL compiler's preprocessor will eliminate the IDL comments and
expand the #define statements while processing the IDL file so that they
don't appear in the generated C code. If you need to put untouched
#define directives, comments, or any C/C++ syntax into the generated
code, a handy IDL keyword is cpp_quote, which allows you to place any
text directly into the generated header file (including comments if
you'd like). So the following IDL line
|
cpp_quote("#define UNICODE")
|
would appear in the generated code like this: |
Much of the time your IDL file will need to see data type definitions from other header files and even other IDL or ODL files to process base data types. In these cases, you will have to use the import directive, which actually includes the file in the MIDL processing. The include directive just makes files available to the source code from which the proxy/stub code is compiled. It's important to remember that any function declarations in imported files are ignored, and that in the resulting source code, the generated header file will not directly contain the imported types. Instead an #include directive is generated for the header corresponding to the imported interface. The following import statement |
would be generated in the source code as: |
Since I'm discussing the importing of files, it's probably a good time to mention the standard IDL include files. wtypes.idl, unknwn.idl, objidl.idl, and oaidl.idl ship with Visual C++ and Visual J++. You can find them in their respective include directories. These files document the standard C library of the COM era in one place. They include the data types, #defines, and interfaces that have been defined (and in many cases implemented) by Microsoft for supporting COM.
Of
course, Visual C++ ships with even more IDL files for things like
ActiveX controls and ActiveX documents, but these four files are the
fundamental ones to include in your own interfaces. Figure 3
lists most of the IDL files that ship with Visual C++ 5.0 in order of
dependency. The most dependent ones are at the bottom. One of the best
ways that you can familiarize yourself with IDL is by browsing through
these files.
Your First Attributes
In
addition to specifying the basic data types for marshaling, there are
many more qualities that IDL can specify about your interfaces that
aren't supported by regular C-style syntax. These annotations are
formally referred to as attributes. Attributes are grouped together in a
comma-delimited list surrounded by brackets, and they always precede
whatever object they are describing.
In
the following function declaration, there are separate attributes
listed for each of the parameters in the function call, with two
attributes listed for the third parameter:
|
void fx([in] long l1, [out] long *pl2,
[in, out] long *pl3);
|
In this case, the attributes are all applied to the arguments of this function. However, attributes are not limited to arguments alone. They can be applied to other things such as functions or libraries. [in] and [out] are two of the fundamental attributes that IDL makes available.
Any
data that is to be marshaled remotely must be placed into a data packet
and transmitted across the network, so the actual transfer of function
parameters can be an expensive operation. To save work, IDL supplies the
[in] and [out] attributes for explicitly describing which way
parameters need to be copied. A parameter specified as an [in] parameter
will only be copied from the client to the server. Likewise, a
parameter specified as an [out] parameter will only be copied from the
server back to the client. A parameter specified as [in,out] will be
copied in both directions. If inbound data is to be modified and
returned to the caller, it is done by passing in a pointer to where the
resulting data is to be placed. For this reason, [out] parameters must
always be pointers.
This
brings me to the next item at hand, marshaling pointers and how they
differ from your typical pass-by-value arguments. For example, an
integer is marshaled by simply copying the value from the address space
of the calling process to the address space of the destination process.
But a pointer to a string would be marshaled by copying the data that
the pointer points to, not the pointer. For out-of-process calls to be
transparent, whatever exists in the client's address space at the time
of the call must be recreated in the server's address space.
Take a look at the following IDL example:
|
long fx([in] const long *pval);
|
If a client called this function, passing the address of a variable whose value was 17, the value 17 would be sent across the network from the client's proxy to the server's stub. On the server, the stub code would copy the value 17 into the server's address space and call the server function with a pointer that pointed to that value (see Figure 4). |
 |
Figure 4 Marshaling a Pointer |
Now
imagine that the caller invoked the function with
a null pointer instead of a valid pointer. The proxy would need a
special tag for null pointers that it passed to the server. The server
stub would then translate that token back to a null pointer on the
server side and call the
server function with the null pointer.
It
is common for functions to disallow null pointers as parameters,
checking the pointers to see if they are null, and if so, returning with
an error code. That's fine for in-process code, but when performing RPC
across a network, these round-trips are much too expensive for frequent
use. So somebody thought of a way to prevent this all-too-common
occurrence by making the pointer declarations more explicit. If you
could disallow null pointers ahead of time, the marshaling code could
catch illegal null values and return an error code before any data has
to be sent across the wire. This pointer, called a reference pointer, is
the simplest type of pointer in IDL. It must always reference a valid
address; if it is null, the marshaling code will return with an error.
To indicate that a pointer is a reference pointer, you must use the
[ref] attribute.
|
// pval cannot be null
long fx([in, ref] const long *pval);
|
Pointers that can be null are called unique pointers and are indicated by the [unique] attribute. |
// pval can be null
long fx([in, unique] const long *pval);
|
There is another characteristic of pointers that marshaling must emulate as well: duplication. If you look at the following IDL example, you will notice that the function takes two pointers: |
long fx([in] long *pval1, [in] long *pval2);
|
Now consider the following client code: |
long l1 = 10;
long l2 = fx(&l1, &l1);
|
This code is noteworthy because it passes the same pointer twice in the same function invocation. This means that the value pointed to by both pointers will be passed across the network twice, once for each value. When the stub unmarshals the parameters on the server side, it will allocate two distinct memory blocks, one for each value, and set each pointer to point to its value's newly allocated memory. So on the server side, even though both pointers will point to memory with a value of 10, they will be pointing to different memory locations, unlike on the client side.
This
side effect can result in erroneous execution if the behavior of the
server code actually takes into account the equivalence of the two
pointers. IDL can handle this through the use of full pointers. A full
pointer comes closest to fully simulating C pointers because its
interface marshaler
performs duplicate pointer detection and will make sure that duplicate
pointers are unmarshaled as duplicate
pointers, pointing to one memory location instead of two. A full pointer
may be used by declaring a pointer with the
full attribute:
|
long fx([in, full] long *pval1,
[in, full] long *pval2);
|
It's important to note that full pointers incur a reasonable performance penalty searching for duplicate pointers, so only use them in place of reference pointers or unique pointers when the semantics of the function take duplicate pointers into account.
Believe
it or not, the issue with pointers and memory management gets even more
complex. When you pass [in] values into a function via a pointer,
things are still simple. The caller will always allocate the memory,
point the pointer to the memory, and put the value in that memory before
calling the function. However, with an [out] parameter, things aren't
so straightforward because the caller isn't providing a value, so you
might be tempted to pass in a null pointer and have the callee allocate
the memory.
This
opens up a can of worms. How can the caller free the memory? This can
be done by the COM task allocator which I'll talk about later. Suffice
it to say that things are much easier when you depend on the caller to
always allocate the memory for [out] parameters. For this reason,
caller-allocated memory is the standard for [out] parameters in COM.
This means that an [out] parameter should never be null, and hence
should always be a reference pointer.
Things get even more interesting when user-defined types begin to contain pointers. Look at the following IDL:
|
typedef struct tagELEMENT {
long lValue;
[unique] struct tagELEMENT *pPrev;
[unique] struct tagELEMENT *pNext;
} ELEMENT;
void GetElementList([out] ELEMENT *pList);
|
This example contains pointers within a user-defined structure. These types of pointers are embedded pointers, whereas your typical pointer (which isn't embedded in a structure) is a top-level pointer. Let's say that the purpose of this function is to return a doubly linked list of elements to the caller.
First,
let's assume that the caller acts appropriately and allocates memory
for a single ELEMENT, then passes its address into the GetElementList
function. Because there was only one element in the list, the caller
left pNext and pPrev as null. This violates the rule about passing null
pointers for [out] parameters because all allocation should be done by
the caller. But if you think about it, how could the caller know up
front how many elements to place in the list if that's what
GetElementList is to return? Besides, you use null pointers to indicate
the top and bottom of the list. This isn't what you want. With embedded
pointers, you want them to be able to be null going in, and you want the
function to be able to allocate the memory for the elements, setting up
the list by assigning the pPrev and pNext pointers appropriately.
To
handle this situation, embedded pointers are treated differently. It
makes more sense for the callee to allocate the memory since the caller
can never know how much to allocate. Thus, you use the [unique]
attribute to allow embedded pointers to be null going in, and assigned
to memory on the function return. However, for top-level pointers the
old logic still applies, and they must still be made reference pointers.
If you look at the previous code, you will notice that the pPrev and
pNext pointers both have the [unique] attribute, yet the pList parameter
in the GetElementList function call would still default to a reference
pointer as an [out] parameter in the function declaration. This makes
all of the pointers legal, but leaves a more serious problem.
Embedded
pointers use callee-allocated memory. But if the callee allocates the
memory, how can the caller free it after the function call? The caller
doesn't know how much has been allocated, and even if she did know,
there is still the issue that memory has been allocated on both the
server and client side. The caller has no idea how to free memory on the
server side. There needs to be some way that a callee can allocate
memory that the caller can free—and there is.
This
is where the COM task allocator comes to the rescue. The COM task
allocator is a per-process memory allocator that is used precisely for
allocating memory to be shared between processes on either side of an
interface. The task allocator is an implementation of the IMalloc
interface, but is typically used via the following three COM API
convenience functions:
|
void *CoTaskMemAlloc(ULONG cb);
void *CoTaskMemRealloc(void *pv, ULONG cb);
void CoTaskMemFree(void *pv);
|
When you're working with embedded pointers and linked user-defined types, you will be using callee-allocated memory, and these functions will help. Further discussion of the COM task allocator is beyond the scope of this article, but if you'd like to see it explained and put into use, refer to Don Box's OLE Q&A column in the October 1995 issue of MSJ.
Strings and Arrays
Basically,
marshaling a string is easy because of the [string] attribute. This
attribute lets the marshaler know that a pointer points to a
null-terminated character array. As a result, the marshaler knows how
much data it has to copy to the server process. Even though the [string]
parameter can be used for null-terminated arrays of bytes or chars, COM
is based on 16-bit Unicode characters, so for all COM methods you will
want to base your strings on OLECHAR type characters. OLECHAR is defined
in wtypes.idl, and is a platform-independent typedef for wchar_t.
|
void MyFunction([in,
string] const OLECHAR
*pwszName);
|
It is interesting to note that with the [string] attribute, the marshaler behaves as it should and will copy the data pointed to until it hits a null. Without the [string] attribute, the data pointed to would have been treated as a single OLECHAR and only two bytes would have been copied by the marshaler. This point is significant when discussing arrays. Without an attribute or some sort of qualifier telling MIDL otherwise, pointers are assumed to point to single instances of a data type.
The simplest form of an array can be passed by fixing its size at design time using C array syntax:
|
void Fx([in] long alValues[4]);
|
This example passes a fixed array of four longs with which the marshaler can easily figure out how much data to copy to the server process. However, the most common case is where the size of the array will not be known until runtime. In this situation, IDL provides a series of attributes for specifying the array's size at compile time or runtime. These types of arrays are called conformant arrays, and the size of the array may be defined via the [size_is] attribute. Typically, you will use one of the other arguments in a function to specify the array size using [size_is]. |
void Fx([in] long cItems,
[in, size_is(cItems)] short aItems[]);
|
You may have noticed that I used C-style variable-length syntax for aItems; a pointer works just the same. The [max_is] attribute may be used in the same situations where you use [size_is]. [size_is] indicates the maximum number of elements in an array, while [max_is] indicates the maximum value for a valid array index. So if you use [max_is] on an array of size n, where the first array element starts at zero, then [max_is] would be set to the maximum valid index, which would be n–1. Both of the attributes use constants for the array size. This is not recommended because it is slower than using a fixed array.
There
is another case that I must cover: arrays used as [out] parameters.
Imagine that the caller wants to pass in a caller-allocated array that
is empty and have the callee fill it up with valid values. This can be
done by simply passing in the array with [out] and [size_is] attributes,
but that would be inefficient. What if the callee function uses only
one-third of the elements in the list? The marshaler would still marshal
the entire array back to the caller function's proxy. To get around
this problem, IDL has the varying array, which will only transmit back
to the caller the array elements that are being used. This is
accomplished via the [length_is] attribute, which is only used for [out]
parameters. The number used by [length_is] to define the contents of an
array is called the variance of the array.
The
following example illustrates how the [size_is] and [length_is]
attributes may be used together to explicitly specify how many
array elements need to
be marshaled.
|
void Fx([in] long cMax,
[in, out] long *pcUsed,
[in,out,size_is(cMax),
length_is(*pcUsed)] long *aValues);
|
On the way in, the [size_is] attribute lets the marshaler know that the array is cMax longs so the stub will allocate the required memory on the server. But the [length_is] attribute tells the marshaler that only *pcUsed longs need to be marshaled, so they are the only elements of the array that are actually transmitted to the server. Quite efficient!
The
[size_is] attribute has [max_is] as its counterpart. They are the same
except [max_is] specifies the array size by defining the maximum valid
index. [length_is] and [last_is] have a similar relationship. They are
the same except [last_is] specifies the number of elements used in the
array by defining the last index used by an element in the array. Once
again, an array specified as [length_is(n)] is equivalent to one
specified as [last_is(n–1)]. To make things even more flexible, IDL also
has a [first_is] attribute that can be used to define the index at
which the array begins to be used. So in the following example, out of a
100-element array, only 11 elements are actually transmitted—elements
12 through 22.
|
void Fx([in, size_is(100), first_is(12),
last_is(22)] long *aValues);
|
For another look at strings and arrays in IDL, check out Don Box's November 1996 ActiveX/COM column. He covers the techniques in greater detail, as well as multidimensional arrays and performance comparisons between the different array-passing techniques.
Type Libraries
The
majority of what I've covered so far has been around since the
inception of DCE RPC. When most people think of type libraries, they
think of Automation. Everyone seems to think that type libraries are
just for supplying IDispatch logical interface definitions to a client.
Well, type libraries can do that, but they can also do much more. Type
libraries can be used to describe vtable interfaces, data types, regular
functions, COM components, and even DLL modules. Keep in mind that type
libraries are the compiled metadata and IDL is the source.
When
you use MIDL to compile your IDL into type libraries, the TypeLib
Viewer in OLEVIEW is like your decompiler. The Visual Basic Object
Viewer does the same thing, but displays the interfaces in a less
explicit manner. Figure 5
shows the IDL for the DAO type library from within the TypeLib Viewer.
Here I opened dao350.dll because the type information was stored as a
resource in the DLL, but in many cases the type library is available as a
separate .TLB file. In fact, I don't know why OLEVIEW doesn't do this
automatically, but it's exceptionally useful so that you can just click
on the type libraries in Windows Explorer and see the IDL immediately to
associate the .OLB and .TLB extensions with OLEVIEW. (Do a Find on
*.TLB, right-click a .TLB file, select Open With, and choose C:\Program
Files\DevStudio\
Vc\Oleview.exe as the viewer).
If
you develop in Visual Basic, you live and die by type libraries.
Especially when you are developing components in C++ and clients in
Visual Basic, being able to see explicit definitions of the actual
interface can save untold hours of work. When your COM object's
interface doesn't work from Visual Basic, the interface has to change to
accommodate Visual Basic.
Type
libraries are repositories for metadata that describe interfaces and
data types in a potentially rich manner. They are much more fundamental
than Automation. As tools begin to take advantage of this interface
metadata, development will become much easier because the metadata takes
the burden off the developer and puts it on the tools. Instead of
requiring programmatic access to services, the developer can take a
declarative approach to accessing services, specifying an attribute
instead of coding.
You
can see this kind of functionality in Visual C++ 5.0 and Visual Basic
5.0. In Visual C++, you can use the MFC Class Wizard to add classes
based on dispatch interfaces defined in type libraries instead of
writing the classes yourself. The #import directive in Visual C++
imports type information from a type library and generates C++ classes
for accessing those COM interfaces defined in the library.
Visual
Basic is an even more sophisticated user of type libraries, and can
take advantage of all that they have to offer. For instance, I regularly
switch between Visual Basic and Visual C++. The Auto Quick Info feature
in Visual Basic makes using Visual C++ seem tedious in comparison. Figure 6 shows how the type library information appears in Visual Basic.
MIDL
does not create type libraries automatically. To instruct MIDL to make a
type library, you must use the library statement. Like any other
statement, this one must end with a semicolon. Following the library
keyword is the name of the library and two curly braces, which contain
everything meant for the type library. The library statement can have
attributes, and even has one required attribute: [uuid]. UUIDs hail from
the DCE RPC world and were the precursor to GUIDs. For all intents and
purposes, they are the same. The [uuid] attribute specifies the GUID to
uniquely identify the type library, and MIDL will not generate the
library without this. You can use GUIDGEN to make one.
A
type library can contain the following five categories of elements:
typedefs, modules, interfaces, dispinterfaces, and coclasses. IDL has a
statement for each, and they all appear within the braces of the library
statement. IDL typedefs work just like C/C++ typedefs. All of the
typedef information can go into a type library as long as the typedef
appears within the library statement's braces.
The
module statement is used to define a DLL module, including names and
ordinals for exported functions. Like the library statement, a module
has braces, and all function prototypes that appear within those braces
are associated with that module. A module statement may also have
several attributes. The first one, [dllname], is required and specifies
the name of the DLL. All functions that are to be exported from a module
also need to supply an [entry] attribute for the function. If given a
number, the [entry] attribute assigns the function an ordinal entry
point. If given a string, the [entry] attribute assigns the function a
name as an entry point. For both modules and functions, the [helpstring]
attribute allows you to associate a help string with that function or
library.
Now
you have enough information to make your first type library from
scratch. Let's say you put the following IDL in a file called user.idl:
|
[uuid(54674299-3A82-101B-8181-00AA003743D3)] library MyLib
{
typedef enum {
btError = 0x0010,
btQuestion = 0x0020,
btWarning = 0x0030,
btInformation = 0x0040
} BeepTypes;
[dllname("USER32")]
module MyUser32
{
[entry("MessageBeep"),
helpstring(
"Makes the sound
specified by btSound")]
long _stdcall
MessageBeep(BeepTypes
btSound);
};
};
|
I've defined a type library called MyLib, which contains an enumeration definition and a DLL interface. The DLL's file name is USER32.DLL, but I am calling the module MyUser32. This module has one entry point: the MessageBeep function. Note that the typedef is within the library statement's braces, but not the module. You can compile it into a type library by running MIDL with the following command line: |
After it's compiled, you can look at the type library with TypeLib Viewer. You can also import the type lib- rary into a Visual Basic-based project by choosing Projects | References, pressing the Browse button, and selecting USER.TLB. After the reference is added and you look in the Object Browser, you will find the module name (in this case MyUser32) appearing in the classes pane on the left. In the members pane on the right, you will see the functions and types exposed for this module. In the bottom pane, you will see the function declaration complete with the help string that I supplied for the function (see Figure 7). |
 |
Figure 7 MyUser32 Info |
As
you learn how to use IDL to generate type information, you will begin
to realize just how fully Visual Basic embraces it. Using Visual Basic,
create a form, place a command button on it, and place the following
code in the command button click handler:
|
Private Sub Command1_Click()
Dim Sound As BeepTypes
Sound = btQuestion
MessageBeep (Sound)
End Sub
|
As you type in the code, you will notice that Auto List Members has picked up your type information and can offer you choices for the enumeration values (see Figure 8). When you type in the function call, you will see that Auto Quick Info lists all of the data types and data members for you (see Figure 9). |
 |
Figure 8 Auto List Members |
 |
Figure 9 Auto Quick Info |
The
name I gave to the parameter in MessageBeep was btSound. Visual Basic
can automatically use this as a named parameter, so if the IDL
declaration had more than one parameter
|
long _stdcall MessageBeep(int iVolume,
BeepTypes btSound);
|
the Visual Basic code could make the call with the parameters out of order: |
Private Sub Command1_Click()
MessageBeep (btSound:=btQuestion, iVolume:=100)
End Sub
|
Integration also occurs with the [optional] attribute. If this attribute is applied to a parameter in the IDL file, Visual Basic treats the parameter as if it were declared with the Optional keyword.
Interfaces
The
interface statement causes MIDL to generate proxy/stub source code for a
remote interface. The interface statement is preceded by any attributes
and followed by the interface name and curly braces. All of the
elements of the interface must be within those braces. By default, MIDL
will generate the files necessary for an RPC interface, but if the
interface statement has the [object] attribute,
MIDL will generate the code for a COM interface instead. Just like type
libraries, an interface requires the [uuid] attribute to uniquely
identify the interface. Without it, MIDL will not generate the
proxy/stub code. The following is the complete IDL source to generate a
simple one-function RPC interface:
|
[uuid(348ACF20-C9B9-11d1-ABE5-966A46661731)]
interface MyRPCInterface
{
void Fx(int iValue);
}
|
COM interfaces have a few more requirements. Once the [object] attribute is specified, you are required to derive your interface from another interface using standard C++ syntax. All interfaces must derive somewhere from IUnknown. Another requirement is that all COM interface methods must return HRESULT so that all methods may return error values in response to network failure. Because IUnknown is not known to MIDL, you must import unknwn.idl, and you must import wtypes.idl for HRESULT. The following is the complete IDL source to generate a simple COM interface proxy and stub: |
[object, uuid(348ACF20-C9B9-11d1-ABE5-966A46661731)]
interface IDerivedInterface : IUnknown
{
import "unknwn.idl";
import "wtypes.idl";
HRESULT Fx(int iValue);
}
|
If you place an interface inside of a library, proxy/stub code will not be generated for it, though its type information will be included in the type library. Figure 10 shows the COM interface in the type library that I created earlier.
To
generate networking code for an interface, just leave it out of the
library statement. If you have a function defined in an interface that
is outside the type library but you do not want to generate proxy/stub
code for it, you can use the [local] attribute on that function to
suppress the generation of networking code. This also frees the function
from needing HRESULT as its return type.
So
now that you are defining interfaces, you can reference them as well.
You can use the interface keyword with the typedef keyword to define an
interface data type:
|
typedef interface IStorage *LPSTORAGE;
|
You can even pass interface pointers as function parameters: |
HRESULT StoreData([in] IStorage *pstg);
|
However, sometimes you will not know the interface type at design time, so IDL provides support for dynamically typed interfaces with the [iid_is] parameter attribute: |
HRESULT CreateInstance([in] REFIID riid,
[out, iid_is(riid)] void **ppv);
|
In the code that called this method, the riid parameter would take the IID of the dynamically typed parameter: |
CreateInstance(IID_Car, &ICar);
|
Coclasses
The
coclass statement is used to define a component object and the
interfaces that it supports. The coclass statement is similar to the
interface statement, and also requires the [uuid] attribute (which will
hold 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.
Here's a sample coclass:
|
[uuid(0D248C00-CA6D-11d1-ABE5-8DDA2C299A21)] library MyLib2
{
[object,
uuid(0D248C01-CA6D-11d1-ABE5-8DDA2C299A21)]
interface IDerivedInterface : IUnknown
{
import "unknwn.idl";
import "wtypes.idl";
HRESULT Fx(int iValue);
};
[uuid(CE395E80-CA6C-11d1-ABE5-8DDA2C299A21)]
coclass Drone {
interface IDerivedInterface;
};
};
|
The [source] attribute can be used to signify that a member of a coclass—whether an interface, property, or method—is a source of events, which means that it implements IConnectionPointContainer. With properties and methods, the [source] attribute means that the member returns an object or VARIANT that is a source of events. In this example, the IDerivedInterface interface is made an event source: |
[uuid(CE395E80-CA6C-11d1-ABE5-8DDA2C299A21)]
coclass Drone {
[source] dispinterface IDerivedInterface;
};
|
Dispatch Interfaces
The
dispinterface statement is used to create the type information for an
IDispatch logical interface that will be executed via IDispatch::Invoke.
Because it derives from IDispatch, you should import the Automation
type library (stdole32.tlb) to get in all of the types.
The
dispinterface keyword is similar to the interface keyword in that it
requires the [uuid] attribute and lists all of its methods within the
braces following its name. However, these keywords differ because
dispinterface has two sections inside the body of the definition:
properties and methods. The properties section lists variable
declarations of Automation-compatible types, and the methods section
contains method declarations of Automation-compatible types. Every
method and property in the dispinterface is also required to have the
[id] attribute, which is used to assign a DISPID to each property and
attribute.
|
importlib(stdole32.tlb)
[uuid(348ACF20-C9B9-11d1-ABE5-966A46661731)]
dispinterface DFlyingSaucer
{
properties:
[id(1)] long Altitude;
[id(2)] long Speed;
methods:
[id(3)] HRESULT Land([in] long lDirection);
[id(4)] HRESULT TakeOff();
}
|
A much more important difference is that a dispinterface doesn't have a vtable. All dispinterface methods and properties are accessed using an index with Invoke.
IDL
also provides an easier way to define dual interfaces—by applying the
[dual] attribute to the regular interface statement for an interface
derived from IDispatch. Defining the interface this way defines the
vtable, and then the dispinterface is taken from that same definition.
Here's the same interface as in the previous example, but defined the
new way:
|
importlib(stdole32.tlb)
[object, dual,
uuid(348ACF20-C9B9-11d1-ABE5-966A46661731)]
interface DIFlyingSaucer : IDispatch
{
[id(1), propget] HRESULT Altitude(long);
[id(1), propput] HRESULT Altitude(long);
[id(2), propget] HRESULT Speed(long);
[id(2), propput] HRESULT Speed(long);
[id(3)] HRESULT Land([in] long lDirection);
[id(4)] HRESULT TakeOff();
}
|
Note the naming convention; dispatch interfaces have a D prefix, dual interfaces have a DI prefix, and regular interfaces have an I prefix.
Because
IDispatch is a standard interface, Microsoft provides a standard
marshaler for marshaling parameters. This marshaler is special because
it not only marshals dispinterfaces, but it can also marshal vtable
interfaces that meet certain criteria. For this reason it is referred to
as the Universal Marshaler or the type library marshaler. No other
marshaler is needed if the type library is installed on both the client
and server machines.
This
marshaler can marshal vtable interfaces if they meet the following
criteria: the interface uses Automation (IDispatch)-compatible data
types (anything that can go into a VARIANT); the interface has a type
library where the IDispatch marshaler can get the interface information;
and the interface has the appropriate registry entries that identify
its type library and that this interface is using the Universal
Marshaler as its proxy and stub.
The
effects of this are great. You don't need MIDL to generate the proxy
and stub, so you don't need to declare the interfaces outside of the
library block in the IDL file. However, the trick is getting the
registry entries into the registry. Fortunately, MIDL will do it for
you, but you have to follow closely.
Let's
consider three scenarios: a dispinterface, a custom interface with the
[dual] attribute, and a custom interface. Any interface that is declared
in the file as a dispinterface is implicitly Automation-compatible and
will automatically get the Universal Marshaler entries put in the
registry. Specifying [dual] on a custom interface implies that the
interface is compatible with Automation, and so both the custom
interface and the dispinterface will use the Universal Marshaler and
will get the entries placed in the registry.
A
custom interface can use the marshaler and will get the entries placed
in the registry if that interface is Automation-compatible and uses the
[oleautomation] attribute. The [oleautomation] attribute is the key
here; that attribute is what turns on the Universal Marshaler for a
custom interface. So basically, the only interfaces for which you need
MIDL to generate a proxy/stub DLL are custom interfaces that are not
Automation-compatible.
Conclusion
IDL can be a little confusing because of the overlap in marshaling and type information keywords (see Figure 11
for a summary of the fundamental IDL attributes). But if you're
currently doing any COM-based programming, you need to learn IDL. An
understanding of IDL will help you with marshaling issues and overall
component design and integration. IDL is an integral part of COM
development, and it also enables you to do some pretty cool things with
sophisticated type-aware tools like Visual Basic. Learning IDL now will
only serve to put you on a stronger foundation for the future.
From the August 1998 issue of Microsoft Systems Journal.
Get it at your local newsstand, or better yet, subscribe. |
|