web-dev-qa-db-fra.com

Ce code dangereux devrait-il également fonctionner dans .NET Core 3?

Je refactorise mes bibliothèques pour utiliser Span<T> Pour éviter les allocations de tas si possible, mais comme je cible également des frameworks plus anciens, j'implémente également des solutions de secours générales. Mais maintenant, j'ai trouvé un problème étrange et je ne sais pas trop si j'ai trouvé un bogue dans .NET Core 3 ou si je fais quelque chose d'illégal.

Le problème:

// This returns 1 as expected but cannot be used in older frameworks:
private static uint ReinterpretNew()
{
    Span<byte> bytes = stackalloc byte[4];
    bytes[0] = 1; // FillBytes(bytes);

    // returning bytes as uint:
    return Unsafe.As<byte, uint>(ref bytes.GetPinnableReference());
}

// This returns garbage in .NET Core 3.0 with release build:
private static unsafe uint ReinterpretOld()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1; // FillBytes(bytes);

    // returning bytes as uint:
    return *(uint*)bytes;
}

Chose intéressante, ReinterpretOld fonctionne bien dans .NET Framework et .NET Core 2.0 (donc je pourrais en être satisfait après tout), mais cela me dérange un peu.

Btw. ReinterpretOld peut également être corrigé dans .NET Core 3.0 par une petite modification:

//return *(uint*)bytes;
uint* asUint = (uint*)bytes;
return *asUint;

Ma question:

Est-ce un bogue ou ReinterpretOld ne fonctionne-t-il dans les anciens frameworks que par accident et dois-je également appliquer le correctif pour eux?

Remarques:

  • La version de débogage fonctionne également dans .NET Core 3.0
  • J'ai essayé d'appliquer [MethodImpl(MethodImplOptions.NoInlining)] à ReinterpretOld mais cela n'a eu aucun effet.
42
György Kőszeg

Ooh, c'est une trouvaille amusante; ce qui se passe ici, c'est que votre section locale est optimisée - il n'y a plus de section locale, ce qui signifie qu'il n'y a pas de .locals init, ce qui signifie que stackalloc se comporte différemment , et n'efface pas l'espace;

private static unsafe uint Reinterpret1()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1;

    return *(uint*)bytes;
}

private static unsafe uint Reinterpret2()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1;

    uint* asUint = (uint*)bytes;
    return *asUint;
}

devient:

.method private hidebysig static uint32 Reinterpret1() cil managed
{
    .maxstack 8
    L_0000: ldc.i4.4 
    L_0001: conv.u 
    L_0002: localloc 
    L_0004: dup 
    L_0005: ldc.i4.1 
    L_0006: stind.i1 
    L_0007: ldind.u4 
    L_0008: ret 
}

.method private hidebysig static uint32 Reinterpret2() cil managed
{
    .maxstack 3
    .locals init (
        [0] uint32* numPtr)
    L_0000: ldc.i4.4 
    L_0001: conv.u 
    L_0002: localloc 
    L_0004: dup 
    L_0005: ldc.i4.1 
    L_0006: stind.i1 
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: ldind.u4 
    L_000a: ret 
}

I pensez Je serais heureux de dire qu'il s'agit d'un bogue du compilateur, ou du moins: un effet secondaire et un comportement indésirables étant donné que des décisions antérieures ont été mises en place pour dire "émettre le .locals init" , spécifiquement pour essayer de garder stackalloc sain d'esprit - mais si les gens du compilateur sont d'accord est C'est à eux de voir.

La solution de contournement est la suivante: traitez l'espace stackalloc comme indéfini (ce qui, pour être juste, est ce que vous êtes censé faire); si vous pensez qu'il s'agit de zéros: mettez-le à zéro manuellement.

35
Marc Gravell