关于com自定义参数的传递

本文详细探讨了在COM中如何处理自定义参数传递,特别是当参数类型为void*时。通过[local]属性和[call_as()]属性,解释了如何在IDL中处理不能直接汇集的类型,以及如何利用MIDL生成的代码实现跨进程传递类对象的指针。此外,文章还介绍了[wire_marshal()]属性在提高效率方面的应用,以及如何编写列集代码以处理自定义数据类型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

COM深入理解(下)

——方法参数类型为CRuntimeClass*void*

    本文上篇已经说明了类对象实际是一个结构实例,并且为了实现进程间传递类对象指针以达到引用的目的,需要为此类专门编写一个代理类,并在传递时例示(即实例化)其一个对象以实现代理对象。而此代理类必定分成两部分,即一部分的成员函数专门在客户进程被调用,另一部分专门在组件进程被调用以实现进程间的数据传递进而将客户的调用命令原封不动地传递给组件对象以实现客户操作组件对象。
   
上面的做法实际就是编写自定义汇集操作时应该干的事,只不过还需照着COM的几个附加规定来做,如必须实现IMarshal接口等。本文说明如何为这样的类型传递编写标准的代理/占位组件以跨进程传递类对象的指针(使用MIDL来完成)。

    为了在客户端生成一个代理对象,必须将某些信息传递过去,然后在客户端根据传递的信息构建一个代理对象。在IDL语言的类型定义中,没有类这种类型,因此是不可能让接口方法的参数类型为某个自定义类的指针。但是的确有此需要,则只能将类对象指针转成某种IDL中识别的类型,最好的候选人就是void*,然后借助MIDL生成的代码将构建代理对象的信息传递过去。
    void*
不带有任何语义,其仅表示一个地址,因此在IDL中传递void*是错误的,因为MIDL无法根据void*所带的语义确定应该如何汇集其指向内存中的内容。但是MIDL还是提供了多种途径来解决这个问题的,下面仅说明其中两个用得最多的方法:[call_as()]属性和[wire_marshal()]属性。


[local][call_as()]

    [local]  接口或接口方法都可以加上[local]属性以表示此方法或此接口中的方法不需要生成汇集代码,进而就避免了上面由于void*不带有任何语义而不能汇集其指向内容这个问题,因为不需要生成汇集代码,进而其所修饰的方法的参数可以为void*。此属性所修饰的方法或接口被称为本地方法或本地接口,因为这些方法没有汇集代码,不能进行远程调用。这在COM的标准接口中应用十分广泛。如查看IUnknownIDL代码,其就是一个本地接口。再如查看IClassFactory接口的IDL定义,如下:
[
    object,
    uuid(00000001-0000-0000-C000-000000000046),
    pointer_default(unique)
]
interface IClassFactory : IUnknown
{
    typedef [unique] IClassFactory * LPCLASSFACTORY;
    [local]
    HRESULT CreateInstance(
        [in, unique] IUnknown * pUnkOuter,
        [in] REFIID riid,
        [out, iid_is(riid)] void **ppvObject);
    [call_as(CreateInstance)]
    HRESULT RemoteCreateInstance(
        [in] REFIID riid,
        [out, iid_is(riid)] IUnknown ** ppvObject);
    [local]
    HRESULT LockServer(
        [in] BOOL fLock);
    [call_as(LockServer)]
    HRESULT __stdcall RemoteLockServer(
        [in] BOOL fLock);
}
   
其中的CreateInstanceLockServer就是本地函数,MIDL将不会为这两个函数生成汇集代码,也就是代理/占位代码,其表现就是类似下面的两个函数原型的代码:
HRESULT STDMETHODCALLTYPE IClassFactory_LockServer_Proxy( IClassFactory * This,
                                                          BOOL fLock );
HRESULT STDMETHODCALLTYPE IClassFactory_LockServer_Stub( IClassFactory * This,
                                                         BOOL fLock );
   
也就是说,当在.idl文件中检测到一个接口方法的定义时,MIDL都会为这个方法生成两个附加的函数,名字分别为<InterfaceName>_<MethodName>_Proxy<InterfaceName>_<MethodName>_Stub,以分别作为代理和占位的代码。如上面的RemoteCreateInstance,将生成IClassFactory_RemoteCreateInstance_ProxyIClassFactory_RemoteCreateInstance_Stub这么两个函数的声明和定义。
   
但是当方法被[local]属性修饰时,则不会生成上面的两个函数的声明和定义,因为它们被假定一定用于直接调用,不会有汇集的需要,因此没有汇集代码,并被称为本地方法。但它们还是会被加入接口这个函数指针数组的行列,即生成的接口头文件中依旧可以看见这类方法的声明(但是在类型库中却没有,这可以认为是MIDL的一个BUG,不过是可以绕过的)。
    [call_as()] 
接口方法可以被加上[call_as()]属性进行修饰,以指定此方法将被作为括号中指定的本地方法调用的替代品,即被作为什么调用。它不像[local]属性修饰的方法,其依旧会生成汇集代码,但却不会出现在接口中,即生成的头文件中,看不见这类方法的声明(但是在类型库中却看得见,这是一个BUG,可以通过预定义宏绕过)。此被称为方法别名,因为其将两个方法关联了起来,其中一个([local]修饰的)是另一个([call_as]修饰的)的别名,被实际使用。
   
如前面的RemoteLockServer就带有属性[call_as(LockServer)]以表示此函数是当客户调用LockServer时,并且需要进行汇集操作时调用的。将[local]修饰的方法称为本地版,[call_as()]修饰称为远程版,则可以认为远程版函数解决了本地版函数没有生成汇集代码的问题,因为本地版函数可能有某些特殊要求(如参数类型为void*)而不能生成汇集代码。
   
既然[call_as()]产生了一个函数别名,对两个函数进行了关联,因此必须有一种机制实现这种关联。MIDL就是通过要求开发人员自己编写本地版方法的汇集代码来实现这个关联关系。对于上面的LockServerMIDL将会为其生成两个函数原型,如下:
HRESULT STDMETHODCALLTYPE IClassFactory_LockServer_Proxy( IClassFactory * This,
                                                          BOOL fLock );
HRESULT __stdcall IClassFactory_LockServer_Stub( IClassFactory * This,
                                                 BOOL fLock );
   
但仅仅是原型,即声明,没有定义。因此开发人员需自己编写上面两个函数的定义。注意:虽然名字是IClassFactory_LockServer_Stub,但它的原型正好和RemoteLockServer对调,以实现将远程版函数传递过来的参数再转成本地版的参数。
   
因此关联的过程就是:客户调用IClassFactory_LockServer_Proxy,然后开发人员编写此函数,并在其中将传进来的MIDL不能或不希望被处理的参数类型转成IClassFactory_RemoteLockServer_Proxy的参数形式,并调用之以传递参数。在组件端,COM运行时期库调用开发人员编写的IClassFactory_LockServer_Stub(注意:此函数的原型不是LockServer,而是RemoteLockServer)以将通过网络传过来的参数换成原始的MIDL不能或不希望被处理的参数形式,并调用传进来的IClassFactory*参数的LockServer方法以实现调用了组件对象的方法,然后返回。下面举个简例:
   
有个自定义类CA,如下:
class CA
{
    long m_a, m_b;
public:
    long GetA();
    void SetA( long a );
};
   
欲在下面的接口中传递其对象指针:
///////////////////////abc.idl/////////////////////////
import "oaidl.idl";
import "ocidl.idl";

[
    object,
    uuid( 1A 201ABC-A669 -4ac 7-9E02-2DA772E927FC),
    pointer_default(unique)
]
interface IAbc : IUnknown
{
    [local] HRESULT GetA( [out] void* pA );
    [call_as( GetA )] HRESULT RemoteGetA( [out] long *pA, [out] long *pB );
};
   
新建一DLL工程,关掉“预编译头文件”编译开关,将生成的abc_i.cabc_p.cdlldata.cabc.h加到工程中,并建立一个abc.def文件加入到工程中以导出几个必要的用于注册的函数,如下:
;;;;;;;;;;;;;;;;;;;;;;;;abc.def;;;;;;;;;;;;;;;;;;;;;;;;;
LIBRARY "abc"
EXPORTS
    DllCanUnloadNow     PRIVATE
    DllGetClassObject   PRIVATE
    DllRegisterServer   PRIVATE
    DllUnregisterServer PRIVATE
   
并新添加一个abc.cpp文件,如下:
///////////////////////abc.cpp/////////////////////////
#include "abc.h"
#include <new>

class CA
{
public:
    long m_a, m_b;
    long GetA();
    void SetA( long a );
};
HRESULT STDMETHODCALLTYPE IAbc_GetA_Proxy( IAbc *This, void *pA )
{
    if( !pA )
        return E_INVALIDARG;
    CA *pAA = reinterpret_cast< CA* >( pA );

//
调用远程版的代理函数以传递参数,由MIDL生成
    return IAbc_RemoteGetA_Proxy( This, &pAA->m_a, &pAA->m_b );
}
HRESULT STDMETHODCALLTYPE IAbc_GetA_Stub( IAbc *This, long *pA, long *pB )
{
    void *p = CoTaskMemAlloc( sizeof( CA ) );
    if( !p )
        return E_FAIL;
    CA *pAA = new( p ) CA;  //
生成一个类对象

//
调用对象的本地方法
    HRESULT hr = This->GetA( pAA );
    if( SUCCEEDED( hr ) )
    {
        *pA = pAA->m_a;
        *pB = pAA->m_b;
    }

//
释放资源
    pAA->~CA();
    CoTaskMemFree( p );
    return hr;
}
   
最后添加预定义宏REGISTER_PROXY_DLL_WIN32_WINNT=0x500,并连接rpcrt4.lib库文件,确保没有打开/TC/TP编译开关以保证对上面的abc.cpp进行C++编译,而对MIDL生成的.c的源文件进行C编译。
   
使用时如下:
IAbc *pA;  //
假设已初始化
CA    a;
pA->GetA( reinterpret_cast< void* >( &a ) );
   
而组件实现的代码如下:
STDMETHODIMP CAbc::GetA( void *pA )
{
    if( !pA )
        return E_INVALIDARG;

    *reinterpret_cast< CA* >( pA ) = m_A;
    return S_OK;
}
   
如上就实现了将类CA的对象进行传值操作,但不是传址操作。前面已说明,欲进行后者,必须编写相应的代理类。先使用上面的方法将必要的信息传递后,再根据传递的信息初始化类CA的代理对象以建立连接。一般如非得已最好不要编写代理对象,而通过将类转成接口形式,由MIDL辅助生成代理/占位组件以变相实现。
   
下面介绍使用[wire_marshal()]属性进行传值操作。


[wire_marshal()]

    前面使用方法别名机制实现了传递自定义数据类型,但是其是以方法为单位进行处理的,当要多次使用某一个数据类型时,如前面的CA*,如果对每个使用到CA*的方法都进行上面的操作,很明显地效率低下,为此MIDL提供了[wire_marshal()]属性(当然不止这么一个属性)。
    [wire_marshal()]
属性只能用于类型定义,即typedef中,使用语法如下:
typedef [wire_marshal(wire_type)] type-specifier userm-type;
   
其将一个线类型(wire-type,即MIDL可以直接处理的类型)和一个描述类型(type-specifier,即不能或不打算被MIDL处理的特殊数据类型)相关联,并用一个可识别名字(userm-type)标识。其和[transmit_as()]属性类似,都是将两个类型进行关联,就如前面的[local][call_as()]将两个方法进行关联一样,只不过[wire_marshal()]是直接将描述类型按IDL的列集格式(网络数据描述NDR——Network Data Representation)列集到指定的缓冲区中,而[transmit_as()]还需汇集代码在中间再转换一次,因此[wire_marshal()]的效率要更高,只不过由于需要编写列集代码,因此需要了解NDR格式,处理数据对齐等问题,所以显得麻烦和复杂。最常见的应用就是句柄的定义,如下:
typedef union _RemotableHandle switch( long fContext ) u
{
    case WDT_INPROC_CALL:   long   hInproc;
    case WDT_REMOTE_CALL:   long   hRemote;
} RemotableHandle;
typedef [unique] RemotableHandle * wireHWND;
#define DECLARE_WIREM_HANDLE(name)  /
                             typedef [wire_marshal(wire ## name)] void * name
DECLARE_WIREM_HANDLE( HWND );
   
也就是说我们常用的HWND类型是:
typedef [wire_marshal( wireHWND )] void* HWND;
   
即其在应用程序中(即客户或组件,即代理/占位的使用者)是void*类型,当需要传输时,实际是传输结构RemotableHandle的一个实例,而此结构是一个以fContext为标识的联合,实际为8字节长。
   
为了实现上面提到的void*RemotableHandle*的关联,开发人员必须提供下面四个函数的定义:
unsigned long __RPC_USER < userm-type >_UserSize(  //
返回欲请求的缓冲区大小
    unsigned long __RPC_FAR *pFlags,  //
一个标志参数,后叙
    //
给出当前已经请求的缓冲区大小,返回的大小应该以此作为起点
    unsigned long StartingSize,
    < userm-type > __RPC_FAR * pUser_typeObject );  //
欲传递的描述类型的实例
unsigned char __RPC_FAR * __RPC_USER < userm-type >_UserMarshal(  //
列集
    unsigned long __RPC_FAR * pFlags,   //
标志参数
    unsigned char __RPC_FAR * Buffer,   //
已分配的缓冲器有效指针
    < userm-type > __RPC_FAR * pUser_typeObject );  //
欲列集的描述类型的实例
unsigned char __RPC_FAR * __RPC_USER < userm-type >_UserUnmarshal(  //
散集
    unsigned long __RPC_FAR *  pFlags,   //
标志参数
    unsigned char __RPC_FAR *  Buffer,   //
列集数据的缓冲器指针
    //
描述类型的实例指针,从列集数据中散集出描述类型后,放在此指针所指内存之中
    < userm-type > __RPC_FAR * pUser_typeObject );
void __RPC_USER < userm-type >_UserFree(  //
释放UserUnmarshal中分配的内存
    unsigned long __RPC_FAR * pFlags,     //
标志参数
    // UserUnmarshal
中的pUser_typeObject参数,一个描述类型的实例的指针
    < userm-type > __RPC_FAR * pUser_typeObject );
   
对于前面的HWND,开发人员就必须提供如下四个函数的定义(当然Microsoft是已经提供了的):
unsigned long __RPC_USER
    HWND_UserSize( unsigned long*, unsigned long, HWND* );
unsigned char* __RPC_USER
    HWND_UserMarshal( unsigned long*, unsigned char*, HWND* );
unsigned char* __RPC_USER
    HWND_UserUnmarshal( unsigned long*, unsigned char*, HWND* );
void __RPC_USER
    HWND_UserFree( unsigned long*, HWND* );
   
MIDL生成的汇集代码中,遇到方法参数类型为HWND时,发生如下事情:
    1.
调用HWND_UserSize并传递应用程序(客户或组件,视HWNDin参数还是out参数)传进来的HWND的实例以得到欲传递此实例需要的缓冲区大小
    2.
RPC通道上分配相应的内存块
    3.
调用HWND_UserMarshal,依旧传递前面的HWND实例以及分配到的缓冲区的指针以将此HWND实例列集到缓冲区中
    4.
通过RPC通道将缓冲区内容传递到对方进程空间中
    5.
调用HWND_UserUnmarshal,并传递通过RPC通道得到的列集数据缓冲区的指针和生成的一临时HWND实例的指针以记录散集出来的HWND实例
    6.
以返回的HWND实例为参数调用应用程序的方法
    7.
调用HWND_UserFree,传递前面因调用HWND_UserUnmarshal而生成的临时记录散集出的HWND实例的指针以释放因此分配的内存
   
以上,就是[wire_marshal()]属性对线类型和描述类型的绑定的实现。但其中漏了一点,就是标志参数pFlags的使用。此标志参数是一个4字节数字,其高16位是一些关于NDR格式的编码规则,以使得NDR引擎(将填写好的缓冲区内容按NDR格式串的规则进行排列以在网上传输的程序)能做出正确的数据转换。其低16位是一个MSHCTX枚举值,指明调用环境,是进程内还是跨进程、是远程还是本地(具体信息还请查阅MSDN),因而可以在上面的四个函数中根据此值作出相应的优化。
   
下面为上面的CA*实现[wire_marshal()]属性。
   
前面已经了解到,CA*由于在IDL中没有对应的类型,应该使用void*来进行传递,在abc.idl中增加如下代码:
typedef struct _SA
{
    long a, b;
} *PSA;
typedef [wire_marshal( PSA )] void* PA;
   
并为接口IAbc增加一个方法:
HRESULT SetA( [in] PA a );
   
接着在abc.cpp中增加如下代码:
unsigned long __RPC_USER PA_UserSize( unsigned long* /* pFlags */,
                                      unsigned long  StartingSize,
                                      PA* /* ppA */ )
{
//
之所以有StartingSize,因为此参数可能并不是第一个被列集的参数,
//
如:HRESULT SetA( [in] long tem1, [in] char tem2, [in] PA a );
//
此时的StartingSize就为sizeof( long ) + sizeof( char )
//
而之所以还要再将其传进来是为了对齐需要

//
此处没有进行对齐处理,因为结构_SA是只有两个unsigned long的简单
//
结构,无须再刻意对齐。
    return StartingSize + sizeof( _SA );
}
unsigned char* __RPC_USER PA_UserMarshal( unsigned long *pFlags,
                                          unsigned char *Buffer,
                                          PA *ppA )
{
//
按线种类(即结构_SA)的定义填冲缓冲区,注意必须按照NDR传输格式
//
进行填充,这里由于_SA简单,所以只是简单地复制,没有什么对齐及一
//
致性数据的问题。关于NDR传输格式的详细内容,请参考
// http://www.opengroup.org/onlinepubs/9629399/chap14.htm
    if( *pFlags & MSHCTX_INPROC )
    {
    //
是进程内调用,直接将CA*进行传递,而不进行拷贝
        *reinterpret_cast< void** >( Buffer ) = *ppA;
    }
    else
    {
        CA *pA = reinterpret_cast< CA* >( *ppA );
        PSA pSA = reinterpret_cast< PSA >( Buffer );
        pSA->a = pA->m_a;
        pSA->b = pA->m_b;
    }

//
返回缓冲区的有效位置,当前位置后的sizeof( _SA )个字节
    return Buffer + sizeof( _SA );
}
unsigned char* __RPC_USER PA_UserUnmarshal( unsigned long *pFlags,
                                            unsigned char *Buffer,
                                            PA *ppA )
{
    if( *pFlags & MSHCTX_INPROC )
    {
    //
是进程内调用,直接将CA*进行传递,而不进行拷贝
        *ppA = *reinterpret_cast< void** >( Buffer );
    }
    else
    {
        void *p = CoTaskMemAlloc( sizeof( CA ) );
        if( !p )
            return Buffer + sizeof( _SA );
        CA *pAA = new( p ) CA;  //
生成一个类对象

        PSA pSA = reinterpret_cast< PSA >( Buffer );
        pAA->m_a = pSA->a;
        pAA->m_b = pSA->b;

        *ppA = p;
    }

//
返回缓冲区的有效位置,当前位置后的sizeof( _SA )个字节
    return Buffer + sizeof( _SA );
}
void __RPC_USER PA_UserFree( unsigned long *pFlags,
                             PA *ppA )
{
    if( !( *pFlags & MSHCTX_INPROC  ) )
    {
    //
不是进程内汇集,分配了内存,释放资源
        CA *pAA = reinterpret_cast< CA* >( *ppA );
        pAA->~CA();
        CoTaskMemFree( pAA );
    }
}
   
使用中,则:
IAbc *pA;  //
假设已初始化
CA a;
a.SetA( 654 );
PA pAA = &a;
pA->SetA( pAA );  //
或者直接pA->SetA( &a );
pA->GetA( &a );

    非常明显,MIDL提供的可用于自定义类型传递的属性很正常地不止上面几个,如:[transmit_as()][handle]等,在此仅起抛砖引玉的作用,关于MIDL提供的其他属性,还请参考MSDN。上面的实现方法中,都不仅仅提供了汇集自定义数据类型的渠道,还提供了优化的途径(如上面的pFlags标志参数)。因此在编写代理/占位组件时,应考虑在关键地方应用类似的属性进行生成代码的优化。

Using user defined type in COM&ATL

·  Preface

The reason I got into this is that I've rarely used any help from newsgroups or similar communities. On the other hand since I've used code provided by other developers/programmers on CodeProject and CodeGuru it seemed reasonable to join a couple of them and just have a look.

Early in May 2000 I noticed several posts about UDTs and their interaction with VB and ATL. At this point I may say I had not any real experience on the subject. As a matter of fact I've never developed professionally in COM with C++ or ATL. In addition I've learned the hard way that one cannot apply the same coding techniques one uses with C or C++ to VB. Still I consider myself novice in the COM environment.

It is true that there is very little help in implementing UDTs in COM and even less in implementing arrays of UDTs. In the past it was not even thinkable to use UDTs in COM. Nowadays there is support for UDTs in COM but there are no real example projects on how to use this feature. So a personal mail by a fellow developer inspired me to go onto this.

I am going to present a step by step approach on creating an ATL project which using UDTs to communicate with a VB Client. Using it with a C++ Client will be easy as well.

This document will proceed along with the project. I assume you are familiar with ATL, COM and VB. On the way I may present practices I use myself, which may be irrelevant to the cause of this example, but on the other hand you may have also used these practices as well or beginners may benefit from these.

Create the ATL project.

As a starting point create an ATL DLL project using the wizard. Set the name of the project to UDTDemo and accept the defaults. Now let's have a look at the generated "IDL" file.

//UDTDemo.IDL

 

import "oaidl.idl";

import "ocidl.idl";

 

[

    uuid(C 21871A 1-33EB-11D4-A 13A -BE 2573A 1120F ),

    version(1.0),

    helpstring("UDTDemo 1.0 Type Library")

]

library UDTDEMOLib

{

    importlib("stdole32.tlb");

    importlib("stdole2.tlb");

 

};

Modify the type library name

As you expected this, there is nothing unknown in this file so far. Well, the fact is that I do not really like the "Lib" portion added to the name of the projects I create, and I always change it before any object is being inserted into the project. This is very easy.

As a first step edit the "IDL" file and set the library name to what you like. You have only to remember that this is case sensitive when the MIDL generated code is used. The modified file is shown bellow.

//UDTDemo.IDL

 

import "oaidl.idl";

import "ocidl.idl";

 

[

    uuid(C 21871A 1-33EB-11D4-A 13A -BE 2573A 1120F ),

    version(1.0),

    helpstring("UDTDemo 1.0 Type Library")

]

library UDTDemo   //UDTDEMOLib

{

    importlib("stdole32.tlb");

    importlib("stdole2.tlb");

 

};

The second step is to replace any occurrence of the previous library name with the new one. The only file apart the "IDL" one, where the library name is found is the main project implementation file, "UDTDemo.cpp", where DllMain is called and the _module is initialized. You may also use the "Find in Files" command from the toolbar and search for the "UDTDEMOLib" string.

What ever way we use we have to replace the "LIBID_UDTDEMOLib" string with the "LIBID_UDTDemo" one. Mind the case of the strings. It is case sensitive.

Now you are ready to change the name of your type library to what you really like. Again keep in mind that this is not going to be trivial unless it is done before any object is added into the project, or before any early compilation of the project.

Bellow is the modified DllMain function of our project.

//UDTDemo.cpp

 

extern "C"

BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)

{

    if (dwReason == DLL_PROCESS_ATTACH)

    {

        //_Module.Init(ObjectMap, hInstance, &LIBID_UDTDEMOLib);

        _Module.Init(ObjectMap, hInstance, &LIBID_UDTDemo);

        DisableThreadLibraryCalls(hInstance);

    }

    else if (dwReason == DLL_PROCESS_DETACH)

        _Module.Term();

    return TRUE;    // ok

}

You may Compile the project now. Be sure everything is done right. In case something goes wrong you should make sure all occurrences of "UDTDEMOLib" are replaced with "UDTDemo".

Defining the structure.

An empty project is not of any use. Our purpose is to define a UDT, or struct respectively, and this is what I am going to do right now.

The demo project will handle named variables. This means we need a structure for holding the Name, and the Value of a variable. Although I haven't tested it yet, we may add a VARIANT to hold some other Special data.

The above types where chosen so as you may see the whole story, and not take any homework at all. :)

So open the UDTDemo.idl file and add these lines before the library block.

//UDTDemo.idl

 

    typedef

    [

        uuid(C 21871A 0-33EB-11D4-A 13A -BE 2573A 1120F ),

        version(1.0),

        helpstring("A Demo UDT variable for VB projects")

    ]

    struct UDTVariable {

        [helpstring("Special case variant")]    VARIANT  Special;

        [helpstring("Name of the variable")]    BSTR     Name;

        [helpstring("Value of the variable")]   long     Value;

    } UDTVariable;

Save and build again. Everything should compile without any problems. Well you have to follow this pace in this demo project. :)

User Defined data Types. The Theory.

Whenever a structure is created in IDL we need to specify a UUID for it so as the type library manager interfaces can get information about the UDT and access it. (I also realized why on this project :) ).

UUIDs

How is the UUID for the structure acquired? No, we do not execute the guidgen utility. My next free hack is this. It may not be approved, but it works. Go to the library section, copy the UUID of the library, and paste it after the typedef keyword of the structure inside the angle brackets. Then go to the 8th digit and subtract (one) 1.

The library     uuid(C 21871A 1-33EB-11D4-A 13A -BE 2573A 1120F )

                            |

                           /./

The UDTVariable uuid(C 21871A 0-33EB-11D4-A 13A -BE 2573A 1120F )

As it is documented the UUIDs are created using the current date, the current time and the unique number of the network card of the computer. Well, the time and date part resides in the first eight-digit part of the UUID. The next four-digit part is not known to me so far. The rest of it is unique and, we may say, identifies your PC. So, subtracting one (1) from the time part takes us to the past. Finally this UUID is still unique!!!

As a rule of thumb, after the library UUID is created, I add one (1) to the time part of the UUID for every interface and coclass I insert into the project. Subtract one (1) for the structures or enumerations I use. Basically the interface UUIDs are replaced and will be demonstrated later.

The only reason I get into this kind of trouble is because it is said that Windows handle consequent UUIDs in the registry faster!

More type attributes.

After the definition of the UUID for our structure we define its version number. This is a hack discovered after checking the type library of a VB created object. VB adds a version number to everything it adds to a type library. This will never be used in this project but why not use it?

Then add a help string. This brief description is very useful to everyone using our library. I recommend using it all the time.

We could also add the public keyword to make the structure visible from the library clients. This is not necessary as it will finally be implicitly visible to the clients. Clients should not be able to create any structure which might not be used in the interfaces exposed by our object.

The UDT Data members.

Let's proceed to the data members now. First every data member of our UDT must be an automation compatible type. In the simpler form, as I've conclude, in a UDT we are allowed to use only the types defined in the VARIANT union, if you have checked the code, or whatever VB allows us to use inside a variant type.

This is only for our sake to avoid creating marshaling code for our structure. Otherwise you are free to pass even a bit array with a structure :).

The data types of our UDT members were chosen so as we can expect some difficulty and make the demonstration as complete as possible.

  • long Value : a member of type long was chosen because it behaves like any other built in type. There are no extra considerations to be taken for built in types. (long, byte, double, float, DATE, VT_BOOL).
  • BSTR Name : Although strings are easily handled in VB, here we have some considerations to take into account. Creation, Initialization and Destruction of the string are good reasons to take care of and use a string in the demo.
  • VARIANT Special : This came up just now. Since we are going to do it, variants are more difficult to use than BSTR's, Not only in terms of initialization and termination, but also in checking what is the real type of the actual variants. This is not so bad!

Arrays as structure members.

At this point you should know how to declare a structure of simple types in IDL. Finally, now that you know how to declare a UDT structure to be used in VB we have to take the exercise 1 and create a UDT which holds an array of UDTs. The reason is, that arrays are also special cases, and since we haven't put an array in our structure in the first place, lets make a case with an array. Using an array of longs or what ever other type would be the same at this point of the demonstration.

//UDTDemo.idl

 

    typedef

    [

        uuid(C 21871 9F -33EB-11D4-A 13A -BE 2573A 1120F ),

        version(1.0),

        helpstring("A Demo UDT Holding an Array of Named Variables")

    ]

    struct UDTArray {

 

        [helpstring("array of named variables")]   

                   SAFEARRAY(UDTVariable) NamedVars;

 

    } UDTArray;

As you have noticed, the only difference is that we used the SAFEARRAY declaration in the first place, but we also included the name of our newly declared UDT. This is the right way to give the COM mechanism the knowledge of what the array will be holding. At this point we have declared a UDT holding a typed array.

Declaring an array of longs it would be as simple, as declaring the following.

SAFEARRAY(long)

Performing a test.

We may compile our project once more. At this point it would be nice to create a test VB project , and add our library into this client project through the references in the project menu. Now press F2 to check what VB may see in our library. Well, nothing but the globals appears in the window.

This is due to the fact that we have declared our UDT's outside the library block in the IDL file. Well, if any declared item, (enum, UDT, Interface) outside the library block, is not explicitly or implicitly imported inside the library block, then this item is unknown (not known) to the clients of the type library.

Lets make a simple test. Save the VB project, and then close it. Otherwise the UDTDemo project will not pass the link step. Inside the "UDTDemo.idl" file go inside the library block and add the following lines.

//UDTDemo.IDL

 

library UDTDemo   //UDTDEMOLib

{

    importlib("stdole32.tlb");

    importlib("stdole2.tlb");

   

    struct UDTVariable;

    struct UDTArray;

   

};

Build the UDTDemo project once more and open the VB demo project. Open the references dialog, uncheck the UDTDemo line close it and then register our UDTDemo again with the VB project through the references.

Opening the object browser now, will show both the UDT's we have defined in our library. Close the VB project, and comment out the previous lines in the "UDTDemo.idl" file. These structures will be added implicitly into the library through the interfaces we are going to define.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值