Представляю свою библиотеку для обнуления байт выравнивания (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 типах:

  • sbytebyteshortushortintuintlongulongnintnuintcharfloatdoubledecimal, or bool

  • Любой enum

  • Любой pointer

  • Кортеж, члены которого являются неуправляемым типом

  • Любой определяемый пользователем тип структуры, содержащий только поля неуправляемых типов.