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》
相关文章:

C#与C/C++交互(1)——需要了解的基础知识
【前言】 C#中用于实现调用C/C的方案是P/Invoke(Platform Invoke),让托管代码可以调用库中的函数。类似的功能,JAVA中叫JNI,Python中叫Ctypes。 常见的代码用法如下: [DllImport("Test.dll", E…...

LeetCode笔记:Weekly Contest 356
LeetCode笔记:Weekly Contest 356 1. 题目一 1. 解题思路2. 代码实现 2. 题目二 1. 解题思路2. 代码实现 3. 题目三 1. 解题思路2. 代码实现 4. 题目四 1. 解题思路2. 代码实现 比赛链接:https://leetcode.com/contest/weekly-contest-356/ 1. 题目一…...

2 Python的基础语法
概述 在上一节的内容中,我们介绍了Python的诞生、发展历程、特色、缺点和应用领域。从本节开始,我们将正式学习Python。Python是一门简洁和优雅的语言,有自己特殊的一些语法规则。因此,在介绍Python编程的有关知识之前,…...

抖音seo矩阵系统源代码开发搭建技术分享
抖音SEO矩阵系统是一个较为复杂的系统,其开发和搭建需要掌握一定的技术。以下是一些技术分享: 技术分享 抖音SEO矩阵系统的源代码可以使用JAVA、Python、PHP等多种语言进行开发。其中,JAVA语言的应用较为广泛,因为JAVA语言有良好…...

python#django数据库一对一/一对多/多对多
一对一OneToOneField 用户和用户信息 搭建 # 一对一 class TestUser(models.Model): usernamemodels.CharField(max_length32) password models.CharField(max_length32) class TestInfo(models.Model): mick_namemodels.CharField(max_length32) usermode…...

记RT-Thread rt_timer_start函数的问题
我使用的RT-Thread版本为4.0.3。 我看了5.0.1的代码,此问已经被修复。 在4.0.3版本中的rt_timer_start函数源码如下: rt_err_t rt_timer_start(rt_timer_t timer) {unsigned int row_lvl;rt_list_t *timer_list;register rt_base_t level;rt_list_t *r…...

C++初阶——拷贝构造和运算符重载(const成员)
目录 1. 拷贝构造函数 1.2 拷贝构造函数特征: 2. 默认拷贝构造函数 2.1 未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝 3. 运算符重载 3.1…...

go练习 day01
DTO: note_dto.go package dtoimport "king/model"type NoteAddDTO struct {ID uintTitle string json:"title" form:"title" binding:"required" message:"标题不能为空"Content string json:"conten…...

C# Blazor 学习笔记(0.1):如何开始Blazor和vs基本设置
文章目录 前言资源推荐环境如何开始Blazor个人推荐设置注释快捷键热重载设置 前言 Blazor简单来说就是微软提供的.NET 前端框架。使用 WebAssembly的“云浏览器”,集成了Vue,React,Angular等知名前端框架的特点。 资源推荐 微软官方文档 Blazor入门基础视频合集 …...

原码的乘法运算 补码乘法运算
补码乘法 对比...

找不到d3dx9_43.dll丢失怎么解决(分享几种解决方法)
为什么我们打开电脑软件或许游戏时候,电脑会报错出现d3dx9_43.dll丢失,或许找不到d3dx9_43.dll等等的提示。下面来详细介绍一下d3dx9_43.dll详细解决方法跟d3dx9_43.dll是什么。 如果你的系统中没有安装或安装不完整的d3dx9_43.dll运行时,应…...

篇四:建造者模式:逐步构造复杂对象
篇四:“建造者模式:逐步构造复杂对象” 设计模式是软件开发中的重要组成部分,建造者模式是创建型设计模式中的一种。建造者模式旨在逐步构造复杂对象,将对象的构造与其表示分离,从而使得同样的构建过程可以创建不同的…...

vs导出和导入动态库和静态库
1. 动态库和导出和导入 1.1 动态库的导出 1. 创建新项目 新建新项目,选择动态链接库(DLL)。 填写项目名称,并选择项目保存的路径,然后点击创建。 创建完成后,会自动生成如下所示文件,可以根据…...

30 使用easyExcel依赖生成Excel
30.1 导入依赖 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.2.6</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId&…...

排序进行曲-v2.0
文章目录 小程一言直接插入排序步骤举例复杂度分析应用场景实际举例代码实现 希尔排序步骤举例复杂度分析应用场景实际举例代码实现 堆排序步骤举例复杂度分析应用场景实际举例代码实现 小程一言 这篇文章是在排序进行曲1.0之后的续讲, 由于在上一篇讲的排序的基本…...

反弹shell的N种姿势
预备知识1. 关于反弹shell 就是控制端监听在某TCP/UDP端口,被控端发起请求到该端口,并将其命令行的输入输出转到控制端。reverse shell与telnet,ssh等标准shell对应,本质上是网络概念的客户端与服务端的角色反转。2. 反弹shel…...

创意视频剪辑教程:快速合并视频并标题,让你的作品更吸睛!
想要让你的视频作品脱颖而出,引人注目?不再担心,我们为你带来了一款创意视频剪辑教程,教你如何快速合并视频并添加令人惊艳的标题效果!让你的作品在分钟内变得酷炫而精彩,向世界展示你的创意! …...

解决Hadoop审计日志hdfs-audit.log过大的问题
【背景】 新搭建的Hadoop环境没怎么用,就一个环境天天空跑,结果今天运维告诉我说有一台服务器磁盘超过80%了,真是太奇怪了,平台上就跑了几个spark测试程序,哪来的数据呢? 【问题调查】 既然是磁盘写满了&…...

【Java】java和kotlin关于Json写文件
Java写json文件 public class WriterJson {public static void main(String[] args) {// 创建一个 JSON 对象JSONObject jsonObject new JSONObject();jsonObject.put("case", "testtest");JSONObject jsonObjects new JSONObject();jsonObjects.put(&q…...

【深度学习】采用自动编码器生成新图像
一、说明 你知道什么会很酷吗?如果我们不需要所有这些标记的数据来训练 我们的模型。我的意思是标记和分类数据需要太多的工作。 不幸的是,大多数现有模型从支持向量机到卷积神经网,没有它们,卷积神经网络就无法训练。无监督学习不…...

华为云交付
文章目录 一、华为云-公有云架构华为公有云的主要服务1.华为云服务—计算类2.华为云服务——存储类3.华为云服务—网络类4.华为云服务—管理和监督类5.华为云数据库 二、待续 一、华为云-公有云架构 华为公有云的主要服务 ECS:弹性云服务器( Elastic Cl…...

dns瞅一瞅
正向解析—域名到ip 反向解析–ip到域名 域名本身是从又往左来解释的 根域—最顶层的域,用null字符标识,通常会省略最后的点和null字符,但是应用程序会在解析dns之前添加这些字符 顶级域— 两种类型,一种国家、地区代码的顶级域…...

springAOP的实例
文章目录 前言一.用户登录权限校验1.1 spring 拦截器1.2 传统的用户登录权限验证1.3 使用拦截器的方式1.4 案例1.5 拦截器实现原理 三.统一异常处理3.1 什么是统一异常处理3.2 具体步骤 四.统⼀数据返回格式4.1 为什么需要统一的数据返回4.2 统一返回数据的格式4.3 统一移除处理…...

【JavaEE】深入了解Spring中Bean的可见范围(作用域)以及前世今生(生命周期)
【JavaEE】Spring的开发要点总结(4) 文章目录 【JavaEE】Spring的开发要点总结(4)1. Bean的作用域1.1 一个例子感受作用域的存在1.2 通过例子说明作用域的定义1.3 六种不同的作用域1.3.1 singleton单例模式(默认作用域…...

P1320 压缩技术(续集版)
题目描述 设某汉字由 N N N \times N NN 的 0 \texttt 0 0 和 1 \texttt 1 1 的点阵图案组成。 我们依照以下规则生成压缩码。连续一组数值:从汉字点阵图案的第一行第一个符号开始计算,按书写顺序从左到右,由上至下。第一个数表示连续有…...

k8s(七) 叩丁狼 service Ingress
负责东西流量(同层级/内部服务网络通信)的通信 service的定义 apiVersion: v1 kind: Service metadata:name: nginx-svclabels:app: nginx-svc spec:ports:- name: http # service 端口配置的名称protocol: TCP # 端口绑定的协议,支持 TCP、…...

Android Studio 关于BottomNavigationView 无法预览视图我的解决办法
一、前言:最近在尝试一步一步开发一个自己的软件,刚开始遇到的问题就是当我们引用 com.google.android.material.bottomnavigation.BottomNavigationView出现了无法预览视图的现象,我也在网上查了很多中解决方法,最后在执行了如下…...

【STM32】小电流FOC驱控一体板(开源)
FOC驱控一体板 主控芯片stm32f103c8t6 驱动芯片drv8313 三相电流采样 根据B站一个UP主的改的(【【自制】年轻人的第一块FOC驱动器】),大多数元器件是0805,实验室具备且便于自己动手焊接 。 晶振用的是无源晶振,体…...

代码分析:循环创建N个子进程——为什么最后一个属于父进程?
黑马C/C 2018年32期代码分析 //循环创建n个子进程 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h>int main() {int i 0;for(i0; i<3; i){//创建子进程pid_t pid fork();if(pid&…...

【SpringBoot面试题整理-超级有效】
文章目录 1.SpringBoot如何解决跨域问题?2.为什么要用Spring Boot?3. Spring Boot的约定优于配置,你的理解是什么?4. SpringBoot有哪些优点?5. Spring Boot中自动装配机制的原理?6.SpringBoot支持哪些日志框…...