• C#调用C/C++从零深入讲解


    C#调用非托管DLL从零深入讲解

    一、结构对齐

    结构对齐是C#调用非托管DLL的必备知识。

    在没有#pragma pack声明下结构体内存对齐的规则为:

    • 第一个成员的偏移量为0,
    • 每个成员的首地址为自身大小的整数倍
    • 子结构体的第一个成员偏移量应当是子结构体最大成员的整数倍
    • 结构体总大小必须是内部最大成员的整数倍

    案例1

    struct  TestFrame
    {
       
    	unsigned char id; //0-1
    	int width; //4-8 必须是本身的整数倍
    	long long height; //8-16
    	unsigned char* data; //16-24 64位系统下,地址占8字节,32位占4字节
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    案例2

    struct TestInfo
    {
       
    	char username[10]; //0-10
    	double userdata;//16-24
    };
    struct  TestFrame
    {
       
    	unsigned char id; //0-1
    	int width; //4-8 必须是本身的整数倍
    	long long height; //8-16
    	unsigned char* data; //16-24 64位系统下,地址占8字节,32位占4字节
    	char mata;//24-25
    	TestInfo info;//32-56 要从子结构体中最大成员的整数倍开始
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    查看具体的地址

    //使用神器0,0可以转换为任意类型的NULL指针
    #define FIELDOFFSET(TYPE,MEMBER) (int)(&(((TYPE*)0)->MEMBER))
    
    //使用
    int infoLen = sizeof(TestInfo);
    int offsetusername = FIELDOFFSET(TestInfo, username);
    int offsetuserdata = FIELDOFFSET(TestInfo, userdata);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    使用#pragma pack 这个宏声明按照几个字节对齐

    #pragma pack(1)
    struct TestInfo
    {
       
    	char username[10]; //0-10
    	double userdata;//10-18
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果我设置#pragma pack(10)则结果

    struct TestInfo
    {
       
    	char username[10]; //0-10
    	double userdata;//16-24
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这与不使用#pragma pack(10)结果相同,也就是说是按照宏声明的和实际数据类型中最大值中的较小的那个来决定

    在C#中自定义结构对齐

    //设置c#的结构对齐,按照1字节对齐
    [StructLayout(LayoutKind.Sequential,Pack =1)]
    public struct TestFrame
    {
       
        public char id; 
        int width; 
        long height; 
        char mata;
    };
    //手动指定偏移量
    [StructLayout(LayoutKind.Explicit)]
    public struct TestFrame
    {
       
        [FieldOffset(0)]
        public char id;
        [FieldOffset(10)]
        int width;
        [FieldOffset(15)]
        long height;
        [FieldOffset(40)]
        char mata;
    };
    //也可以在字段定义使用什么类型
    [MarshalAs(UnmanagedType.BStr)]
    public string name;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    二、调用约定

    经常用到的调用约定有两种,C语言调用约定(_cdecl)和标准调用约定(_stdcall)。两种方式都是按照从右至左的方式入栈,但是C语言调用约定函数本身不清理栈,此工作由调用者负责,所以允许可变参数。而标准调用约定则是函数本身调用栈。

    c语言调用约定和标准调用约定的最大区别在于,谁来清理参数所在的栈,C则调用者来清理,标准则是函数本身来清理。当所调用的程序中有可变参数的函数时,建议采用C语言约定

    三、常用数据对应关系

    常用的数据结构类型对比

    C# C/C++
    sbyte/char char
    short short
    int int
    long long long/int64_t
    float float
    double double
    intPtr/[] void *

    image-20231023160146261

    四、创建并调用dll

    本章节使用C++创建一个dll库,并使用C#和C++来调用

    1. 新建一个dll链接库项目,删除所有文件,新增Native.h和Native.cpp
    2. 在Native.h中
    #pragma once
    //判断是否为C++
    #ifdef __cplusplus
    #define EXTERNC extern "C" //如果是C++,则使用extern "C"
    #else
    #define EXTERNC //如果不是C++,则什么都不用加,后面是空的
    #endif 
    
    #ifdef DLL_IMPORT
    //如果不使用dllimport则函数只能自己使用,别人不能使用
    #define HEAD EXTERNC __declspec(dllimport) //HEAD宏定义--extern "C" __declspec(dllexport)
    #else
    #define HEAD EXTERNC __declspec(dllexport)
    #endif
    
    #define  CallingConvention __cdecl //定义调用约定
    //使用宏定义替代了下面的语句
    //extern "C" __declspec(dllexport) void __cdecl Test1();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    1. 在Native.cpp中实现函数,并编译为DLL路径
    #include "Native.h"
    #include
    #include
    HEAD void CallingConvention Test1()
    {
       
    	printf("调用成功\n");
    }
    //其实是使用上面的宏定义替代下面的语句
    //extern "C" __declspec(dllexport) void __cdecl Test1()
    //{
       
    //	printf("调用成功\n");
    //}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在C#中调用

    一定要设置好对应的NativeDll.dll路径

    [DllImport("../../NativeDll.dll")]
    public static extern void Test1(); //定义外部函数
    static void Main(string[] args)
    {
       
        Test1();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在C++中调用

    #include 
    #include 
    #define DLL_IMPORT //其实可以不用,为了遵循dll库的定义
    #include "../NativeDll/Native.h" //引入Native.h
    #pragma comment(lib,"../bin/NativeDll.lib") //导入库,除了dll,一定要有Lib,lib可以当做是DLL中方法的索引
    int main(int argc,char* argv[])
    {
       
        Test1();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    五、DllImpoert常用参数

    DllImport常见参数有:

    1. dllName:动态链接库的名称,必填

      引用路径: (1)exe运行程序所在的目录

      ​ (2)System32目录

      ​ (3)环境变量目录

      ​ (4)自定义路径,如:DllImport(@“C:\OJ\Bin\Judge.dll”)

    2. CallingConvention:调用约定,常用的为CdeclStdCall,默认约定为StdCall

    3. CharSet:设置字符串编码格式

    4. EntryPoint:函数入口名称,默认使用方法本身的名字

    5. ExactSpelling:是否必须与入口点的拼写完全匹配,默认为true,如果为false,则根据CharSet来找函数的A版本还是W版本。

      这是一个CreateWindow的定义,后面有两个版本W和A。

      image-20231019154616465

      ExactSpelling,如果为true则只会使用CreateWindow,如果为False,则会选择W或者A版本,会自动根据当前系统采用的是那种UNICODE选择

    6. SetLastError:指示方法是否保留Win32的上一个错误,默认false。如果为true,则使用Marshal.GetLastWin32Error()来获取错误码。

    六、基本数据传递和函数返回值

    基本数据类型

    //c++
    HEAD void CallingConvention TestBasicData(char d1, short d2, int d3, long long d4, float d5, double d6)
    {
       
    	printf("d1:%d,d2:%d,d3:%d,d4:%lld,d5:%f,d6:%lf\n", d1, d2, d3, d4, d5, d6);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    [DllImport("../../NativeDll.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern void TestBasicData(
        char d1,
        short d2,
        int d3,
        long d4,
        float d5,
        double d6
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    按引用传递基本数据类型

    HEAD void CallingConvention TestBasicDataRef(char& d1, short& d2, int& d3, long long& d4, float& d5, double& d6)
    {
       
    	d1 = 1;
    	d2 = 2;
    	d3 = 3;
    	d4 = 4;
    	d5 = 5.6f;
    	d6 = 6.7;
    	printf("d1:%d,d2:%d,d3:%d,d4:%lld,d5:%f,d6:%lf\n", d1, d2
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 相关阅读:
    .NET 控制台NLog 使用
    CSS进阶篇——背景图
    赶紧收藏!2024 年最常见 20道 Kafka面试题(五)
    猿创征文 | 云服务器部署——将项目部署到云服务器上
    Linux开发工具
    【SpringCloud微服务项目实战-mall4cloud项目(2)】——mall4cloud-gateway
    设计模式---单例模式
    大规模语言模型高效参数微调--BitFit/Prefix/Prompt 微调系列
    nacos集群部署
    怎么样创建私服 nexus --- maven配置文件的各个标签的作用是什么
  • 原文地址:https://blog.csdn.net/weixin_44064908/article/details/134005627