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

news2024/10/5 19:31:29

【前言】

 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.coloradmin.cn/o/841871.html

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

相关文章

关于游戏的笔记

关于搭建秦时明月2一键端&#xff0c;并且开启秘境神秘商人东海寻仙幻化 1.该游戏下主要的目录 gm端 服务框架 服务端 2.修改对应的文件 C:\qs\Q2Server\server\conf_common\ManagerAddress.xmlC:\qs\Q2Server\server\conf_manager\GateServer.xml修改ip 3.启动gm startup…

SpringCloud(32):Nacos配置管理应用于分布式系统

1 从单体架构到微服务 1.1 单体架构 Web应用程序发展的早期&#xff0c;大部分web工程师将所有的功能模块打包到一起并放在一个web容器中运行&#xff0c;所有功能 模块使用同一个数据库&#xff0c;同时&#xff0c;它还提供API或者UI访问的web模块等。 尽管也是模块化逻辑…

事务,不只ACID | 京东物流技术团队

1. 什么是事务&#xff1f; 应用在运行时可能会发生数据库、硬件的故障&#xff0c;应用与数据库的网络连接断开或多个客户端端并发修改数据导致预期之外的数据覆盖问题&#xff0c;为了提高应用的可靠性和数据的一致性&#xff0c;事务应运而生。 从概念上讲&#xff0c;事务…

ML之特征工程进阶

术语表 术语 释义 sklearn fraternization 特征工程 Feature scaling 特征缩放 Feature Retrieval 特征检索 NLP 全称: Natural Language Processing 自然语言处理 Corpus 语料库 特征工程概述 定义 特征工程并非是一个问题&#xff0c;而是关于特征的一系列问题…

这应该是最全的,Fiddler手机App抓包详解,看完还不会来找我...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 什么是抓包&#…

Centos7配置网卡信息及固定IP

找到网卡配置文件 Centos7之后的网卡配置文件统一放在/etc/sysconfig/network-scripts&#xff0c;在这个目录会找到以ifcfg开头的&#xff0c;和本机网卡数量对应的配置文件&#xff0c;如下: 执行该命令,进入该目录: cd /etc/sysconfig/network-scripts 再执行该命令 ll …

DAY02_Spring—第三方资源配置管理Spring容器Spring注解开发Spring整合Mybatis和Junit

目录 一 第三方资源配置管理1 管理DataSource连接池对象问题导入1.1 管理Druid连接池1.2 管理c3p0连接池 2 加载properties属性文件问题导入2.1 基本用法2.2 配置不加载系统属性2.3 加载properties文件写法 二 Spring容器1 Spring核心容器介绍问题导入1.1 创建容器1.2 获取bean…

Killing LeetCode [83] 删除排序链表中的重复元素

Description 给定一个已排序的链表的头 head &#xff0c; 删除所有重复的元素&#xff0c;使每个元素只出现一次 。返回 已排序的链表 。 Intro Ref Link&#xff1a;https://leetcode.cn/problems/remove-duplicates-from-sorted-list/ Difficulty&#xff1a;Easy Tag&am…

生信学院|08月18日《基于Flow Simulation的冷链运输产品案例》

课程主题&#xff1a;基于Flow Simulation的冷链运输产品案例 课程时间&#xff1a;2023年08月18日 14:00-14:30 主讲人&#xff1a;江流洋 生信科技 CAE专家 1、达索仿真方案介绍 2、项目介绍 3、案例分析 请安装腾讯会议客户端或APP&#xff0c;微信扫描海报中的二维码…

消息队列常见问题(1)-如何保障不丢消息

目录 1. 为什么消息队列会丢消息&#xff1f; 2. 怎么保障消息可靠传递&#xff1f; 2.1 生产者不丢消息 2.2 服务端不丢消息 2.3 消费者不丢消息 3. 消息丢失如何快速止损&#xff1f; 3.1 完善监控 3.2 完善止损工具 1. 为什么消息队列会丢消息&#xff1f; 现在主流…

0140 数据链路层2

目录 3.数据链路层 3.6局域网 3.7广域网 3.8数据链路层设备 部分习题 3.数据链路层 3.6局域网 3.7广域网 3.8数据链路层设备 部分习题 1.如果使用5类UTP来设计一个覆盖范围为200m的10BASE-T以太网&#xff0c;需要采用的设备是&#xff08;&#xff09; A.放大器 …

11. Redis基础知识

文章目录 一、概述二、数据类型STRINGLISTSETHASHZSET 三、数据结构字典跳跃表 四、使用场景计数器缓存查找表消息队列会话缓存分布式锁实现其它 五、Redis 与 Memcached数据类型数据持久化分布式内存管理机制 六、键的过期时间七、数据淘汰策略八、持久化RDB 持久化AOF 持久化…

网络可靠性之链路聚合

网络的可靠性 网络的可靠性指当设备或者链路出现单点或者多点故障时保证网络服务不间断的能力网络的可靠性是可以从单板、设备、链路多个层面实现。 链路聚合 以太网链路聚合&#xff1a; 通过将多个物理接口捆绑成为一个逻辑接口&#xff0c;可以再不进行硬件升级的条件下&a…

新手注意事项-visual studio 来实现别踩白块儿

自己之前为了熟悉easyx练习过一个简单的项目&#xff0c;别踩白块儿&#xff0c;链接在这里&#xff0c;别踩白块儿&#xff0c;当时比较稚嫩&#xff0c;很多东西都不会&#xff0c;可以说是只知道最基本的语法&#xff0c;头文件都不知道&#xff0c;一个一个查资料弄懂的&am…

实现无限存储:基于JuiceFS 创建 Samba 和 NFS 共享

随着企业数据量的持续增长&#xff0c;存储容量需求日益增大。如何采用没有容量上限的云存储替换本容量有限的本地磁盘&#xff0c;已成为广泛的需求和共识。特别是在企业中常用的 Samba 和 NFS 共享&#xff0c;如果能够使用云存储作为底层存储&#xff0c;就能有效解决存储扩…

产品体系架构202308版

1.前言 当我们不断向前奔跑时&#xff0c;需要回头压实走过的路。不断扩张的同时把相应的内容沉淀下来&#xff0c;为后续的发展铺垫基石。 不知从何时起&#xff0c;产品的架构就面向了微服务/中台化/前后端分离/低代码化/分布式/智能化/运行可观测化的综合体&#xff0c;让…

API接口用例生成器

一、前言 随着自动化测试技术的普及&#xff0c;已经有很多公司或项目&#xff0c;多多少少都会进行自动化测试。 目前本部门的自动化测试以接口自动化为主&#xff0c;接口用例采用 Excel 进行维护&#xff0c;按照既定的接口用例编写规则&#xff0c;对于功能测试人员来说只…

SQL Server数据库如何添加Oracle链接服务器(Windows系统)

SQL Server数据库如何添加Oracle链接服务器 一、在添加访问Oracle的组件1.1 下载Oracle的组件 Oracle Provider for OLE DB1.2 注册该组件1.2.1 下载的压缩包解压位置1.2.2 接着用管理员运行Cmd 此处一定要用管理员运行&#xff0c;否则会报错 二、配置环境变量三、 重启SQL Se…

再探C++——默认成员函数

目录 一、构造函数 二、析构函数 三、赋值运算符 四、拷贝构造 如果一个类中没有成员&#xff0c;我们称为空类。空类&#xff0c;也存在6个默认的类成员函数。 默认成员函数&#xff1a;用户不显示地写&#xff0c;编译器会默认生成的函数叫做默认成员函数。 6个默认成员…

系统架构设计高级技能 · 系统质量属性与架构评估(二)【系统架构设计师】

系列文章目录 系统架构设计高级技能 软件架构概念、架构风格、ABSD、架构复用、DSSA&#xff08;一&#xff09;【系统架构设计师】 系统架构设计高级技能 系统质量属性与架构评估&#xff08;二&#xff09;【系统架构设计师】 系统架构设计高级技能 软件可靠性分析与设计…