Представляю свою библиотеку для обнуления байт выравнивания (padding) в unmanaged структурах.
Зачем это нужно?
Обнуление байт паддинга (padding) обеспечивает детерминированное состояние памяти, что критически важно для двоичного сравнения или вычисления хэша. И не менее важно при бинарной сериализации.
Подробнее о том, что такое паддинг, можно прочитать здесь.
[StructLayout(LayoutKind.Sequential)] struct ExampleStruct { public byte A; // 1 байт // --- 3 байта выравнивания (padding) --- public int B; // 4 байта }
Структуры с неинициализированным паддингом могут быть получены из сторонних библиотек или своего кода.
Если вы когда-нибудь делали что-то подобное, то мои поздравления, у вас в паддинге лежат мусорные байты или возможно конфиденциальные данные.
[SkipLocalsInit] ExampleStruct[] Method0() { Span<ExampleStruct> 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<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; } 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<MyData> arr) { Zeroer.ZeroArray(arr); // После вызова: байты паддинга гарантированно равны 0 } public void Example1(MyData[] arr) { Zeroer.ZeroArray(arr); // После вызова: байты паддинга гарантированно равны 0 } public void Example2(MyData[] arr) { // Тоже самое что и в предыдущем примере, но только через метод-расширение. arr.ZeroPadding(); }
Обнулить паддинги можно только в unmanaged типах: