本文讲解如何使用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 extern IntPtr CreateObject(); [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate void voidMethod1(IntPtr thisPtr); static void Main(string[] args)
{
IntPtr dummyClass = CreateObject();
IntPtr vfptr = Marshal.ReadIntPtr(dummyClass);
IntPtr funcPtr = Marshal.ReadIntPtr(vfptr);
voidMethod1 voidMethod = (voidMethod1)Marshal.GetDelegateForFunctionPointer(funcPtr, typeof(voidMethod1));
voidMethod(dummyClass); Console.ReadKey();
}
}
}

(因为调用的是c++的函数,所以this指针是第一个参数,当然,不同调用约定时它入栈方式和顺序不一样)

下面有一种另外的写法

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices; namespace ConsoleApp2
{
class Program
{
[DllImport("Dll1", EntryPoint = "newObj")]
static extern IntPtr CreateObject(); //[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
//delegate void voidMethod1(IntPtr thisPtr); static void Main(string[] args)
{
IntPtr dummyClass = CreateObject();
IntPtr vfptr = Marshal.ReadIntPtr(dummyClass);
IntPtr funcPtr = Marshal.ReadIntPtr(vfptr);
/*voidMethod1 voidMethod = (voidMethod1)Marshal.GetDelegateForFunctionPointer(funcPtr, typeof(voidMethod1));
voidMethod(dummyClass);*/ AssemblyName MyAssemblyName = new AssemblyName();
MyAssemblyName.Name = "DummyAssembly";
AssemblyBuilder MyAssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(MyAssemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder MyModuleBuilder = MyAssemblyBuilder.DefineDynamicModule("DummyModule");
MethodBuilder MyMethodBuilder = MyModuleBuilder.DefineGlobalMethod("DummyFunc", MethodAttributes.Public | MethodAttributes.Static, typeof(void), new Type[] { typeof(int) });
ILGenerator IL = MyMethodBuilder.GetILGenerator(); IL.Emit(OpCodes.Ldarg, 0);
IL.Emit(OpCodes.Ldc_I4, funcPtr.ToInt32()); IL.EmitCalli(OpCodes.Calli, CallingConvention.ThisCall, typeof(void), new Type[] { typeof(int) });
IL.Emit(OpCodes.Ret); MyModuleBuilder.CreateGlobalFunctions(); MethodInfo MyMethodInfo = MyModuleBuilder.GetMethod("DummyFunc"); MyMethodInfo.Invoke(null, new object[] { dummyClass.ToInt32() }); Console.ReadKey();
}
}
}

上文中的方法虽然复杂了一点,但……就是没什么用。不用怀疑!

文章写到这里,可能有童鞋就要发问了。你说这么多,tmd到底有啥用?那接下来,我举一个栗子,activex组件的直接调用!

以前,我们调用activex组件需要做很多复杂的事情,首先需要使用命令行调用regsvr32将dll注册到系统,然后回到vs去引用com组件是吧

  仔细想想,需要吗?并不需要,因为两个原因:

  • COM组件规定DLL需要给出一个DllGetClassObject函数,它就可以为我们在DLL内部new一个所需对象
  • COM组件返回的对象其实就是一个只有虚函数的C++类对象(COM组件规定属性和事件用getter/setter方式实现)
  • COM组件其实不需要用户手动注册,执行regsvr32会操作注册表,而且32位/64位会混淆,其实regsvr32只是调用了DLL导出函数DllRegisterServer,而这个函数的实现一般只是把自己注册到注册表中,这一步可有可无(特别是对于我们已经知道某个activex的dll存在路径且它能提供的服务时,如果你非要注册,使用p/invoke调用该dll的DllRegisterServer函数是一样的效果)

因此,假如我们有一个activex控件(例如vlc),我们希望把它嵌入我们程序中,我们先看看常规的做法(本文没有讨论带窗体的vlc,因为窗体这块儿又复杂一些),直接贴图:

看起来很简单,但当我们需要打包给客户使用时就很麻烦,涉及到嵌入vlc的安装程序。而当我们会动态内存调用之后,就可以不注册而使用vlc的功能,我先贴出代码:

using System;
using System.Runtime.InteropServices; namespace ConsoleApp3
{
class Program
{
[DllImport("kernel32")]
static extern IntPtr LoadLibraryEx(string path, IntPtr hFile, int dwFlags);
[DllImport("kernel32")]
static extern IntPtr GetProcAddress(IntPtr dll, string func); delegate int DllGetClassObject(Guid clsid, Guid iid, ref IntPtr ppv); delegate int CreateInstance(IntPtr _thisPtr, IntPtr unkown, Guid iid, ref IntPtr ppv); delegate int getVersionInfo(IntPtr _thisPtr, [MarshalAs(UnmanagedType.BStr)] out string bstr); static void Main(string[] args)
{
IntPtr dll = LoadLibraryEx(@"D:\Program Files\VideoLAN\VLC\axvlc.dll", default, 8);
IntPtr func = GetProcAddress(dll, "DllGetClassObject");
DllGetClassObject dllGetClassObject = (DllGetClassObject)Marshal.GetDelegateForFunctionPointer(func, typeof(DllGetClassObject)); Guid vlc = new Guid("2d719729-5333-406c-bf12-8de787fd65e3");
Guid clsid = new Guid("9be31822-fdad-461b-ad51-be1d1c159921");
Guid iidClassFactory = new Guid("00000001-0000-0000-c000-000000000046");
IntPtr objClassFactory = default;
dllGetClassObject(clsid, iidClassFactory, ref objClassFactory);
CreateInstance createInstance = (CreateInstance)Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(Marshal.ReadIntPtr(objClassFactory) + IntPtr.Size * 3), typeof(CreateInstance));
IntPtr obj = default;
createInstance(objClassFactory, default, vlc, ref obj);
getVersionInfo getVersion = (getVersionInfo)Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(Marshal.ReadIntPtr(obj) + IntPtr.Size * 18), typeof(getVersionInfo));
string versionInfo;
getVersion(obj, out versionInfo); Console.ReadKey();
}
}
}

  上文中的代码有几处可能大家不容易懂,特别是指针偏移量的运算,这里面有比较复杂的地方,文章篇幅有限,下来咱们细细研究。

  从11年下半年开始学习编程到现在已经很久了,有时候会觉得没什么奔头。其实人生,无外乎两件事,爱情和青春,我希望大家能有抓住的,就不要放手。两年前,我为了要和一个女孩子多说几句话,给人家讲COM组件,其实我连c++有虚函数表都不知道,时至今日,我已经失去了她。今后怕是一直会任由灵魂游荡,半梦半醒,即是人生。

如何使用C#调用C++类虚函数(即动态内存调用)的更多相关文章

  1. MFC浅析(7) CWnd类虚函数的调用时机、缺省实现

    CWnd类虚函数的调用时机.缺省实现 FMD(http://www.fmdstudio.net) 1. Create 2. PreCreateWindow 3. PreSubclassWindow 4 ...

  2. c++纯虚函数在父类中调用的规避

    构造和析构函数不允许调用纯虚函数,可以先调用虚函数,里面再调用纯虚函数实现. class Base{public:    virtual void foo()=0;    Base() { call_ ...

  3. C++学习之路—多态性与虚函数(一)利用虚函数实现动态多态性

    (根据<C++程序设计>(谭浩强)整理,整理者:华科小涛,@http://www.cnblogs.com/hust-ghtao转载请注明) 多态性是面向对象程序设计的一个重要特征.顾名思义 ...

  4. CWnd类虚函数的调用时机、缺省实现

    MFC(VC6.0)的CWnd及其子类中,有如下三个函数: class CWnd : public CCmdTarget{    public:        virtual BOOL PreCrea ...

  5. c++虚函数、子类中调用父类方法

    全部 代码: 1 #include<stdio.h> 2 #include<string.h> 3 #include<iostream> 4 #include< ...

  6. C++类虚函数内存分布(这个 你必须懂)

    转自:http://www.cnblogs.com/jerry19880126/p/3616999.html C++类内存分布 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来 ...

  7. c++动态库封装及调用(3、windows下动态库调用)

    1.DLL的隐式调用 隐式链接采用静态加载的方式,比较简单,需要.h..lib..dll三件套.新建“控制台应用程序”或“空项目”.配置如下: 项目->属性->配置属性->VC++ ...

  8. 布尔类型、操作符别名、C++函数、动态内存分配(new\delete)、引用(day02)

    六 C++的布尔类型 bool类型是C++中基本类型,专门表示逻辑值:true/false bool在内存上占一个字节:1表示true,0表示false bool类型可以接收任意类型和表达式的结果,其 ...

  9. C++指针与数组、函数、动态内存分配

    C++指针 指针是用来存储地址的变量. 对于二维数组来说: a:代表的是首行地址: *a:代表的是首元素地址: **a:首元素: a+1:第二行地址: *a+2:首先*a是首元素地址,在首元素地址上+ ...

随机推荐

  1. 百万年薪python之路 -- 基本数据类型练习

    1.代码敲一遍,然后整理笔记 2.有变量name = "aleX leNb" 完成如下操作: 移除 name 变量对应的值两边的空格,并输出处理结果 name = "al ...

  2. 基于docker的mysql8的主从复制

    基于docker的mysql8的主从复制 创建mysql的docker镜像 构建docker镜像,其中数据卷配置内容在下面,结构目录如下 version: '3.7' services: db: # ...

  3. 手把手实现微信网页授权和微信支付,附源代码(VUE and thinkPHP)

    wechat github 手把手实现微信网页授权和微信支付,附源代码(VUE and thinkPHP) 概述 公众号开发是痛苦的,痛苦在好多问题开发者文档是没有提到的,是需要你猜的. 在开发过程中 ...

  4. KMP算法关键

    Knuth-Morris-Pratt Algorithm 当初写这个博客之后一年多,再次看发现当初并不是完全弄明白了.这里为了“避免重复制造轮子”,引用大神博客. http://blog.csdn.n ...

  5. Java ArrayList底层实现原理源码详细分析Jdk8

    简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用 ...

  6. python的多线程和多进程(一)

    在进入主题之前,我们先学习一下并发和并行的概念: --并发:在操作系统中,并发是指一个时间段中有几个程序都处于启动到运行完毕之间,且这几个程序都是在同一个处理机上运行.但任一时刻点上只有一个程序在处理 ...

  7. PHP限制上传文件大小

    在php.ini中修改如下变量,如要限制为100M upload_max_filesize = 100M post_max_size = 100M 重启Apache

  8. 小白学 Python(14):基础数据结构(集合)(上)

    人生苦短,我选Python 前文传送门 小白学 Python(1):开篇 小白学 Python(2):基础数据类型(上) 小白学 Python(3):基础数据类型(下) 小白学 Python(4):变 ...

  9. NOIP模拟 11

    差点迟到没赶上开题 开题后看了T1,好像一道原题,没分析复杂度直接敲了个NC线段树,敲了个暴力,敲了个对拍,就1h了.. 对拍还对出错了,发现标记下传有点问题,改了以后对拍通过,就把T1扔掉看T2 觉 ...

  10. python学习之【第四篇】:Python中的列表及其所具有的方法

    1.前言 列表是Python中最常用的数据类型之一,是以[ ]括起来,每个元素以逗号隔开,而且里面可以存放各种数据类型,而且列表是有序的,有索引值,可切片,方便取值. 2.创建列表 li = ['he ...