我正在使用“Reflection.Emit”,并希望生成一个静态类,在C#中公开带有此签名的方法:
unsafe static void CopyStruct<T>(void * dest, ref T src) where T : struct
{
// copy struct from src to dest using ldobj/stobj
}
这是为了能够非常快速(并且不安全地)将blittable结构复制到非托管内存。但是,我遇到了一些关于 OpCodes.Ldobj 和 alignment 的问题。
使用以下两种结构和方法:
struct Def { byte fill0; Int64 fill1; } // size = 16
static void CopyDef(ref Def dest, ref Def src) { dest = src; }
[StructLayout(LayoutKind.Sequential, Pack = 1)] // size = 9
struct Seq { byte fill0; Int64 fill1; }
static void CopySeq(ref Seq dest, ref Seq src) { dest = src; }
我希望这些方法可以生成如下内容:
{ // CopyDef
ldarg.0
ldarg.1
ldobj Def
stobj Def
ret
}
{ // CopySeq
ldarg.0
ldarg.1
unaligned. 1
ldobj Seq
unaligned. 1
stobj Seq
ret
}
相反,我得到的 Seq 的输出与 Def 的输出相同,即在任何地方都没有未对齐的x 操作码。我期望ldobj / stobj将除了自然机器对齐之外的任何东西(例如8/4)视为未对齐,但我显然是错误的。
我的问题是后续跟进:
由于ldobj / stobj操作码必须指定它们正在操作的地址类型,因此“Pack = 1”会以某种方式隐式“处理”吗?
如果是这种情况,这是否意味着具有“Pack = 4”的结构被隐含地“照顾”,只要该地址不引用低于4的偏移(例如2,1),其中如果操作码必须以未对齐x 开头?当然,这只会出现在初始方法签名中的非托管指针。
更新
来自 OpCodes.Ldind_I4 MSDN(1):
所有ldind指令都是Ldobj指令的快捷方式,指定相应的内置值类。
来自 OpCodes.Unaligned MSDN(2):
未对齐指定堆栈上的地址...可能不会与紧随其后的ldind,stind,... ldobj,stobj ...指令的自然大小对齐。也就是说,对于Ldind_I4指令,地址的对齐可能不是4字节边界。
来自 OpCodes.Ldobj MSDN(3):
复制的字节数取决于类的大小(由class参数指定)。 class参数是表示值类型的元数据标记。
来自 OpCodes.Ldind_I4 MSDN(4):
返回地址的所有 MSIL指令的结果(例如,Ldloca和Ldarga)安全对齐。
建议:
任何blittable结构的数组中最差(最低)可能的偏移量是:
int offset = sizeof(myStruct) & 7; // 3 for 32bit
// offset == 0 -> correct
// offset == 1,3,7 -> unaligned. 1
// offset == 2,6 -> unaligned. 2
// offset == 4 -> unaligned. 4
Seq 的大小为9,它给出了7的偏移量,这意味着我们(最坏的情况)未对齐1。
以下CopySeq方法不会生成未对齐的操作码
static void CopySeq(ref Seq dest, Seq[] src) { dest = src[1]; }
{ // CopySeq
ldarg.0
ldarg.1
ldc.i4.1
ldelema Seq
ldobj Seq
stobj Seq
ret
}
除非ldelema对后续的ldobj做了一些魔术,否则ldobj 应该知道如何处理在指定类型的数组中可能发生的任何错位,这可以通过大小来确定那种类型。