首页  编辑  

COM实现过程

Tags: /超级猛料/COM、ActiveX,DDE/COM_ DCOM/   Date Created:

COM 实现过程

原创:吴剑明(foxnt)

前言

COM已经成为一个必需的东西了。在我们周围,可以说处处充满了COM - 如果你是在使用WINDOWS,并在其下面编写程序的话。然而,无论你是用VC,还是使用DELPHI进行COM编程时,在大多数情况下,编程工具的IDE已经向你隐藏了COM的大部分实现过程,使得程序员根本不需要了解COM,只专心致志地写其所关心的逻辑代码。这就意味着,我们很少有机会,能够揭开COM的神秘面纱,来看到它下面到底是什么东西。这对于一个WINDOWS程序员来说,不能不是个遗憾。

因此,本文的宗旨,就是抛开现有的IDE提供的各种向导工具,引导大家从最基本的地方入手,完整地从一个空白的程序里,建立起一个COM程序,从而达到能够比较清晰地了解一个COM,到底是如何生成出来并实现在程序中。

本文假设,您是一个有COM编程经验的DELPHI/VC程序员,并希望了解COM的基本实现过程。限于篇幅和时间,我们只讨论进程内的COM(DLL)的实现,并引导大家亲手建立起一个最简单的COM程序。

COM是什么?

COM有各种表现形式,可以是进程内,也可以是进程外;可以在本机调用,也可以远程调用。记得国外有个研究COM的组织,他的主题就叫作:COM就是爱! 这当然是外国人的幽默,他只是想说明,COM是个多么重要的东西。那么COM到底是个什么东西呢?

很早以前,在我刚开始学习COM的时候,身边就有些程序员告诉我:COM不是DLL,虽然它通常也是以DLL来作为扩展名的,可他完全与DLL完全不同。那么,这种说法是否正确呢?我们来看看,要实现一个进程内的COM,到底需要经过哪些步骤,那么,我们就能很清楚的知道答案了。

完成一个进程内的COM,通常需要以下几步:

1. 建立一个DLL项目,并导出以下四个函数:

DllGetClassObject,

DllCanUnloadNow,

DllRegisterServer,

DllUnregisterServer;

2. 定义自定义的接口,同时必须实现Iunknown接口。

3. 建立GUID,以标识这个组件以及自定义的接口。

4. 在注册表中注册以标记这个DLL。

大家都看到了,在第一个步骤里,需要建立一个DLL项目,那么,是不是意味着,COM就是一个DLL呢?在这里,我可以明确地告诉大家,从技术上讲,一个进程内的COM完全可以被认为就是一个普通的DLL-动态连接库!如果你抛弃常用的COM API,比如DELPHI中常用的:

CreateCOMObject()或者

CreateOLEObject()

那么您完全可以直接采用加载普通DLL的方式来调用这个COM组件,比如说,您可以直接用LoadLibrary()函数来加载这个DLL,然后使用GetProcAddress来调用从这个DLL里输出的接口,从而完成各项操作。这是完全可行的。然而,我不得不告诉大家,把一个COM仅仅看成一个DLL,那是非常肤浅的看法 - DLL仅仅是一种表现形式而已。更重要的是,COM实现了一种规则。因此我们可以说:

l COM是一种包含了许多处理逻辑、符合了某种接口规范(如Iunknown规范)的DLL组件。

(注:如果没有特别说明,我在本文里所指的COM,都是指进程内的DLL形式的COM)

l COM实现了Iunknown接口。因此,任何只要符合Iunknown规范,实现了Iunknown接口的DLL组件,我们都可以把他看成是一个COM。

那么,什么是Iunknown接口呢?如何实现一个Iunknown接口呢?我们看看,在DELPHI中是如何定义一个Iunknown接口的:

IInterface = interface

['{00000000-0000-0000-C000-000000000046}']

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

end;

IUnknown = IInterface;

简单一点看,我们直接这样理解就行了:

IUnknown = interface

['{00000000-0000-0000-C000-000000000046}']

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

end;

在DELPHI里,interface是编译器所认识的一种类型。如果是在VC++中,Iunknown将被定义为一种结构(struct)。如果要实现一个Iunknown接口,我们必须用一个类来实现它,比如在DELPHI中,要实现Iunknown接口可以写成为:

TMyCOMObject = class (Tobject, Iunknown)

……

end;

有心的读者可能会立即问:这个Iunknown接口由Tobject来实现,那么,可不可以是由其他类来实现呢?比如说用Tcomponent类来实现?答案是: 完全可以!!

例如,我们要实现一个自定义的接口IMyCOM,可以写成这样:

IMyCOMTest = interface(Iunknown);

TMyCOMTest = class(Tcomponent, IMyCOMTest)

…….

End;

这样是完全可以的!因为COM关注的只是如何实现一个接口,至于程序员使用什么类来实现,COM是不管的。

后面我们要实现一个COM的例子,而且我打算就用这个IMyCOMTest接口来做。所以我们把这个接口声明成为例1,以便后面使用。

COM的产生

假如我们已经完成了一个COM,并且已经在系统中注册了。那么,一个客户端需要来调用这个COM,这时,系统中发生了哪些事呢?

一般来说,以DELPHI为例,客户程序使用CreateCOMObject或者CreateOLEObject调用COM组件时,会发生以下几个步骤:

1. CreateCOMObject或者CreateOLEObject的动作。

我们看看这两个函数都干了些什么:

function CreateComObject(const ClassID: TGUID): IUnknown;

begin

OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or

CLSCTX_LOCAL_SERVER, IUnknown, Result));

end;

CreateOLEObject稍微复杂些:

function CreateOleObject(const ClassName: string): IDispatch;

var

ClassID: TCLSID;

begin

ClassID := ProgIDToClassID(ClassName);

OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or

CLSCTX_LOCAL_SERVER, IDispatch, Result));

end;

看到了吗?CreateOLEObject多了一个ProgIDToClassID函数。这意味着,如果我们要用CreateOLEObject来调用我们的COM组件,我们将要多一些步骤来编写我们的COM。这个我将会在后面说明。现在,我们要关注的是CoCreateInstance API函数。

2. CoCreateInstance API函数将调用CoGetClassObject API,这个调用过程我们是看不到相关的代码的,因为微软已经把他封装好了。而CoGetClassObject函数的作用是什么呢?它将调用LoadLibrary来寻找我们指定的COM组件(DLL),然后使用GetProcAddress 来寻找组件的入口函数 - 还记得我们上面说过的那四个被导出的函数吗?对,其中的DllGetClassObject 函数就在这里将被调用。该函数的原形在DELPHI中是:

function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HResult;

其中第三个参数:Obj ,将向我们返回COM中的定义的接口。但是,要注意,这个接口并不是我们自定义的接口,而是向我们返回了一个被成为是"类工厂"接口的IclassFactory的接口。当我们获得类工厂接口后,就可以获得我们所需要的、那个我们自定义的接口了。看看IclassFactory 的接口声明:

IClassFactory = interface(IUnknown)

['{00000001-0000-0000-C000-000000000046}']

function CreateInstance(const unkOuter: IUnknown; const iid: TIID;

out obj): HResult; stdcall;

function LockServer(fLock: BOOL): HResult; stdcall;

end;

看到那个CreateInstance 的方法了吗?对了,它的第三个参数 obj 将向我们返回那个我们定义的接口,比如是我们的IMyCOMTest接口(例1)。这样,我们就可以调用我们自定义的接口方法了。

以上的众多步骤看起来有点让人迷惑。那么我们就用一个简单的流程来描绘我们刚才所发生的步骤。不要被那些步骤吓倒,其实他们是非常简单的。

l CreateCOMObject --à CoCreateInstance。 CoCreateInstance 在注册表中查找COM的注册信息。

l CoCreateInstance -à CoGetClassObject 。注册信息被交给CoGetClassObject。这时候CoGetClassObject将知道COM组件在磁盘上的位置。

l CoGetClassObject -à LoadLibrary 。LoadLibrary 将寻找COM DLL的入口,然后GetProcAddress调用其输出函数DllGetClassObject

l DllGetClassObject 的输出参数将向我们返回"类工厂"接口IClassFactory。

l IclassFactory --à CreateInstance 。CreateInstance方法建立其我们实现接口的类。该类将调用自身的QueryInterface 方法,查看用户指定的接口是否被自己实现,如果实现了,则向返回自定义的接口。

l 调用结束后,COM客户将调用COM的DLL输出函数DllCanUnloadNow 。如果该函数返回S_OK,则释放该组件。

实际的COM例子

下面我们来做一个实际的例子。包括如何建立一个COM Server和一个COM Client。

对于COM Server,我们将实现以下功能:

l 单线程,单客户支持。

l 实现自定义的接口

l 能够使用Regsvr32 在系统中注册和反注册。

l 能够被DELPHI或者VC++程序调用。

我们只关注实现最基本的功能。当大家清楚整个流程后,自然就能写出更高级的功能,比如多线程支持等。

下面,让我们开始COM实现之旅。

COM Server程序

l 在DELPHI中,新建一个DLL工程。注意是DLL,而不是 Activex Library。并把工程名保存为MyCOM。然后依次建立两个单元文件:

MyCOMServer 单元: 此单元描述了COM的逻辑实现

COMDef 单元: 此单元描述了COM的输出函数定义。

l 在MyCOM单元里,我们定义DLL的输出函数,整个代码:

library MyCOM;

uses

SysUtils,

Classes,

COMDef,

MyCOMServer in 'MyCOMServer.pas';

//在这里导出四个函数。

exports

DllGetClassObject,

DllCanUnloadNow,

DllRegisterServer,

DllUnregisterServer;

{$R *.res}

begin

end.

先做好定义,不要考虑他们是如何实现的。这个在后面我会做详细解说。在这里我先说明这四个函数的作用:

DllGetClassObject : 返回类工厂接口。

DllCanUnloadNow : 告诉客户端该COM是否可以被正常卸载。

DllRegisterServer : 向系统注册COM组件信息。Regsvr32.exe 就是调用这个函数来进行注册的。

DllUnregisterServer :从系统中反注册一个COM。Regsvr32.exe 就是调用这个函数来进行反注册的。

l 现在为我们的COM定义GUID。注意,在一个COM组件里,有三种GUID:

CLASS_xx 类型的GUID,是用来标识COM组件的。

IID_xx类型的GUID,是用来标识一个接口的。可以有多个。

LIBID_xx类型的GUID,是用来标识一个TypeLib的。关于如何实现一个TypeLib,我会在COM实现过程(2)中会讲到。在这里先略过。

现在,我们定义我们的COM组件的GUID,在COMDef单元里加入的代码为:

const

Class_MyCOM: TGUID = '{CE38847E-A386-4753-89F1-34BE80042107}';

LIBID_MyCOM: TGUID = '{C2387E2A-0F08-442E-8947-D1AB36A9BDD0}';

l 开始定义我们的接口。我们采用例一所定义的接口,在MyCOMServer单元里加入代码:

IMyCOMTest = interface(IUnknown)

['{D1C4A022-7F6F-42F0-A9B0-4A91703EB124}']

function msg: integer;stdcall;

end;

注意,在这里,我们为IMyCOMTest 定义了一个IID类型的GUID,它唯一表示我们的接口。同时我们定义了一个方法 msg ,它向我们返回一个整数值。

定义了接口,当然要实现它:

TMyCOMServer = class(TObject, IMyCOMTest)

protected

FLock: integer;

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

public

Constructor Create;

function msg: integer;stdcall;

end;

现在,我们一个个的来说明并实现TMyCOMServer 的几个方法。

_AddRef 方法:

该方法用于增加一个COM的计数器。我们知道COM Server是需要支持多客户调用的,在COM中每实现一次调用,就必须进行计数。如果COM的计数>0 ,则COM继续生存。否则,该COM接口实例就必须被注销。

_Release 方法:

该方法和_AddRef方法相反,用于减少一个计数,当计数为0,则注销该接口。

QueryInterface 方法:

该方法用于查询一个接口是否存在,如果存在则在输出参数里返回该接口指针。

为实现这几个方法,所添加的代码如下:

function TMyCOMServer._AddRef: Integer;

begin

Inc(FLock);

end;

function TMyCOMServer._Release: Integer;

begin

Dec(FLock);

if FLock = 0 then

Free;

end;

function TMyCOMServer.QueryInterface(const IID: TGUID; out Obj): HResult;

begin

//在类工厂中实现的一个MC接口,在这里返回。由于我们只有一个接口,所以那个IID参数没有用。当然,如果我们实现了多个接口,就需要维护一个数组,用个CASE语句,根据不同的IID,来返回不同的接口。

Pointer(Obj) := Pointer(MC);

Result := S_OK;

end;

其中,MC是一个IMyCOMTest 接口,是个全局变量:

var

MC: IMyCOMTest;

CF: IClassFactory;

别忘了还有我们的构造器:

constructor TMyCOMServer.Create;

begin

Inc(FLock);

end;

在一开始就增加一个引用计数,使得COM计数大于1,这样COM就不会自动销毁。

然后去实现我们的接口方法:

function TMyCOMServer.msg: integer;

begin

result := 1978;

end;

该方法直接向我们返回一个整数值。

l 类工厂的实现

正如我前面所说的,一个类工厂必须去建立我们自定义的接口。在上面,我们定义了自定义的接口,并由类TMyCOMServer 去实现。那么,现在我们还要做的是,实现类工厂,然后由类工厂建立一个TMyCOMServer 的接口实例。类工厂接口定义如下:

IClassFactory = interface(IUnknown)

['{00000001-0000-0000-C000-000000000046}']

function CreateInstance(const UnkOuter: IUnknown; const IID: TGUID;

out Obj): HResult; stdcall;

function LockServer(fLock: Boolean): HResult; stdcall;

end;

注意,IclassFactory是系统预先定义了的,在ACTIVEX单元有,所以不需要自己再去定义一次。我们只要去实现它就是:

TClassFactory = class(TObject, IClassFactory)

protected

FLock: integer;

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

public

Constructor Create;

function CreateInstance(const UnkOuter: IUnknown; const IID: TGUID;

out Obj): HResult; stdcall;

function LockServer(fLock: Boolean): HResult; stdcall;

end;

我们只关注CreateInstance 方法。LockServer 用于在多客户调用COM时,锁定COM,以免一个客户退出时销毁了COM,那么其他客户的调用将发生错误。但是我们在这里只实现单客户,所以不考虑这个函数,把他置空就是。

function TClassFactory.CreateInstance(const UnkOuter: IInterface;

const IID: TGUID; out Obj): HResult;

begin

//我们的自定义接口,就是在这里被创建的。

MC := TMyCOMServer.Create;

Pointer(Obj) := Pointer(MC);

end;

function TClassFactory.LockServer(fLock: Boolean): HResult;

begin

end;

同样的,TclassFactory也必须实现引用计数,因为它也实现了Iunknown接口。

function TClassFactory._AddRef: Integer;

begin

Inc(FLock);

end;

function TClassFactory._Release: Integer;

begin

Dec(FLock);

if FLock = 0 then

Free;

end;

function TClassFactory.QueryInterface(const IID: TGUID; out Obj): HResult;

begin

end;

其中,QueryInterface 我把它置空,因为在这个例子中,不需要向它查询什么接口。如果以后读者需要向它查询接口时,自己实现相关代码。

同样,在它的构造器中,也预先对计数加1

constructor TClassFactory.Create;

begin

Inc(FLock);

end;

到目前为止,我们已经基本实现了一个COM需要的大部分功能。现在,我们需要把它注册到系统中,以便被其他程序调用。

l COM的注册和反注册

我们回过头来,看看如何去实现那四个DLL的输出函数。这四个函数的原形如下:

function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HResult;stdcall;

function DllCanUnloadNow: HResult;stdcall;

function DllRegisterServer: HResult;stdcall;

function DllUnregisterServer: HResult;stdcall;

我们上面所说的类工厂的实例,就是在DllGetClassObject 中创建的。代码如下:

function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HResult;

begin

CF := TClassFactory.Create;

Pointer(obj) := Pointer(CF);

Result := S_OK;

end;

同样的,我们只有一个类工厂,所以可以不理会前面那两个参数。否则,就要根据不同GUID,来创建不同的类工厂对象。在这里,我们直接把类工厂对象给返回了。

函数DllCanUnloadNow 用来注销一个COM。在正常使用中,要根据引用计数,来判断是否允许用户注销。在这里我们直接返回S_OK,让用户直接注销。

function DllCanUnloadNow: HResult;

begin

Result := S_OK;

end;

函数DllRegisterServer 用来向注册表注册一个COM组件信息。要注册一个COM,用户必须知道COM在注册表中的信息是如何组织的。结构如下:

HKEY_CLASSES_ROOT

---- CLSID

---- GUID

----- InprocServer32 标明 COM所在磁盘的路径以及线程模型

----- ProgID 标明COM所实现的接口

----- TypeLib 标明 COM 的类型库的GUID

----- Version 标明 COM的版本号。

当发生CreateCOMObject()调用时,输入参数为COM的CLASS类型的GUID,系统将在注册表中搜索到相关信息,然后就可以找到该COM的位置,就可以开始调用了。

注意,如果您希望COM组件支持客户端的CreateOLEObject()函数的调用,您必须还要注册一个信息:

HKEY_CLASSES_ROOT

----- 接口声明

----- CLSID 标明 COM 接口和CLASS类型的GUID的对应关系。

那么,当发生 CreateOLEObject 调用时,系统将会根据输入参数(一个COM接口声明,如a.b),去查找和接口对应的CLASS GUID,然后就可以读到COM的相关信息了。

全部代码如下:

function DllRegisterServer: HResult;

var

lp: pchar;

ns: Dword;

begin

Result := S_FALSE;

Reg := TRegistry.Create;

GetMem(lp, 255);

try

Reg.RootKey := HKEY_CLASSES_ROOT;

if Reg.OpenKey('\MyCOM.MyCOMTest',true) then

begin

Reg.CreateKey('CLSID');

if Reg.OpenKey('CLSID',true) then

Reg.WriteString('',GUIDToString(Class_MyCOM));

end;

if Reg.OpenKey('\CLSID\' + GUIDToString(Class_MyCOM), true) then

begin

if Reg.CreateKey('InprocServer32') = false or

Reg.CreateKey('ProgID') = false or

Reg.CreateKey('TypeLib') = false or

Reg.CreateKey('Version') = false then

Exit;

Reg.WriteString('','MyCOM');

if Reg.OpenKey('\CLSID\' + GUIDToString(Class_MyCOM) +

'\InprocServer32', false) then

begin

Windows.GetModuleFileName(HInstance,lp, 255);

Reg.WriteString('', lp);

Reg.WriteString('ThreadingModel', 'Single');

end;

if Reg.OpenKey('\CLSID\' + GUIDToString(Class_MyCOM) + '\ProgID', false) then

Reg.WriteString('','MyCOM.MyCOMTest');

if Reg.OpenKey('\CLSID\' + GUIDToString(Class_MyCOM) + '\Version', false) then

Reg.WriteString('','1.0');

if Reg.OpenKey('\CLSID\' + GUIDToString(Class_MyCOM) + '\TypeLib', false) then

Reg.WriteString('',GUIDToString(LIBID_MyCOM));

Reg.CloseKey;

Result := S_OK;

end;

finally

begin

FreeMem(lp);

Reg.Free;

end;

end;

end;

函数DllUnregisterServer 则向系统注销一个COM的注册信息。它比较简单,直接把COM的相关注册键给删除就是:

function DllUnRegisterServer: Hresult;

begin

Result := S_False;

Reg := TRegistry.Create;

try

Reg.RootKey := HKEY_CLASSES_ROOT;

Reg.DeleteKey('\CLSID\' + GUIDToString(Class_MyCOM));

Reg.CloseKey;

Finally

Reg.Free;

end;

end;

l 最后工作。

现在,我们编译程序,然后生成一个DLL文件,在命令行下,使用:

regsvr32 MyCOM.dll

向系统注册COM。

COM Client程序

在DELPHI中调用

新建一个项目,然后在单元中,定义接口信息:

IMyCOMTest = interface(IUnknown)

['{D1C4A022-7F6F-42F0-A9B0-4A91703EB124}']

function msg: integer;stdcall;

end;

定义变量:

class_wjm: TGUID = '{CE38847E-A386-4753-89F1-34BE80042107}';

a: IMyCOMTest;

然后在窗口的OnCreate 事件里,添加如下代码:

procedure TForm1.FormCreate(Sender: TObject);

begin

//随便用哪个都可以

a := createcomobject(class_wjm) as IMyCOMTest;

//或者使用 a := createoleobject('MyCOM.MyCOMTest') as IMyCOMTest;

end;

然后,放一个按钮,并在其事件里添加代码:

procedure TForm1.Button1Click(Sender: TObject);

begin

showmessage(inttostr(a.msg));

end;

在窗口的OnCLose事件里加上:

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);

begin

a := nil;

end;

注意一定要释放接口,否则可能是个难看的 AV 错误哦。

运行我们的程序,点下按钮,你将看到输出信息"1978"。

如果你看不到正确的信息,请仔细查看你的代码是否和文中一致。你也可以直接向我索要源代码。

在VC6中调用

稍微复杂点。先把GUID翻译过来:

//{CE38847E-A386-4753-89F1-34BE80042107};

static const CLSID CLSID_MyCOM = {0xCE38847E,0xA386,0x4753,

{0x89,0xF1,0x34,0xBE,0x80,0x04,0x21,0x07}};

//{D1C4A022-7F6F-42F0-A9B0-4A91703EB124}

static const IID IID_MyCOM = {0xD1C4A022,0x7F6F,0x42F0,

{0xA9,0xB0,0x4A,0x91,0x70,0x3E,0xB1,0x24}};

然后在声明一次接口的定义:

struct IMyCOMTest : public IUnknown

{

virtual LONG __stdcall msg();

};

IMyCOMTest* pLink;

然后放个按钮上去,并在相关事件里写代码:

void CMyvcView::OnButton6()

{

pLink = NULL;

int a =0;

CoInitialize(NULL);

a = CoCreateInstance(CLSID_MyCOM, NULL,

CLSCTX_INPROC_SERVER,IID_MyCOM, (void**)&pLink);

if (a==S_OK){

LONG a= pLink->msg();

};

}

注意,一定要记住调用 CoInitialize(NULL); 这个函数,否则COM无法使用的。

编译运行,你应该能看到 a 是等于1978 的。

总结

到目前为止,我们成功的编写了一个最简单的COM组件,并且在DELPHI和VC中成功调用。这都说明我们的工作是成功的。同时我们也看到,实现一个COM,并不难。

关于进程外的COM以及DCOM,前者是基于LPC 本地过程调用,后者是基于RPC远程过程调用。除了协议不同外,其他的都一样。大家有兴趣,可以以后继续讨论。

关于COM的线程模式,我曾经以为,是COM向导中自动会产生对应的线程代码,来实现多线程的。但是我后来又认为,根本没有这回事,COM只是做了个标记,告诉操作系统他的线程模型,至于如何产生线程,则是操作系统做的。有关这方面的讨论,还需要进一步研究。

一个小尾巴

我们知道,在DELPHI里,有一个Import Type Library 的功能。可以把一个COM组件导到DELPHI中直接使用。但是,如果我们试图把我们刚才写的那个组件,也ADD进去的时候,DELPHI会提示:

加载类型库/DLL时出错。

这是怎么回事呢? 原来,这是MS/BORLAND的一个小花招。我们看看VCL的代码就知道了,在DELPHI的向导为你创建一个COM时,它偷偷地加了一个IprovideClassInfo 的接口进去,该接口使用ItypeInfo 接口,主要用于向外提供COM的类型信息的。大家仔细跟下TtypedComObject 这个类,就会发现这个奥秘了。在前例中,我们没有实现这个接口,那么当然无法被DELPHI加载了。关于如何实现这个接口,已经超出了本文的范围,所以不于讨论。

有兴趣的朋友,可以继续关注 "COM实现过程(2)",主要讲述如何实现类型库的。

2002/6/27

版权所有

转载时请包括作者姓名