如何使用C#调用C++类虚函数(即动态内存调用)
本文讲解如何使用C#调用只有.h头文件的c++类的虚函数(非实例函数,因为非虚函数不存在于虚函数表,无法通过类对象偏移计算地址,除非用export导出,而gcc默认是全部导出实例函数,这也是为什么msvc需要.lib,如果你不清楚但希望了解,可以选择找我摆龙门阵),并以COM组件的c#直接调用(不需要引用生成introp.dll)举例。
我们都知道,C#支持调用非托管函数,使用P/Inovke即可方便实现,例如下面的代码
[DllImport("msvcrt", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl)] public static extern void memcpy(IntPtr dest, IntPtr src, int count);
不过使用DllImport只能调用某个DLL中标记为导出的函数,我们可以使用一些工具查看函数导出,如下图
一般会导出的函数,都是c语言格式的。
C++类因为有多态,所以内存中维护了一个虚函数表,如果我们知道了某个C++类的内存地址,也有它的头文件,那么我们就能自己算出想要调用的某个函数的内存地址从而直接call,下面是一个简单示例
#include <iostream> class A_A_A { public: virtual void hello() { std::cout << "hello from A\n"; }; }; //typedef void (*HelloMethod)(void*); int main() { A_A_A* a = new A_A_A(); a->hello(); //HelloMethod helloMthd = *(HelloMethod *)*(void**)a; //helloMthd(a); (*(void(**)(void*))*(void**)a)(a); int c; std::cin >> c; }
(上文中将第23行注释掉,然后将其他注释行打开也是一样的效果,可能更便于阅读)
从代码中大家很容易看出,c++的类的内存结构是一个虚函数表二级指针(数组,多重继承时可能有多个),每个虚函数表又是一个函数二级指针(数组,多少个虚函数就有多少个指针)。上文中我们假使只知道a是一个类对象,它的第一个虚函数是void (*) (void)类型的,那么我们可以直接call它的函数。
接下来开始骚操作,我们尝试用c#来调用一个c++的虚函数,首先写一个c++的dll,并且我们提供一个c格式的导出函数用于提供一个new出的对象(毕竟c++的new操作符很复杂,而且实际中我们经常是可以拿到这个new出来的对象,后面的com组件调用部分我会详细说明),像下面这样
dll.h
class DummyClass { private: virtual void sayHello(); };
dll.cpp
#include "dll.h" #include <stdio.h> void DummyClass::sayHello() { printf("Hello World\n"); } extern "C" __declspec(dllexport) DummyClass* __stdcall newObj() { return new DummyClass(); }
我们编译出的dll长这样
让我们编写使用C#来调用sayHello
using System; using System.Runtime.InteropServices; namespace ConsoleApp2 { class Program { [DllImport("Dll1", EntryPoint = "newObj")] static