结构对齐是C#调用非托管DLL的必备知识。
在没有#pragma pack
声明下结构体内存对齐的规则为:
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字节
};
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 要从子结构体中最大成员的整数倍开始
};
查看具体的地址
//使用神器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-10
double userdata;//10-18
};
如果我设置#pragma pack(10)
则结果
struct TestInfo
{
char username[10]; //0-10
double userdata;//16-24
};
这与不使用#pragma pack(10)
结果相同,也就是说是按照宏声明的和实际数据类型中最大值中的较小的那个来决定
//设置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/char | char |
short | short |
int | int |
long | long long/int64_t |
float | float |
double | double |
intPtr/[] | void * |
本章节使用C++创建一个dll库,并使用C#和C++来调用
#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();
#include "Native.h"
#include
#include
HEAD void CallingConvention Test1()
{
printf("调用成功\n");
}
//其实是使用上面的宏定义替代下面的语句
//extern "C" __declspec(dllexport) void __cdecl Test1()
//{
// printf("调用成功\n");
//}
一定要设置好对应的NativeDll.dll路径
[DllImport("../../NativeDll.dll")]
public static extern void Test1(); //定义外部函数
static void Main(string[] args)
{
Test1();
}
#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();
}
DllImport
常见参数有:
dllName:动态链接库的名称,必填
引用路径: (1)exe运行程序所在的目录
(2)System32目录
(3)环境变量目录
(4)自定义路径,如:DllImport(@“C:\OJ\Bin\Judge.dll”)
CallingConvention:调用约定,常用的为Cdecl
和StdCall
,默认约定为StdCall
CharSet:设置字符串编码格式
EntryPoint:函数入口名称,默认使用方法本身的名字
ExactSpelling:是否必须与入口点的拼写完全匹配,默认为true,如果为false,则根据CharSet来找函数的A版本还是W版本。
这是一个CreateWindow的定义,后面有两个版本W和A。
ExactSpelling,如果为true则只会使用CreateWindow,如果为False,则会选择W或者A版本,会自动根据当前系统采用的是那种UNICODE选择
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