从零开始:C#与C++函数传参——调包侠秘籍

一、为什么需要调用C++

在日常开发中,C# 和 C++ 都是非常强大的编程语言。C# 有着简洁的语法和强大的框架支持,而 C++ 在性能和底层操作上有着独特的优势。有时候,我们可能需要在 C# 项目中调用 C++ 编写的高性能函数,比如处理复杂的图像算法、调用底层硬件接口等,还有就是可以更多的丰富咱们的代码库,因为很多性能好、实用、跨平台的库都是C++写的,可以极大扩展咱们程序的应用场景。
但对于初学者来说,第一大难题就是方法/函数的调用。接下来我们举例说明一下C#调用C++动态链接库函数的详细实现(以windows系统为例)。

二、准备工作

2.1 C++动态链接库(dll)

首先,你需要一个 C++ 编写的动态链接库(DLL)。你想要调用的函数需要在这个 DLL 项目中被正确暴露出来,即包含在头文件中。如:

// 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写一个声明:

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 作为数组长度值。

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]参数实现:

 [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

extern "C" __declspec(dllexport) char* AddStr(char* a)
{
    std::string str(a);
    str += +"end";
    return _strdup(str.c_str());
}

在C#中可直接用string类型匹配,或者用StringBuilder类均可:

  [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

 var str = Marshal.PtrToStringAnsi(ptr); 

3.3 C#字符串数组传参至C++(以char**为例)

C++接收字符串数组形式可以为 char**

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[]类型匹配

  [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*存储字符串:

struct infoData {
    char Name[256];
    char ID[32];
    char Value[64];
};

struct infoData2 {
    char* first;
    char* last;
};

在C#中定义对应结构体与之对应的匹配,匹配后如C#内置值类型(如int)一样,直接传入类型实例即可。

 [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*

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[]类型匹配

  [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) 类型的函数,不妨另其为:

extern "C" __declspec(dllexport) double AddFuncPtr(double value, double (*fun)(double)) {
    return fun(value);
}

在C#中可定义代理来描述函数,并作为参数类型匹配传递:

// 定义一个非托管的函数代理
[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++中假设存在时间函数:

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:


  [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并返回得到时间的值:

    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#项目提供有力支持。希望本文的介绍能够为你在实际开发中节省时间和精力,避免走弯路。
如果你在阅读过程中有任何疑问,或者在实际操作中遇到了困难,欢迎随时与我们交流。我们非常期待听到你的反馈和建议,以便我们能够进一步完善内容,帮助更多开发者。请继续关注我们的公众号“萤火初芒”,我们将持续分享更多有趣且实用的技术内容,与大家一起学习交流,共同进步。

QR

posted on 2025-11-07 15:03  LdotJdot  阅读(130)  评论(0)    收藏  举报