/**/
/* ********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个不同对象调用各自成员函数的结果,
由此证明我们的思路完全正确