C语言访问Windows COM组件函数

COM(Component Object Model)是Windows里常用的组件对象模型,在Windows上是可以上升到操作系统级别,甚至到网络分布式级别的面象对象技术,也就是按照微软定义的标准实现的COM组件,不仅可以在本地Windows操作系统上让其它程序调用,而且可以供网络内的其它系统调用(DCOM)。

OLE是对象连接/嵌入技术,它其实就是COM,只是在COM的标准上主要增加了自动化功能,OLE大家最熟悉的应用就是在窗口程序上嵌入IE内核(WebBrowser类),然后程序通过WebBrowser提供的接口与里面的网页交互,然后就有了我们的遨游,360浏览器等等。而且OLE是脚本语言最重要的精神支持者之一

另外要指出的是COM是一种对象建模模型,任何人都可以使用这种模型为自己的项目建模,像腾讯的QQ软件,整个窗口上就嵌了一个OLE对象,但他们却又没有完全按照标准去写的,你无法拿到这个对象的包含的其它对象、属性和方法,这也是肯定的,要随便让人拿到那QQ软件就随便被其它程序控制了。

本文是以讲Windows上C语言访问COM为主的文章,并不打算讨论COM的实现细节,如果要了解COM的技术细节,可以参考微软的MSDN。文章后面附有我以前用汇编写的COM调用函数,我现在基本上不写汇编了,发上来是希望它能对汇编的学习人员提供帮助。

本文链接:
http://www.hoverlees.com/blog/?p=746


COM整个模型实现是比较复杂的,但最后我们拿到的对象结构却是很简单的,它的结构如下图:

上图可以看出来,之所以我们经常把对象指针称为ppv,因为它是指向一个指针,这个指针指向对象的函数表vtable。函数表的顺序及每个函数的定义是根据不同的接口有不同的定义的,但在Windows的COM上面,所有接口都是实现IUnknown接口的,所以前三个函数肯定是IUnknown定义的方式,即QueryInterface,AddRef,Release.AddRef和Release是提供系统真正释放对象的信息,觉得我没有必要多说,QueryInterface就是查询且取得这个组件的其它接口。
每一个对象包含自己的私有变量,它在指向vtable变量接下来的连续空间里,如果是你自己实现的当然好找到变量位置,如果是别人实现的嘛,只有靠兴趣去研究了。我觉得唯一需要强调的是C语言调用vtable里的函数的时候,不要忘了第一个参数,这个参数即是对象自身的引用,它是在MSDN中的函数声明里没有公开写出来的,但实际却存在的实体。
所以,例如访问COM的Release的时候,就要按照如下方式:

typedef ULONG        (__stdcall *_Release)(LPVOID _this); //定义Release函数指针,第一个参数为对象本身
//其它实现...
...
//pObject通过CoCreateInstance取得
_Release* vtable=*(void**)pObject;  //取得函数表指针,我们假设这个函数表全部都是Release函数的指针,当然实际上不是。
vtable[2](pObject);                    //调用vtable函数表第三个函数,其实就是Release.本例即实现了pObject->Release();

接下来是自动化IDispatch接口,IDispatch接口也是标准的COM实现,只是,标准的IDispatch不需要它的调用者知道vtable的排列次序。就是你的vtable不管怎么排(当然IDispatch自己的函数QueryInterface,AddRef,Release,GetTypeInfoCount,GetTypeInfo,GetIDsOfNames,Invoke 这七个函数是标准排的,其它的函数都可以在vtable里随便排,就算开发人员改了后面vtable的结构,也同样可以使用IDispatch的Invoke函数正确地调用到。IDispatch对于对象的变量也是这样的“自动化”操作,只要你知道函数的形式和变量名,你就可以正确调用。
IDispatch提供的四个函数大致吹一下:

  • GetTypeInfoCount 取得对象的信息数量
  • GetTypeInfo 取得对象的信息,这些信息包括接口信息,接口的函数信息,接口的变量信息等。有了这两个函数,就可以了解这个对象的所有数据类型了。用过javascript的都知道javascript有一个for..in结构可以取得一个对象的所有变量和函数,在windows上,最终就是这两个函数提供的数据支持。当然你自己写的COM这两个函数可以不提供任何信息,以达到保护的目的。
  • GetIDsOfNames 通过变量/函数名称取得这个变量/函数的ID。
  • Invoke 包括读/写变量,调用函数,是通过上面得到的ID直接调用的。

我写的这个函数库,主要以调用IDispatch接口为主,当然也可以调用COM。代码可以很方便使用,如果不满足读者的需求,可以参考我的代码写成自己需要的。函数库的文件定义如下:

/**
 * @name	C语言 COM 辅助函数
 * @author	Hoverlees me[at]hoverlees.com http://www.hoverlees.com
 * @comment	自动化COM辅助函数,简化IDispatch组件的创建,属性访问及函数调用。
 *
 *			因为简化了调用,所以大多数函数只简单返回成功(数据)或者失败,没有复杂错误判断,
 *			如果读者需要进行错误判断可以修改代码后使用.
 *			原文地址:http://www.hoverlees.com/blog/?p=746
**/
#include <objbase.h>

//定义IDispatch函数指针,库里和程序可能会用到。
typedef HRESULT		(__stdcall *_QueryInterface) (LPVOID _this,REFIID iid,void ** ppvObject);
typedef ULONG		(__stdcall *_AddRef)(LPVOID _this);
typedef ULONG		(__stdcall *_Release)(LPVOID _this);
typedef HRESULT		(__stdcall *_GetTypeInfoCount)(LPVOID _this,unsigned int FAR*  pctinfo);
typedef HRESULT		(__stdcall *_GetTypeInfo)(LPVOID _this,unsigned int  iTInfo,LCID  lcid,ITypeInfo FAR* FAR*  ppTInfo);
typedef HRESULT		(__stdcall *_GetIDsOfNames)(LPVOID _this,REFIID riid,OLECHAR FAR* FAR* rgszNames,unsigned int cNames,LCID lcid,DISPID FAR* rgDispId);
typedef HRESULT		(__stdcall *_Invoke)(LPVOID _this,DISPID dispIdMember,REFIID riid,LCID lcid,WORD wFlags,DISPPARAMS FAR* pDispParams,VARIANT FAR* pVarResult,EXCEPINFO FAR* pExcepInfo,unsigned int FAR* puArgErr);

//下面两个函数其实就是帮助用户调用CoInitialize,CoUninitialize而已。
void CComInit();
void CComUnInit();

//定义GUID,IID常量,为简化用户转换GUID和IID省点事儿
#define CCOM_CLSID_TYPE_REFIID		0		//参数是指向CLSID结构的指针
#define CCOM_CLSID_TYPE_STRINGID	1		//参数是指向"{XXXXXXXXX-XXXXXXX-XXXXX-XXXXX}"这样的CLSID字符串
#define CCOM_CLSID_TYPE_WSTRINGID	2		//参数是指向L"{XXXXXXXXX-XXXXXXX-XXXXX-XXXXX}"这样的CLSID宽字符串
#define CCOM_CLSID_TYPE_PROGID		4		//参数是指向如"Word.Application"这样的progid字符串
#define CCOM_CLSID_TYPE_WPROGID		8		//参数是指向如L"Word.Application"这样的progid宽字符串
#define CCOM_IID_TYPE_REFIID		0		//IID参数,跟上同
#define CCOM_IID_TYPE_STRINGID		16
#define CCOM_IID_TYPE_WSTRINGID		32
#define CCOM_IID_TYPE_PROGID		64
#define CCOM_IID_TYPE_WPROGID		128

//创建对象函数,函数返回成功创建的对象,或NULL
//clsid和iid可以是指向CLSID结构的指针,也可以是CLSID字符串,也可以是progid.
//dwClsContext指定上下文,可以参考MSDN上CoCreateInstance函数声明.
//idType表示clsid和iid的type,用|连接
//调用示例:
//pObject=CComCreateInstance(L"Word.Application",&IID_IDispatch,4,NULL,CCOM_CLSID_TYPE_WPROGID | CCOM_IID_TYPE_REFIID);
//因为clsid使用的宽字符proid,iid用的CLSID指针,所以idType是 CCOM_CLSID_TYPE_WPROGID | CCOM_IID_TYPE_REFIID
LPVOID CComCreateInstance(void* clsid,void* iid,DWORD dwClsContext,LPUNKNOWN pUnknown,DWORD idType);

//查询接口函数,pObject是上函数返回的结果。iid和idType参考上函数。
LPVOID CComQueryInterface(LPVOID pObject,void* iid,DWORD idType);

//通过函数/变量名取得对应的ID。
DISPID CComGetDispIDByName(LPVOID pObject,OLECHAR* name);

//通过变量ID取得变量的值。
int CComGetPropertyByID(LPVOID pObject,DISPID pid,VARIANT* ret);
//通过变量名取得变量值,其实就是先调用CComGetDispIDByName取得ID,再调用上面的函数。下同。
int CComGetPropertyByName(LPVOID pObject,OLECHAR* name,VARIANT* ret);
//通过ID设置变量
int CComSetPropertyByID(LPVOID pObject,DISPID pid,VARIANT* value);
//通过变量名设置变量
int CComSetPropertyByName(LPVOID pObject,OLECHAR* name,VARIANT* value);
//通过函数ID调用函数,args是参数数组,注意参数是从后向前设置
int CComInvokeMethodByID(LPVOID pObject,DISPID pid,int numArgs,VARIANT args[],VARIANT* ret);
//通过函数名调用函数。
int CComInvokeMethodByName(LPVOID pObject,OLECHAR* function,int numArgs,VARIANT args[],VARIANT* ret);

最后附带一个调用该函数库的例子,例子是调用Word服务将Word转换成纯文本文件。

/**
 * @name	C语言 COM 辅助函数示例  打开word文档并转换成txt文件。前提是已经安装word。
 * @author	Hoverlees me[at]hoverlees.com http://www.hoverlees.com
**/

#include <windows.h>
#include "ccom.h"

void main(int argc,char* argv[]){
	LPVOID inst;
	LPVOID documents=NULL;
	LPVOID document=NULL;
	VARIANT var;
	VARIANT params[10];
	int result;
	//初始化COM
	CComInit();
	//取得Word.Application对象
	inst=CComCreateInstance(L"Word.Application",(void*)&IID_IDispatch,4,NULL,CCOM_CLSID_TYPE_WPROGID | CCOM_IID_TYPE_REFIID);
	//显示word窗口
	var.vt=VT_BOOL;
	var.boolVal=1;
	result=CComSetPropertyByName(inst,L"Visible",&var);
	//通过Application->Documents 取得Documents对象
	result=CComGetPropertyByName(inst,L"Documents",&var);
	documents=var.pdispVal;

	//如果要循环转换文件,这里开始循环。
	//调用Documents->Open 返回Document.
	params[0].vt=VT_BSTR;
	params[0].bstrVal=SysAllocString(L"C:/a.doc");
	CComInvokeMethodByName(documents,L"Open",1,params,&var);
	SysFreeString(params[0].bstrVal);
	document=var.pdispVal;
	//调用Document->SaveAs(filename,format);
	//注意参数是两个参数,应该从后向前设置参数,所以第一个param是wdFileFormat,第二个参数是文件名
	params[0].vt=VT_I4;
	params[0].lVal=2;	//wdFormatText
	params[1].vt=VT_BSTR;
	params[1].bstrVal=SysAllocString(L"C:/bb.txt");	//C:/bb.txt
	CComInvokeMethodByName(document,L"SaveAs",2,params,&var);
	SysFreeString(params[1].bstrVal);
	//Document->Close关闭打开的文档
	CComInvokeMethodByName(document,L"Close",0,NULL,&var);
	//如果要循环转换文件,这里结束循环。

	//Application->Quit.
	CComInvokeMethodByName(inst,L"Quit",0,NULL,&var);
	CComUnInit();
}

源代码下载:点击这里

Join the Conversation

16 Comments

Leave a Reply to

Your email address will not be published. Required fields are marked *

  1. 你查一下这两个符号是在哪个头文件里定义的, 经引入对应的头文件

  2. 你好!我是c语言的初学者,根据你的源码我用tcc编译出现如下错误,该怎么解决啊?
    D:\我的配置\桌面\ccom>tcc word_test.c ccom.c -lOle32 -lOleAut32
    tcc: error: undefined symbol ‘IID_IDispatch’
    tcc: error: undefined symbol ‘GUID_NULL’

  3. 您好!我用C语言调用了第三方COM控件(第三方COM控件是个独立窗口,采用OpenGL编写,主程序会不断传递数据给第三方COM控件,第三方COM控件会动态刷新显示),运行一段时间后,第三方控件就像死机了一样(不再动态刷新),主程序没有死,在主程序里面把第三方控件关闭后再重新调出来,第三方控件又可以正常运行;而在VB里面采用容器的方式调用第三方COM控件,就不会出现运行时间长而死机的现象。找了很久都没有找到这个原因,希望您帮我分析一下问题出现在哪里,谢谢了!!!

  4. 对象获取成功,是对象放置路径和注册路径不一致才造成获取不到。现在有个新问题:对象有个函数SetFilePath(CString path);我用
    var1.vt= BSTR;
    var1.bstrVal=(BSTR)(“c:\\csCom”);//这是对象的路径
    result=CComInvokeMethodByName(inst,L”SetFilePath”,1,&var1,NULL);总会提示打不开路径中的文件,测试表明路径设置没有成功。是参数设置有问题吗?

  5. COM有很多的对象啊,只是自动化对象一般都会实现IDispatch接口。 你想创建的对象不能初始化有很多的原因,说不定还有可能是那个控件不开放调用呢。

  6. 您好!我使用您写的函数调用别人编写的COM实现(PVCMLathe.Document),inst=CComCreateInstance(L”PVCMLathe.Document”,(void*)&IID_IDispatch,4,NULL,CCOM_CLSID_TYPE_WPROGID | CCOM_IID_TYPE_REFIID);返回值inst总是为,不知道问题出现在哪里?