一、为什么需要调用C++
在日常开发中,C# 和 C++ 都是非常强大的编程语言。C# 有着简洁的语法和强大的框架支持,而 C++ 在性能和底层操作上有着独特的优势。有时候,我们可能需要在 C# 项目中调用 C++ 编写的高性能函数,比如处理复杂的图像算法、调用底层硬件接口等,还有就是可以更多的丰富咱们的代码库,因为很多性能好、实用、跨平台的库都是C++写的,可以极大扩展咱们程序的应用场景。
但对于初学者来说,第一大难题就是方法/函数的调用。接下来我们举例说明一下C#调用C++动态链接库函数的详细实现(以windows系统为例)。
二、准备工作
2.1 C++动态链接库(dll)
首先,你需要一个 C++ 编写的动态链接库(DLL)。你想要调用的函数需要在这个 DLL 项目中被正确暴露出来,即包含在头文件中。如:
c++
// XXX.h
extern "C" __declspec(dllexport) int Add(int a, int b) {
return a + b;}
extern "C" :在C++中,函数名是有修饰的(也就是所谓的"名称修饰"或"名称修饰符")。简单来说,C++编译器会根据函数的参数类型、返回值等信息,对函数名进行一些特殊的编码,这样在链接时就能区分不同的重载函数。但这种修饰方式对于C语言来说是不存在的,C语言的函数名就是简单的文本名。所以,当C++函数要被C语言或者其他语言调用时,就需要告诉编译器:"嘿,别给我做那些复杂的修饰,就用C语言的规则来处理函数名。"这就是extern "C"的作用。
__declspec(dllexport): 这个关键字是Windows平台特有的,它的作用是告诉编译器:"这个函数要导出到动态链接库(DLL)中。"简单来说,当你把一个C++函数编译成DLL文件时,如果你想让其他程序(比如C#程序)能够调用这个函数,就需要用__declspec(dllexport)来标记它。
2.2 C#调用C++函数的声明
了解了C++端的准备工作后,我们来看看C#端怎么调用这个函数。C#通过一个叫做"平台调用"(P/Invoke)的机制来调用C++的DLL函数。具体来说,你需要使用DllImport属性来声明一个外部函数。假设我们刚才的C++函数已经编译成了一个叫MyLibrary.dll的DLL文件,那么在C#中可以这样在你的class写一个声明:
csharp
internal class Program
{
// 声明引用动态链接库中函数
[DllImport("D:\\Mylib\\MyLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int add(int a, int b); // int 为内置值类型,普通情况下可与C++中int直接匹配
static void Main(string[] args)
{
// 实现调用动态链接库中函数并返回值
Console.WriteLine(add(9,5));
}
}
DllImport\] 是C#中用于声明外部函数的一个属性,它告诉C#运行时,某个函数是定义在外部的DLL文件中的,而不是在当前的C#代码中实现的。通过DllImport,我们可以调用C++编写的DLL函数,实现跨语言调用。 *Charset*: CharSet是一个枚举类型,用于指定字符串的字符集。在跨语言调用中,字符串的编码方式很重要,因为不同的语言可能使用不同的编码。CharSet有以下几个选项: * CharSet.Ansi: 表示使用ANSI编码。ANSI编码是一种基于系统默认代码页的编码方式,通常用于Windows系统中的本地化字符。在C++中,如果你的字符串是以char类型表示的(比如char\*),那么在C#中应该使用CharSet.Ansi。 * CharSet.Unicode: 表示使用Unicode编码。在C++中,如果你的字符串是以wchar_t类型表示的(比如wchar_t\*),那么在C#中应该使用CharSet.Unicode。 * CharSet.Auto: 是一个自动选择的选项。在Windows系统中,它会根据当前的运行环境自动选择CharSet.Ansi或CharSet.Unicode。通常情况下,CharSet.Auto会优先选择Unicode,但如果系统不支持Unicode,则会回退到ANSI。 *CallingConvention*: CallingConvention是一个枚举类型,用于指定函数的调用约定。调用约定决定了函数参数的传递方式、栈的清理方式等。不同的语言和编译器可能使用不同的调用约定,所以在跨语言调用时,必须确保调用约定一致。 * CallingConvention.Cdecl: CallingConvention.Cdecl是C语言的标准调用约定,通常用于C语言编写的函数。在这种调用约定下,函数的调用者(即调用函数的代码)负责清理栈。如果你在C++中使用了extern "C"来声明函数,那么在C#中应该使用CallingConvention.Cdecl。 * CallingConvention.StdCall: CallingConvention.StdCall是Windows平台的标准调用约定,通常用于Windows API函数。在这种调用约定下,函数的被调用者(即被调用的函数)负责清理栈。如果你的C++函数是按照__stdcall约定编写的,那么在C#中应该使用CallingConvention.StdCall。 * CallingConvention.ThisCall: CallingConvention.ThisCall是C++类成员函数的默认调用约定。在这种调用约定下,this指针(指向对象实例的指针)通过寄存器传递,其他参数通过栈传递。如果你需要调用C++类的成员函数,可以使用CallingConvention.ThisCall,但这种情况比较复杂,通常需要额外的处理。 * CallingConvention.FastCall: CallingConvention.FastCall是一种优化的调用约定,它通过寄存器传递前两个参数,其他参数通过栈传递。这种调用约定的性能较好,但兼容性较差。如果你的C++函数是按照__fastcall约定编写的,可以使用CallingConvention.FastCall,但需要注意兼容性问题。 *需要保证C++与C#在系统平台编译,如均为x64。否则会出现C#一直无法加载dll的报错* # 三、传参类型匹配 C#的内置值类型(built-in value types, 如 int, double, bool 等) 可以与C++类型几乎直接匹配。但是其他如数组、字符串等类型缺需要做一些处理。为了避免大家走弯路,除去C#内置值类型外,下面我们对一些其他的典型常用类型一一详细说明。 ## 3.1 C#数组传参至C++(以int\*为例) 在C++中,数组参数可以 int\* 作为入参类型,然后加上辅助的 int 作为数组长度值。 ```c++ extern "C" __declspec(dllexport) int AddInts(int* a, int size) { int sum = 0; for (int i = 0; i < size; i++) { sum += a[i]; } return sum; } ``` 在C#中使用 int\[\] 类型匹配即可,若需要考虑C++部分对数组内部数据进行修改,可增加\[in,out\]参数实现: ```csharp [DllImport(dllPath, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] private static extern int AddInts(int[] a, int size); ``` ## 3.2 C#字符串传参至C++(以char\*为例) C++接收字符串形式可以为 char*,不用特别指定长度。返回字符串亦为char* ```c++ extern "C" __declspec(dllexport) char* AddStr(char* a) { std::string str(a); str += +"end"; return _strdup(str.c_str()); } ``` 在C#中可直接用string类型匹配,或者用StringBuilder类均可: ```csharp [DllImport(dllPath, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]//ansi统一 private static extern IntPtr AddStr(string str); // 或 [DllImport(dllPath, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]//ansi统一 private static extern IntPtr AddStr(StringBuilder str); ``` 调用时,C++返回的char\*由C#中的IntPtr类型进行匹配,再通过Marshal方法将IntPtr类型转化为普通string ```csharp var str = Marshal.PtrToStringAnsi(ptr); ``` ## 3.3 C#字符串数组传参至C++(以char\*\*为例) C++接收字符串数组形式可以为 char\*\* ```c++ extern "C" __declspec(dllexport) char* AddStrArray(char** a,int count) { std::string str; for (int i = 0; i < count; i++) { std::string tmp(a[i]); str += tmp; } return _strdup(str.c_str()); } ``` 在C#中可直接用string\[\]类型匹配 ```csharp [DllImport(dllPath, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]//ansi统一 private static extern IntPtr AddStrArray(string[] str,int count); ``` ## 3.4 C#字符串属性结构体传参至C++ C++中考虑两种含字符串的结构体,一种使用定长char数组存储字符串,另一种使用char\*存储字符串: ```c++ struct infoData { char Name[256]; char ID[32]; char Value[64]; }; struct infoData2 { char* first; char* last; }; ``` 在C#中定义对应结构体与之对应的匹配,匹配后如C#内置值类型(如int)一样,直接传入类型实例即可。 ```csharp [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct infoData { [System.Runtime.InteropServices.MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Name; [System.Runtime.InteropServices.MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string ID; [System.Runtime.InteropServices.MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string Value; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct infoData2 { public string First; public string Last; } ``` ## 3.5 C#结构体数组传参至C++(以char\*\*为例) C++接收结构体数组类型可以为 infoData\* ```c++ extern "C" __declspec(dllexport) char* AddDataStruct(infoData* values, int size) { std::string total; for (int i = 0; i < size; i++) { std::string name(values[i].Name); std::string id(values[i].ID); std::string value(values[i].Value); total += name + "-" + id + "-" + value + "\r\n"; } return _strdup(total.c_str()); } ``` 在C#中可用前述定义好的infoData\[\]类型匹配 ```csharp [DllImport(dllPath, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]//ansi统一 private static extern IntPtr AddDataStruct(infoData[] data, int size); ``` ## 3.5 C#函数传参至C++(以double (\*fun)(double)为例) C++函数参数中可定义函数之战 double (\*fun)(double),表示传入一个 `double fun(double inputValue)` 类型的函数,不妨另其为: ```c++ extern "C" __declspec(dllexport) double AddFuncPtr(double value, double (*fun)(double)) { return fun(value); } ``` 在C#中可定义代理来描述函数,并作为参数类型匹配传递: ```csharp // 定义一个非托管的函数代理 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate double CallbackDelegate(double x); // 原函数指针类型由函数代理类型来匹配即可直接调用 [DllImport(dllPath, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]//ansi统一 private static extern double AddFuncPtr(double data, CallbackDelegate f); // 示例如下: CallbackDelegate data = (d) => d * d; var res = AddFuncPtr(5, data); Debug.Assert(res == 25); ``` ## 3.6 C#函数传参时间至C++(以struct为例) 在C++中假设存在时间函数: ```c++ VOID GetSystemTime(LPSYSTEMTIME lpSystemTime); internal static extern void GetSystemTime([In, Out] SystemTime st); typedef struct _SYSTEMTIME { WORD wYear; WORD wMonth; WORD wDayOfWeek; WORD wDay; WORD wHour; WORD wMinute; WORD wSecond; WORD wMilliseconds; } SYSTEMTIME, *PSYSTEMTIME; ``` 可对应在C#中定义一个class: ```csharp [DllImport("Kernel32.dll")] internal static extern void GetSystemTime([In, Out] SystemTime st); [StructLayout(LayoutKind.Sequential)] public class SystemTime { public ushort year; public ushort month; public ushort weekday; public ushort day; public ushort hour; public ushort minute; public ushort second; public ushort millisecond; } ``` 注意: 此时在C#调用GetSystemTime时,将传入一个class,并由C++代码写入这个class并返回得到时间的值: ```csharp public static void Main() { Console.WriteLine("C# SysTime Sample using Platform Invoke"); SystemTime st = new SystemTime(); NativeMethods.GetSystemTime(st); // 执行后,st的值被更新 Console.Write("The Date is: "); Console.Write($"{st.month} {st.day} {st.year}"); } ``` # 四、最后 在本文中,我们详细探讨了如何在C#项目中调用C++函数,从基本的函数调用,到复杂的参数类型匹配,再到一些高级的用法,如函数指针和结构体数组的传递。通过这些内容,希望能够帮助初学者更好地理解和掌握C#与C++之间的交互方式。 跨语言调用虽然在实现上可能会遇到一些挑战,但其带来的性能优化和功能扩展是显而易见的。无论是处理复杂的图像算法,还是调用底层硬件接口,C++的强大功能都能为C#项目提供有力支持。希望本文的介绍能够为你在实际开发中节省时间和精力,避免走弯路。 如果你在阅读过程中有任何疑问,或者在实际操作中遇到了困难,欢迎随时与我们交流。我们非常期待听到你的反馈和建议,以便我们能够进一步完善内容,帮助更多开发者。请继续关注我们的公众号"萤火初芒",我们将持续分享更多有趣且实用的技术内容,与大家一起学习交流,共同进步。