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》

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IPv6地址分类,EUI-64转换规则

1、可聚合的单全球单播地址Global Unique Address&#xff1a; Aggregate global unicast address&#xff0c;前3位是001&#xff0c;即2000::/3&#xff0c;目前IANA已经将一部分可聚合全球单播进行了专门使用&#xff0c;如&#xff1a;2001::/16用于IPV6互联网&#xff0c;…

流量分析日志查看

一流量分析 buuctf wireshark 从题目出发&#xff0c;既然是上传登录信息&#xff0c;就直接过滤post请求&#xff0c;即搜索 http.request.methodPOST&#xff0c;因为上传用户登录信息使用的一定是http里的post方法 模式过滤 http.request.method “GET” http.request.…

无涯教程-Perl - getpriority函数

描述 此函数返回进程(PRIO_PROCESS),进程组(PRIO_PGRP)或用户(PRIO_USER)的当前优先级。 参数WHICH指定要为PRIO_PROCESS,PRIO_PGRP或PRIO_USER之一设置优先级的实体,WHO是要设置的进程ID或用户ID。 WHO的值为0定义了当前流程,流程组或用户。这会在不支持系统getpriority()函…

Springboot后端通过路径映射获取本机图片资源

项目场景&#xff1a; 项目中对图片的处理与查看是必不可少的&#xff0c;本文将讲解如何通过项目路径来获取到本机电脑的图片资源 如图所示&#xff0c;在我的本机D盘的图片测试文件夹(文件夹名字不要有中文)下有一些图片&#xff0c; 我们要在浏览器上访问到这些图片&#…

RISC-V基础之函数调用(二)栈与寄存器(包含实例)

堆栈是一种后进先出&#xff08;LIFO&#xff09;的队列&#xff0c;用于存储函数调用时的临时数据和现场数据。堆栈指针sp&#xff08;寄存器2&#xff09;是一个普通的RISC-V寄存器&#xff0c;按照惯例&#xff0c;指向堆栈的顶部。堆栈从高地址向低地址增长&#xff0c;即当…

【UE4 RTS】04-Camera Pan

前言 本篇实现了CameraPawn的旋转功能。 效果 步骤 1. 打开项目设置&#xff0c;添加两个操作映射 2. 打开玩家控制器“RTS_PlayerController_BP”&#xff0c;新建一个浮点型变量&#xff0c;命名为“PanSpeed” 在事件图表中添加如下节点 此时运行游戏可以发现当鼠标移动…

WEB集群——负载均衡集群

目录 一、 LVS-DR 群集。 1、LVS-DR工作原理 2、LVS-DR模式的特点 3、部署LVS-DR集群 3.1 配置负载调度器&#xff08;192.168.186.100&#xff09; 3.2 第一台web节点服务器&#xff08;192.168.186.103&#xff09; 3.3 第二台web节点服务器&#xff08;192.168.186.…

Q-Vision+Kvaser CAN/CAN FD/LIN总线解决方案

智能联网技术在国内的发展势头迅猛&#xff0c;随着汽车智能化、网联化发展大潮的到来&#xff0c;智能网联汽车逐步成为汽车发展的主要趋势。越来越多整车厂诉求&#xff0c;希望可以提供本土的测量软件&#xff0c;特别是关于ADAS测试。而Softing中国推出的Q-Vision软件不仅可…

f12 CSS网页调试_css样式被划了黑线怎么办

我的问题是这样的 class加上去了,但是样式不生效,此时可能是样式被其他样式覆盖了, 解决方案就是 给颜色后边添加一个!important

Docker的入门与使用

什么是Docker&#xff1f; docker官网 简介与概述 Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言 并遵从 Apache2.0 协议开源。 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#x…

RISCV 5 RISC-V调用规则

RISCV 5 RISC-V调用规则 1 Register Convention1.1 Integer Register Convention1.2 Floating-point Register Convention 2. Procedure Calling Convention2.1 Integer Calling Convention2.2 Hardware Floating-point Calling Convention2.3 ILP32E Calling Convention2.4 Na…

【flink】使用flink-web-ui提交作业报错

使用WebUI提交作业出现错误。 错误截图&#xff1a; 弹框信息&#xff1a; Server Response Message: org.apache.flink.runtime.rest.handler.RestHandlerException: Could not execute application.at org.apache.flink.runtime.webmonitor.handlers.JarRunHandler.lambda$h…

webshell免杀项目-Auto-JSPwebshell(五)

Auto-JSPwebshell/jsp免杀/webshell免杀/自动生成 项目地址&#xff1a; https://github.com/G0mini/Bypass 具体使用请参考&#xff1a; https://mp.weixin.qq.com/s/9-__B0MBRSXHla6O0KU7Gg

缓解针对LLM应用程序的存储提示注入攻击

推荐&#xff1a;使用 NSDT场景编辑器 助你快速搭建可编辑的3D应用场景 LLM提供提示文本&#xff0c;并根据其已训练和访问的所有数据进行响应。为了用有用的上下文补充提示&#xff0c;一些 AI 应用程序捕获来自用户的输入&#xff0c;并在将最终提示发送到 LLM 之前将用户看不…

08. 容器间通信

目录 1、前言 2、容器间通信 2.1、通过IP地址进行通信 2.2、通过DNS Server进行通信 2.3、通过Joined方式通信 3、容器跨节点通信 3.1、通过容器在宿主机上的端口映射实现 3.2、通过Docker Overlay网络实现 4、小结 1、前言 上一篇《07.Docker网络通信模式》我们初步认…

Qt应用开发(基础篇)——时间微调输入框 QDateTimeEdit、QDateEdit、QTimeEdit

一、前言 QAbstractSpinBox是全部微调输入框的父类&#xff0c;这是一种允许用户通过点击上下箭头按钮或输入数字来调整数值的图形用户界面控件&#xff0c;父类提供了当前值text、对齐方式align、只读readOnly等通用属性和方法。在上一篇数值微调输入框中有详细介绍。 QDateTi…

24届近5年南京航空航天大学自动化考研院校分析

今天给大家带来的是南京航空航天大学控制考研分析 满满干货&#xff5e;还不快快点赞收藏 一、南京航空航天大学 学校简介 南京航空航天大学创建于1952年10月&#xff0c;是新中国自己创办的第一批航空高等院校之一。1978年被国务院确定为全国重点大学&#xff1b;1981年经…

TM4C123库函数学习(1)--- 点亮LED+TM4C123的ROM函数简介+keil开发环境搭建

前言 &#xff08;1&#xff09; 首先&#xff0c;我们需要知道TM4C123是M4的内核。对于绝大多数人而言&#xff0c;入门都是学习STM32F103&#xff0c;这款芯片是采用的M3的内核。所以想必各位对M3内核还是有一定的了解。M4内核就是M3内核的升级版本&#xff0c;他继承了M3的的…

Vue3项目中使用原生input实现excel导入导出功能

重写input样式 首先我们先来重写input的原生样式&#xff0c;毕竟实在不好看。这里的思路很简单input外面套一层div然后让input撑满盒子然后给input隐藏了就行 <div class"bg-[#f8f8f8] w-[430px] h-[220px] rounded-md cursor-pointer relative outline-0">…

智慧影院--java开源电影票优惠券制作系统快速开发

搭建一个智慧影院可以通过使用Java开源电影票优惠券制作系统来快速开发。这个系统可以帮助影院管理电影票的销售和优惠活动&#xff0c;提供便捷的购票方式和优惠券的生成与使用功能。 首先&#xff0c;我们需要建立一个数据库来存储电影、影厅、放映计划、订单等信息。在数据…