CPP代码(最详细的代码已在Visual Studio 中实现!)
#include
#include //返回字符串的时候,要加上该头文件; extern "C" __declspec(dllexport) void HelloWorld(); extern "C" __declspec(dllexport) int Add(int, int); extern "C" __declspec(dllexport) bool IsLengthGreaterThan5(const char*); extern "C" __declspec(dllexport) BSTR GetName(); //注意 //#include //BSTR GetName() //{ // return SysAllocString(L"wang"); //} void HelloWord() { std::cout<< "WK"; } int Add(int a, int b) { return a + b; } bool IsLengthGreaterThan5(const char* value) { return strlen(value) > 5; } BSTR GetName() { return SysAllocString(L"wang"); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
.cs文件代码
using System; using System.Runtime.InteropServices; namespace ConsoleApplication { class Program { [DllImport("NativeLibrary.dll")] public static extern void HelloWord(); [DllImport("NativeLibrary.dll", EntryPoint = "Add")] //EntryPoint中的名字必须与dll中的名字一致! public static extern int AddNumbers(int num1, int num2); [DllImport("NativeLibrary.dll", EntryPoint = "IsLengthGreaterThan5")] public static extern bool IsLengthGreaterThan5(string value); [DllImport("NativeLibrary.dll")] [return: MarshalAs(UnmanagedType.BStr)] public static extern string GetName(); static void main(string[] args) { Console.WriteLine(AddNumbers(5, 3)); Consoe.WriteLine(GetName()); //此行代码并不会起到任何的作用;因为CPP中const char* GetName()函数在完成操作后,它将被销毁。所以需要在内存中分配此字符串,以便在清理时不会对其进行清理;它超出了此功能的范围。为此,需要在CPP中使用SysAllocString Console.WriteLine(IsLengthGreaterThan5("test")); Console.ReadLine(); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
注意:C++与C#的一些类型是不匹配的,具体详情见Marshaling Data with Platform Invoke - .NET Framework | Microsoft Docs
(本课程会着重讲解常用数据格式的数据交换)
为什么会有库?
- 减少可执行文件;
- 实现资源共享;
- 便于维护和升级;
- 比较安全;
内存对齐规则
- 结构体的数据成员,第一个成员的偏移量为0,后面的每个数据成员存储的起始位置要从自己大小的整数倍开始;
- 子结构体中的第一个成员偏移量应当是子结构体中最大成员的整数倍;
- 结构体 总大小必须是其内部最大成员的整数倍;
查看结构体的偏移量
- 使用神器0(0可以转换为任意类型的NULL指针)
- #define FIELDOFFSET(TYPE, MEMBER)(int)(&(((TYPE*)0)->MEMBER))
#include
#define FIELDOFFSET(TYPE, MEMBER) (int)(&(((TYPE*)0)->MEMBER)) //若不知道当前的对齐方式,则让结构体入栈;那么如何设置对齐方式呢? #pragma pack(push) //入栈 #pragma pack(1) //设置为1字节对齐 struct Info { char username[10]; //24-34 double userdata; //40-48 }; #pragma pack(pop) //出栈 struct Frame { unsigned char id; //0-1 int width; //4-8 long long height; //8-16 unsigned char* data; //16-20 Info info; //24- }; int main() { int len = sizeof(Frame); int offset_width = FIELDOFFSET(Frame, width); int offset_height = FIELDOFFSET(Frame, height); int offset_data = FIELDOFFSET(Frame, data); int offset_info = FIELDOFFSET(Frame, info); return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
(调用者与被调用者之间的一种约定的关系:可在项目属性->配置属性->C/C+±>高级 查看!)
- _cdecl称之为C调用约定,参数按照从右至左的方式入栈,函数本身不清理栈,此工作由调用者负责,所以允许可变参数函数存在;(若程序中有可变参数,则使用该调用约定!)
- _stdcall称之为标准调用约定,参数按照从右至左的方式入栈,函数本身清理栈;
常用数据类型
C# C/C++ ubyte byte unsigned char short short int32 int32_t long int64_t float float double double IntPtr,[] void*
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
设置结构体对齐方式
[StructLayout(LayoutKind.Sequential, Pack = 1)]
- 1
内容见代码—— CallNativeDllCSharp项目!
- dllName:动态链接库名称;
- CallingConvention:调用约定;
- CharSet:设置字符串编码格式;
- EntryPoint:函数入口名称,默认使用方法本身的名称;
- ExactSpelling:指示EntryPoint是否必须与指示的入口点的拼写完全匹配,默认为true,若为false,会根据CharSet查找对用入口函数的A版本或者W版本。找不到再去找入口函数。
- SetLastError:指示方法是否保留Win32“上一错误"。用默认值false,Win32错误是否设置到调用者线程中,C#通过Marshal.GetLastWin32Error()获取错误码。
- 总体来说,有两种方法:
- 利用C++/CLI作为代理中间层;
- 实现简单,并且可以实现C#调用C++写的类;但问题是MONO架构不支持C++/CIL功能,所以无法实现脱离.NET Framework跨平台运行。
- 利用PInvoke实现直接调用;
- 简单的实现并不麻烦,只需要添加DllImportAttribute特性即可导入C++的函数,但是问题是PInvoke不能简单的实现C++类的调用。
- 注:PInvoke从功能上来说,只支持函数调用,在被导出的函数前面一定要添加 extern “C” 来指明导出函数的时候使用C语言方式编译和链接的,这样保证函数定义的名字相同,否则如果默认按C++方式导出,那个函数名字就会变的乱七八糟,程序就无法找到程序的入口。
- 字符串只支持C中的 char* 和 w_char* 不支持STL的string。
- 各种指针句柄都可以使用C#的 IntPtr 和 UIntPtr 来对应;
- 向非托管的C传递字符串如果是通过变量 可以用Marshal.StringToHGlobalAuto(csstr) 得到复制到非托管内存的字符串IntPtr句柄,然后可以IntPtr.ToInt32()或IntPtr.ToInt64()方法转成int传递给PInvoke方式声明的C函数调用。这个字符串的非托管副本用完之后记得在C#中使用Marshal.FreeHGlobal(cstrAddr)来释放掉。
C++和C#混合编程,C#调用C++生成的dll以及字符串传递_Ripo_za的博客-CSDN博客