StructPadding
Представляю свою библиотеку для обнуления байт выравнивания (padding) в unmanaged структурах.
Зачем это нужно?
Обнуление байт паддинга (padding) обеспечивает детерминированное состояние памяти, что критически важно для двоичного сравнения или вычисления хэша. И не менее важно при бинарной сериализации.
Подробнее о том, что такое паддинг, можно прочитать здесь.
[StructLayout(LayoutKind.Sequential)]
struct ExampleStruct
{
public byte A; // 1 байт
// --- 3 байта выравнивания (padding) ---
public int B; // 4 байта
}
Структуры с неинициализированным паддингом могут быть получены из сторонних библиотек или своего кода.
Если вы когда-нибудь делали что-то подобное, то мои поздравления, у вас в паддинге лежат мусорные байты или возможно конфиденциальные данные.
[SkipLocalsInit]
ExampleStruct[] Method0()
{
Span arr = stackalloc ExampleStruct[10];
for (var i = 0; i < arr.Length; i++)
{
ref var s = ref arr[i];
s.A = (byte) i;
s.B = i * 10;
}
return arr.ToArray();
}
[SkipLocalsInit]
ExampleStruct[] Method1()
{
var arr = GC.AllocateUninitializedArray(10);
for (var i = 0; i < arr.Length; i++)
{
ref var s = ref arr[i];
s.A = (byte) i;
s.B = i * 10;
}
return arr;
}
ExampleStruct Method2()
{
Unsafe.SkipInit(out ExampleStruct s);
s.A = 5;
s.B = 10;
return s;
}
И в этом нет ничего страшного, пока вы не решите посчитать хеш, сделать двоичное сравнение или сохранить структуру в бинарном виде, например, в файл. Хеши и сравнения будут сломаны, потому что при, казалось бы, одинаковых значениях реальные байты структур будут отличаться. А сериализация будет потенциальным местом для утечки конфиденциальных данных, т. к. в паддинг попадёт то, что было ранее записано в этот участок памяти.
Что делать?
Решение в лоб. Для каждой структуры можно прописать оффсеты с паддингом и обнулять по этим оффсетам. Но это даже не обсуждается, такой способ подходит разве что при обучении программированию.
Наивное решение. Пройтись рефлексией по всем полям структуры, посчитать оффсеты паддингов и сохранить их в массив. Массив оффсетов кешировать для переиспользования. Когда нужно обнулить падиинги, делать это по оффсетам, полученным ранее.
И это вполне рабочее решение, если не важна производительность.
Не забываем, что структуры могут иметь десятки полей, которые могут быть другими структурами, а уровень вложенности ничем не ограничен.
Но можно пойти дальше. Собрать оффсеты. И создать в рантайме DynamicMethod, который будет равносилен тому, как если бы мы руками для каждой структуры прописали, какие байты нужно обнулить:
*(ptr + offset) = (byte) 0;
Т.е. это то, что было предложено в «решении в лоб», но не требует участия человека.
Пример кода из моей библиотеки StructPadding:
private static ZeroAction? CreateZeroer(Type type)
{
var regions = AnalyzePadding(type);
if (regions.Count == 0) return null;
var method = new DynamicMethod($"ZeroPadding_{type.Name}",
null,
[ typeof(byte*) ],
typeof(Zeroer).Module,
true);
var il = method.GetILGenerator();
foreach (var region in regions)
{
switch (region.Length)
{
case 1:
il.Emit(OpCodes.Ldarg_0); // push ptr
il.Emit(OpCodes.Ldc_I4, region.Offset); // push offset
il.Emit(OpCodes.Add); // ptr + offset
il.Emit(OpCodes.Ldc_I4_0); // 0
il.Emit(OpCodes.Stind_I1); // *(ptr+offset) = (byte) 0
break;
case 2:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, region.Offset);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Stind_I2); // *(short*) = 0
break;
case 4:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, region.Offset);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Stind_I4); // *(int*) = 0
break;
case 8:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, region.Offset);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ldc_I8, 0L);
il.Emit(OpCodes.Stind_I8); // *(long*) = 0
break;
default:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, region.Offset);
il.Emit(OpCodes.Add); // Destination address
il.Emit(OpCodes.Ldc_I4_0); // Value (0)
il.Emit(OpCodes.Ldc_I4, region.Length); // Size
il.Emit(OpCodes.Initblk); // memset
break;
}
}
il.Emit(OpCodes.Ret);
return (ZeroAction) method.CreateDelegate(typeof(ZeroAction));
}
Такой динамический метод скомпилируется при первом вызове, а все последующие вызовы не будут отличаться от любого другого метода, который был написан руками в IDE.
А это означает, что нет рефлексии и итерации по списку полей в Hot Path. Поиск паддингов делается только один раз, поддерживаются структуры с произвольным количеством полей и любым уровнем вложенности.
Это я и сделал в StructPadding.
StructPadding
Скачать можно здесь:
Github: https://github.com/viruseg/StructPadding
Nuget: https://www.nuget.org/packages/StructPadding
Как использовать?
Обнуление паддинга в структуре:
using StructPadding;
[StructLayout(LayoutKind.Sequential)]
public struct MyData
{
public byte Id; // После этого поля будет 7 байт паддинга
public long Value;
}
void Example(MyData data)
{
Zeroer.Zero(ref data);
// После вызова: байты паддинга гарантированно равны 0
}
Обнуление паддинга в массиве:
public void Example0(Span arr)
{
Zeroer.ZeroArray(arr);
// После вызова: байты паддинга гарантированно равны 0
}
public void Example1(MyData[] arr)
{
Zeroer.ZeroArray(arr);
// После вызова: байты паддинга гарантированно равны 0
}
public void Example2(MyData[] arr)
{
// Тоже самое что и в предыдущем примере, но только через метод-расширение.
arr.ZeroPadding();
}
Обнулить паддинги можно только в unmanaged типах: