将标准C++动态链接库封装到.NET程序集dll全攻略

/* ********cppDll.h********* */
#ifdef CPPDLL_EXPORTS
#define  CPPDLL_API __declspec(dllexport)
#else
#define  CPPDLL_API __declspec(dllimport)
#endif


class  CPPDLL_API CcppDll  {
public :
 CcppDll(
void );
 
//  TODO: 在此添加您的方法。
     void  ChangeValue( int  i);
    
int  GetValue();
private :
    
int  var;
    CcppDll 
*  CreateCcppDll();
}
;

extern  CPPDLL_API  int  ncppDll;

CPPDLL_API 
int  fncppDll( void );
首先,在标准C++中使用标准C++dll的通常做法是预编译时导入lib文件,于是有人希望能够开一个managed C++ dll,用这种方法导入标准C++ dll,然后再在 Winform等其他.NET程序中调用,实际上这是不可能的,因为.NET程序在调用这个库时根本找不到入口.下面我一2个例子详细说明,其中标准C++库(cppDll)一个,C#dll(CSDll)一个,以及一个测试的C#WinForm(CSFormTest)一个
   实际上 标准C++生成的dll文件本身是可以查找导出函数入口的,这一点比较麻烦,如果是C函数,还强一点,因为可以在前面声明extern "C" 这样 Dll里面的函数名就是入口,只要头文件就可以知道入口了.例如这样

  extern   " C "  __declspec(dllexport)  int  fncppDll( void )
{
    
return 42;
}

 

但是在类中的成员函数就不能声明为extern  " C " 了,因此我们必须查找他的入口,举个简单例子 


/***********cppDll.cpp********/


#include 
" stdafx.h "
#include 
" cppDll.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

/***************cppDll.cpp*******/

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

CPPDLL_API 
struct  VS  * vs;
//  这是导出函数的一个示例。
CPPDLL_API  int  fncppDll( void )
{
 
return 42;
}

//  这是已导出类的构造函数。
//  有关类定义的信息,请参阅 cppDll.h
CcppDll::CcppDll()
{
   
return;
}


void  CcppDll::ChangeValue( int  i)
{
    var
=i;
}

int  CcppDll::GetValue()
{
    
return var;
}

CcppDll 
*  CcppDll::CreateCcppDll()
{
     CcppDll 
* cs=new CcppDll();
     
return cs;
}

/***************************/

 

前头的外部函数和变量您不用看了,相信您只要会用DllImport的基本语法就明白怎么导,这里主要看看3个CcppDll的成员函数怎么封装.注意这个CcppDll::CreateCcppDll()这里相当于显式调用构造函数,这个函数是我们封装到.NET的关键,必须有,至于为什么后文再说.我写了几个读写变量的函数以便您新建几个对象测试.
    然后简要介绍一下DllImport在C#中的语法

2007年1月10日

DllImport的第一个参数是库完整文件名,第二个就是函数入口了,正如刚才所说C++类成员函数不能被声明为extern "C" 因此入口不是dll中定义的函数名,而是如下

6    5 000111D6 ?ChangeValue@CcppDll@@QAEXH@Z = @ILT+465(?ChangeValue@CcppDll@@QAEXH@Z)
而导入以后的函数定义部分的函数名可以任意指定.

从问号开始等号以前是刚才哪个CCppDll::ChangeValue的真实入口,其中@后是类明,@@后是编译器产生的.
查看这些入口可以用VS的dumpbin命令,您可以选择标准输出或者输出到文件,输出到文件的好处我最后在讨论.

注意:使用

dumpbin -exports 文件完整路径

不要用/exports否则看不到入口

 

 

 

 

 

 

 

 





下面是C#的dll代码

 

using  System;
using  System.Collections.Generic;
using  System.Text;
using  System.Runtime.InteropServices;
namespace  CSDLL
{
    
public class Class1
    
{
        [DllImport(
@"D:/dotNetApps/Client Applications/cppDll/debug/cppDll.dll", EntryPoint = "?ChangeValue@CcppDll@@QAEXH@Z", CallingConvention = CallingConvention.ThisCall)]
        
private  extern static void ChangeValue(IntPtr pThis,int i);
        [DllImport(
@"D:/dotNetApps/Client Applications/cppDll/debug/cppDll.dll", EntryPoint = "?GetValue@CcppDll@@QAEHXZ", CallingConvention = CallingConvention.ThisCall)]
        
private  extern static int GetValue(IntPtr pThis);
        [DllImport(
@"D:/dotNetApps/Client Applications/cppDll/debug/cppDll.dll", EntryPoint = "?CreateCcppDll@CcppDll@@AAEPAV1@XZ", CallingConvention = CallingConvention.Winapi)]
        
private extern static IntPtr CreateCppDll();

        
public Class1()
        
{
            p 
= CreateCppDll();
        }

        
public int Value
        
{
            
get return GetValue(p); }
            
set { ChangeValue(p, (int)value); }
        }

        
public IntPtr p;
    }

}




其中using System.Runtime.InteropServices;使您能够使用DllImport
下面我们来分析一下这段代码.在导入函数部分,三个外部函数均声明了库的绝对路径,和入口全称,还有就是调用方式.您可能认为就算函数可以导入,全部声明为static extern 还如何以面向对象方式调用? 难道只能当作静态函数,全局只用一个对象,显然不能实现原有的C++库的功能.解决这个问题关键就在调用方式.
CallingConvention枚举有4个值,我们主要使用2种,一种是默认的CallingConvention.StdCall(windows下与Winapi等效),相当于静态调用,另一种则是CallingConvention.ThisCall其中  CreateCppDll相当于构造函数,是为对象开辟内存空间的,因此在调用它以前对象还没有被分配,所以它必须使用这种调用方式,

 

[DllImport( @" D:/dotNetApps/Client Applications/cppDll/debug/cppDll.dll " , EntryPoint  =   " ?CreateCcppDll@CcppDll@@AAEPAV1@XZ " , CallingConvention  =  CallingConvention.Winapi)]
        
private   extern   static  IntPtr CreateCppDll();

 

在封装这个标准C++类的C#类中,构造函数就应调用它以得到一个指向原C++对象的指针

 

public  Class1()
{
      p 
= CreateCppDll();
}

public  IntPtr p;

 

而Class1的成员变量p就是用来存储这个对象指针的,注意由于安全代码C#对象中禁止使用指针,这里无论C++是何种类型 C#一律使用平台指针IntPtr,定义外部函数也是如此.
然后其他所有的成员非静态函数则使用 CallingConvention.ThisCall进行导入,如下
 [DllImport(@"D:/dotNetApps/Client Applications/cppDll/debug/cppDll.dll", EntryPoint = "?ChangeValue@CcppDll@@QAEXH@Z", CallingConvention = CallingConvention.ThisCall)]
        private  extern static void ChangeValue(IntPtr pThis,int i);
        [DllImport(@"D:/dotNetApps/Client Applications/cppDll/debug/cppDll.dll", EntryPoint = "?GetValue@CcppDll@@QAEHXZ", CallingConvention = CallingConvention.ThisCall)]
        private  extern static int GetValue(IntPtr pThis);
注意到刚才我们定义标准C++库时ChangeValue有一个参数 GetValue没有参数,而这次声明的时候ChangeValue有两个参数,GetValue有一个参数.这个多处来的参数就是真实的对象指针,因此,在Class1中封装这些函数时,每次调用都把p传给这些函数(注意,前提是p已经分配)如下(这里为了符合C#风格,干脆封到属性里面去了,反正性质是一样的):
       

  public   int  Value
        
{
            
get return GetValue(p); }
            
set { ChangeValue(p, (int)value); }
        }

 

这样就完全做到了形式上用C函数调用,实际上是对象在调用自己的成员函数,由于然后经过这一道封装后,在调用这个库的应用程序中所看到的仍然是面向对象的类方式.这样就完全达到了我们预期的目的.
下面是一个C#WinForm调用这个dll的示例:
//....................默认包含的库我省了

using  CSDLL;

namespace  CSFormTest
{
    
public partial class Form1 : Form
    
{
        
public Form1()
        
{
            InitializeComponent();
        }


        
private void button1_Click(object sender, EventArgs e)
        
{
            Class1 c1 
= new Class1();
            Class1 c2 
= new Class1();
            c1.Value 
= 3;
            c2.Value 
= 4;
            MessageBox.Show(
"c1=" + c1.Value + ",c2=" + c2.Value);
        }

    }

}

 

运行结果:对象c1,c2的属性Value都得到改变,证明他们是2个不同对象调用各自成员函数的结果,
                     由此证明我们的思路完全正确

 
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值