IntPtrをInt64に変換:conv.u8またはconv.i8?


8

私は、ILGeneratorを使用してILフラグメントを放出するのに役立つ拡張機能に取り組んでいExpressionます。整数変換の部分に取り掛かるまで、すべてが順調でした。私には本当に直感に反するものがあります。

  • conv.i8に変換するInt32ために使用UInt64
  • conv.u8に変換するUInt32ために使用Int64

これらはすべて、評価スタックが整数の符号を追跡しないためです。その理由は完全に理解しています。処理するのは少し難しいです。

今、私はを含む変換をサポートしたいと思いIntPtrます。長さが可変であるため、トリッキーになる必要があります。私はC#コンパイラーがそれをどのように実装するかを調べることにしました。

今特定に焦点を当てIntPtrInt64変換。どうやら望ましい動作は次のとおりです。64ビットシステムでは何もしないか、32ビットシステムでは符号拡張します。

C#ではstruct native intによってラップされてIntPtrいるため、そのInt64 op_Explicit(IntPtr)メソッドの本体を確認する必要があります。以下は、.NETコア3.1.1のdnSpyによって逆アセンブルされます。

.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
}

conv.u8ここに登場するのは変だ!32ビットシステムでゼロ拡張を実行します。私は次のコードでそれを確認しました:

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

ただし、次のC#メソッドのx86命令を見ると、

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

実際に起こっていることは、符号拡張です!

属性Int64 op_Explicit(IntPtr)があることに気づきましたIntrinsic。メソッド本体がランタイムJITによって完全に無視され、一部の内部実装によって置き換えられるのは事実ですか?

最後の質問:変換IntPtrを実装するには、の変換方法を参照する必要がありますか?

付録私のILAsm実装:

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);
}

これはトリッキーなコーナーケースであり、理想的なソリューションはありません。何よりもあなたをつまずかせるものは、2つの異なる変換があることをILから見ていないことです。32ビットプラットフォームで使用される(int)キャストは、64ビットフレーバーには適していません。
ハンスパッサント

@HansPassantあなたは正しいです。x86モードでInt64 op_Explicit(IntPtr)は、x64モードとは異なるILバイト配列を取得します。これはどのように達成されますか?System.Private.CoreLib(によってAssembly.Location)アセンブリが読み込まれるファイルパスを調査しましたが、x86とx64で同じです。
kevinjwz

同じパスではありません、c:\ programファイルとc:\ programファイル(x86)。しかし、それはポイントではありません、それは本質的に非常に異なるジッターです。見にくいですが、アンマネージデバッガーを使用する必要があります。
ハンスパッサント

@HansPassant再びあなたは正しいです。.Net Core 3.0以降は「32ビット優先」オプションが無視されることを知りませんでした。実際、さまざまなアセンブリファイルがあります。
kevinjwz

自分で答えを書きます。
kevinjwz

回答:


3

間違えました。Int64 op_Explicit(IntPtr)2つのバージョンがあります。64ビットバージョンは「C:\ Program Files \ dotnet ...」にあり、その実装は次のとおりです。

.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
}

32ビットバージョンは「C:\ Program Files(x86)\ dotnet ...」にあり、その実装は次のとおりです。

.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
}

パズルが解けました!

それでも、32ビットと64ビットの両方のビルドで1つの同じ実装を使用することは可能だと思います。1conv.i8、ここで作業を行います。

実際、IntPtr実行時に 'IntPtr'の長さがわかっているため(私の知識では32または64のいずれか)、変換を発行するタスクを簡略化でき、ほとんどの発行されたメソッドは保存および再利用されません。しかし、私はまだランタイムに依存しないソリューションを望んでおり、私はすでにそれを見つけたと思います。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.