老生常谈之C++和C#之间互相调用
最近实在是太不给力了,算了,这些就不这里抱怨了,直接进入正题把。
关于C++和C#之间相互调用,不管是C++做父C#做子,还是C#为父C++为子(这里的父与子是指调用和被调用的关系,通常情况下 exe为父,调用子dll相关功能),网上随便一搜,遍地都是,而且有的把调用方式划分的也相当细。
那为什么我还要谈呢?其实有以下几点原因。
1. 因为工作需要,平台需要支持.NET模块,即VB.NET、C#、WPF、有可能还要支持Silverlight。这些要完全嵌入到我们的软件中。
2. 网上资料绝大部分都不是动态加载,即都在编译时指定要加载的DLL或者类型库信息。没有了运行时动态加载,软件当然逊色不少,这种静态绑定东西不是我所要的。
3. 在正常的父与子交互中,没有统一、完全的讲解和实例。
4. 父与子类型转换没有讲解。即C++和.NET 类型如何对应。
5. 在子需要调用父相关功能时,父需要将某些已存在的功能再次封装为一个dll,供子静态绑定调用。这是C++为父C#为子的一个典型情况。这显然太多余了。这里我们将采用非常规情况来实现同样功能。
6. 兴趣。
先提前说明下,此文章并不是强调实现两者相互调用的具体方法以及具体的技术,因为这部分网上已经有太多,这里只不过是本人对两者的一些理解,以及自己是如何实现的(当然部分内容少不了也参考了网上的)这一点提前注明下,省的说我标题党。当然既然是自己的理解,错误之处在所难免。能指出错误并加以修改是我希望看到的。
先说说程序大概组织逻辑。主程序有一套公用接口(其实就是纯虚类),在加载DLL时候将此接口传到DLL中,这样子模块在需要的时候就可以调用父的逻辑了,至于父调子,那就更简单了,主程序有一个纯虚类,子模块都继承此接口,并进行重写,主程序按照一定的顺序分别调用,这样父与子的逻辑交互就完成了,这些对都是C++程序来说,这当然没问题。现在问题是,要嵌入.NET的类库,由此引发一系列问题。。。。。
软件是以C++为父,DLL作为子的项目。
开发环境:WIN7 64BIT+VS2010+MFC+ATL+COM。
.NET环境下先以C#为例,其他的大部分一样下,不排除做一些简单或者复杂的修改。
下面正式开始把。
1. 动态加载 即父调子。
COM确实是好东西(他的褒与贬我们无作评论),她的语言无关性,不仅是我们实现动态加载的关键,更是实现加载其他.NET类库的核心。如VB.NET。有了她,才是这一切皆有可能。
由于.NET下的类库(DLL),和传统的WIN DLL 不太一样,毕竟托管的东西。她一些函数对外是不可见的,但对COM可见。因为我们就以COM方式定义一套接口,并把此接口当成普通C++的纯虚接口,来完成父到子的调用。
这一点不论在理论上、代码上都比较简单,而且网上大多也是这样子,所以我们直接上代码。
如下为COM接口定义。
[ComVisible(true),
Guid("B86D71F4-FE07-4B60-8246-F5AE283ED2A3"),
InterfaceType(ComInterfaceType.InterfaceIsDual)
]
public interface IHMI
{
[PreserveSig, DispId(1)]
void OnCreate(int a);
[PreserveSig, DispId(2)]
void SetRect(int left, int top, int width, int height);
//其他接类似
}
[ ComVisible(true),
ClassInterface(ClassInterfaceType.AutoDual),
ProgId("xxxxxxx.xxxxxxx") //ProgId 主程序根据此,运行时动态创建。
]
C#在使用时要继承并实现接口逻辑,如下类似。
public class CustomCOMClient : IHMI
{
public CustomCOMClient()
{
}
[DispId(1)]
public void OnCreate(int a)
{
//逻辑
}
[DispId(2)]
public void SetRect(int left, int top, int width, int height)
{
//逻辑
}
}
当然了,在建项目时,项目类型要为类库。至此类库部分已经完毕。接下来再看看主程序如何加载,以及如何调用把。
其中在动态创建时,ProgId是关键。这一部分对搞过COM,在加上ATL的人来说,可能太简单了,‘可能’这个词也许用的不太恰当,因为她不是‘可能’,她确实简单。不信看代码。
::CoInitialize(NULL);
const OLECHAR lpszProgID[]=OLESTR("xxxxxxx.xxxxxxx"); //ProgID
CComPtr<IDispatch> m_NetCustomer;
HRESULT hr = m_NetCustomer.CoCreateInstance(lpszProgID);
if(SUCCEEDED(hr))
{
const LPCOLESTR szMember=OLESTR("OnCreate");
VARIANT v;
v.vt = VT_I4; v.lVal = 1024;
hr = m_NetCustomer.Invoke1(szMember,&v);
if(SUCCEEDED(hr))
{
}
}
::CoUninitialize();
怎么样?没有撒谎把,几行代码就把创建、调用搞定了。
郁闷,从C++拷出来代码没有格式,还的手工加。。。。
2. 回调 即子调父。
主程序肯定按照自己的逻辑顺序依次调用子模块的接口,如先创建、子的相关逻辑、最后销毁。如果说在实际运用中,子模块完全不会在调用父的相关功能,那么此时框架已经完全实现了,我们之前做的工作就是。难道不是吗?,但应用程序往往也有父与子相互调用,下面就来看看,子如何回调父的功能把
前面也说过,子调父往往是这样,从父身上分离出部分代码,重新封装一个dll,由子静态绑定,这步最简单、最方便。不过这显然不是正道,让人觉得别扭。
同时维护两份相同功能代码? 也许你会说,主程序从此也可以调用DLL啊,那不就一致了,你要真这样说,我的回答是,“我只是在说明问题,不涉及到架构问题”
还有每个子模块都静态绑定这个DLL?
还有你在分离这个DLL时,如果依赖主程序太多,你怎么办?
还有你能保证分离后的稳定性吗?回带来其他的问题吗?
还有你仅仅是为了满足功能,才这样做的?
你觉得这样看着顺眼吗?
等等。反正我觉得是古怪之急。
接下来就要需找其他替代方案了。
先考虑下在C++中这一部分是如何实现的把。 父传给子一个虚接口(虚类),子在适当的时候调用。仅此而已。让我们把调用函数想的深入一点。直接看汇编代码把。
看代码之前,还要先简单说一下函数调用相关信息。在汇编层调用一个函数无非也就是JMP、CALL 之类的指令,若函数还有参数就是一些PUSH指令。好了知道这些就足够了,下面看看在VC中的伪代码。
__asm
{//类虚函数的汇编模拟调用,函数无参数、无返回值。
mov eax,xxxxx //存放函数地址
mov ecx,xxxxx //this指针
call eax //调用
}
这样调用就完成了,其实真正的调用也如此,只不过指令多几条而已。因为她要得到某些信息。
好了,如果说.NET支持内敛汇编,那我们完全可以自己模拟虚函数调用,不用在封装什么DLL,这所有的一切都可以搞定,但可惜的时,常规下内敛汇编是不支持的。不错,我说的是常规,那非常规呢?答案是肯定的。
关于内敛汇编网上也是一大片,底层思想是,在内存开辟一段空间,并放入相应指令,到时侯执行这一部分逻辑即可,这样就可以完成内敛汇编了。
其中网上有一个封装好的DLL(AsmClassLibrary.dll),提供接口编写汇编代码,用Reflector 查看了发现其最后执行采用远程线程注入方式,对于嵌入一两个模块的,可以这样做,但如果模块很多的话,毕竟注入涉及到安全的问题,这一点不太好,当然这也太另类了,我可不想应用程序到处以这种方式来执行。
所以我们采用Marshal.GetDelegateForFunctionPointer方式。
因为从底层上讲,是不分什么语言编写,只认机器指令的,因此只要我们模拟的合理、正确,这一点是没有问题的。
好了,现在我们目标很明确,用内敛方式在C#模拟虚函数的调用。
在给出代码之前,也先说下思路。
根据之前所讲以及常规知识,以下几点是必须的。
A 类对象指针,因为我们要将此值给ECX。
B 成员函数地址,当然了,我们要CALL嘛。
C 参数,这值是在C#中使用的。
这就是主要内容,实现他们方式有很多种,以下是我的方案。
因为接口会很多,因此我将this指针、函数地址都放到数组中,然后在传递给C#中,其实按道理说,只传递一个this指针就够了,其他部分应该在C#中实现,但操作指针C++中比较简便,所以这部分代码就在C++中做了。
得到this指针 太简单啦,根据虚表布局得到其地址也很简单。如下。
接口定义如下。
class CInterface
{
public:
virtual void test1( LPSTR p)
virtual void test2();
virtual void test3( int a);
};
得到this指针及成员函数地址。
CInterface *pInterface = new CInterface;
DWORD base_proc = (*((DWORD *)(pInterface))); //虚表指针
DWORD f1 = *(( DWORD *)base_proc); //第1个
DWORD f2 = *(( DWORD *)(base_proc + 4)); //第2个
DWORD f3 = *(( DWORD *)(base_proc + 8)); //第三个
到时将值赋值到SAFEARRAY 安全数组中,在传递到C#中。
看看在C#中时如何使用的把。当然这一部分的内敛、委托、开辟内存、托管到非托管转换时少不了的,老规矩,看代码把。
先定义委托和内敛。
//委托 参数分别为 this指针 成员函数地址 参数
delegate void testcall(int pthis, int pfun, int param);
byte[] codetest = {
// 0xCC,
0x8B, 0x5C, 0x24, 0x0C, //mov ebx,[esp+0Ch] 第三个参数 @@
0x8B, 0x44, 0x24, 0x08, //mov eax,[esp+08h] 函数地址
0x8B, 0x4C, 0x24, 0x04, //mov ecx,[esp+04h] this 指针
0x53, //push ebx 参数入栈 @@
0xFF, 0xD0, //call eax
0xC3 // ret
};
书写内敛汇编当然可以考研我们的功底啦,看看你知道不知道底层是如何实现的、如何入栈、出栈、传值、传指针、传引用、堆栈平衡等。还有一点,书写汇编虽容易,但是机器指令我们并不都知道,山人自有妙计,汇编代码贴到VC中,ALT+8看反汇编,在拷贝回来即可。
以上代码中,完成接口第三个函数调用,带有一个整形参数,并且传值。
注释掉@@部分完成接口第二个函数调用,无参数。
为了简便都写在一个里面,实际运用中,你可以按照不同格式分开。
接下来看看如何调用,,主要代码如下。
VirtualAlloc。。。。。。之前肯定得先开辟内存啊
Marshal.Copy(codetest, 0, handle, codetest.Length);
testcall Customer = Marshal.GetDelegateForFunctionPointer(handle, typeof(testcall)) as testcall;
int bb = 22;
Customer (fun[0], fun[4], bb);
不错,这就是子模块调用父相关逻辑的主要实现。
3. 后话
这就是相互调用的所有部分吗?这次答案是否定,实际上远远不至于此,我们此次实现的,只是最最基本的部分,尤其在参数上,我们用的最简单的类型 int,实际使用中,对于两者之间都存在的基本类型,还好说一点,当涉及到字符串、数组、结构体等这些类型时,真的会让你很麻烦的,尤其是字符串,两边还不一样。。。。。
其中对参数类型来说,我们用的是传值方式,直接将值push,对于引用或者指针要把其地址push,就可以实现了,当然还是针对最基本的类型来说的。
对于字符串参数的,我用全局函数实现了一个接口(具体的可以看代码),这样其中大部分转换操作,对我们就透明了,为何不自己搞?我有时间在补充进去把,这些就留给你们了,同样你们搞出来之后要告诉我啊,这里给大家一个建议,处理字符串时,在C#中最好使用char数组,但在书写内敛汇编时要注意,数组前面可有数组的大小,要偏移过去。
。。。。
。。。。
等把这一切都搞定之后,动态创建、嵌入VB的、C#的、WPF的以及她3D部分、硬件加速部分。。。。。。。。。
不错,如此看来,现在才刚刚开始。。。。。。。。。。。。。
希望能给大家起到一个抛砖引玉作用。
最后附一个类型转换的帖,供使用参考,类型转换我就不啰嗦了。
http://topic.csdn.net/u/20090225/15/a6bc50ad-9721-4749-b189-dc4a4bc045a1.html
再附效果图一张,图中部分为嵌入C#的类型。
为了嵌入到父窗口上,使用了API SetParent 并且我有建了一个项目,就是封装一些常用功能,具体看代码把。