c# Task.Factory.StartNew 传参数_C#/C++混合编程一二事

本文介绍了C#/C++混合编程中如何使用`Task.Factory.StartNew`传递参数,涉及C++导出函数声明、实现、C#导入函数声明、参数传递、返回值传递及复杂数据类型(结构体)的处理。文章详细阐述了预编译指令、调用约定和数据类型转换等关键点,并提供了示例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C#/C++混编的情形经常会碰到,下面就来讲一讲一些需要注意的点。废话不多说,Let's get started. (时间有限,暂时没有写完,后续会持续更细。如果有写的不严谨甚至错误的地方欢迎大家指正)

一、C++导出函数的声明

在导出函数的头文件,经常见到如下的模板

#pragma once

下面就来给出解释。

  • #ifdef __cplusplus这条预编译指令。如果项目是C++的项目,则用extern "C" {} 将所有的代码包起来。这样的好处是导出的函数名仍和代码中定义的一致,否则函数名前后会加上一些看起来很奇怪的符号。这是因为C++的函数有重载机制,同一个函数名,可以有多种参数形式,不加extern "C"包起来的话,编译器就会在函数名前后加上一些符号,来具体到这个函数的具体的某一种形式。
  • #ifdef YOURPROJECT_EXPORTS这条预编译指令。这段话的意思就是如果定义了YOURPROJECT_EXPORTS这个宏,则把YOURPROJECT_API这个宏定义成__declspec(dllexport),否则把YOURPROJECT_API定义成__declspec(dllimport)。这个设计是为了开发者和调用者的便利性设计的。因为这个头文件,后来也会拷贝给调用者用,开发者的导出函数的这个项目中定义一个宏YOURPROJECT_EXPORTS,而调用者那边没有这个宏。所以YOURPROJECT_API对于开发者就是__declspec(dllexport),对于调用者就是__declspec(dllimport)。__declspec(dllexport)告诉编译器这是一个需要导出的函数,__declspec(dllimport)告诉编译器这是一个从外部的库文件中导入的函数。
  • #define CALLINGCONVENTION _cdecl 这个宏,就是调用约定,可以根据需要把它定义成__cdecl或者__stdcall或者其他。调用约定关系到函数参数入栈和出栈的清理方式。常见的有__cdecl、_stdcall、_fastcall. 最常用的就是_cdecl、_stdcall. 如果调用方用C#,则推荐用_cdecl. 因为_stdcall也会像没加extern "C" 那样导致导出的函数名和头文件中定义的不一致。(__cdecl许多人记不住,其实它就是C Declaration的缩写)

二、C++导出函数的实现

声明写好了,实现就简单了。参考如下,不解释。

#include 

三、C#导入函数的声明

C#调用之前,先写一个类,里面用来声明从C++导入的函数

class 

说明:

  • 记得添加命名空间using System.Runtime.InteropServices;
  • 函数声明前写上DllImport属性,包含导入的库文件名,调用约定,字符集,入口点。
  • 库文件名可以写相对路径,也可以写绝对路径。
  • 调用约定和C++保持一致,推荐用Cdecl方式,如果强行用Stdcall会直接报错。
  • 字符集可写可不写,一般不会用上。但是如果C++参数中有char*或者wchart*时,就需要根据实际情况填写字符集这个属性。如果参数为wchar_t*时,指定字符集为Unicode, C#的参数使用string即可(当然也可以使用StringBuilder,具体不讨论)。当然也可以不指定字符集,在参数前加上如下声明[MarshalAs(UnmanagedType.LPWStr)]也可。
  • 入口点,就是dll中导出的函数的符号,当且仅当调用约定为__cdecl、并且加了extern "C"包装的情况下,入口点才和函数名相同。

四、参数的传递

  • 基本类型,比如int、float、double这些直接传就好,不再赘述。
  • 字符串类型。举个例子:
//C++:
  • 一般指针,比如void*、自定义类的指针YourClass*、句柄如HANDLE、HWND等。举个例子
//C++:
typedef void* IDicomSCU;
YOURPROJECT_API void CALLING_CONVENTION IDicomSCU_Close(IDicomSCU handle);
   
//C#:    
[DllImport(strDllName, CallingConvention = ccDef, CharSet = csDef, EntryPoint = "IDicomSCU_Close")] 
public static extern void IDicomSCU_Close(IntPtr handle);
//说明:既然这么多种东西都可以用C#的IntPtr来传递,那在编码时一定需要注意IntPtr具体指向的一个什么东西,切不可混用。
  • 函数指针。这种情形,C++需要一个函数指针,C#传委托即可。举个例子
//C++
typedef bool(*IDriverCommandEvent)(int command, wchar_t *val);
YOURPROJECT_API bool CALLING_CONVENTION IDriver_Open(HWND hWnd, IDriverCommandEvent event);

//C#
//先声明一个委托,注意委托的参数中有string类型,我就在前方加上了[MarshalAs(UnmanagedType.LPWStr)]以和C++的wchar_t*对应
//并且声明这个委托的调用约定是Cdecl还是Stdcall,这个很重要,必须和C++端的调用约定保持一致。
[UnmanagedFunctionPointerAttribute(CallingConvention.Cdecl)]
public delegate bool DriverCommandEventHandler(int command, [MarshalAs(UnmanagedType.LPWStr)] string val);
//导出函数声明     
[DllImport(strDllName, CallingConvention = ccDef, CharSet = csDef, EntryPoint = "IDriver_Open")]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool IDriver_Open(HWND hWnd, DriverCommandEventHandler driverCommandEvent);
//说明:注意C#在传递形参的时候,形参不要是一个临时对象,因为临时对象的引用计数为0后会被GC回收,
//导致C++调用这个函数指针的时候找不到而报错
  • 结构体指针。
  • 参照C++端定义的结构体也在C#端定义同样的结构体,具体参见第六节。然后C#的形参用ref YourStruct类型,C++端用YourStruct*接收。

五、返回值的传递

基本类型基本可以直接声明,这里就把特殊的返回值拿出来提一下

  • bool类型:如果按照常规的写法,返回值总是错误的。我搜索了stackoverflow,发现了这个问题的解决方案,参考链接如下:
C# DllImport with C++ boolean function not returning correctly​stackoverflow.com
dec6145952b9630157d3a6883ba2eddd.png

因此,声明就应该这样写:

YOURPROJECT_API 
[DllImport(strDllName, CallingConvention = ccDef, CharSet = csDef, EntryPoint ="DoSomething")]

六、复杂的数据类型的定义(结构体)

### C++C# 中的任务队列实现与使用 #### C++中的任务队列实现 在C++中,可以创建一个线程安全的任务队列来支持并发编程。下面是一个简单的无锁队列的实现例子: ```cpp #include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue> #include <functional> class TaskQueue { public: void enqueue(std::function<void()> task) { std::unique_lock<std::mutex> lock(mutex_); tasks_.push(task); condition_.notify_one(); } std::function<void()> try_dequeue() { std::unique_lock<std::mutex> lock(mutex_); while (tasks_.empty()) { condition_.wait(lock); } auto task = tasks_.front(); tasks_.pop(); return task; } private: std::queue<std::function<void()>> tasks_; mutable std::mutex mutex_; std::condition_variable condition_; }; ``` 此代码展示了如何定义`enqueue`方法向队列添加新任务以及通过`try_dequeue`尝试移除并返回最前面的任务[^1]。 对于多线程测试环境下的性能评估,通常会提供样例文件展示每种队列的具体应用方式,并测量完成多线程测试所需的时间长度。 #### C#中的任务队列实现 而在C#环境中,则可以通过利用`BlockingCollection<T>`类轻松构建阻塞生产者-消费者模式的任务队列: ```csharp using System; using System.Collections.Concurrent; using System.Threading.Tasks; public class TaskScheduler : IDisposable { private readonly BlockingCollection<Func<Task>> _taskQueue = new BlockingCollection<Func<Task>>(); public TaskScheduler(int workerCount) { for (int i = 0; i < workerCount; ++i) { Task.Factory.StartNew(ConsumeTasks, TaskCreationOptions.LongRunning); } } public void ScheduleTask(Func<Task> taskFunc) { _taskQueue.Add(taskFunc); } private async Task ConsumeTasks() { foreach (var task in _taskQueue.GetConsumingEnumerable()) { await task().ConfigureAwait(false); } } public void Dispose() { _taskQueue.CompleteAdding(); } } ``` 上述实现了基于`BlockingCollection<Func<Task>>`类型的简单任务调度器,在构造函数里启动指定数量的工作线程去消费来自队列里的异步操作;当调用`Dispose()`时通知所有工作线程停止运行[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值