LCOM:带数据的接口

【摘要】本文试图在LCOM编程模型中引入面向接口的变量,为程序员提供更加灵活的编程模型。

1 问题的来由

前面的文章LCOM:轻量级组件对象模型引入了一种类似于COM的一种编程模式,可以理解为COM的简化版本。其中以接口作为编程关注的核心,所有的功能都用接口定义,一个软件模块实现一个或多个对象,每个对象实现一个或多个接口,对外提供服务。每个接口定义为一组函数的集合,在COM中,这点规定得比较死,接口中是不允许出现变量的,以致于要访问一个对象的内部变量,出现了var_Get, var_Set这样的接口函数(Visual Basic中可以自动生成)。
用函数控制对象内部的变量访问,具有很多优点,比如可以控制对变量的修改符合变量的逻辑,保持对象内部的数据一致性,访问时确保不出现非法访问,比如数组访问,如果下标超访问,那就是灾难,很多软件的bug都由此产生,还可以提供变量只读,只写等功能。读取变量时也可以提供一些特别的服务,比如虚拟化的服务,对象内部实际上不存在对应的变量,可以提供一个虚拟的变量Get,这样可以保持内部实现灵活的同时,保持外部接口符合用户逻辑,毕竟用户看到和内部实现是两回事嘛。
然而不允许接口中出现变量,也带来了很多不便,一个感觉就是太教条了,比如经典的双向链表数据结构,就是在每个数据对象中加入next和last指针,然后操控这两个指针,即可构成一个循环的双向链表。当然可以设计一个双向链表接口,用来提供next和last的访问,但是这样做总觉的有点怪怪的,限制了程序员的自由发挥了,原来data->pNext->pLast = data;这样的用法不好用了,程序员要适应一套新的东西,关键是没有带来实质性的好处。当然你可以争辩说通过接口函数访问变量可以增加代码安全性,比如验证链表节点的合法性什么的,但是不能流畅地编码,以及访问个变量就要调用一个函数,对于一些程序员会造成很大的心理负担。无论如何,函数调用的运行效率肯定不如直接的变量访问嘛,还想在C51下面用LCOM呢,这么一来CPU大量耗费在这些繁文缛节中。程序员需要自由,有权利在安全性和效率之间做平衡,要允许高手写出同样安全但是效率更高的代码,对专业拍照人士不能只提供傻瓜相机。因此本文在LCOM中试图引入一种在接口中定义变量的方法,就以双向链表为例,来演示这个方法的可行性。

2 接口数据存在什么地方

我们面临的第一个问题是,接口相关的数据如何表示,存储在什么地方?前面的LCOM接口中,接口函数声明在对象实现文件的一个静态变量中,来复习一下前面文章中实现的MandelBrotApp对象中实现的IObject接口和IGLApp接口:

下面声明公共接口的函数和对象管理所需要的函数,这对每个对象实现都是必须的*/
static int mandelbrotappQueryInterface(HOBJECT object, IIDTYPE iid, const void **pInterface); 
static int mandelbrotappAddRef(HOBJECT object); 
static int mandelbrotappRelease(HOBJECT object); 
static int mandelbrotappIsValid(HOBJECT object); 
static void mandelbrotappDestroy(HOBJECT object); 
static int mandelbrotappCreate(const PARAMITEM * pParams, int paramcount, HOBJECT* pObject); 
static int mandelbrotappValid(HOBJECT object);
/*定义一个对象实现IObject接口的IObject对象,
MandelBrotApp对象的第一个成员必须是指向该数据的指针*/
static const IObject mandelbrotapp_object_interface = { 		    
  0, /*IObject的指针必须作为对象实现的第一个成员,因此偏移为0*/ 
  mandelbrotappQueryInterface, 
  mandelbrotappAddRef, 
  mandelbrotappRelease, 
  mandelbrotappIsValid 
}; 

上面这段代码提供了mandelbrotapp对象中IObject接口的实现,在对象中则这些函数只是存储了一个指向mandelbrotapp_object_interface 这个变量的指针。同样:

/*下面声明IGLApp接口的函数*/	
static int mandelbrotapp_glapp_Render(HOBJECT object, int x, int y, int width, int height);   
static int mandelbrotapp_glapp_SwitchIn(HOBJECT object); 
static int mandelbrotapp_glapp_SwitchOut(HOBJECT object); 
static int mandelbrotapp_glapp_GetOption(HOBJECT object, int option); 
static int mandelbrotapp_glapp_MouseLeftDoubleClick(HOBJECT object, int x, int y, int ctrlkey); 
static int mandelbrotapp_glapp_MouseLeftDown(HOBJECT object, int x, int y, int ctrlkey); 
static int mandelbrotapp_glapp_MouseLeftUp(HOBJECT object, int x, int y, int ctrlkey); 
static int mandelbrotapp_glapp_MouseRightDoubleClick(HOBJECT object, int x, int y, int ctrlkey); 
static int mandelbrotapp_glapp_MouseRightDown(HOBJECT object, int x, int y, int ctrlkey); 
static int mandelbrotapp_glapp_MouseRightUp(HOBJECT object, int x, int y, int ctrlkey); 
static int mandelbrotapp_glapp_MouseMove(HOBJECT object, int x, int y, int ctrlkey); 
static int mandelbrotapp_glapp_KeyDown(HOBJECT object, int key, int ctrlkey); 
static int mandelbrotapp_glapp_KeyUp(HOBJECT object, int key, int ctrlkey); 
/*下面定义一个IGLApp的指针,对象中应该由一个指针成员指向这个结构*/
static const IGLApp mandelbrotapp_glapp_interface = { 
  (int)&(((const sMandelBrotApp*)0)->__IGLApp_ptr), 
  /*这个接口的偏移就是指向这个结构的指针在对象中的偏移*/
  mandelbrotappQueryInterface, 
  mandelbrotappAddRef, 
  mandelbrotappRelease, 
  mandelbrotappIsValid,
  mandelbrotapp_glapp_Render, 
  mandelbrotapp_glapp_SwitchIn, 
  mandelbrotapp_glapp_SwitchOut, 
  mandelbrotapp_glapp_GetOption, 
  mandelbrotapp_glapp_MouseLeftDoubleClick, 
  mandelbrotapp_glapp_MouseLeftDown, 
  mandelbrotapp_glapp_MouseLeftUp, 
  mandelbrotapp_glapp_MouseRightDoubleClick, 
  mandelbrotapp_glapp_MouseRightDown, 
  mandelbrotapp_glapp_MouseRightUp, 
  mandelbrotapp_glapp_MouseMove, 
  mandelbrotapp_glapp_KeyDown, 
  mandelbrotapp_glapp_KeyUp, 
};

定义了IGLApp接口的实现函数,对象中也只存放了指向mandelbrotapp_glapp_interface 这个变量的指针__IGLApp_ptr,我们依靠这个指针在对象中的相对位置,从而计算处对象的地址,于是就可以正常访问这个对象中的各个数据对象。对象的QueryInterface接口函数实现时则返回这个指针的地址。然而如果要实现接口相关的变量,肯定不能放在这个静态的数据结构中,因此直接将变量写到接口中时不行的,因为这个静态的数据结构将被所有的对象实例共享,不可能为每个对象实例提供一个独立的变量存储空间。
让我们再仔细看看IObject的实现,上次讲通过宏来辅助每个对象实现IObject的函数时,其中AddRef和Release的实现依靠一个引用计数实现。对象中还有一个可以判别是否是某个类的CLSID的变量,为用户提供是否是某个对象类的支持,这个也是用对象中存储的CLSID实现的,这两个变量用来实现IObject接口,其实可以视为是IObject接口相关的变量吧,我们规定在每个对象中要这么声明:

typedef struct _sMandelBrotApp {
    /*Object接口指针*/
	const IObject * __IObject_ptr;
	/*引用计数*/
	int __object_refcount;
	/*类ID指针*/
	IIDTYPE __object_clsid;
	/*IGLApp接口指针*/
	const IGLApp* __IGLApp_ptr;
	......

后面支持用宏来辅助实现,定义了这样的宏:

/*这个宏用来声明每个对象的基本成员,包括指向IObject的指针
引用计数,和CLSID,直接放在对象实现的结构最前面即可
*/
#define OBJECT_HEADER   \
	const IObject * __IObject_ptr; \
	int __object_refcount; \
	IIDTYPE __object_clsid; \

嗯,规则往往是被规则制定者打破的,微软公司有个著名的程序员制定的编程规则第一条就是:任何规则都是可以违反的,只要有足够的理由。程序员一方面要敬畏规则,另一方面要藐视它。不知道微软公司的c语言版本COM实现时如何做的,反正LCOM这样做是最顺的,所以也就不顾忌变量的对外公开了。
从这个可以看到,其实接口相关的变量是可以实现的,最简单的办法就是声明在接口指针的后面。这样一个接口IObject **变量同时可以当作一个指向

struct sObject {
	const IObject * __IObject_ptr; 
	int __object_refcount; 
	IIDTYPE __object_clsid; 
};

这个结构的指针使用,于是IObject接口就自带变量了。当然这仅仅适用于用我们提供的宏来实现IObject接口时的情况。事实上对象实现者可以选择自己实现自己的AddRef和Release,根本不是通过上面的宏或者类似的数据结构实现,这样上面的所谓IObject接口相关变量的说法其实只是宏辅助实现时的一个附加效果,不能作为LCOM中的规范性条文,使用LCOM的IObject接口时不能假定IObject**就是一个sObject的指针,万一有程序员不用我们提供的辅助宏来实现自己的对象呢。
不管怎么说,我们有了一个实现对象相关变量的办法,就是将接口指针变量和接口变量声明在一起,这样通过QueryInterface得到的接口指针变量的指针,同时也是接口相关变量的结构体指针(当然这个结构体必须以一个接口定义变量指针开始)。

3 双向链表接口

本节开始我们以双向链表为例,来演示接口相关变量的实现方法。事实上,我们走个极端,这个接口干脆没有函数(当然IObject相关的函数还是必须实现的,LCOM规定每个接口必须实现IObject的每个函数,这样通过任意接口都可以调用AddRef, Release和QueryInterface等函数),只有两个接口相关的变量,pNext和pLast。我们采用双向循环链表的方式来描述,用户自己记住表头,然后从表头开始通过pNext和pLast可以双向遍历链表,当然表头一般来说不是链表中的项。下面来定义双向链表接口,我们提供辅助宏:

DEFINE_GUID(IID_DLIST, 0xcdad8509, 0xb8, 0x4c3e, 0xa3, 0xcc, 0xbe, 0x7c, 0xf0, 0xc, 0x20, 0x8e);

struct sIDList;
struct sIDListVar;

typedef struct sIDList IDList;
typedef struct sIDListVar IDListVar, *IDListVarPtr;

struct sIDList {
  OBJECT_INTERFACE
};

#define DLIST_VARDECLARE \
	INTERFACE_DECLARE(IDList) \
	IDListVar* __dlist_pLast; \
	IDListVar* __dlist_pNext;

struct sIDListVar {
	DLIST_VARDECLARE
};

#define DLIST_VARINIT(_objptr, _obj) \
	INTERFACE_INIT(IDList, _objptr, _obj, dlist); \
	_objptr->__dlist_pLast =  \
	_objptr->__dlist_pNext = (IDListVarPtr)&_objptr->INTERFACE_VAR(IDList);

#define DLIST_FUNCIMPL(_obj, _clsid, _localstruct) \
static const IDList _obj##_dlist_interface = { \
	INTERFACE_HEADER(_obj, IDList, _localstruct) \
};

可以看到,双向链表的接口没有任何函数,但是我们定义了DLIST_VARDECLARE宏,要求要支持双向链表的对象,在对象数据结构中用这个宏声明双向链表相关的变量,这样用IID_DLIST接口名QueryInterface出来的接口指针可以当做一个IDListVar的指针来用,反过来IDListVar也可以当一个接口或者HOBJECT来用,通过它也可以QueryInterface得到其他接口,或者用objectThis宏得到实现双向链表的对象的指针,总之就是一个正常的LCOM接口,但是没有任何DLIST相关的接口函数,可以直接访问接口变量__dlist_pNext和__dlist_pLast,这两个变量是IDListVar的指针,可以作为HOBJECT使用,当然也可以作为IDList**使用了。于是我们达到了目的,实现了一个双向链表接口,其中没有任何DLIST相关的函数,只有两个变量。这个实现似乎比传统的方法要方便一些,一般传统的办法是要求将pNext和pLast放在对象的最前面,在LCOM实现中,由于offset的支持,我们可以将这些变量放在对象的任何地方。这个做法可能的好处是,可以将一个对象放在不同的数据结构中了,比如放在两个不同的链表中(可能要重新定义一下接口),或者一方面放在一个双向链表中,另一方面放在一个二叉树中,不用考虑两个数据结构争着要占用对象数据结构的最前面(其实最前面的好处仅仅在于偏移固定为零,不需要存储一个额外的偏移量,LCOM的接口自带偏移量,可以解决这个问题)。
这样在一个对象中如果要支持双向链表,只要简单地在对象实现结构中声明双向链表接口即可。比如:

typedef struct _sMandelBrotApp {
	OBJECT_HEADER
	INTERFACE_DECLARE(IGLApp)
	GLAPP_VARDECLARE
	DLIST_VARDECLARE 
	GLuint m_program;
	double psize;
	double pfromx;
	double pfromy;
	int	psizeLoc;
	int pfromLoc;
	int vertexLoc;
	int inited;
	int x, y, w, h;
}sMandelBrotApp;

OBJECT_FUNCDECLARE(mandelbrotapp, CLSID_MANDELBROT);
GLAPP_FUNCDECLARE(mandelbrotapp, CLSID_MANDELBROT, sMandelBrotApp);
DLIST_FUNCIMPL(mandelbrotapp, CLSID_MANDELBROT, sMandelBrotApp};
OBJECT_FUNCIMPL(mandelbrotapp, sMandelBrotApp, CLSID_MANDELBROT);

QUERYINTERFACE_BEGIN(mandelbrotapp, CLSID_MANDELBROT)
QUERYINTERFACE_ITEM(IID_GLAPP, IGLApp, sMandelBrotApp)
QUERYINTERFACE_ITEM(IID_DLIST, IDList, sMandelBrotApp)
QUERYINTERFACE_END

加了三行代码,就实现了LCOM的双向链表接口,我们的MandelBrotApp可以放在一个双向链表中维护了。

4 双向链表接口操作函数

这就实现了吗?当然实现了,传统的双向链表也就是在对象的最前面增加两个指针嘛。如何操作这个双向链表呢,有三种方案,一种是仍然定义接口函数,来操作pNext和pLast变量完成链表操作,这种办法的好处是可以跟LCOM的传统做法一样,比如可以使用安全的objectCall宏等,但是缺点是代码冗余,每个对象类都实现了一套完全一样的代码,高手应该感觉心里不爽才是。一种是用宏来实现,一般的双向链表支持是通过宏来提供的,当然我们这里也可以支持。还有一种是通过一个函数库来支持,这样代码可以不冗余,同时可以做一些安全性验证,确保系统的安全性。三种办法使用者可以自行选择,这里建议用第三种方式。定义双向链表的操作函数如下,当然不够用的话仍然可以通过宏或者其他函数来实现:

typedef int (*dlist_traversan_func)(IDListVarPtr item, void *param);

/*初始化一个表头*/
int dlistInit(IDListVarPtr list);
/*在表的最后面追加一个项*/
int dlistAppendItem(IDListVarPtr list, HOBJECT item);
/*在表的最前面插入一个项*/
int dlistInsertItem(IDListVarPtr list, HOBJECT item);
/*将list2表中的项链接到表list中,list2则清空*/
int dlistCancat(IDListVarPtr list, IDListVarPtr list2);
/*遍历表list,对表list中的每一项调用func,param作为参数传到func中*/
int dlistTraversal(IDListVarPtr list, dlist_traversan_func func, void * param);
/*将item项插入到项before之前*/
int dlistInsertBefore(HOBJECT before, HOBJECT item);
/*将item项插入到项after之后*/    
int dlistInsertAfter(HOBJECT after, HOBJECT item);    
/*将项item从所在表list中拆下来*/
int dlistDetach(HOBJECT item);
/*得到表中项的个数*/
int dlistItemCount(IDListVarPtr list);
/*删除表中的每一项(每一项将调用Release)*/
int dlistRemoveAll(IDListVarPtr list);

下面是这些函数的实现,可以看出虽然是LCOM模式定义的,但是实现方法似乎又回到了经典的模式,这种感觉就美妙很多了,感觉很顺滑啊:

#include "object.h"
#define IMPLEMENT_GUID
#include "../include/dlist.h"
#undef IMPLEMENT_GUID

/*初始化一个表头*/
int dlistInit(IDListVarPtr plist)
{
	if (plist==NULL)
		return -1;
	plist->__dlist_pNext = plist;
	plist->__dlist_pLast = plist;
	return 0;
}

/*在表的最后面追加一个项*/
int dlistAppendItem(IDListVarPtr plist, HOBJECT item)
{
	IDListVarPtr pitem = NULL;
	if (plist == NULL)
		return -1;
	objectQueryInterface(item, IID_DLIST, &pitem);
	if (pitem == NULL) {
		return -1;
	}
	pitem->__dlist_pNext = plist;
	pitem->__dlist_pLast = plist->__dlist_pLast;
	pitem->__dlist_pNext->__dlist_pLast = pitem;
	pitem->__dlist_pLast->__dlist_pNext = pitem;
	objectRelease(pitem);
	return 0;
}

/*在表的最前面追加一个项*/
int dlistInsertItem(IDListVarPtr plist, HOBJECT item)
{
	IDListVarPtr pitem = NULL;
	if (plist == NULL)
		return -1;
	objectQueryInterface(item, IID_DLIST, &pitem);
	if (pitem == NULL) {
		return -1;
	}
	pitem->__dlist_pLast = plist;
	pitem->__dlist_pNext = plist->__dlist_pNext;
	pitem->__dlist_pLast->__dlist_pNext = pitem;
	pitem->__dlist_pNext->__dlist_pLast = pitem;
	objectRelease(pitem);
	return 0;
}
/*将list2表中的项链接到表list中,list2则清空*/
int dlistCancat(IDListVarPtr plist, IDListVarPtr plist2)
{
	if ( (plist == NULL) || (plist2 == NULL) ) {
		return -1;
	}
	if (plist2->__dlist_pNext != plist2) {
		plist ->__dlist_pLast->__dlist_pNext = plist2->__dlist_pNext;
		plist2->__dlist_pNext->__dlist_pLast = plist ->__dlist_pLast;
		plist2->__dlist_pLast->__dlist_pNext = plist;
		plist ->__dlist_pLast = plist2->__dlist_pLast;
		plist2->__dlist_pNext = plist2;
		plist2->__dlist_pLast = plist2;
	}
	return 0;
}

/*遍历表list,对表list中的每一项调用func,param作为参数传到func中*/
int dlistTraversal(IDListVarPtr plist, dlist_traversan_func func, void * param)
{
	IDListVarPtr pitem, pitemtemp;
	if (plist == NULL)
		return -1;
	pitem = plist->__dlist_pNext; 
	while (pitem != plist) {
		pitemtemp = pitem->__dlist_pNext;
		if (func(pitem, param) != 0)
			break;
		pitem = pitemtemp;
	}
	return 0;
}

/*将item项插入到项before之前*/
int dlistInsertBefore(HOBJECT before, HOBJECT item)
{
	IDListVarPtr pitem = NULL;
	IDListVarPtr pbefore = NULL;
	objectQueryInterface(item, IID_DLIST, &pitem);
	objectQueryInterface(before, IID_DLIST, &pbefore);
	if ( (pitem == NULL) || (pbefore==NULL) ) {
		objectRelease(item);
		objectRelease(before);
		return -1;
	}
	pitem->__dlist_pNext = pbefore;
	pitem->__dlist_pLast = pbefore->__dlist_pLast;
	pitem->__dlist_pLast->__dlist_pNext = pitem;
	pitem->__dlist_pNext->__dlist_pLast = pitem;
	objectRelease(item);
	objectRelease(before);
	return 0;
}

/*将item项插入到项after之后*/    
int dlistInsertAfter(HOBJECT after, HOBJECT item)    
{
	IDListVarPtr pitem = NULL;
	IDListVarPtr pafter = NULL;
	objectQueryInterface(item, IID_DLIST, &pitem);
	objectQueryInterface(after, IID_DLIST, &pafter);
	if ( (pitem == NULL) || (pafter==NULL) ) {
		objectRelease(item);
		objectRelease(after);
		return -1;
	}
	pitem->__dlist_pLast = pafter;
	pitem->__dlist_pNext = pafter->__dlist_pNext;
	pitem->__dlist_pLast->__dlist_pNext = pitem;
	pitem->__dlist_pNext->__dlist_pLast = pitem;
	objectRelease(item);
	objectRelease(after);
	return 0;
}

/*将项item从所在表list中拆下来*/
int dlistDetach(HOBJECT item)
{
	IDListVarPtr pitem = NULL;
	objectQueryInterface(item, IID_DLIST, &pitem);
	if (pitem == NULL) {
		return -1;
	}
	pitem->__dlist_pLast->__dlist_pNext = pitem->__dlist_pNext;
	pitem->__dlist_pNext->__dlist_pLast = pitem->__dlist_pLast;
	pitem->__dlist_pLast = pitem;
	pitem->__dlist_pNext = pitem;
	objectRelease(item);
	return 0;
}

/*得到表中项的个数*/
int dlistItemCount(IDListVarPtr plist)
{
	int count;
	IDListVarPtr pitem;
	if (plist==NULL)
		return 0;
	count = 0;
	while (pitem != plist) {
		pitem = pitem->__dlist_pNext;
		count++;
	}
	return count;
}

/*删除表中的每一项(每一项将调用Release)*/
int dlistRemoveAll(IDListVarPtr plist)
{
	IDListVarPtr pitem, pnextitem;
	if (plist==NULL)
		return -1;
	pitem = plist->__dlist_pNext; 
	while (pitem != plist) {
		pnextitem = pitem->__dlist_pNext;
		objectRelease((const IDList **)pitem);
		pitem = pnextitem;
	}
	plist->__dlist_pNext = plist->__dlist_pLast = plist;
	return 0;
}

5 双向链表的链表表头对象

前面说过,要使用循环双向链表,得记住一个表头,表头可以直接声明一个IDListVar对象,优点是不需要动态分配内存,这样少维护一个指针,缺点是这个表头不能作为一个LCOM对象使用。这里给出一个选项,直接实现一个表头类对象,使用时可以直接声明这个表头类对象,然后按照LCOM对象来管理,也可以用isClass宏来查询是否是表头,这样可以减少很多维护工作。
下面是定义:

DEFINE_GUID(CLSID_DLISTHEADER, 0x7ba10549, 0x8c42, 0x4301, 0xbb, 0xc7, 0x75, 0x5a, 0x7f, 0x8, 0x9, 0xae);
IDListVar * dlistheaderCreate(); 
#define IsDListHeader(obj) objectIsClass(obj, CLSID_DLISTHEADER)

实现如下:

/*表头对象*/
typedef struct _sDListHeader {
    OBJECT_HEADER
    DLIST_VARDECLARE
}sDListHeader;

OBJECT_FUNCDECLARE(dlistheader, CLSID_DLISTHEADER);

DLIST_FUNCIMPL(dlistheader, CLSID_DLISTHEADER, sDListHeader);
OBJECT_FUNCIMPL(dlistheader, sDListHeader, CLSID_DLISTHEADER);

QUERYINTERFACE_BEGIN(dlistheader, CLSID_DLISTHEADER)
QUERYINTERFACE_ITEM(IID_DLIST, IDList, sDListHeader)
QUERYINTERFACE_END

static const char* dlistheaderModuleInfo()
{
    return "1.0.0-20210503.1247 Dual List Header";
}

static int dlistheaderCreate(const PARAMITEM* pParams, int paramcount, HOBJECT* pObject)
{
    sDListHeader* pobj;
    pobj = (sDListHeader*)malloc(sizeof(sDListHeader));
    if (pobj == NULL)
        return -1;

    memset(pobj, 0, sizeof(sDListHeader));

    *pObject = 0;
    DLIST_VARINIT(pobj, dlistheader);

    /*返回生成的对象*/
    OBJECT_RETURN_GEN(dlistheader, pobj, pObject, CLSID_DLISTHEADER);
    return EIID_OK;
}


static void dlistheaderDestroy(HOBJECT object)
{
    sDListHeader* pobj;
    pobj = (sDListHeader*)objectThis(object);
    free(pobj);
}

/*
    功能:判断对象是否是一个有效对象
    参数:
        object -- 对象数据指针
    返回值:
        0 -- 对象是无效的
        1 -- 对象是有效的
*/
static int dlistheaderValid(HOBJECT object)
{
    return 1;
}

IDListVar* dlistheaderCreate()
{
	int ret;
	IDListVar* pListHeader;
	A_u_t_o_registor_dlistheader();
	ret = objectCreateEx(CLSID_DLISTHEADER, NULL, 0, IID_DLIST, (const void**)&pListHeader);
	if (ret == 0)
		return pListHeader;
	else
		return NULL;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

饶先宏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值