C#与C++间传递数组

eakey 2019-05-11 11:52:04
srhouyu,您好!
您之前在我发的一个帖子(https://bbs.csdn.net/topics/392568307)中说到

调用API期间数组内存有效的方法:

1.在API声明中将待传输的数组参数加上[In, Out]的限定符,能保证这个数组不会跑。

2. 用fixed获得固定的数组指针。但这需要使用unsafe关键字。

后面我自己看了《精通.net互操作》这本书,书中提到blittable数据类型会被封送拆收器锁定,实际传递到非托管代码的是指向管理对象的指针。如果是这样,那么传递blittable类型的数组,是不是不需要fixed或者in,out。




我测试下了面这样的代码,好像没有什么问题,但我又不确定会不会在特殊的情况下会出问题:
c#:

[System.Runtime.InteropServices.DllImport(@"NativeLib.dll",
EntryPoint = "MthCopy",
CharSet = System.Runtime.InteropServices.CharSet.Ansi,
CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
private static extern int MthCopy(byte[] Source, ref IntPtr Trans, int Len);

[System.Runtime.InteropServices.DllImport(@"NativeLib.dll",
EntryPoint = "MthPtrArrayFree",
CharSet = System.Runtime.InteropServices.CharSet.Ansi,
CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
private static extern int MthPtrArrayFree(IntPtr PtrArray);

调用代码:

int Len = 4;
byte[] Source = new byte[] { 1, 2, 3, 4 };
IntPtr Trans = IntPtr.Zero;

MthCopy(Source, ref Trans, 4);

byte[] TransArr = new byte[Len];
//复制或者用unsafe直接访问指针
System.Runtime.InteropServices.Marshal.Copy(Trans, TransArr, 0, TransArr.Length);


MthPtrArrayFree(Trans);


c++ dll

extern "C" __declspec(dllexport) int__stdcall MthCopy(BYTE *Source, BYTE *&Trans, int Len);
extern "C" __declspec(dllexport) int__stdcall MthPtrArrayFree(void *PtrArray);

int MthCopy(BYTE *Source, BYTE *&Trans, int Len)
{
Trans = new BYTE[Len];
memcpy_s(Source, Len, Trans, Len);
}

int MthPtrArrayFree(void *PtrArray)
{
if (PtrArray)
{
delete[] PtrArray;
PtrArray = nullptr;
}
return 0;
}



程序以预期的结果运行。
互操作方面很菜,请多指教。
...全文
122 点赞 收藏 18
写回复
18 条回复
eakey 2019年09月02日
引用 5 楼 srhouyu 的回复:
如果你自己很确定你的数组是会被固定而不是被复制,那么当然可以不加out。但加上[In, Out]没有任何坏处。在内存布局兼容时候,相比不加,没有任何性能损失。在内存布局不兼容的时候,它会有一点内存复制,但是效果没有区别,即无论如何都能察觉到C++方的修改。 能更明确你意图的修饰符,能加的最好都加上,尽量把涵义收窄一点,是个习惯问题。你的想法暂时成立,但是以后代码扩充了或者重构了会怎样?多加点修饰符能最小化你的烦恼。
srhouyu 这个贴子能帮忙看下吗: https://bbs.csdn.net/topics/392660951
回复 点赞
srhouyu 2019年05月13日
引用 15 楼 eakey 的回复:
1. 给函数另外加一个参数,delete时候按情况来。

	switch (nBytes)
	{
		case 1: 
			delete[] reinterpret_cast<int8_t*>(PtrArray);
			break;
		case 2:
			delete[] reinterpret_cast<int16_t*>(PtrArray);
			break;
		case 4:
			delete[] reinterpret_cast<int32_t*>(PtrArray);
			break;
		case 8:
			delete[] reinterpret_cast<int64_t*>(PtrArray);
			break;
		// 其他case,如果你还有其他长度的自定义类型的话
		default:
			break;
	}
不要去直接delete[] void*类型,因为这样做的后果是未定义的。总体上来说,这样写一个释放各种类型数组的方法是有些奇怪的。不如用下面的方式,让C++在合适的时候自己销毁,不用C#通知。它自己自然知道类型是什么。 2. 要缓存,但这也没什么不好的,请求附有上下文就不会混淆。例如你请求一次长度,C++不仅返回长度,而且返回一个号码。号码和缓存的对应放在unordered_map中。你要求填充时候,呈上对应的号码就能得到正确的缓存。这样多次请求就能够区分开。缓存的东西被取一次就会被销毁,过了很长时间没人取也会被销毁。 这自然需要多一次内存复制。但是512*512的32位图像是1 MB,内存复制至多0.5 ms。你的一帧就得17~33 ms。究竟瓶颈在不在这里,是不需要过早下结论的。
回复 点赞
赵4老师 2019年05月13日
不要做A语言代码修改为B语言代码的无用功。 也不要做用A语言代码直接调用B语言代码库这样复杂、这样容易出错的傻事。 只需让A、B语言代码的输入输出重定向到文本文件,或修改A、B语言代码让其通过文本文件输入输出。 即可很方便地让A、B两种语言之间协调工作。 比如: A将请求数据写到文件a.txt,写完后改名为aa.txt B发现aa.txt存在时,读取其内容,调用相应功能,将结果写到文件b.txt,写完后删除aa.txt,再将b.txt改名为bb.txt A发现bb.txt存在时,读取其内容,读完后删除bb.txt 以上A可以替换为任何一种开发语言或开发环境,B可以替换为任何一种与A不同的开发语言或开发环境。 除非A或B不支持判断文件是否存在、文件读写和文件更名。 但是谁又能举出不支持判断文件是否存在、文件读写和文件更名的开发语言或开发环境呢? 可以将临时文件放在RamDisk上提高效率减少磨损磁盘。 数据的结构很复杂的话,文本文件的格式问题可参考json或xml 共享临时文本文件这种进程之间的通讯方法相比其它方法的优点有很多,下面仅列出我现在能想到的: ·进程之间松耦合 ·进程可在同一台机器上,也可跨机,跨操作系统,跨硬件平台,甚至跨国。 ·方便调试和监视,只需让第三方或人工查看该临时文本文件即可。 ·方便在线开关服务,只需删除或创建该临时文本文件即可。 ·方便实现分布式和负载均衡。 ·方便队列化提供服务,而且几乎不可能发生队列满的情况(除非硬盘空间满) ·…… “跨语言、跨机,跨操作系统,跨硬件平台,跨国,跨*.*的”苦海无边, 回头是“使用共享纯文本文件进行信息交流”的岸!
回复 点赞
eakey 2019年05月12日

[System.Runtime.InteropServices.DllImport(@"NativeLib.dll",
            EntryPoint = "MthCopy",
            CharSet = System.Runtime.InteropServices.CharSet.Ansi,
            CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
private static extern int MthCopy(in byte[] Source, ref IntPtr Trans, int Len);
 

另外,如上我在byte前加上了in修饰符,程序返回的数组值变成的随机的,这个in该怎么用?
回复 点赞
eakey 2019年05月12日
引用 2 楼 stherix 的回复:
基本上没必要加in out,因为默认会自动判断加上的 非托管dll调用期间,数组会被固定,不用担心
能帮我看下,上面的代码,从传入到传出再到释放,是否有bug或不合规的地方吗?
回复 点赞
eakey 2019年05月12日
引用 2 楼 stherix 的回复:
基本上没必要加in out,因为默认会自动判断加上的 非托管dll调用期间,数组会被固定,不用担心
谢谢
回复 点赞
eakey 2019年05月12日
引用 12 楼 srhouyu 的回复:
[quote=引用 11 楼 eakey 的回复:] 上面这个id是什么意思,能不能说详细点。 1.c#调用了一个c++函数,其中有一个参数是ref IntPtr Ptr,调用Ptr被赋值IntPtr.Zero,同时还有一个标识数组长度的ref int Len。 2.c++函数中new出一个数值型数组,把数组地址赋值给传进来的Ptr。同时c++还曝露出一个我之前提的 delete[] void*的数组释放函数。 3.然后c#用unsafe的方式直接访问这个数组并显示到c#的图形界面上(数组比较大所以不用复制)。 4.c#调用c++曝露的数组释放函数。 数组有byte/int/double等
id就是给数组一个编号,例如0号数组,1号数组,等等。既然你C#这边需要访问数组内容,那么这个不需要在意。 看你的情况,是C++创建、写入数组,C#用unsafe方式读取数组,C++删除数组。那C++那边自然就是用void*。此时类型信息已经丢失。你必须另外传入一个参数,告诉C++元素长度多少,好让C++从void*恢复对应长度的类型。类型不必完全一致,长度一样即可,例如4字节的无论是恢复为int还是float都行。 实际上,我还是建议C#创建数组,C++写入数据,C#读取数据,C#自己回收数组。这样不需要任何unsafe代码。而且释放资源时候,C#明显比C++方便。[/quote]1.释放数组我现在是这样写的:delete[] Ptr(void*的指针),我多传入一个长度用在什么地方? 2.如果我用c#创建数组: a.c#调用的c++函数时传入了一些数据,c++根据传入参数计算出一个结果数组(长度只能结果数组出来时才有,结果是c++调用是第三方图形库才产生的),然后计算长度返回给c#。 b.c#根据长度new出数组传递给c++数组填充函数,c++填充数据。 问题:c++要缓存第一次调用产生的结果数组,这样在多线程时感觉有点不好处理。
回复 点赞
eakey 2019年05月12日
引用 12 楼 srhouyu 的回复:
[quote=引用 11 楼 eakey 的回复:] 上面这个id是什么意思,能不能说详细点。 1.c#调用了一个c++函数,其中有一个参数是ref IntPtr Ptr,调用Ptr被赋值IntPtr.Zero,同时还有一个标识数组长度的ref int Len。 2.c++函数中new出一个数值型数组,把数组地址赋值给传进来的Ptr。同时c++还曝露出一个我之前提的 delete[] void*的数组释放函数。 3.然后c#用unsafe的方式直接访问这个数组并显示到c#的图形界面上(数组比较大所以不用复制)。 4.c#调用c++曝露的数组释放函数。 数组有byte/int/double等
id就是给数组一个编号,例如0号数组,1号数组,等等。既然你C#这边需要访问数组内容,那么这个不需要在意。 看你的情况,是C++创建、写入数组,C#用unsafe方式读取数组,C++删除数组。那C++那边自然就是用void*。此时类型信息已经丢失。你必须另外传入一个参数,告诉C++元素长度多少,好让C++从void*恢复对应长度的类型。类型不必完全一致,长度一样即可,例如4字节的无论是恢复为int还是float都行。 实际上,我还是建议C#创建数组,C++写入数据,C#读取数据,C#自己回收数组。这样不需要任何unsafe代码。而且释放资源时候,C#明显比C++方便。[/quote]谢谢
回复 点赞
eakey 2019年05月12日
1.释放数组我现在是这样写的:delete[] Ptr(void*的指针),我多传入一个长度用在什么地方? 2.如果我用c#创建数组: a.c#调用的c++函数时传入了一些数据,c++根据传入参数计算出一个结果数组(长度只能结果数组出来时才有,结果是c++调用是第三方图形库才产生的),然后计算长度返回给c#。 b.c#根据长度new出数组传递给c++数组填充函数,c++填充数据。 问题:c++要缓存第一次调用产生的结果数组,这样在多线程时感觉有点不好处理。
回复 点赞
stherix 2019年05月12日
基本上没必要加in out,因为默认会自动判断加上的 非托管dll调用期间,数组会被固定,不用担心
回复 点赞
srhouyu 2019年05月12日
引用 11 楼 eakey 的回复:
上面这个id是什么意思,能不能说详细点。 1.c#调用了一个c++函数,其中有一个参数是ref IntPtr Ptr,调用Ptr被赋值IntPtr.Zero,同时还有一个标识数组长度的ref int Len。 2.c++函数中new出一个数值型数组,把数组地址赋值给传进来的Ptr。同时c++还曝露出一个我之前提的 delete[] void*的数组释放函数。 3.然后c#用unsafe的方式直接访问这个数组并显示到c#的图形界面上(数组比较大所以不用复制)。 4.c#调用c++曝露的数组释放函数。 数组有byte/int/double等
id就是给数组一个编号,例如0号数组,1号数组,等等。既然你C#这边需要访问数组内容,那么这个不需要在意。 看你的情况,是C++创建、写入数组,C#用unsafe方式读取数组,C++删除数组。那C++那边自然就是用void*。此时类型信息已经丢失。你必须另外传入一个参数,告诉C++元素长度多少,好让C++从void*恢复对应长度的类型。类型不必完全一致,长度一样即可,例如4字节的无论是恢复为int还是float都行。 实际上,我还是建议C#创建数组,C++写入数据,C#读取数据,C#自己回收数组。这样不需要任何unsafe代码。而且释放资源时候,C#明显比C++方便。
回复 点赞
eakey 2019年05月12日
上面这个id是什么意思,能不能说详细点。 1.c#调用了一个c++函数,其中有一个参数是ref IntPtr Ptr,调用Ptr被赋值IntPtr.Zero,同时还有一个标识数组长度的ref int Len。 2.c++函数中new出一个数值型数组,把数组地址赋值给传进来的Ptr。同时c++还曝露出一个我之前提的 delete[] void*的数组释放函数。 3.然后c#用unsafe的方式直接访问这个数组并显示到c#的图形界面上(数组比较大所以不用复制)。 4.c#调用c++曝露的数组释放函数。 数组有byte/int/double等
回复 点赞
srhouyu 2019年05月12日
引用 9 楼 eakey 的回复:
我知道有些解决方案用泛型,问题是这个函数是用于c#调用的,我应该怎么做比较合适[/quote] 你需要确定,你delete的数组只能是C++这边new出来的,而不是C#那边new出来的。 如果C#这边需要访问数组内容,那你C++这边就只能用void*。这是比较麻烦的,因为C#那边要么得复制整个数组,要么得用unsafe代码。还不如把访问数组的代码都写在C++这边。 如果C#这边不需要直接访问数组内容,只是传来传去,那么它不需要直接接触数组本身。C++代码负责给new出来的数组编个ID,只把ID传给C#就行。这个ID类似于“句柄”的效果。
回复 点赞
eakey 2019年05月12日
引用 8 楼 srhouyu 的回复:
[quote=引用 7 楼 eakey 的回复:] [quote=引用 5 楼 srhouyu 的回复:]如果你自己很确定你的数组是会被固定而不是被复制,那么当然可以不加out。但加上[In, Out]没有任何坏处。在内存布局兼容时候,相比不加,没有任何性能损失。在内存布局不兼容的时候,它会有一点内存复制,但是效果没有区别,即无论如何都能察觉到C++方的修改。 能更明确你意图的修饰符,能加的最好都加上,尽量把涵义收窄一点,是个习惯问题。你的想法暂时成立,但是以后代码扩充了或者重构了会怎样?多加点修饰符能最小化你的烦恼。
int MthPtrArrayFree(void *PtrArray) { if (PtrArray) { delete[] PtrArray; PtrArray = nullptr; } return 0; } 如果只是用于删除值类型的数组,这个函数有问题吗?我在有些地方看到不提倡使用void*,但我有多种new出来的值类型数组,是否需要针对每种类型(如BYTE、Double、Int等)写专门的释放函数。[/quote] C++里面不说“值类型”这个词。也许你的意思是聚合类型? 用void*当然不好,这是C语言的风格,等于没有任何类型约束。如果是C++风格的,一般是写成模板函数。

template<typename T>
int MthPtrArrayFree(T *PtrArray)
{
	if (PtrArray)
	{
		delete[] PtrArray;
	}
	return 0;
}
调用:

float * arr = new float[100];
MthPtrArrayFree(arr);
arr = nullptr;
调用的时候不需要写成MthPtrArrayFree<float>(arr)。因为你传入的数组自带类型,编译器能自动推断出来T是什么。 但最好还是不要写这样的代码。 1. 方法里面的 PtrArray = nullptr 这句没有用,因为PtrArray只是你传入的指针的副本。想让外面数组的指针变为nullptr,要么像上面的代码这样调完MthPtrArrayFree后再写arr = nullptr,要么这个方法变成int MthPtrArrayFree(T ** PtrPtrArray),里面写*PtrPtrArray = nullptr。调用时候MthPtrArrayFree(&arr)。 2. 尽量用RAII的方法管理资源。能用智能指针就尽量用智能指针。 [/quote]我知道有些解决方案用泛型,问题是这个函数是用于c#调用的,我应该怎么做比较合适
回复 点赞
srhouyu 2019年05月12日
引用 7 楼 eakey 的回复:
[quote=引用 5 楼 srhouyu 的回复:]如果你自己很确定你的数组是会被固定而不是被复制,那么当然可以不加out。但加上[In, Out]没有任何坏处。在内存布局兼容时候,相比不加,没有任何性能损失。在内存布局不兼容的时候,它会有一点内存复制,但是效果没有区别,即无论如何都能察觉到C++方的修改。 能更明确你意图的修饰符,能加的最好都加上,尽量把涵义收窄一点,是个习惯问题。你的想法暂时成立,但是以后代码扩充了或者重构了会怎样?多加点修饰符能最小化你的烦恼。
int MthPtrArrayFree(void *PtrArray) { if (PtrArray) { delete[] PtrArray; PtrArray = nullptr; } return 0; } 如果只是用于删除值类型的数组,这个函数有问题吗?我在有些地方看到不提倡使用void*,但我有多种new出来的值类型数组,是否需要针对每种类型(如BYTE、Double、Int等)写专门的释放函数。[/quote] C++里面不说“值类型”这个词。也许你的意思是聚合类型? 用void*当然不好,这是C语言的风格,等于没有任何类型约束。如果是C++风格的,一般是写成模板函数。

template<typename T>
int MthPtrArrayFree(T *PtrArray)
{
	if (PtrArray)
	{
		delete[] PtrArray;
	}
	return 0;
}
调用:

float * arr = new float[100];
MthPtrArrayFree(arr);
arr = nullptr;
调用的时候不需要写成MthPtrArrayFree<float>(arr)。因为你传入的数组自带类型,编译器能自动推断出来T是什么。 但最好还是不要写这样的代码。 1. 方法里面的 PtrArray = nullptr 这句没有用,因为PtrArray只是你传入的指针的副本。想让外面数组的指针变为nullptr,要么像上面的代码这样调完MthPtrArrayFree后再写arr = nullptr,要么这个方法变成int MthPtrArrayFree(T ** PtrPtrArray),里面写*PtrPtrArray = nullptr。调用时候MthPtrArrayFree(&arr)。 2. 尽量用RAII的方法管理资源。能用智能指针就尽量用智能指针。
回复 点赞
eakey 2019年05月12日
引用 5 楼 srhouyu 的回复:
如果你自己很确定你的数组是会被固定而不是被复制,那么当然可以不加out。但加上[In, Out]没有任何坏处。在内存布局兼容时候,相比不加,没有任何性能损失。在内存布局不兼容的时候,它会有一点内存复制,但是效果没有区别,即无论如何都能察觉到C++方的修改。 能更明确你意图的修饰符,能加的最好都加上,尽量把涵义收窄一点,是个习惯问题。你的想法暂时成立,但是以后代码扩充了或者重构了会怎样?多加点修饰符能最小化你的烦恼。
int MthPtrArrayFree(void *PtrArray) { if (PtrArray) { delete[] PtrArray; PtrArray = nullptr; } return 0; } 如果只是用于删除值类型的数组,这个函数有问题吗?我在有些地方看到不提倡使用void*,但我有多种new出来的值类型数组,是否需要针对每种类型(如BYTE、Double、Int等)写专门的释放函数。
回复 点赞
eakey 2019年05月12日
引用 5 楼 srhouyu 的回复:
如果你自己很确定你的数组是会被固定而不是被复制,那么当然可以不加out。但加上[In, Out]没有任何坏处。在内存布局兼容时候,相比不加,没有任何性能损失。在内存布局不兼容的时候,它会有一点内存复制,但是效果没有区别,即无论如何都能察觉到C++方的修改。 能更明确你意图的修饰符,能加的最好都加上,尽量把涵义收窄一点,是个习惯问题。你的想法暂时成立,但是以后代码扩充了或者重构了会怎样?多加点修饰符能最小化你的烦恼。
明白
回复 点赞
srhouyu 2019年05月12日
如果你自己很确定你的数组是会被固定而不是被复制,那么当然可以不加out。但加上[In, Out]没有任何坏处。在内存布局兼容时候,相比不加,没有任何性能损失。在内存布局不兼容的时候,它会有一点内存复制,但是效果没有区别,即无论如何都能察觉到C++方的修改。 能更明确你意图的修饰符,能加的最好都加上,尽量把涵义收窄一点,是个习惯问题。你的想法暂时成立,但是以后代码扩充了或者重构了会怎样?多加点修饰符能最小化你的烦恼。
回复 点赞
发动态
发帖子
C++ 语言
创建于2007-09-28

3.1w+

社区成员

24.8w+

社区内容

C++ 语言相关问题讨论,技术干货分享
社区公告
暂无公告