精通
英语
和
开源
,
擅长
开发
与
培训
,
胸怀四海
第一信赖
One severe limitation of the standard C++ reuse model is that if I wanted to sell my fancy database object, I would have to distribute my complete and valuable know-how in form of the source code.C++重用模式里最严重的限制是,如果我想销售做好的数据库组件,我不得不分发我完整的且最有价值的源代码。
A standard solution to this problem under Windows is to package the implementation into a DLL and provide one or more header files that declare the functions and structures used. (Microsoft currently ships a large part of Windows and Windows NT in this form; in fact, the headers are shipped separately from the DLLs in the form of software development kits.)在Windows下标准的作法是打包到DLL,向客户提供头文件,头文件里有DLL里函数和结构体的声明(微软当前对于Win操作系统的操作模式就是这样。实际上,头文件在SDK里是和DLL分离放置的)。
To package the implementation into a DLL, you must consider the following issues: 使用DLL,要注意:
Exporting of member functions导出成员函数
Memory allocation only in the DLL or only in the EXE只在DLL或只在EXE里进行内存分配
Unicode™/ASCII interoperability UNICODE/ASCII的互操作性
One simple way to export functions is by using the __declspec(dllexport) modifier (since Visual C++ 2.0), which can be applied to any function, including member functions. This instructs the compiler to place an entry into the exports table, just like declaring an export in the module definition file (.DEF) for the linker. [In the 16-bit world, _export did the same thing; in addition, the compiler provided additional code to change to the data segment of the DLL before entering the function, and then switch back to the caller's data segment before leaving the function (prologue/epilogue).]导出函数最简单的办法是使用__declspec(dllexport)修饰符(从VC2里就可以支持了),这个修饰符可以应用到任何函数上,包含成员函数。这个修饰符指示编译器放置一个入口到导出表里,就象在模块定义文件(.DEF)里对链接器声明一样。
For C++ this is the only practical way to export big numbers of functions, because C++ provides function overloading (that is, using one function name for many functions that differ only in the kind of parameter declared). Thus, the C++ compiler combines all the information it has about a member function (return type, class, parameter types, public/private) into one big name. (See the technical article “Exporting with Class" in the MSDN Library Archive for more details.)对于C++来说,这是导出大量函数的唯一的可实践的办法,因为C++提供了函数重载(函数名相同,参数不相同)。这样,C++编译器结合了所有关于成员函数的信息到一个宽名称。
By simply applying the _declspec(dllexport) modifier to all the functions in the CDB class, we make the class exportable in a DLL. We then just have to provide a make file to create the binary.通过简单地应用_declspec(dllexport)到CDB类里的所有函数,我们让这个类导出到DLL里了。我们接着只是必须提供一个makefile来创建二进制应用程序。
Due to the name mangling, it is very difficult for the client to use dynamic loading: We would have to pass all the decorated names to GetProcAddress and save the returned pointer somewhere. Then we would have to set up a simulation class that calls each of these functions. Therefore, it's definitely better to use implicit linking (using the DB.LIB file generated by the linker).由于命名上可能晦涩难懂,让客户端动态加载非常困难:我们必须传递所有修饰的名称到GetProcAddress且保存返回的指针到别的地方。接着我们必须设置一个操作类,来调用每个这类函数。所以,它比使用不明显的链接方式很明显地要好(使用由链接器生成的DB.Lib文件)
Another issue, related to name mangling, is incompatibility between compilers. The name mangling is not standardized, and thus each compiler decorates functions differently. A DLL compiled by one compiler cannot be used by another. If you did not want to give away your source code, you would have to provide all the compiled versions yourself. Using this technique in a component software scenario is simply not acceptable. There would have to be many objects with the same functionality to satisfy all possible clients.另外一个问题,和命名混乱相关,和编译器的不兼容性。命名混乱不是标准化的,每个编译器修饰函数的办法不一样。一个编译器编译出来的DLL,不一定能在其它编译器里用。如果你不想放弃你的源代码,你必须提供每个编译器下编译的结果。这个方法在组件开发里是行不通的。会有太多相同函数名的对象来适应所有可能的客户端。
Both the DLL and the executable file (EXE) maintain their own lists of allocated memory blocks, which they maintain through the malloc memory allocator. The C++ new and delete functions also rely on these lists of memory blocks, so that C++ tends to use dynamic memory allocation more often than C. If the DLL allocates some memory—for example, for the creation of a new instance of a class—this memory is marked in the allocation list of the DLL. If the EXE tries to free this memory, the run-time library looks through its list of allocated memory blocks and fails (usually with a GP fault). Thus, even if the memory between the DLL and the EXE is completely shared, the logic for managing allocation breaks if two modules mix their allocation schemes.DLL和可执行文件都维护他们的分配内存块的列表,这些内存块通过malloc内存分配器来维护。C++的new和delete函数同样依赖这些内存块列表,所以c++比C更倾向于使用动态内存分配。举例来说,如果DLL分配一些内存来创建类的实例,这些实例用的内存被标记到DLL里。如果EXE尝试来分配这些内存,运行时库检查它的内存分配块的列表且失败(通常带有GP错误)。这样,即使DLL和EXE之间的内存是完全共享的,如果2个模块混淆了分配方案,管理分配的逻辑也会打破。
There are basically three solutions to this problem: 基本上,有3个办法能够解决此问题:
Have the EXE always allocate and free a given kind of memory.EXE始终分配和释放给定内存
Have the DLL always allocate and free a given kind of memory.DLL始终分配和释放一个给定种类的内存
Have something neutral (the operating system) allocate and free all the memory. 让中立方(操作系统)分配和释放所有内存
The third approach seems to be the most flexible one, but unfortunately cannot be used easily for operations involving the C/C++ run-time libraries, even for basic functions such as new and delete.第三个方法好象更复杂,但是不幸地是不能容易地解决C/C++交叉使用运行库造成的问题,甚至处理基本功能new和delete也不行。
From an object-based point of view, it is more convenient to have the memory allocation done by the object (encapsulation). This way the client does not need to be aware of the size of the object. If the object's implementation changes, chances are that the client will still be able to use the object (if the object's exported functions do not change).从基于对象的视角看,用对象(封装过)自己管理内存是更容易的。在这个方法里,客户端不需要知晓对象的大小。如果对象的实现有所改变,客户端还有机会依然能够使用对象(如果对象的导出函数没有修改)。
In this sample, we will use a global function to have the object instantiate a copy of itself (using new) and return the pointer to the client (later, we will see that COM takes the same approach). The client will then use this pointer in all its calls to the object's member functions (as a hidden first parameter). The client obtains the address of the functions (the same for all instances of the object) through implicit linking of the DLL.在本例里,我们会使用一个全局的函数来让对象实例化自身的一个拷贝(使用new)且返回指针给客户端(随后,我们会看到COM使用了同样的方法)。客户端将使用这个指针,使用范围在客户端里调用了所有对象的成员函数里(就象隐藏的第一个参数)。客户端通过隐含的DLL链接获取了函数的地址(对象的所有例子都是一样的)
From a global perspective, there can be many things involved in creating an object. For example, you probably need to provide some password or security information before you are allowed to create an object. Thus, it can be convenient to have an additional object that handles instantiation of the actual object instead of having the client create the object directly. 从全局的视角来看,在创建对象时有很多事情要处理。比如,在你允许创建对象前,你需要提供一些密码或安全信息。这样,有一个额外的对象来处理实际对象的实例化而不是让客户端直接创建对象会更方便。
Our global instantiation function will return an object of another class that will allow us to produce instances of the CDB class. This class will be called CDBSrvFactory, and it will have only one member function: CDBSrvFactory::CreateDB(CDB** ppDB). This function creates the object and returns a pointer to it through the parameter ppDB (basically, *ppDB=new CDB): It is a "factory" that produces object instances of a given class. We will also call this object a class factory object.我们的全局实例化函数会返回一个其它类的对象,这个类会让我们产生CDB类的实例。这个类会调用CDBSrvFactory,且它只有一个成员函数:CDBSrvFactory::CreateDB(CDB**ppDB)。这个函数创建对象且通过参数ppDB返回指针(基本上是*ppDB=new CDB):它是一个“工厂”,它产生一个给定类的实例。我们同样称呼这个对象为类工厂对象。
All samples associated with the DB project can be built in both Unicode and ASCII. Some of the functions take parameters that are strings. These change their binary representation when being compiled for Unicode rather than for ASCII (see the Win32 SDK for details on Unicode). This works as long as both the client and the object are compiled within the same project—they will always match. If you compile them separately, as we will do here, you can take any of the following approaches: 所有和DB工程相关的例子能以UNICODE或ASCII方式来构造。有些函数以字符串为参数。当以UNICODE方式而不是ASCII方式构造时,会修改这些参数的二进制表示形式。只有客户端和对象工程在一个工作区里,是没问题的,因为他们会始终匹配。如果你单独进行编译,有下面的方法可以采用:
Provide two versions of your object. 提供2个对象的版本。
Standardize all function parameters to be ASCII, and convert inside the client and/or the server if they compile for Unicode.对函数参数标准化为ASCII,如果客户端且/或服务器端以UNICODE方式编译,则在客户端和/或服务器端内部进行转化。
Standardize all function parameters to be Unicode, and convert inside the client and/or the server if they compile for ASCII. 对函数参数标准化为UNICODE,如果客户端且/或服务器端以ASCII方式编译,则在客户端和/或服务器端内部进行转化。
Again, providing two versions can be very expensive: You double the size of your object if you want to be available for both kinds of clients. For a global component management system like COM, this is definitely not a good idea.再说一次,提供2个版本会非常麻烦:如果有2类客户端,对象的大小成倍增加。对于一个全局的组件管理系统,比如COM,这不是个好主意。
If you want to standardize on one of the two without loosing functionality, the choice is obviously Unicode, because it is a superset of ASCII.如果你想在2个里面选择一个进行标准化,且不想丧失功能性,明显地选择UNICODE比较好,因为它是ASCII的超集。
These samples (and COM) standardize on Unicode for any parameters to any interface that is to be seen by another object. The cost is minimal: You will need to convert your strings to and from ASCII to Unicode before calling, or when receiving a parameter. If you compile for Unicode, there is no performance penalty.这些例子(和COM)以UNICODE为标准化。代价小:你需要转换你的字符串,在调用前是从ASCII到UNICODE,或接收参数时。如果你以UNICODE方式编译,性能不受影响。
First we will work on the object: 我们对对象首先要这样操作:
Use AppWizard to create a new Dynamic-Link Library project called Object\DB.MAK (not an MFC AppWizard DLL, which creates an MFC Extension DLL). Add Object\DBSrv.cpp to the project and copy Client\stdafx.h to Object\stdafx.h. Create new targets for Unicode (Win32 Unicode Debug and Win32 Unicode Release; in Project Settings include a preprocessor symbol UNICODE). Through stdafx.h, set precompiled headers for all targets. 使用AppWizard来创建一个新的DLL工程,名称为Object\DB.MAK(不是MFC AppWizard DLL,这个会创建一个MFC扩展DLL)。添加Object\DBSrv.cpp到工程里且拷贝Client\stdafx.h到Object\stdafx.h。创建新的服务于UNICODE的目标(Win32 UNICODE Debug和 Win32 UNICODE Release;在工程设置里包含一个预编译宏UNICODE)。通过stdafx.h,设置所有文件预编译头。
Now we will change the interface that the client sees (Interface\DBSrv.h).现在我们修改客户端的接口,参考Interface\DBSrv.h。
Export Member Functions导出成员函数。
Export all the interface member functions of CDB in Interface\DBSrv.h. Use __declspec(dllexport) to instruct the compiler to export these functions. 导出CDB的所有接口成员函数,CDB在Interface\DBSrv.h。使用__declspec(dllexport)来指示编译器导出这些函数。
Memory Allocation内存分配
Add the ULONG CDB::Release() function that will delete the object when it is no longer needed (in Interface\DBSrv.h).添加ULONG CDB::Release()函数,当不需要对象时,让它删除对象(在Interface\DBSrv.h里)
Declare the class factory object CDBSrvFactory in Interface\DBSrv.h.在Interface\DBSrv.h里声明类工厂对象CDBSrvFactory
Declare the function that will return the factory object and export it (in Interface\DBSrv.h). 声明返回工厂对象的函数,且导出它(在Interface\DBSrv.h)
The resulting header file up to this point should look as follows (new parts in bold):形成结果的头文件里如下所示:
#define DEF_EXPORT _declspec(dllexport) // Step 2 class CDB { // Interfaces public: // Interface for data access HRESULT DEF_EXPORT Read(short nTable, short nRow, LPTSTR lpszData); HRESULT DEF_EXPORT Write(short nTable, short nRow, LPCTSTR lpszData); (. . .) // Step 3 ULONG DEF_EXPORT Release(); // Need to free an object from within the DLL.需要释放对象时,在DLL内部触发 // Implementation(. . .) }; class CDBSrvFactory { // Step 4 // Interface public: HRESULT DEF_EXPORT CreateDB(CDB** ppObject); ULONG DEF_EXPORT Release(); }; HRESULT DEF_EXPORT DllGetClassFactoryObject(CDBSrvFactory ** ppObject); //Step 5
Now we will change the implementation of the object (Object\DBSrv.cpp)现在我们需要修改对象的实现(Object\DBSrv.cpp)
ULONG CDB::Release() { delete this; // Cannot access data member after this!!!在本行语句执行过后不能再访问数据成员了。 return 0; }
#include "stdafx.h" #include "..\interface\bdsrv.h" // Create a new database object and return a pointer to it.创建一个新的数据库对象且返回指针 HRESULT CDBSrvFactory::CreateDB(CDB** ppObject) { *ppObject=new CDB; return NO_ERROR; } ULONG CDBSrvFactory::Release() { delete this; return 0; } HRESULT DEF_EXPORT DllGetClassFactoryObject(CDBSrvFactory ** ppObject) { *ppObject=new CDBSrvFactory; return NO_ERROR; }
Now we will make the necessary changes to the client:现在我们对客户端做必要修改:
CDBDoc::~CDBDoc() { if (m_pDB) { m_pDB->Release(); m_pDB=NULL; } }
// Create a database object through the exported function and class factory // object. CDBSrvFactory *pDBFactory=NULL; DllGetClassFactoryObject(&pDBFactory); pDBFactory->CreateDB(&m_pDB); pDBFactory->Release(); // We no longer need the factory.
Standardize on Unicode对UNICODE标准化
Modify the object again: Change all parameters in CDB that have the string LPxTSTR to LPxWSTR. The T version is the portable one, which compiles to ASCII or Unicode, depending on the preprocessor symbol UNICODE. The W version always uses wide characters (short instead of char).再次修改对象工程:把CDB里的参数修改一下,从LPxTSTR到LPxWSTR。T版本是可移植的,它可以以ASCII或UNICODE方式进行编译,具体要看有没有预编译宏UNICODE。W版本始终使用宽字节(用short代替char)。
Put in conditional statements (#ifdef UNICODE) to convert incoming parameters to ASCII, if you are not compiling for UNICODE (Write/Create: Use the Win32 API MultiByteToWideChar), and convert outgoing parameters to Unicode (Read/GetTableName: Use WideCharToMultiByte).加入条件语句(#ifdef UNICODE)来转换获取到的参数到ASCII,如果你不以UNICODE编译(Write/Create:使用Win32的API MultiByteToWideChar),且转换输出参数到Unicode形式(Read/GetTableName:使用WideCharToMultiByte)。
In the client, convert outgoing parameters to Unicode (#ifdef UNICODE)—Create: Use L"xxx" to declare a Unicode string; Write: use MultiByteToWideChar)—and incoming parameters to ASCII (Read: Use WideCharToMultiByte).在客户端里,修改输出参数到Unicode形式(#ifdef UNICODE)—Create:使用L”xxx”来声明Unicode字符串;Weite:使用MultiByteToWideChar(),输入参数到ASCII形式(Read:使用WideCharToMultiByte()。
void CDBDoc::OnDatabaseCreate() { m_pDB->Create(m_nTable, L"Testing"); m_nCount=0; // Set number of writes to 0 } void CDBDoc::OnDatabaseWrite() { m_nCount++; CString csText; csText.Format(_T("Test data #%d in table %d, row 0!"), m_nCount, (int) m_nTable); #ifdef UNICODE m_pDB->Write(m_nTable, 0, csText); #else WCHAR szuText[80]; // Special treatment for ASCII client MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, csText, -1, szuText, sizeof(szuText)); m_pDB->Write(m_nTable, 0, szuText); #endif } void CDBDoc::OnDatabaseRead() { #ifdef UNICODE m_pDB->Read(m_nTable, 0, m_csData.GetBuffer(80)); #else WCHAR szuData[80]; m_pDB->Read(m_nTable, 0, szuData); WideCharToMultiByte(CP_ACP, 0, szuData, -1, m_csData.GetBuffer(80), NULL, NULL); #endif m_csData.ReleaseBuffer(); UpdateAllViews(NULL); }
Compile the object before compiling the client, because the client needs DB.LIB in order to link.在编译客户端前编译对象,因为客户端需要DB.LIB来链接。
Be sure to either copy the appropriate DB.DLL to the client's directory or include the directory of the DLL in the path. Any client should be able to work with any server (except, perhaps, for debug versions, since name mangling can be different). 对于拷贝合适的DB.DLL到客户端目录下或包含DLL的目录到VC路径设置里,要进行认真核实。任何客户端应该能够和任何服务器匹配工作(除非,对调试版本不一样,因为命名求异后名称结果不一样了)
Don't miss the opportunity to run DumpBin.EXE /exports db.dll and see all the cryptic names that the compiler generates for the member function!不要忘记运行DumpBin.exe/导出db.dll且观察编译器为成员函数指定的隐藏的名称。