• 如何使用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
    ";
    	};
    };
    
    //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
    ");
    }
    
    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 FilesVideoLANVLCaxvlc.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++有虚函数表都不知道,时至今日,我已经失去了她。今后怕是一直会任由灵魂游荡,半梦半醒,即是人生。

  • 相关阅读:
    「B/S端开发」DevExtreme初级入门教程 Vue篇入门指南
    界面控件DevExpress WinForm MVVM命令讲解(三)
    界面控件DevExpress WPF入门级教程 调用表达式编辑器
    界面控件Telerik UI for WinForms入门教程 Telerik Upgrade API Analyzer
    响应式UI部件DevExtreme正式发布v21.2.4
    python枚举之Enum模块
    MessageBox的用法
    dotnet中的时间差
    在C#中API函数的调用
    关于C#中timer类 在C#里关于定时器类就有3个
  • 原文地址:https://www.cnblogs.com/Johness/p/csharp-do-memory-call.html
Copyright © 2020-2023  润新知