Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow open generic in dynamic method (experimental, not a legal operation, with unpredictable problems) #556

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

wwh1004
Copy link
Contributor

@wwh1004 wwh1004 commented May 8, 2024

Fix #551

Test code for dynamic method with open generic:
DynamicTest.zip

Test code for normal importing:
See #466

Description:
This PR introduces a new constructor for Importer to enable open generic in dynamic method supporting.
When pass a non-null value to the new parameter 'originalDeclaringType', this PR is enabled. It changes some importing behavior to be compatible with illegal open generic dynamic methods.
This PR does not introduce other API changes (including additions, deletions, behavior changes), and will not have any impact on normal importing.
To put it simply, this PR will apply MustTreatTypeAsGenericInstType into whole method body importing, not only method sig and so on.

Known issues:
Due to quirks of the .NET reflection system, it is impossible to distinguish whether the operand in ldtoken is a typedef or a typespec when open generics exist. To keep normal importing behavior can get correct result of ldtoken typedef, when open generics exist and it is impossible to distinguish between typedef and typespec, they are uniformly imported as typedef.

void Method<T>()
{
_ = typeof(List<>);
_ = typeof(List<T>);
}

Test code in DynamicTest.zip if you don't want to download it.

class Program
{
    public static void Main()
    {
        var module = ModuleDefMD.Load(typeof(Program).Assembly.Location);
        module.Context = ModuleDef.CreateModuleContext();
        var mi = typeof(My<,>).GetMethod("Test");
        var md = (MethodDef)module.ResolveToken(mi.MetadataToken);
        var dm = DynamicMethodHelper.ConvertFrom(mi);
        dm.GetType().GetMethod("GetMethodDescriptor", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(dm, null);
        var dmr = new DynamicMethodBodyReader(module, dm, new Importer(module, ImporterOptions.TryToUseDefs, GenericParamContext.Create(md), null, typeof(My<,>)), DynamicMethodBodyReaderOptions.UnknownDeclaringType);
        dmr.Read();
        var instrs = dmr.Instructions;
        instrs.SimplifyMacros(dmr.Locals, dmr.Parameters);
        var origInstrs = md.Body.Instructions;
        origInstrs.SimplifyMacros(md.Body.Variables, md.Parameters);
        var comparer = new SigComparer(SigComparerOptions.DontCompareTypeScope | SigComparerOptions.CompareMethodFieldDeclaringType | SigComparerOptions.IgnoreModifiers);
        for (int i = 0; i < instrs.Count; i++)
        {
            var instr = instrs[i];
            var origInstr = origInstrs[i];
            Console.WriteLine(instr);
            bool b = instr.OpCode == origInstr.OpCode;
            Debug.Assert(b);
            if (instr.Operand is IMethod opmeth && opmeth.IsMethod)
                b = comparer.Equals(opmeth, (IMethod)origInstr.Operand);
            else if (instr.Operand is IField opfld && opfld.IsField)
                b = comparer.Equals(opfld, (IField)origInstr.Operand);
            else if (instr.Operand is ITypeDefOrRef optyp)
                b = comparer.Equals(optyp, (ITypeDefOrRef)origInstr.Operand);
            else if (instr.OpCode.OperandType == OperandType.InlineVar || instr.OpCode.OperandType == OperandType.InlineBrTarget)
                b = true;
            else
                b = instr.Operand == null || instr.Operand.Equals(origInstr.Operand);
            if (!b)
            {
                Debug.Assert(instr.OpCode.Code == Code.Ldtoken);
                var optyp1 = instr.Operand as ITypeDefOrRef;
                var optyp2 = origInstr.Operand as TypeSpec;
                var optyp2instSig = optyp2.TypeSig as GenericInstSig;
                Debug.Assert(optyp1 != null && !optyp1.IsTypeSpec && optyp2 != null && optyp2instSig != null);
                var origTypeDef = optyp1.ResolveTypeDef();
                Debug.Assert(optyp2instSig.GenericArguments.Select(t => (t as GenericVar)?.Number).SequenceEqual(origTypeDef.GenericParameters.Select(t => (uint?)t.Number)));
            }
        }
        var exceptions = dmr.ExceptionHandlers;
        var origExceptions = md.Body.ExceptionHandlers;
        var locals = dmr.Locals;
        var origLocals = md.Body.Variables;
        for (int i = 0; i < exceptions.Count; i++)
        {
            var ex = exceptions[i];
            var origEx = origExceptions[i];
            bool b = comparer.Equals(ex.CatchType, origEx.CatchType);
            Debug.Assert(b);
        }
        for (int i = 0; i < locals.Count; i++)
        {
            var local = locals[i];
            var origLocal = origLocals[i];
            bool b = comparer.Equals(local.Type, origLocal.Type);
            Debug.Assert(b);
        }
        Console.WriteLine("OK");
        Console.Read();
    }
}

class My<T1, T2> : Exception where T1 : Exception where T2 : Exception
{
    public static T1 StaticField;
    public T1 InstanceField;

    public static void Test()
    {
        try
        {
            Console.WriteLine(typeof(T1));
            Console.WriteLine(typeof(My<,>));
            Console.WriteLine(typeof(My<T1, T2>));
            Console.WriteLine(typeof(My<T2, T1>));
            Console.WriteLine(StaticField);
            Console.WriteLine(My<Exception, Exception>.StaticField);
            Console.WriteLine(new My<Exception, Exception>().InstanceField);
            Console.WriteLine(new My<T1, T2>().InstanceField);
            Console.WriteLine(new My<T2, T1>().InstanceField);
            Activator.CreateInstance<T1>();
            var v1 = Activator.CreateInstance<My<T1, T2>>();
            var v2 = Activator.CreateInstance<My<T2, T1>>();
            Static();
            GenericStatic(StaticField);
            new My<Exception, T1>().Instance();
            new My<Exception, T2>().GenericInstance("");
            new My<T1, T2>().Instance();
            new My<T2, T1>().GenericInstance("");
            new List<T1>().Add(default);
            new List<int>().Add(0);
            new List<My<T1, T2>>().Add(default);
            new List<List<T1>>().Add(default);
        }
        catch (My<T1, T2> ex)
        {
        }
        catch (My<T2, T1> ex)
        {
        }
        catch (My<Exception, T2> ex)
        {
        }
        catch (T1 ex)
        {
        }
    }

    public static void Static() { }

    public static void GenericStatic<U>(U a) { }

    public void Instance() { }

    public void GenericInstance<U>(U a) { }
}

@ElektroKill @CreateAndInject

@CreateAndInject
Copy link
Contributor

How strange code, type is "System.Console", but declaringType is "My`2"

image

@wwh1004
Copy link
Contributor Author

wwh1004 commented May 8, 2024

How strange code, type is "System.Console", but declaringType is "My`2"

image

This is expected behavior. When type is consistent with originalDeclaringType, it will be treated as GenericInstSig.

@CreateAndInject
Copy link
Contributor

  1. Why extra parameter originalDeclaringType is requirement when DynamicMethodBodyReady already know how to generate instructions exactly (except ldtoken)? originalDeclaringType doesn't support any information which DynamicMethodBodyReady doesn't know.
  2. Currently, Equals/GetHashCode(Import(xxx)) == Equals/GetHashCode(xxx), this PR will break it, that's why I add new ImportAsOperand

@ElektroKill
Copy link
Contributor

ElektroKill commented May 8, 2024

  1. Why extra parameter originalDeclaringType is requirement when DynamicMethodBodyReady already know how to generate instructions exactly (except ldtoken)? originalDeclaringType doesn't support any information which DynamicMethodBodyReady doesn't know.

The information regarding the declaring type is used to determine whether we should interpret the type as an unbound generic instantiation signature or retain original handling. In the case of invalid dynamic methods, we only face a problem with the "declaring type" (in this case My`2) being imported incorrectly. By supplying this declaring type we can more accurately apply the already existing logic for handling unbound types.

2. Currently, Equals/GetHashCode(Import(xxx)) == Equals/GetHashCode(xxx), this PR will break it, that's why I add new ImportAsOperand

The behavior is only changed if you use the new Importer constructor, other constructors behave the same way.

@CreateAndInject
Copy link
Contributor

The information regarding the declaring type is used to determine whether we should interpret the type as an unbound generic instantiation signature or retain original handling.

@ElektroKill That's why I said what your design(eg. Can only find location from ModuleDef, can't determine from MemberDef) is always clumsy yesterday,

originalDeclaringType is passed in outside of dnlib, it means code still doesn't work unless it get updated but it work fine already on old dnlib(eg. 3.3), it's like something when we support an API public void Do<T>(T[] array, int length), a length parameter is requirement when we already know it from array in fact.

I don't know why don't we do everything inside of dnlib as possible.

@ElektroKill
Copy link
Contributor

The information regarding the declaring type is used to determine whether we should interpret the type as an unbound generic instantiation signature or retain original handling.

@ElektroKill That's why I said what your design(eg. Can only find location from ModuleDef, can't determine from MemberDef) is always clumsy yesterday,

originalDeclaringType is passed in outside of dnlib, it means code still doesn't work unless it get updated but it work fine already on old dnlib(eg. 3.3), it's like something when we support an API public void Do<T>(T[] array, int length), a length parameter is requirement when we already know it from array in fact.

I don't know why don't we do everything inside of dnlib as possible.

Implementing strange behavior by default which is only needed for an extremely rare case as default is not a good idea as we might introduce a regression, especially since dnlib does not have any unit tests to verify that everything that worked previously works properly now. The reason older dnlib worked is because its handling of generic import was wrong even for valid dynamic methods. Correcting the behavior to match information from reflection resulted in this edge case occurring for invalid dynamic methods.

@CreateAndInject
Copy link
Contributor

I don't agree it's strange behavior, why there's ImportDeclaringType in dnlib many years ago? Since it's expected behavior rather than strange behavior. DynamicMethodBodyReader can generate such code more than 10 years, no one feel that it's strange, you think strange just because wwh said that 1 month ago. I don't want to talk about this issue anymore, good luck.

Copy link
Contributor

@wtfsck wtfsck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I'll merge this if this works for @CreateAndInject and if @ElektroKill doesn't find any problems with it.

@CreateAndInject
Copy link
Contributor

So everyone must use the Importer's constructor with gp context overload. This will change everyone's usage habits.

@wwh1004 You don't like one PR change everyone's usage habits, but this is the exact thing what you do, we have to use the Importer's constructor with originalDeclaringType parameter for every method to be decrypted, when it's only 1 Importer instance needed in fact.

@ElektroKill
Copy link
Contributor

I don't agree it's strange behavior, why there's ImportDeclaringType in dnlib many years ago? Since it's expected behavior rather than strange behavior. DynamicMethodBodyReader can generate such code more than 10 years, no one feel that it's strange, you think strange just because wwh said that 1 month ago. I don't want to talk about this issue anymore, good luck.

ImportDeclaringType is currently an obsolete member. It redirects to normal Import method since the importing logic was adjusted to properly convert fully valid reflection objects to dnlib objects. The logic using ImportDeclaringType was incorrect and was thus corrected. It's not the same case as the one we are dealing with now. Currently, we have invalid reflection objects which will lead to a crash on execution and we are trying to extract as much of a correct dnlib object as possible (not always possible like in ldtoken case). My stance on whether the creation of unbound generic types being strange is not influenced by wwh. I did my own research and came to conclusion that dynamic methods which are created in this code as invalid and thus, in my opinion, its fine if dnlib cannot produce valid object from invalid object. I hope this clears everything up.

This PR allows partly fixing the importing logic as an opt-in solution which is the best in my opinion. We shouldn't risk potential regressions just to support an edge case which involves invalid reflection objects.

So everyone must use the Importer's constructor with gp context overload. This will change everyone's usage habits.

@wwh1004 You don't like one PR change everyone's usage habits, but this is the exact thing what you do, we have to use the Importer's constructor with originalDeclaringType parameter for every method to be decrypted, when it's only 1 Importer instance needed in fact.

Importer is a struct and is therefore stack allocated, this means that there is very little performance penalty which is negligible in the grand scheme of your application. Furthermore, it's suggested you use a new importer for every method in order to pass in the appropriate GenericParamContext for the given method. Doing it as an opt-in feature means that we don't risk accidentally introducing regression or "breaking change" to people using dnlib currently. Instead, if they want to use the new feature they can.

@CreateAndInject
Copy link
Contributor

var mi = typeof(My<,>).GetMethod("Test");
var md = (MethodDef)module.ResolveToken(mi.MetadataToken);
var dm = DynamicMethodHelper.ConvertFrom(mi);

There's an issue, in my sample, mi and dm represent a same method, but I don't always have the MethodBase instance, if I only have a dynamic object (DynamicMethod/RTDynamicMethod instance), how to prepare the 'originalDeclaringType' parameter?

@ElektroKill
Copy link
Contributor

var mi = typeof(My<,>).GetMethod("Test");
var md = (MethodDef)module.ResolveToken(mi.MetadataToken);
var dm = DynamicMethodHelper.ConvertFrom(mi);

There's an issue, in my sample, mi and dm represent a same method, but I don't always have the MethodBase instance, if I only have a dynamic object (DynamicMethod/RTDynamicMethod instance), how to prepare the 'originalDeclaringType' parameter?

If you are unpacking a method using dynamic approach you can always just resolve the method using reflection. If you access dynamic method from a detour/hook then you can probably use something like Stack trace to find the method.

@CreateAndInject
Copy link
Contributor

Let me say something like this:

      static void Do()
        {
            var dm1 = DynamicMethodBuilder.Build(1057);
            dm1.Invoke(null, ..args..);
            var dm2 = DynamicMethodBuilder.Build(9527);
            dm2.Invoke(null, ..args..);
            var dm3 = DynamicMethodBuilder.Build(15);
            dm3.Invoke(null, ..args..);
        }

        class DynamicMethodBuilder
        {
            public static DynamicMethod Build(int methodID) => throw new NotImplementedException();
        }

@CreateAndInject
Copy link
Contributor

More clear:

        static void Do()
        {
            var walk = DynamicMethodBuilder.Build("walk");
            walk.Invoke(null, ..args..);
            var run = DynamicMethodBuilder.Build("run");
            run.Invoke(null, ..args..);
            var fly = DynamicMethodBuilder.Build("fly");
            fly.Invoke(null, ..args..);
            var jump = DynamicMethodBuilder.Build("jump");
            jump.Invoke(null, ..args..);
        }

        class DynamicMethodBuilder
        {
            public static DynamicMethod Build(string action)
            {
                return action switch
                {
                    "walk" => EmitInstrs(),
                    "run" => ConvertFromMethodBase(),
                    "fly" => BuildByByteArray(),
                    "jump" => BuildByAnyOtherWay(),
                    _ => throw new NotSupportedException(),
                };
            }

            static DynamicMethod EmitInstrs() => throw new NotImplementedException();

            static DynamicMethod ConvertFromMethodBase() => throw new NotImplementedException();

            static DynamicMethod BuildByByteArray() => throw new NotImplementedException();

            static DynamicMethod BuildByAnyOtherWay() => throw new NotImplementedException();
        }

@wwh1004
Copy link
Contributor Author

wwh1004 commented May 9, 2024

If you don't know the the MethodDef corresponding to this dynamic method, how do you restore it? If you know that, the declaring type of the MethodDef is originalDeclaringType. This PR is insensible for normal importing. No one need to change calling parameters or adapt to new behavior. The new constructor for Importer is an optional feature to support the dynamic method with open generic. If you don't use this constructor, this PR can be considered non-existent.

@CreateAndInject
Copy link
Contributor

CreateAndInject commented May 9, 2024

If you don't know the the MethodDef corresponding to this dynamic method, how do you restore it?

Why a dynamic method must have a corresponding MethodDef/MethodBase?

original:

        static void Main(string[] args)
        {
            args.ToList();
            Console.WriteLine("hello");
            Console.ReadKey();
        }

protected:

        static void Main(string[] args)
        {
            Delegates[215](args);
            Delegates[1701](Delegates[51]());
            Delegates[20]();
        }

There're 4 dynamic method instances, we restore 3 calls and 1 ldstr from 4 dynamic method instances, but not every dyanmic method has a corresponding MethodDef/MethodBase.

@wwh1004
Copy link
Contributor Author

wwh1004 commented May 9, 2024

The declaring type of Main is the originalDeclaringType. Where you get the generic arguments as type instantiation, where is the originalDeclaringType.

@CreateAndInject
Copy link
Contributor

CreateAndInject commented May 9, 2024

ClassLibrary1.zip

Add this file to references, and code following, how to prepare originalDeclaringType? My PR doesn't care about such issue and work well.
I protected ClassLibrary1.dll to prevent people from analyzing, I don't use a hard protector, since unpacking ClassLibrary1.dll isn't what we are talking about here. Assume a man can't unpack ClassLibrary1.dll, how to prepare originalDeclaringType?

using ClassLibrary1;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using System;
using System.Reflection;

class Program
{
    public static void Main()
    {
        Test(1);
        var dm = DynamicMethodBuilder.Build(null);
        dm.GetType().GetMethod("GetMethodDescriptor", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(dm, null);
        var module = ModuleDefMD.Load(typeof(Program).Assembly.Location);
        module.Context = ModuleDef.CreateModuleContext();
        var originalDeclaringType = typeof(Program); // how
        var dmr = new DynamicMethodBodyReader(module, dm, new Importer(module, ImporterOptions.TryToUseDefs, default, null, originalDeclaringType), DynamicMethodBodyReaderOptions.UnknownDeclaringType);
        dmr.Read();
        var instrs = dmr.Instructions;
        foreach (var instr in instrs)
            Console.WriteLine(instr);
        Console.WriteLine("OK");
        Console.Read();
    }

    static void Test(object obj)
    {
        var dm = DynamicMethodBuilder.Build(obj);
        dm.Invoke(null, null);
    }
}

@CreateAndInject
Copy link
Contributor

@wtfsck As they can't resolve the issue I mentioned, can we eliminate this PR now?

@ElektroKill
Copy link
Contributor

This is an unrealistic scenario. In this case, the method returned by var dm = DynamicMethodBuilder.Build(null); is not invocable and will create an exception. This means this dynamic method object is useless. In the case you mentioned, which is unpacking, even if you create such methods by not fulfilling generic arguments, you still know which method you are trying to unpack and therefore the declaring type. There is no real-world case other than purposefully crafting dynamic method that won't execute where the declarying type will be known. Protectors that hide code using dynamic method will always generate the correct dynamic method unless some information is missing like generic arguments. In such a case, you can provide the original declaring type to the importer since you can figure out by context which type the method belongs to. What use would it be for a protector to generate an invalid dynamic method that it can't later invoke?

I think you are stretching this problem out and creating unreasonable and unrealistic scenarios to try to show that a fix you wanted is suddenly not necessary because this implementation does not satisfy a scenario that will not happen outside of such a forced example.

@ElektroKill
Copy link
Contributor

ElektroKill commented May 13, 2024

Furthermore, the dynamic method created and invoked by Test(1); is actually valid as it does contain proper instantiation and thus does not throw at runtime. Th inclusion of it here is a bit strange.

@CreateAndInject
Copy link
Contributor

CreateAndInject commented May 13, 2024

I think you are stretching this problem out and creating unreasonable and unrealistic scenarios to try to show that a fix you wanted is suddenly not necessary because this implementation does not satisfy a scenario that will not happen outside of such a forced example.

@ElektroKill Absurd, are you slave of wwh1004? I build DynamicMethod by a complete same way as his demo, the instance of DynamicMethod is complete same object, when he build DynamicMethod like this you said good, when I build DynamicMethod like this you said unreasonable.
Your opinion isn't based on science and fact, just superstition and worship, and depends on who made it.

image

This means this dynamic method object is useless

Is his DynamicMethod in the picture runnable? Both unrunnable, why his code is good, my code is unreasonable?

Code of ClassLibrary1, I don't do anything else than what I said before.

        public static DynamicMethod Build(object obj)
        {
            var type = typeof(My<>);
            if (obj != null)
                type = type.MakeGenericType(obj.GetType());
            var mi = type.GetMethod("Test");
            return DynamicMethodHelper.ConvertFrom(mi);
        }

@ElektroKill
Copy link
Contributor

I think you are stretching this problem out and creating unreasonable and unrealistic scenarios to try to show that a fix you wanted is suddenly not necessary because this implementation does not satisfy a scenario that will not happen outside of such a forced example.

@ElektroKill Absurd, are you slave of wwh1004? I build DynamicMethod by a complete same way as his demo, the instance of DynamicMethod is complete same object, when he build DynamicMethod like this you said good, when I build DynamicMethod like this you said unreasonable. Your opinion isn't based on science and fact, just superstition and worship, and depends on who made it.

image

        public static DynamicMethod Build(object obj)
        {
            var type = typeof(B<>);
            if (obj != null)
                type = type.MakeGenericType(obj.GetType());
            var mi = type.GetMethod("Test");
            return DynamicMethodHelper.ConvertFrom(mi);
        }

Oh wow, I'm not the worshipper or slave of anyone [1]

First of all, I should mention that it is necessary to read all of this mess of words before making any conclusions as to the contents.

I do not in any way shape or form agree with building uninstantiated dynamic methods. Doing that is invalid in reflection. The dynamic method created in the demo is there to forcefully make the invalid method for the purpose of demonstrating that the PR works. It is in no way, shape, or form correct to make such invalid dynamic methods. Such invalid dynamic methods should not be created at all except for demonstrations like the one in the original PR description. The only somewhat acceptable reason for this is to unpack when you do not want to properly instantiate the types.

If we assume that as the only case, such an invalid method is created, then we also know the declaring type as mentioned before. The way the DynamicMethod is created in the original PR description is not appropriate but it's there to simulate a dynamic method created during such unpacking attempt.

The reason I said that your example is not a realistic scenario is that if we assume that any code has the method Build as you mentioned, and then uses it with the null argument, the returned value is completely unusable. Therefore, we can conclude that such a method cannot exist in real software not meant for demonstration purposes like your snippet. The code wwh posted to create such a dynamic method is also not a realistic scenario and will also not be found in real software since it creates an unusable object. The reason that I didn't mention this is that it seems to have been trying to recreate the scenario that you mentioned with unpacking but instead of implementing everything, it's taking a shortcut to create an incorrect dynamic method. His code is reasonable to create a test case since it features all the information we get when executing the unpacking scenario seen below. Your code is unreasonable since under all circumstances, the result of Build(null) would be useless (just like the result of wwh code) and the example does not demonstrate access to all information that we would have in the context of unpacking.

Example scenario I can see for unpacking:

class MyType<T> {
	public static void Method() {
		SomeDynamicMethodBuilder.Build(...).Invoke(null, null);
	}
}

In order to generate the correct dynamic method here, the code in SomeDynamicMethodBuilder.Build would need to be aware of the values of the generic arguments in order to perform instantiation when building the dynamic method (this is because dynamic methods themselves need to be non-generic). This means that it either has to collect the types of these arguments via a typeof(MyType<T>) expression or typeof(T) expression(s). Using stack trace or MethodBase.GetCurrentMethod will not give us the instantiated types so these cannot be used.

This underlines the fact that just having a Build(object obj) method that does not take in the generic arguments for instantiation is unreasonable and cannot feasibly produce a correct dynamic method in the context of a protector (unless there is some other way to get the generic arguments of a member on the call stack that I'm not aware of, if so please bring it my attention).

This also exposes the fact that in the cases of protectors that use such obfuscation techniques, the generic arguments used for the method must be first explicitly captured before the DynamicMethod builder is executed. This means that the declaring type of the method will also be accessible since the type argument is only available to methods inside it.

[1] Please don't make any weird assumptions about me being biased towards anyone or something like this. I'm trying my best to discuss this weird problem and solution to it in as much detail as possible so that the most optimal solution can be implemented and that all information regarding the occurrence of this problem in the real fully functional software space can be considered. I have no grudges towards anyone here. Please don't make this personal when it is not. This is just an issue and PR discussion. Let's focus on discussing the issue at hand instead and try to find the most optimal solution.

In case of any questions or clarifications please ask or discuss.

@CreateAndInject
Copy link
Contributor

I already made a scene which my code can do better than this PR.
I'm waiting a demo which my code doesn't work when this PR can.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Why don't CreateGenericInstMethodSig
4 participants