5

関数に対してグローバルであり、大文字と小文字を区別して型チェックされた解析済み言語を表す判別ユニオンであり、式の型を表すプロパティである再帰関数emit : Map<string,LocalBuilder> -> exp -> unitがあります。il : ILGeneratorexpInstanceCall of exp * MethodInfo * exp list * TypeTypeexp

次のフラグメントでは、インスタンス呼び出しに対してILオペコードを発行しようとしています。インスタンス呼び出しはである場合とそうでinstance.Typeない場合がありますValueTypeOpCodes.Constrainedしたがって、参照、値、および列挙型に対して柔軟かつ効率的に仮想呼び出しを行うために使用できることを理解しています。私はReflection.Emitと機械語全般に慣れていないので、のリンクされたドキュメントを理解することはOpCodes.Constrained私にとって強くありません。

これが私の試みですがVerificationException、「操作によってランタイムが不安定になる可能性があります。」という結果になります。

let rec emit lenv ast =
    match ast with
    ...
    | InstanceCall(instance,methodInfo,args,_) ->
        instance::args |> List.iter (emit lenv)
        il.Emit(OpCodes.Constrained, instance.Type)
        il.Emit(OpCodes.Callvirt, methodInfo)
    ...

ドキュメントを見ると、キーは「マネージポインタptrがスタックにプッシュされます。ptrのタイプはthisTypeへのマネージポインタ(&)である必要があります。これはプレフィックスなしの場合とは異なることに注意してください。 thisTypeの参照を期待するcallvirt命令。」

アップデート

@Tomasと@descoに感謝します。これで、いつ使用するかがわかりましたOpCodes.Constrainedinstance.TypeはValueTypeですmethodInfo.DeclaringTypeが、参照型です)。

しかし、まだそのケースを考慮する必要はないことがわかりました。私の本当の問題は、スタック上のインスタンス引数でした。値ではなくアドレスが必要であることを知るのに6時間しかかかりませんでした(DLRソースを見てください)。コードから手がかりが得られ、単純なC#プログラムでilasm.exeを使用すると明らかになりました)。

これが私の最終的な作業バージョンです:

let rec emit lenv ast =
    match ast with
    | Int32(x,_) -> 
        il.Emit(OpCodes.Ldc_I4, x)
    ...
    | InstanceCall(instance,methodInfo,args,_) ->
        emit lenv instance
        //if value type, pop, put in field, then load the field address
        if instance.Type.IsValueType then
            let loc = il.DeclareLocal(instance.Type)
            il.Emit(OpCodes.Stloc, loc)
            il.Emit(OpCodes.Ldloca, loc)

        for arg in args do emit lenv arg

        if instance.Type.IsValueType then
            il.Emit(OpCodes.Call, methodInfo)
        else
            il.Emit(OpCodes.Callvirt, methodInfo)
        ...
4

2 に答える 2

3

基本的に、Tomas に同意します。コンパイル時に正確な型がわかっている場合は、正しい呼び出し命令を自分で発行できます。制約付きプレフィックスは通常、汎用コードに使用されます

しかし、ドキュメントには次のようにも書かれています。

制約付きオペコードにより、IL コンパイラは、ptr が値型か参照型かに関係なく、統一された方法で仮想関数を呼び出すことができます。thisType がジェネリック型変数の場合を対象としていますが、制約付きプレフィックスは非ジェネリック型にも機能し、値型と参照型の区別を隠す言語で仮想呼び出しを生成する複雑さを軽減できます。...

制約付きプレフィックスを使用すると、値の型に関する潜在的なバージョン管理の問題も回避できます。制約付きプレフィックスを使用しない場合、値の型が System.Object のメソッドをオーバーライドするかどうかに応じて、異なる IL を発行する必要があります。たとえば、値型 V が Object.ToString() メソッドをオーバーライドする場合、call V.ToString() 命令が発行されます。そうでない場合は、box 命令と callvirt Object.ToString() 命令が発行されます。前者の場合、オーバーライドが後で削除された場合、後者の場合、オーバーライドが後で追加された場合、バージョン管理の問題が発生する可能性があります。

小さなデモンストレーション (残念ながら、私のネットブックには F# がありません):

using System;
using System.Reflection;
using System.Reflection.Emit;

public struct EvilMutableStruct
{
    int i;
    public override string ToString()
    {
            i++;
            return i.ToString();
    }
}

class Program
{
    public static void Main()
    {
            var intToString = Make<int>();
            var stringToString = Make<string>();
            var structToString = Make<EvilMutableStruct>();
            Console.WriteLine(intToString(5));
            Console.WriteLine(stringToString("!!!"));   
            Console.WriteLine(structToString (new EvilMutableStruct())); 
    }

    static MethodInfo ToStringMethod = new Func<string>(new object().ToString).Method;
    static MethodInfo ConcatMethod = new Func<string, string, string>(String.Concat).Method;

    // x => x.ToString() + x.ToString()
    private static Func<T, string> Make<T>()
    {
            var dynamicMethod = new DynamicMethod("ToString", typeof(string), new[] {typeof(T)});
            var il = dynamicMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarga_S, 0);
            il.Emit(OpCodes.Constrained, typeof(T));
            il.Emit(OpCodes.Callvirt, ToStringMethod);

            il.Emit(OpCodes.Ldarga_S, 0);
            il.Emit(OpCodes.Constrained, typeof(T));
            il.Emit(OpCodes.Callvirt, ToStringMethod);

            il.Emit(OpCodes.Call, ConcatMethod);

            il.Emit(OpCodes.Ret);
            return (Func<T, string>)dynamicMethod.CreateDelegate(typeof(Func<T, string>));
     }
}

出力:

55
!!!!!!
12
于 2011-07-30T16:44:51.680 に答える
1

質問の最後に引用したドキュメントの一部が問題の原因だと思います。接頭辞が何のOpCodes.Constrainedためにあるのかよくわかりません (私はあなたよりもドキュメントをよく理解していません)、Microsoft でどのように使用されているか調べてみました :-)。

メソッド呼び出しを発行する動的言語ランタイムのソース コードのスニペットを次に示します。

// Emit arguments
List<WriteBack> wb = EmitArguments(mi, args);

// Emit the actual call
OpCode callOp = UseVirtual(mi) ? OpCodes.Callvirt : OpCodes.Call;
if (callOp == OpCodes.Callvirt && objectType.IsValueType) {
    // This automatically boxes value types if necessary.
    _ilg.Emit(OpCodes.Constrained, objectType);
}
// The method call can be a tail call if [...]
if ((flags & CompilationFlags.EmitAsTailCallMask) == CompilationFlags.EmitAsTail && 
    !MethodHasByRefParameter(mi)) {
    _ilg.Emit(OpCodes.Tailcall);
}
if (mi.CallingConvention == CallingConventions.VarArgs) {
    _ilg.EmitCall(callOp, mi, args.Map(a => a.Type));
} else {
    _ilg.Emit(callOp, mi);
}

// Emit writebacks for properties passed as "ref" arguments
EmitWriteBack(wb);

おそらく彼らの振る舞いに従いたいと思うでしょう-constrainedプレフィックスは値型の仮想呼び出しにのみ使用されるようです。私の解釈では、値型の場合、実際の型が何であるかを知っているので、実際の (制約のない) 仮想呼び出しは必要ありません。

于 2011-07-30T16:04:53.010 に答える