当前位置: 首页 > news >正文

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

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

一、结构对齐

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

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

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

案例1

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

案例2

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

查看具体的地址

//使用神器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);

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

#pragma pack(1)
struct TestInfo
{char username[10]; //0-10double userdata;//10-18
};

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

struct TestInfo
{char username[10]; //0-10double userdata;//16-24
};

这与不使用#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;

二、调用约定

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

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

三、常用数据对应关系

常用的数据结构类型对比

C#C/C++
sbyte/charchar
shortshort
intint
longlong long/int64_t
floatfloat
doubledouble
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. 在Native.cpp中实现函数,并编译为DLL路径
#include "Native.h"
#include<iostream>
#include<Windows.h>
HEAD void CallingConvention Test1()
{printf("调用成功\n");
}
//其实是使用上面的宏定义替代下面的语句
//extern "C" __declspec(dllexport) void __cdecl Test1()
//{
//	printf("调用成功\n");
//}

在C#中调用

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

[DllImport("../../NativeDll.dll")]
public static extern void Test1(); //定义外部函数
static void Main(string[] args)
{Test1();
}

在C++中调用

#include <iostream>
#include <stdarg.h>
#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();
}

五、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);
}
[DllImport("../../NativeDll.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void TestBasicData(char d1,short d2,int d3,long d4,float d5,double d6
);

按引用传递基本数据类型

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
http://www.lryc.cn/news/204036.html

相关文章:

  • MyBatis篇---第五篇
  • 十三水中各种牌型判断LUA版
  • 2023.10.19 关于设计模式 —— 单例模式
  • 4个鲜为人知的Python迭代过滤函数
  • 使用logger.error(“自定义错误信息描述“,e)将错误信息输出到日志文件上
  • 音乐的数字未来:虚拟演唱会与TikTok的巅峰融合
  • 基于图像识别的跌倒检测算法 计算机竞赛
  • NSS [SWPUCTF 2021 新生赛]PseudoProtocols
  • 字节码进阶之JVM Attach API详解
  • Kubernetes 部署 kubeflow1.6.1
  • 设计模式:建造者模式(C#、JAVA、JavaScript、C++、Python、Go、PHP)
  • Maxon Cinema 4D 2024:打造独一无二的视觉效果 模拟模块大更新
  • 16.2 ARP 主机探测技术
  • 三级等保-linux服务器三权分立设置
  • 抓取网页的含义和URL基本构成
  • 计算机毕业设计 机器学习深度学习人工智能
  • 施密特正交化
  • 低代码开发:加速应用开发的利器
  • 数据安全发展趋势与密码保护技术研究
  • 368周赛leetcode
  • Vue 的 nextTick:深入理解异步更新机制
  • SQL关于日期的计算合集
  • shell_44.Linux使用 getopt 命令
  • Linux备份Docker的mysql数据并传输到其他服务器保证数据级容灾
  • 【vue+nestjs】qq第三方授权登录【超详细】
  • 经典卷积神经网络 - VGG
  • 系统集成测试(SIT)/系统测试(ST)/用户验收测试(UAT)
  • Android Gradle8.0以上多渠道写法以及针对不同渠道导入包的方式,填坑!
  • hdlbits系列verilog解答(向量门操作)-14
  • 工厂模式(初学)