白驹过隙
学而不思则罔,思而不学则怠
posts - 4,  comments - 3,  trackbacks - 0

   还有一种方法可以显式导出类成员函数,就是采用虚函数表的方法。 COM 就是这样做的。当在类中声明一组虚函数的时候, 编译器会创建一个虚函数表,将各虚函数的地址按声明的顺序放入其中。当一个类对象被创建时,它的前四个字节是一个指针,指向这个虚函数表。所以,修改以上的代码:

//CTry.h

    virtual void print() ; // 将这个成员函数声明为虚函数

TRY_API CTry* createObject() ; // 声明一个全局函数,创建一个 Ctry 对象的实例

//CTry.cpp

CTry *createObject()

{

    return new CTry() ;

}

 

在测试工程中,显式导出该全局函数,并通过它调用类成员函数。

typedef CTry* (*Fn_FunType)() ;

Fn_FunType pFun=(Fn_FunType)::GetProcAddress(hInstance ,"?createObject@@YAPAVCTry@@XZ") ;

CTry * pTry = pFun() ;

pTry ->print() ;

delete pTry ;


   这个方法虽然同样能达到显式导出类成员函数的目的。但是上面的方法有个缺点,就是在DLLnew 出来的对象,却要放到客户端释放。因为客户端并不知道DLL中的实现方法,很可能会不知道释放这个对象,从而造成内存泄露。<<Effiective C++>>中明文规定禁止这样做。

 

  现在要找一个方法可以把回收内存的任务交 DLL 自己来处理。有没有这样一个方法呢?当然有,它就是引用计数。引用计数是一个简单的垃圾回收机制,它的原理很简单,组件内部维护着一个引用计数的数值,当用户从组件取得一个接口时,该数值 +1 ,当用户使用完这个接口,并释放该接口时,该数值 -1 ,当该数值为 0 时,组件将自己从内存中删除。下面是采用了引用计数的 DLL 实现方法。


#ifdef TRY_EXPORTS
#define  TRY_API __declspec(dllexport)
#else
#define  TRY_API __declspec(dllimport)
#endif
#include 
< iostream >
using   namespace  std;

//  此类是从 Try.dll 导出的
class   CTry  {
public :
    friend TRY_API CTry
*  createObject() ;

    
virtual    void  print() ;

    
virtual   void  addRef() ;

    
virtual   void  removeRef() ;
protected :
    
int  refCount ;

    
// 不允许通过构造函数初始化对象
    CTry( void ) ;
    
    
virtual   ~ CTry() ;


    
// 不允许相互赋值,这里面细节太多,懒得去分析:D
    CTry(CTry  & ths) ;

    CTry
*   operator =  ( const  CTry &  ths ) ;
    
}
;

extern  TRY_API  int  nTry;

TRY_API 
int  fnTry( void );
 
TRY_API CTry
*  createObject() ;

//  Try.cpp : 定义 DLL 应用程序的入口点。
//

#include 
" stdafx.h "
#include 
" Try.h "


#ifdef _MANAGED
#pragma managed(push, off)
#endif

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    
switch  (ul_reason_for_call)
    
{
    
case  DLL_PROCESS_ATTACH:
    
case  DLL_THREAD_ATTACH:
    
case  DLL_THREAD_DETACH:
    
case  DLL_PROCESS_DETACH:
        
break ;
    }

    
return  TRUE;
}


#ifdef _MANAGED
#pragma managed(pop)
#endif

//  这是导出变量的一个示例
TRY_API  int  nTry  =   0 ;

//  这是导出函数的一个示例。
TRY_API  int  fnTry( void )
{
    
return   42 ;
}


//  这是已导出类的构造函数。
//  有关类定义的信息,请参阅 Try.h

void  CTry::print()
{
    cout
<< " CTry " << endl;
}


 CTry 
* createObject()
 
{
     CTry 
* pTry  =   new  CTry() ;
     pTry
-> addRef() ;
     
return  pTry ;
 }


 
void  CTry::removeRef()
 
{
     
if  ( -- refCount  ==   0 )
     
{
         delete 
this  ; // 删除自己
     }

 }


 
void  CTry::addRef()
 
{
     
++ refCount ;
 }


 CTry::CTry()
 
{
     refCount 
=   0  ;
 }


 CTry::
~ CTry()
 
{

 }

 CTry::CTry(CTry 
& ths)
 
{

 }


 CTry 
* CTry:: operator   = ( const  CTry  & ths)
 
{
     
return  NULL;
 }

 

  上面的方法中,用户不能通过构造函数来实例化一个对象,必须通过 createObject 接口才能实例化对象,当用户使用完这个对象时必须调用 removeRef() 接口。代码如下:

#include  " stdafx.h "
#include 
< iostream >
#include 
" Try.h "
using   namespace  std;

typedef CTry
*  ( * Fn_FunType)() ;

int  main()
{

    HINSTANCE hInstance 
=   0 ;
    hInstance 
=  LoadLibrary( " Try.dll " ) ;
    
if  (hInstance)
    
{
        Fn_FunType pFun 
=  (Fn_FunType)::GetProcAddress(hInstance , " ?createObject@@YAPAVCTry@@XZ " ) ;

        
if  (pFun)
        
{
            
// (pTry->*pFun)() ;
            CTry *  pTry  =  pFun() ;
            pTry
-> print() ;
            pTry
-> removeRef();
        }

    }

    system(
" pause " ) ;
    
return   0  ;
}



  当然上面只是我为了方便测试而写的一个简单的例子,还不完善。引用计数还有很多细节和注意事项。具体请看《more effective c++》第29项。

 

关于名字修饰约定

 

   前面代码我都是通过编译器中的修饰名来调用相应的函数的,修饰名是编译器在编译函数定义或者原型时生成的字符串。比如 "?createObject@@YAPAVCTry@@XZ" 就是 CTry::createObject 的修饰名。

名字修饰约定随调用约定和编译种类 (C C++) 的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。

A C 编译时函数名修饰约定规则:

__stdcall 调用约定在输出函数名前加上一个下划线前缀,后面加上一个 "@" 符号和其参数的字节数,格式为 _functionname@number

__cdecl 调用约定仅在输出函数名前加上一个下划线前缀,格式为 _functionname

__fastcall 调用约定在输出函数名前加上一个 "@" 符号,后面也是一个 "@" 符号和其参数的字节数,格式为 @functionname@number

它们均不改变输出函数名中的字符大小写,这和 PASCAL 调用约定不同, PASCAL 约定输出的函数名无任何修饰且全部大写。

B C++ 编译时函数名修饰约定规则:

__stdcall 调用约定:

1 、以 "?" 标识函数名的开始,后跟函数名;

2 、函数名后面以 "@@YG" 标识参数表的开始,后跟参数表;

3 、参数表以代号表示:

X--void

D--char

E--unsigned char

F--short

H--int

I--unsigned int

J--long

K--unsigned long

M--float

N--double

_N--bool

....

PA-- 表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以 "0" 代替,一个 "0" 代表一次重复;

4 、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型 , 指针标识在其所指数据类型前;

5 、参数表后以 "@Z" 标识整个名字的结束,如果该函数无参数,则以 "Z" 标识结束。

其格式为 "?functionname@@YG*****@Z" "?functionname@@YG*XZ" ,例如

          int Test1 char *var1,unsigned long ----- ?Test1@@YGHPADK@Z

          void Test2 ()                        ----- ?Test2@@YGXXZ

 

__cdecl 调用约定:

规则同上面的 _stdcall 调用约定,只是参数表的开始标识由上面的 "@@YG" 变为 "@@YA"

__fastcall 调用约定:

规则同上面的 _stdcall 调用约定,只是参数表的开始标识由上面的 "@@YG" 变为 "@@YI" VC++ 对函数的省缺声明是 "__cedcl", 将只能被 C/C++ 调用 .

 

   通常我们希望我们的 DLL 中的导出函数名能够更易被识别(用户使用才会更方便),也就是说 DLL 应该编译出无修饰的 C 函数名,而不复杂的修饰名。所以当使用 C++ 文件来创建 DLL 时应该使用 extern “c” 来修饰导出函数和变量。实际上 VS 编译器已经为我们准备了一个宏 EXTERN_C 用来代替 extern “c” 。修改以上的代码如下:

EXTERN_C TRY_API int nTry;

EXTERN_C TRY_API int fnTry(void);

生成 DLL ,用 depens 工具查看,他们已经变成了如下模样:

 

这样在客户端就可以通过函数名去调用它们。但是 EXTERN_C 宏却没办法修饰类成员函数,

如果像全局函数那样修饰类成员函数,编译无法通过。用 .DEF 文件可以解决这个问题。

 

关于 .def 文件

模块定义 (.def) 文件是包含一个或多个描述 DLL 各种属性的 Module 语句的文本文件。

def 文件包含下列模块定义语句:

1. 文件中的第一个语句必须是 LIBRARY 语句。此语句将 .def 文件标识为属于 DLL LIBRARY 语句的后面是 DLL 的名称。链接器将此名称放到 DLL 的导入库中。

2.  EXPORTS 语句列出被导出函数的名字;将要输出的函数修饰名罗列在 EXPORTS 之下,这个名字必须与定义函数的名字完全一致,如此就得到一个没有任何修饰的函数名了。

3.  可以使用 DESCRIPTION 语句描述 DLL 的用途 ( 此句可选 )

4.   ";" 对一行进行注释 ( 可选 )

 

创建一 .DEF 文件,命名为 export.def ,用来修饰 CTry::print 函数,如下:

LIBRARY    "Try"

EXPORTS

print      =      ?print@CTry@@UAEXXZ  PRIVATE

 

DLL 工程属性 -> 链接器 -> 输入 -> 模块定义文件中将 export.def 添加进去。这样就可以直接通过函数名来显式调用类成员函数了。下面是 DLL depens 工具中显示的情况。

 

参考资料:

windows 核心编程》

programming windows

《微软 DLL 专题》

posted on 2009-03-08 17:01 隙中驹 阅读(485) 评论(1)  编辑 收藏 引用

FeedBack:
# re: DLL学习笔记 3
2009-03-12 09:32 | guest
图片的路径是“e:/1.bmp”,你本地应该可以显示,但是其他地方都看不到图了。
请修改图片链接。  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理



<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

常用链接

留言簿(2)

随笔档案(4)

文章分类(1)

文章档案(1)

友情链接

最新随笔

搜索

  •  

最新评论

  • 1. re: DLL学习笔记 3
  • 图片的路径是“e:/1.bmp”,你本地应该可以显示,但是其他地方都看不到图了。
    请修改图片链接。
  • --guest
  • 2. re: DLL学习笔记2
  • 图片的路径是“e:/1.bmp”,你本地应该可以显示,但是其他地方都看不到图了。
    请修改下图片链接。
  • --guest
  • 3. re: DLL学习笔记1
  • 好,我跟着你一起学习
  • --bk

阅读排行榜

评论排行榜