C#与C/C++交互(1)——需要了解的基础知识

【前言】

 C#中用于实现调用C/C++的方案是P/Invoke(Platform Invoke),让托管代码可以调用库中的函数。类似的功能,JAVA中叫JNI,Python中叫Ctypes。

常见的代码用法如下:

[DllImport("Test.dll", EntryPoint = "Load", CallingConvention = CallingConvention.Cdecl,SetLastError = true)]
public static extern int Load([MarshalAs(UnmanagedType.LPWStr)] string jarg1, IntPtr jarg2, int jarg3, out int jarg4);

调用过程为

  • 查找dll,例子中为Test.dll'
  • 将该dll加载到内存中
  • 查找函数在内存中的地址,例子为查找Load函数,并将其参数按照函数的调用约定压栈,例子中调用约定为Cdecl
  • 将控制权转移给非托管函数

 【代码含义详解】

Test.dll其表示要加载哪个动态库,EntryPoint显式指定函数入口点,如果没有EntryPoint,那么会把方法名作为入口点,EntryPoint和方法名不一定相同。

SetLastError = true

非托管代码中有报错时,很少像托管代码中抛出异常,将SetLastError设置为true,可以按照C#标准的方式抛出异常,报告错误。

stdcall和cdecl的区别

告诉编译器参数的传递约定,参数的传递约定是指参数的传递顺序(从左到右还是从右到左)和由谁来恢复堆栈指针(调用者或者是被调用者)

cdecl是 C Declaration 的缩写,表示 C 语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。因为调用者知道传递了多少个参数,因此被调用函数无需要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。

stdcall 是Standard Call的缩写,是C 的标准调用方式:所有参数从右到左依次入栈,如果是调用类成员的话,最后一个入栈的是this指针。这些堆栈中的参数由被调用的 函数在返回后清除,使用的指令是 retn X,X表示参数占用的字节数,CPU在ret之后自动弹出X个字节的堆栈空间,称为自动清栈。因为是被调用者恢复堆栈指针,所以被调用者必须要知道传递进来了多少个参数,也即函数在编译的时候就必须确定参数个数。

C#中默认是stdcall调用的,如果你能很确定函数调用参数数量不会变化,用stdcall和cdecl没区别。

[DllImport("__Internal")]

如果使用了静态库,那么需要使用[DllImport("__Internal")] (注意有两条下画横线)。静态库将在链接阶段将函数入口链接好,在运行时会直接调用库中的函数,而动态库需要在运行时加载,然后查找函数入口,相比而言,静态库降低了P/Invoke的消耗。

ref和out参数

如果对基元数据类型存在引用,使用ref或out参数,而不是指针。另外,仅有一个的话,可以考虑作为返回值。尤其是当结构体作为传入参数时,要注意使用ref参数。

MarshalAsAttribute

该特性用于描述字段、方法或参数的封送处理格式,在不同平台对数据类型的表示方式有区别,在传递前需要一些说明,用MarshalAs说明,其可用于参数、字段、返回值。使用范例如下:

using System;
using System.Text;
using System.Runtime.InteropServices;class Program
{//Applied to a parameter.public void M1([MarshalAs(UnmanagedType.LPWStr)]String msg) {}//Applied to a field within a class.class MsgText {[MarshalAs(UnmanagedType.LPWStr)]public String msg = "Hello World";}//Applied to a return value.
[return: MarshalAs(UnmanagedType.LPWStr)]public String GetMessage(){return "Hello World";}static void Main(string[] args){  }
}decimal _money;   public decimal Money 
{[return: MarshalAs(UnmanagedType.Currency)]get { return this._money; }[param: MarshalAs(UnmanagedType.Currency)]set { this._money = value; }
}

UnmanagedType的类型如下,一般来说用的比较多的是关于字符串的:

  • BStr  长度前缀为双字节的 Unicode 字符串,默认的
  • LPStr  单字节、null为终止符的 ANSI 字符串
  • LPWStr  一个 2 字节、null为终止符的 Unicode 字符串

更多的类型需要参考MSDN

【类型传递】 

Blittable和Non-Blittable

有些数据类型在托管和非托管之间传递时不需要特殊处理,可以直接传递,这些数据类型被称为Blittable类型,否则就是Non-Blittable类型。

在使用P/Invoke时,函数的返回值的结构只能是Blittable类型,其包括int、float、byte、short、IntPtr等,由这些Blittable类型组成的数组,也被视为Blittable类型。注意,bool、char、string是Non-Blittable类型。

类型关系对应表

注意托管代码中,像int这样的基元数据类型不会随处理器改变大小,无论16位、32位还是64位处理器,int始终是32位。而在非托管代码中,内存指针会随处理器而变化,因此对于void*等指针类型要映射位System.IntPtr,其大小将随处理器内存布局而滨化。

StructLayoutAtrribute

有些自定义的类型没有非托管和托管的类型对应关系,需要用StructLayoutAtrribute来定义该类型中的字段的内存布局,以便在托管和非托管代码中能够正确从内存中读取数据。(这里的类型指struct、class)。

使用范例为:[StructLayout(LayoutKind.Explicit, Pack = 4,Size=16, CharSet=CharSet.Ansi)]

 内存布局三种情况:

  • 默认(LayoutKind.Sequential)情况下,CLR对struct的Layout的处理方法与C/C++中默认的处理方式相同,即按照结构中占用空间最大的成员进行对齐(Align)
  • 使用LayoutKind.Explicit的情况下,CLR不对结构体进行任何内存对齐(Align),而且需要我们自己设置FieldOffset
  • 使用LayoutKind.Auto的情况下,CLR会对结构体中的字段顺序进行调整,使实例占有尽可能少的内存,并按照4字节的内存对齐(Align)

StructLayout特性支持三种附加字段:CharSet、Pack、Size

CharSet定义在结构中的字符串成员在结构被传给DLL时的排列方式。可以是Unicode、Ansi或Auto。其中Unicode和Auto表示字符串按照Unicode编码(LPWSTR),Ansi表示按照ANSI编码(LPSTR)

  • Pack用于指定按多少位进行内存对齐,默认是0,表示使用当前平台默认的内存对齐,其值可以是1、2、4、8、16、32、64、128。通过示例,可以明白指定Pack对类型实际占用的内存大小的影响。

size用于表明class或struct的绝对大小,其必须大于所有字段大小总和。

【Marshal 常用API】

marshal:直译为“编排”, 在计算机中特指将数据按某种描述格式编排出来。在C#中,Marshal类的定义为:提供一个方法集合,分配非托管内存,拷贝非托管内存块,转换托管和非托管类型,以及一些和非托管代码交互的杂类方法。其所在命名空间为System.Runtime.InteropServices

Marshal.SizeOf

其作用是获取对象占用的内存大小。

参数为类型对象或类型的实例,计算需要分配多少字节的非托管内存,可用于任何对象实例或运行时类型。sizeof运算符参数为类型对象,计算需要为对象的实例分配多少字节的托管内存。在C#中,sizeof运算符仅适用于编译时已知的类型,而不适用于变量。

Marshal.AllocHGlobal 与Marshal.FreeHGlobal

作用分别是从进程的非托管内存中分配和释放内存,一般配合相互配合使用。

分配内存常用的方法为public static IntPtr AllocHGlobal (int cb),通过使用指定的字节数,从进程的非托管内存中分配内存,返回值是指向分配的内存的第一个字节的地址,这块分配的内存用Marshal.FreeHGlobal释放内存。具体指定多少字节数通常用Marshal.SizeOf计算出来。

(GCHandle.Alloc不会分配内存,其只是从托管内存中拿到托管对象的句柄,以便于从非托管代中访问托管对象,需要用GCHandle.Free释放)

Marshal.PtrToStructure和Marshal.StructureToPtr

前者作用是将指针所指的非托管内存中的数据转为托管对象,将托管对象转为非托管内存并返回非托管内存的指针。

注意由于涉及托管和非托管内存,两者之间的数据是copy的,这里的structure必须要是值类型、结构体或者用的StructLayoutAtrribute修饰的类的实例,否则无法确定在分配在非托管内存中需要多少内存。如果structure包含了IntPtr引用类型,例如接口、没有用layout修饰的类、System.Object等,那么这些引用类型所指的托管对象的引用被赋值了一份到非托管内存中;所有其他引用类型,例如字符串和数组,会被copy。在释放非托管内存前,必须主动调用Marshal.DestroyStructure将非托管内存中的数据清理掉。

public static void StructureToPtr (object structure, IntPtr ptr, bool fDeleteOld);该方法有一个fDeleteOld参数,其意义为:

首次调用该方法时,IntPtr所指向的内存没有包含其他数据,该参数必须为false。如果IntPtr已经指向的内存中有数据,必选为true,此时在将数据copy过去前,会自动调Marshal.DestroyStructure将非托管内存中的数据清理掉。如果不这样可能会导致内存泄露。

使用范例如下:

using System;
using System.Runtime.InteropServices;public struct Point
{public int x;public int y;
}class Example
{static void Main(){// Create a point struct.Point p;p.x = 1;p.y = 1;Console.WriteLine("The value of first point is " + p.x + " and " + p.y + ".");// Initialize unmanged memory to hold the struct.IntPtr pnt = Marshal.AllocHGlobal(Marshal.SizeOf(p));try{// Copy the struct to unmanaged memory.Marshal.StructureToPtr(p, pnt, false);// Create another point.Point anotherP;// Set this Point to the value of the// Point in unmanaged memory.anotherP = (Point)Marshal.PtrToStructure(pnt, typeof(Point));Console.WriteLine("The value of new point is " + anotherP.x + " and " + anotherP.y + ".");}finally{// Free the unmanaged memory.Marshal.FreeHGlobal(pnt);}}
}

字符串相关API

Marshal.PtrToStringAnsi和Marshal.StringToHGlobalAnsi

Marshal.PtrToStringAuto和Marshal.StringToHGlobalAuto

Marshal.PtrToStringUni和Marshal.StringToHGlobalUni

这些相当于将Structure换成了String。

Marshal.Copy

将托管数据中的数据拷贝到指针指向的非托管内存中,或者反过来。

使用范例如下:

using System;
using System.Runtime.InteropServices;class Example
{static void Main(){// Create a managed array.int[] managedArray = { 1, 2, 3, 4 };// Initialize unmanaged memory to hold the array.int size = Marshal.SizeOf(managedArray[0]) * managedArray.Length;IntPtr pnt = Marshal.AllocHGlobal(size);try{// Copy the array to unmanaged memory.Marshal.Copy(managedArray, 0, pnt, managedArray.Length);// Copy the unmanaged array back to another managed array.int[] managedArray2 = new int[managedArray.Length];Marshal.Copy(pnt, managedArray2, 0, managedArray.Length);Console.WriteLine("The array was copied to unmanaged memory and back.");}finally{// Free the unmanaged memory.Marshal.FreeHGlobal(pnt);}}
}

Marshal.AddRef和Marshal.Release

public static int AddRef (IntPtr pUnk);

public static int Release (IntPtr pUnk);

其增加和减少对象的引用计数,返回值是当前引用的数量。

Marshal.GetFunctionPointerForDelegate

public static IntPtr GetFunctionPointerForDelegate (Delegate d);

其作用是将一个委托转为能从非托管代码中调用的函数指针,可以通过UnmanagedFunctionPointerAttribute来设置调用约定。必须手动防止垃圾收集器从托管代码中收集委托。垃圾收集器不跟踪对非托管代码的引用。

【其他简要介绍】

MonoPInvokeCallBack

这个特性只在静态方法上有效,用于让Mono的AOT编译器知道这个方法是从native code调用的,在编译时需要生成一些必要的代码以支持native code调用managed code。在常规的ECMA CIL程序中,这是自动发生的,不需要特别标记任何内容。

UnmanagedFunctionPointerAttribute

控制作为指向或来自非托管代码的非托管函数指针传递的委托签名的封送处理行为

fixed 和 unsafe

有时需要用指针直接访问和操纵内存,C#通过“不安全代码”构造提供这方面的支持。通过将代码区指定为unsafe可以绕过C#的类型检查机制,直接操作内存和地址。使用这个关键字时需要在VS中打开项目属性窗口,勾选“生成”标签页中的“允许不安全代码”,unity的话需要在Project Setting中勾选。

在unsafe中,可以像C++一样使用指针。但是引用类型、泛型类型、内部包括引用类型时不能使用 指针,也即string* str是无效的,Status* status(Status是结构体,其中有一个string字段)也是无效的。值类型(int* char* bool* byte* )的指针,void*指针是有效的。

我们知道给指针赋值时先要获取数据的地址,用&操作符来获取值类型的地址。但当对象在托管内存中时,其可能被垃圾回收或转移位置,为了将数据的地址赋值给指针,需要将数据固定住,有如下方法:

1.用fixed固定:其要求数据属于一个非托管的变量。fixed使得限定的代码块中,赋值的数据不会再移动,使用范例如下,bytes被固定不动:

unsafe
{byte[] bytes = { 1, 2, 3 };fixed (byte* pointerToFirst = bytes)//用bytes取代冗长的&bytes[0]{Console.WriteLine($"The address of the first array element: {(long)pointerToFirst:X}.");Console.WriteLine($"The value of the first array element: {*pointerToFirst}.");}
}
// Output is similar to:
// The address of the first array element: 2173F80B5C8.
// The value of the first array element: 1.unsafe
{int[] numbers = { 10, 20, 30 };fixed (int* toFirst = &numbers[0], toLast = &numbers[^1]){Console.WriteLine(toLast - toFirst);  // output: 2}
}

由于垃圾回收器不能压缩已经固定的对象,fixed语句可能导致内存碎片化。为了解决该问题,最好的做法是在执行前期就固定好代码块,而且宁可固定较少的几个大块,也不要固定许多小块。 

 2.分配在栈上:栈上的数据不会被垃圾回收,也不会被终结器清理,可以在栈上分配非托管类型的数组。例如:

int length = 3;
Span<int> numbers = stackalloc int[length];
for (var i = 0; i < length; i++)
{numbers[i] = i;
}unsafe
{int length = 3;int* numbers = stackalloc int[length];for (var i = 0; i < length; i++){numbers[i] = i;}
}

在栈上分配就没有内存碎片化的问题,但只能在栈上分配很小的内存,以防止栈空间被耗尽而导致程序崩溃。一般情况下,程序的栈空间不到1MB。

SafeHandle

当涉及到一些资源的需要手动清理释放,但要求每次都记得手动释放是不现实的,类似C#中非托管资源要继承IDispose,跨平台时可以继承System.Runtime.InteropServices.SafeHandle。

【参考】 

MSDN

《C#本质论8.0》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/30523.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

修改IDEA的idea.vmoptions参数导致IDEA无法打开(ReservedCodeCacheSize)

事发原因 Maven导依赖的时候OOM&#xff0c;因此怀疑是内存太小&#xff0c;尝试修改idea.vmoptions的参数&#xff0c;然后发现IDEA重启后打不开了&#xff0c;卸载重装后也无法打开。。。 实际上如果导包爆出OOM的话应该调整下图参数&#xff0c;不过这都是后话了 解决思路…

【从零开始学习JAVA | 第四十四篇】TCP协议中的握手与挥手

前言&#xff1a; TCP&#xff08;传输控制协议&#xff09;作为计算机网络中的重要协议&#xff0c;扮演着确保数据可靠传输的角色。在TCP的通信过程中&#xff0c;握手与挥手问题是不可忽视的关键环节。握手是指在建立连接时&#xff0c;客户端与服务器相互确认彼此的身份并…

【学习FreeRTOS】第1章——FreeRTOS入门

1.裸机与RTOS介绍 1.1.裸机与RTOS引入&#xff08;举例&#xff09; 设定情景&#xff1a;小明同学一边打游戏一边恢复女友消息&#xff0c;中途突然肚子疼要上医院 裸机的抽象表达 当紧急情况时&#xff0c;如果当前正在打游戏&#xff0c;那么小明只能打游戏和回复信息的流…

Spring 事务管理

目录 1. 事务管理 1.1. Spring框架的事务支持模型的优势 1.1.1. 全局事务 1.1.2. 本地事务 1.1.3. Spring框架的一致化编程模型 1.2. 了解Spring框架的事务抽象&#xff08;Transaction Abstraction&#xff09; 1.2.1. Hibernate 事务设置 1.3. 用事务同步资源 1.3.1…

PHP最简单自定义自己的框架创建目录结构(二)

1、mvc目录结构 2、目录解释 KJ&#xff1a;项目名称 core&#xff1a;框架核心目录 KJ.php 框架运行入口 index: 框架模块 controller:模块控制器 model:模块模型数据库操作 view:页面显示html index.php:index模块框架入口 3、index.php框架入口文件引入框架 <?php r…

【前端】jeecgboot vue3开发过程使用方法整理

【前端】jeecgboot vue3开发过程使用方法整理 //定义变量 let list ref([]) 获取当前用户信息 const { userInfo } useUserStore(); 组件 JDictSelectTag {label: "用户ID",field: userId,component: JDictSelectTag, //默认值defaultValue: userInfo.userId,co…

python 异步IO实践

1、前言 1.1 计算密集型、IO密集型 python因为自身的GIL的问题导致并发不能像java和C一样&#xff0c;但并不是说Python不能实现并发。常见的有两种&#xff1a; 计算密集型 IO密集型 计算密集型 计算密集型任务的特点是要进行大量的计算&#xff0c;消耗CPU资源&#xff0c;比…

虚拟世界探索:科技之下的未来可能性

随着科技的飞速发展&#xff0c;人们对于虚拟世界的憧憬和探索也日益加深。虚拟世界&#xff0c;那是一个超越现实的概念&#xff0c;一个充满想象力和创造力的领域。然而&#xff0c;虚拟世界究竟有可能实现吗&#xff1f;这是一个引人深思的问题。 虚拟世界&#xff0c;首先让…

Java,python,c#,js,c++搞量化交易的接口大全

股票基金api接口地址&#xff1a;https://stockapi.com.cn 不想自己写的&#xff1a;https://stockapi.com.cn 除了牛逼&#xff0c;只剩下牛逼&#xff0c;除了方便&#xff0c;只剩下方便&#xff0c;python不是唯一的量化编程语言 接口说明&#xff1a;日线macd指标 https:/…

Vue.js2+Cesium1.103.0 六、标绘与测量

Vue.js2Cesium1.103.0 六、标绘与测量 点&#xff0c;线&#xff0c;面的绘制&#xff0c;可实时编辑图形&#xff0c;点击折线或多边形边的中心点&#xff0c;可进行添加线段移动顶点位置等操作&#xff0c;并同时计算出点的经纬度&#xff0c;折线的距离和多边形的面积。 De…

mysql死锁分析show engine innodb status

文章目录 1、show engine innodb status命令2、前置知识3、无符号数、有符号数4、innodb关于有符号数的规定5、为什么会有这个规定 1、show engine innodb status命令 最近在使用mysql的show engine innodb status命令分析死锁&#xff0c;发现了一个有意思的点。就是红框里圈…

vue3中使用jsx/tsx写ElMessageBox嵌套ElSelect、ElInput、ElDatePicker

- message: () > h() 必须是函数形式才能update数据。- 简单的 // 绑定参数 const pageData reactive({statusValue: null }) // 更改设备状态弹窗 const openDialogChangeStatus (ids: number[], name: string, doTxt: string) > {console.log(logopenDialogChange…

关于UnityC#当中常见的语法糖

1自动实现属性 (Auto-implemented Properties)&#xff1a;快速实现对象封装 // 传统属性 private int age; public int Age {get { return age; }set { age value; } }// 自动实现属性 public int Age { get; set; }2对象和集合初始化 (Object and Collection Initializers)…

JVM总结2

1.基本概念 java代码执行 代码编译class&#xff1a;javac 源文件通过编译器产生字节码文件&#xff0c;字节码文件通过jvm的解释器编译成机器上的机器码 装载class&#xff1a;ClassLoader执行class&#xff1a; 解释执行编译执行 client compilerserver compiler 内存管理…

常见的服务器安全管理漏洞

常见的服务器安全管理漏洞 企业信息化技术的应用&#xff0c;以不可逆转。随着文件服务器、ERP管理软件等等在企业中生根发芽&#xff0c;应用服务器也逐渐在企业中普及起来。以前在企业中有一台应用服务器已经是了不起的事情&#xff0c;现在有两台、三台的&#xff0c;也不为…

【LeetCode】169. 多数元素

目录标题 2023-8-9 10:43:27 169. 多数元素 2023-8-9 10:43:27 public int majorityElement(int[] nums) {int minTimes nums.length / 2;Arrays.sort(nums);int pre 0;int last pre;while (last < nums.length) {if (nums[pre] ! nums[last]) {if (last - pre > min…

Docker 方式 部署 vue 项目 (docker + vue + nginx)

1.安装好 nginx 。 2. 把 vue 项目的源码克隆到确定目录下。用 git 管理&#xff0c;所以直接 git clone 到既定目录就行了。 如我的目录是&#xff1a;/root/jiangyu/projects/gentle_vue/gentle_vue_code 。 3. 项目打包&#xff1a; npm run build 复制 会自动生成 dist…

自然语言处理(Natural Language Processing,NLP)

自然语言处理(Natural Language Processing&#xff0c;NLP)是人工智能领域的一个重要分支&#xff0c;旨在使计算机能够理解、处理和生成人类自然语言。NLP 的目标是让计算机能够像人类一样有效地理解和交流&#xff0c;从而实现更自然、更智能的人机交互。 NLP的理解概括&…

详解推送Git分支时发生的 cannot lock ref 错误

在码云上建了一个项目仓库,分支模型使用 git-flow ,并在本地新建了一个功能分支 feature/feature-poll。后来在推送时发生错误,提示 cannot lock ref ...... 这样的错误信息。下面复盘一下具体过程和解决办法,以供参考。 在码云中建立仓库时,考虑到想按照 GitFlow 的模式…