IntPtr in Int64 konvertieren: conv.u8 oder conv.i8?

8

Ich arbeite an einer ILGeneratorErweiterung, mit deren Hilfe IL-Fragmente ausgegeben werden können Expression. Alles war in Ordnung, bis ich am Integer-Konvertierungsteil arbeitete. Es gibt etwas, das mir wirklich nicht intuitiv ist, wie:

  • Verwenden Sie conv.i8zum Konvertieren Int32inUInt64
  • Verwenden Sie conv.u8zum Konvertieren UInt32inInt64

Sie sind alle, weil der Evaluierungsstapel die Ganzzahlsignatur nicht verfolgt. Ich verstehe den Grund voll und ganz, es ist nur ein bisschen schwierig damit umzugehen.

Jetzt möchte ich die Konvertierung mit einbeziehen IntPtr. Es muss schwieriger sein, da seine Länge variabel ist. Ich habe mich entschlossen zu sehen, wie der C # -Compiler es implementiert.

Konzentrieren Sie sich nun auf das Besondere IntPtran der Int64Konvertierung. Anscheinend sollte das gewünschte Verhalten sein: No-Op auf 64-Bit-Systemen oder Vorzeichenerweiterung auf 32-Bit-Systemen.

Da in C # das native intvon der IntPtrStruktur umbrochen wird , muss ich mir den Hauptteil seiner Int64 op_Explicit(IntPtr)Methode ansehen . Folgendes wird von dnSpy aus .NET Core 3.1.1 zerlegt:

.method public hidebysig specialname static 
    int64 op_Explicit (
        native int 'value'
    ) cil managed 
{
    .custom instance void System.Runtime.CompilerServices.IntrinsicAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = (
        01 00 00 00
    )
    .maxstack 8

    IL_0000: ldarga.s  'value'
    IL_0002: ldfld     void* System.IntPtr::_value
    IL_0007: conv.u8
    IL_0008: ret
}

Es ist komisch, dass conv.u8hier erscheint! Auf 32-Bit-Systemen wird eine Nullverlängerung durchgeführt. Ich habe das mit folgendem Code bestätigt:

delegate long ConvPtrToInt64(void* ptr);
var f = ILAsm<ConvPtrToInt64>(
    Ldarg, 0,
    Conv_U8,
    Ret
);
Console.WriteLine(f((void*)(-1)));  // print 4294967295 on x86

Wenn Sie sich jedoch die x86-Anweisungen der folgenden C # -Methode ansehen:

static long Convert(IntPtr intp) => (long)intp;
;from SharpLab
C.Convert(IntPtr)
    L0000: mov eax, ecx
    L0002: cdq
    L0003: ret

Es stellt sich heraus, dass das, was wirklich passiert, eine Zeichenverlängerung ist!

Mir ist aufgefallen, dass Int64 op_Explicit(IntPtr)das ein IntrinsicAttribut hat. Ist es der Fall, dass der Methodenkörper von der Laufzeit-JIT vollständig ignoriert und durch eine interne Implementierung ersetzt wird?

ENDGÜLTIGE Frage: Muss ich mich auf die Konvertierungsmethoden von beziehen IntPtr, um meine Konvertierungen zu implementieren?

Anhang Meine ILAsmImplementierung:

static T ILAsm<T>(params object[] insts) where T : Delegate =>
    ILAsm<T>(Array.Empty<(Type, string)>(), insts);

static T ILAsm<T>((Type type, string name)[] locals, params object[] insts) where T : Delegate
{
    var delegateType = typeof(T);
    var mi = delegateType.GetMethod("Invoke");
    Type[] paramTypes = mi.GetParameters().Select(p => p.ParameterType).ToArray();
    Type returnType = mi.ReturnType;

    var dm = new DynamicMethod("", returnType, paramTypes);
    var ilg = dm.GetILGenerator();

    var localDict = locals.Select(tup => (name: tup.name, local: ilg.DeclareLocal(tup.type)))
        .ToDictionary(tup => tup.name, tup => tup.local);

    var labelDict = new Dictionary<string, Label>();
    Label GetLabel(string name)
    {
        if (!labelDict.TryGetValue(name, out var label))
        {
            label = ilg.DefineLabel();
            labelDict.Add(name, label);
        }
        return label;
    }

    for (int i = 0; i < insts.Length; ++i)
    {
        if (insts[i] is OpCode op)
        {
            if (op.OperandType == InlineNone)
            {
                ilg.Emit(op);
                continue;
            }
            var operand = insts[++i];
            if (op.OperandType == InlineBrTarget || op.OperandType == ShortInlineBrTarget)
                ilg.Emit(op, GetLabel((string)operand));
            else if (operand is string && (op.OperandType == InlineVar || op.OperandType == ShortInlineVar))
                ilg.Emit(op, localDict[(string)operand]);
            else
                ilg.Emit(op, (dynamic)operand);
        }
        else if (insts[i] is string labelName)
            ilg.MarkLabel(GetLabel(labelName));
        else
            throw new ArgumentException();
    }
    return (T)dm.CreateDelegate(delegateType);
}
kevinjwz
quelle
Es ist ein kniffliger Eckfall, es gibt keine ideale Lösung. Was Sie am meisten auslöst, ist, dass Sie in der IL nicht sehen, dass es zwei unterschiedliche Konvertierungen gibt . Der in 32-Bit-Plattformen verwendete (int) Cast ist nicht für 64-Bit-Varianten geeignet.
Hans Passant
@ HansPassant Du hast recht. Im x86-Modus erhalte ich ein anderes IL-Byte-Array Int64 op_Explicit(IntPtr)als im x64-Modus. Wie wird das erreicht? Ich habe den Dateipfad untersucht, aus dem die System.Private.CoreLibAssembly geladen wird (von Assembly.Location), aber sie sind zwischen x86 und x64 identisch.
kevinjwz
Nicht der gleiche Pfad, c: \ Programme gegen c: \ Programme (x86). Aber das ist nicht der Punkt, es ist ein intrinsischer, so unterschiedlicher Jitter. Nicht leicht zu erkennen, müssten Sie einen nicht verwalteten Debugger verwenden.
Hans Passant
@ HansPassant Wieder hast du recht. Ich wusste nicht, dass die Option "32-Bit bevorzugen" nach .Net Core 3.0 ignoriert wird, und vielleicht war ich dadurch verwirrt. In der Tat gibt es verschiedene Assembly-Dateien.
Kevinjwz
Ich werde selbst eine Antwort schreiben.
Kevinjwz

Antworten:

3

Ich habe einen Fehler gemacht. Int64 op_Explicit(IntPtr)hat zwei Versionen. Die 64-Bit-Version befindet sich unter "C: \ Programme \ dotnet ..." und implementiert:

.method public hidebysig specialname static 
    int64 op_Explicit (
        native int 'value'
    ) cil managed 
{
    .maxstack 8

    IL_0000: ldarga.s  'value'
    IL_0002: ldfld     void* System.IntPtr::_value
    IL_0007: conv.u8
    IL_0008: ret
}

Die 32-Bit-Version befindet sich unter "C: \ Programme (x86) \ dotnet ..." und hat folgende Implementierung:

.method public hidebysig specialname static 
    int64 op_Explicit (
        native int 'value'
    ) cil managed 
{
    .maxstack 8

    IL_0000: ldarga.s  'value'
    IL_0002: ldfld     void* System.IntPtr::_value
    IL_0007: conv.i4
    IL_0008: conv.i8
    IL_0009: ret
}

Puzzle gelöst!

Dennoch denke ich, dass es möglich ist, eine identische Implementierung sowohl im 32-Bit- als auch im 64-Bit-Build zu verwenden. Man conv.i8wird die Arbeit hier machen.

In der Tat könnte ich meine Aufgabe, IntPtrKonvertierungen zu senden, vereinfachen , da zur Laufzeit die Länge von 'IntPtr' bekannt ist (meines Wissens entweder 32 oder 64) und die meisten ausgegebenen Methoden nicht gespeichert und wiederverwendet werden. Aber ich möchte immer noch eine laufzeitunabhängige Lösung, und ich glaube, ich habe bereits eine gefunden.

kevinjwz
quelle