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

Proxy creation fails where method parameters carry certain modopt combinations #277

Closed
stakx opened this issue Jun 23, 2017 · 50 comments · Fixed by #278
Closed

Proxy creation fails where method parameters carry certain modopt combinations #277

stakx opened this issue Jun 23, 2017 · 50 comments · Fixed by #278
Labels
Milestone

Comments

@stakx
Copy link
Member

stakx commented Jun 23, 2017

I am forwarding issue devlooped/moq#244 here as this appears to be a problem with DynamicProxy, not with Moq.

@mvdtom noticed that trying to mock the following C++/CLI interface with Moq will trigger a TypeLoadException:

public interface class IFoo
{
    virtual void DoWork(const char type);
};

Moq would create a proxy of this interface as follows (simplified):

var proxyGenerator = new ProxyGenerator();
proxyGenerator.CreateClassProxy(typeof(object), new[] { typeof(IFoo) });

The exception thrown is:

System.TypeLoadException: Signature of the body and declaration in a method implementation do not match.  Type: 'Castle.Proxies.ObjectProxy'.  Assembly: 'DynamicProxyGenAssembly2, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'.
   at System.Reflection.Emit.TypeBuilder.TermCreateClass(RuntimeModule module, Int32 tk, ObjectHandleOnStack type)
   at System.Reflection.Emit.TypeBuilder.CreateTypeNoLock()
   at System.Reflection.Emit.TypeBuilder.CreateTypeInfo()
   at Castle.DynamicProxy.Generators.Emitters.AbstractTypeEmitter.CreateType(TypeBuilder type)
   at Castle.DynamicProxy.Generators.Emitters.AbstractTypeEmitter.BuildType()
   at Castle.DynamicProxy.Generators.ClassProxyGenerator.GenerateType(String name, Type[] interfaces, INamingScope namingScope)
   at Castle.DynamicProxy.Generators.ClassProxyGenerator.<>c__DisplayClass1_0.<GenerateCode>b__0(String n, INamingScope s)
   at Castle.DynamicProxy.Generators.BaseProxyGenerator.ObtainProxyType(CacheKey cacheKey, Func`3 factory)
   at Castle.DynamicProxy.Generators.ClassProxyGenerator.GenerateCode(Type[] interfaces, ProxyGenerationOptions options)
   at Castle.DynamicProxy.DefaultProxyBuilder.CreateClassProxyType(Type classToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options)
   at Castle.DynamicProxy.ProxyGenerator.CreateClassProxy(Type classToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options, Object[] constructorArguments, IInterceptor[] interceptors)
   at Castle.DynamicProxy.ProxyGenerator.CreateClassProxy(Type classToProxy, Type[] additionalInterfacesToProxy, IInterceptor[] interceptors)
   at …

This probably has to do with C++/CLI's use of optional modifiers (modopt) in the method signature. Those can be seen in the IL:

.class interface public auto ansi abstract beforefieldinit IFoo
{
    .method public hidebysig newslot abstract virtual instance void DoWork(
      int8
      modopt([mscorlib]System.Runtime.CompilerServices.IsConst)
      modopt([mscorlib]System.Runtime.CompilerServices.IsSignUnspecifiedByte) 'type') cil managed
    { }
}

@mvdtom also noticed that the exception doesn't happen if the C++ const modifier is omitted, or if he chooses a type other than char. Based on this, I ran some additional tests of my own.

I defined a handful of types in IL that differ only in the specific combination of modopts the parameter carries (see TypesWithModopt.il, which was compiled using ilasm and checked with peverify). Then I tried to create proxies for all those types (see Program.cs). Here's the result:

modopt

In summary, DynamicProxy...

  • ... doesn't care about the order of modopts.
  • ... can deal with any number of IsConst modopts.
  • ... can deal with one single IsSignUnspecifiedByte modopt that is not accompanied by any other modopts.
  • ... can deal with one single custom (user-defined) modopt that is not accompanied by any other modopt.

Given that the last two bullet points are very similar, I would guess that DynamicProxy really only supports IsConst, and otherwise tolerates a single modopt. I haven't tried yet what happens if several modopts are spread across more than one parameter.

What is DynamicProxy's position and support for modopts? I realise it is probably geared mainly towards C# and VB.NET, but I'm still curious if DynamicProxy wants to support them at all, or not.

@ghost
Copy link

ghost commented Jun 23, 2017

Interesting that const char has no natural CLI type equivalent. Are you sure this does not need to be marshalled?

@stakx
Copy link
Member Author

stakx commented Jun 23, 2017

When I compiled mvdtom's original interface using MSVC and then inspected the IL, there was no marshal() on the parameter. The original interface had a method with return type bool instead of void. That one caused a marshal()... but on the return type.

As to the rest of my IL types, I can't say whether there should be marshalling instructions. But I guess C# should be able to deal with uint8, int8, uint32 and int32.

@ghost
Copy link

ghost commented Jun 23, 2017

Only see signed char in this, https://msdn.microsoft.com/en-us/library/0wf2yk2k.aspx. Can you post a repro?

@stakx
Copy link
Member Author

stakx commented Jun 23, 2017

That's exactly why the modopts are necessary: to prevent a loss of precision in the type mapping. If C++/CLI simply translated char to either System.Byte or System.SByte without adding the IsSignUnspecifiedByte, you couldn't reverse the mapping accurately. Same for const, which doesn't really exist in the CTS at all.

I've posted repro instructions above. I guess you want something you can just run as is, without much copying & pasting? I should be able to get something ready tomorrow.

@ghost
Copy link

ghost commented Jun 23, 2017

No problems. I checked our hand off using the stacktrace documented in the original issue: devlooped/moq#244. What I found interesting was I landed up at this line: https://github.com/castleproject/Core/blob/master/src/Castle.Core/DynamicProxy/Generators/Emitters/MethodEmitter.cs#L171

With a repro, I can debug the chain and determine whether we got it wrong or recommend the next team. Thanks for being patient.

@ghost
Copy link

ghost commented Jun 23, 2017

My ideal goal, is that we end up with a test(which I am willing to contribute) that guards you from this down there which we quite frankly could push up for you. This is TL;DR; but check this issue out :)

@jonorossi
Copy link
Member

What is DynamicProxy's position and support for modopts? I realise it is probably geared mainly towards C# and VB.NET, but I'm still curious if DynamicProxy wants to support them at all, or not.

We definitely support modopts on the .NET Framework, it's the reason we've got Rhino.Mocks.CPP.Interfaces.dll in our unit tests. The only real code we've got handling them is in MethodEmitter.SetSignature.

A failing unit test would be great, if there is an API in the Rhino DLL you can use to reproduce it that would be better than adding another one.

@stakx
Copy link
Member Author

stakx commented Jun 24, 2017

@jonorossi: Unfortunately, the Rhino DLL contains only a single interface with a single method having a single parameter with a single modopt (System.Runtime.CompilerServices.IsLong), so a different assembly will have to be used.

I am a bit at a loss how to best provide a repro. @Fir3pho3nixx, why did you link to the other issue above? To show the template I should use to summarise this problem? If that's required, I'll oblige, of course.

I'm also uncertain about...

  1. Whether it's OK to just include an already compiled DLL (like was apparently done with the Rhino DLL);
  2. Whether it's important for you to be able to compile said DLL from source code yourself;
  3. Whether that source code needs to be the original C++ one, or if ILASM source code will do.

Given that the only C++/CLI compiler is MSVC, which is tied to the Windows platform, I'd say it would be better to work with (more cross-platform) ILASM source code.

I'll be happy to post a full repro once I understand what exactly you're after.

In the meantime, if you just want some quick code that executes without much setting up and reproduces the reported issue, here you go:

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;

using Castle.DynamicProxy;

class Program
{
    public static void Main()
    {
        var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName("ModoptAssembly"),
            AssemblyBuilderAccess.Run);

        var moduleBuilder = assemblyBuilder.DefineDynamicModule("ModoptAssembly");

        TypeBuilder typeBuilder = moduleBuilder.DefineType(
            "IFoo",
            TypeAttributes.Class | TypeAttributes.Interface | TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.AutoLayout | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit);

        MethodBuilder methodBuilder = typeBuilder.DefineMethod(
            "DoWork",
            MethodAttributes.Public | MethodAttributes.NewSlot | MethodAttributes.HideBySig | MethodAttributes.Abstract | MethodAttributes.Virtual, typeof(void), new[] { typeof(sbyte) });

        methodBuilder.SetSignature(
            returnType: typeof(void),
            returnTypeRequiredCustomModifiers: null,
            returnTypeOptionalCustomModifiers: null,
            parameterTypes: new[]
            {
                typeof(sbyte)
            },
            parameterTypeRequiredCustomModifiers: null,
            parameterTypeOptionalCustomModifiers: new[]
            {
                 // play around with those..
                new Type[] { typeof(IsConst), typeof(IsSignUnspecifiedByte) }
            });

        Type type = typeBuilder.CreateType();

        // ...to make this fail or succeed:
        var proxy = new ProxyGenerator().CreateClassProxy(typeof(object), new[] { type });
    }
}

(The above code produces the same type as the C++/CLI compiler would for the interface initially shown. I've also saved the generated assembly to disk and verified it using peverify.)

@jonorossi
Copy link
Member

@stakx I'd love it if you were able to replace the existing unit tests currently using Rhino.Mocks.CPP.Interfaces.dll with code like you just provided, just include the C++ and IL in a comment so it is clear what the equivalent would be. This type of unit test will get written and never changed so it doesn't need to be pretty, it'll just ensure the scenario doesn't get broken. That Rhino DLL was added around a decade ago and it will be great to get rid of it.

Then obviously turn your code into a unit test. Any idea where the problem might be in the DynamicProxy code?

@jonorossi jonorossi added the bug label Jun 24, 2017
@stakx
Copy link
Member Author

stakx commented Jun 24, 2017

@jonorossi: I can attempt this. It might however take me a while to ensure that running several such unit tests wouldn't result in a completely "overloaded" app domain. Ideally, each unit test would generate its own private dynamic assembly and run it through PEVerify (like you do in BasePEVerifyTestCase) before using it. That means that lots of assembly files would be written to disk and loaded into the app domain during a test run. (Hmm, on second thought, perhaps this isn't necessary. Perhaps we just have a test fixture that generates one dynamic in-memory assembly in a setup method without doing any verifying, then several tests share that dynamic assembly.)

I also need to find out whether the tests can / should work for .NET Core as well.

If you guys already know exactly how to do all of this safely, I'd gladly accept some guidance hints. Otherwise, I just see how it goes and report back with a PR.

Regarding your last question, I have no idea what the error cause might be, but while I am at it, I can do some investigating.

@jonorossi
Copy link
Member

also need to find out whether the tests can / should work for .NET Core as well.

.NET Core doesn't have the APIs to emit custom modifiers, you'll see the conditional compilation in MethodEmitter, so none of these unit tests can pass there, maybe with .NET Standard 2.0 we'll get it back though, but don't worry about that.

Maybe just start with a pull request to replace the existing modopt unit tests using the Rhino DLL and we can work through exactly how the assembly generation will be impacted, not sure without actually looking at the code what will happen. Once we get that sorted and merged, move on to this defect. Good plan?

@stakx
Copy link
Member Author

stakx commented Jun 24, 2017

Sounds good, will do. :)

@ghost
Copy link

ghost commented Jun 24, 2017

I have managed to replicate the issue.

Created a C++/CLI interface and class over here: https://github.com/fir3pho3nixx/Core/blob/modopts-repro/src/Castle.Core.Tests.Cpp/Castle.Core.Tests.Cpp.h

With the corresponding implementation here: https://github.com/fir3pho3nixx/Core/blob/modopts-repro/src/Castle.Core.Tests.Cpp/Castle.Core.Tests.Cpp.cpp

Then wrote 2 tests...

One that proxies the concrete type: https://github.com/fir3pho3nixx/Core/blob/modopts-repro/src/Castle.Core.Tests/RhinoMocksTestCase.cs#L382

And another that proxies the interface: https://github.com/fir3pho3nixx/Core/blob/modopts-repro/src/Castle.Core.Tests/RhinoMocksTestCase.cs#L398

The class passes, but the interface fails.

The IL Method Signature for both the interface and the class is the same except for one being abstract.

.method public hidebysig newslot abstract virtual 
        instance bool 
        marshal( unsigned int8) 
        DoWork(int8 modopt([mscorlib]System.Runtime.CompilerServices.IsSignUnspecifiedByte) modopt([mscorlib]System.Runtime.CompilerServices.IsConst) 'type') cil managed
{
} // end of method ITestInterface::DoWork

.method public hidebysig newslot virtual 
        instance bool 
        marshal( unsigned int8) 
        DoWork(int8 modopt([mscorlib]System.Runtime.CompilerServices.IsSignUnspecifiedByte) modopt([mscorlib]System.Runtime.CompilerServices.IsConst) 'type') cil managed
{
  // Code size       14 (0xe)
  .maxstack  1
  .locals ([0] bool V_0)
  IL_0000:  ldstr      "Doing work!"
  IL_0005:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000a:  ldc.i4.1
  IL_000b:  stloc.0
  IL_000c:  ldloc.0
  IL_000d:  ret
} // end of method TestClass::DoWork

By dropping the const modifier on ITestInterface::DoWork both tests pass quite happily.


.method public hidebysig newslot abstract virtual 
       instance bool 
       marshal( unsigned int8) 
       DoWork(int8 modopt([mscorlib]System.Runtime.CompilerServices.IsSignUnspecifiedByte) 'type') cil managed
{
} // end of method ITestInterface::DoWork


I could also verify the modifiers being passed through using this diagnostic code: https://github.com/fir3pho3nixx/Core/blob/modopts-repro/src/Castle.Core/DynamicProxy/Generators/Emitters/MethodEmitter.cs#L174

When both System.Runtime.CompilerServices.IsSignUnspecifiedByte and System.Runtime.CompilerServices.IsConst are passed through I get

System.TypeLoadException : Signature of the body and declaration in a method implementation do not match.  Type: 'Castle.Proxies.ITestInterfaceProxy'.

Looks like System.Reflection.Emit.MethodBuilder.SetSignature is not compatible with native C++ const modifiers on parameters.

@stakx
Copy link
Member Author

stakx commented Jun 24, 2017

@Fir3pho3nixx: I was also just going to submit a second PR containing some failing tests (but might wait a little longer now). I've chosen a slightly different approach, namely to generate the test classes dynamically instead of using the C++/CLI compiler. So I'm essentially using TypeBuilder.DefineMethod to create methods containing modopts in their signatures. I've also saved the generated assemblies to disk and verified by manual inspection that the modopts were correctly written to the output—they were. So if my tests can write the signature, DynamicProxy should be able to do the same.

It would probably be worth trying out a few additional things:

  • Is there a difference in generated methods when setting the signature directly in TypeBuilder.DefineMethod vs. explicitly calling MethodBuilder.SetSignature?

  • Setting signatures containing modopts might work, but does reading / reflecting over the method signatures work, too?

@ghost
Copy link

ghost commented Jun 24, 2017

@stakx - Your approach makes sense from a maintainability approach.

I have done one better, I found out what the problem is. Check this commit https://github.com/fir3pho3nixx/Core/commit/a5ff169964546cc632eaeeb78de01715efdf0c58

Looks like the parameters are being reversed! :)

My C++ CLI test passes with this change.

@stakx
Copy link
Member Author

stakx commented Jun 24, 2017

@Fir3pho3nixx: Awesome find! I noticed the same thing in my unit tests when I had to replace CollectionAssert.AreEqual with CollectionAssert.AreEquivalent, but I didn't realise that this would be the error!

I think I am going to change the tests back to using CollectionAssert.AreEqual.

@ghost
Copy link

ghost commented Jun 24, 2017

@stakx - Look forward to the PR :)

@stakx
Copy link
Member Author

stakx commented Jun 24, 2017

Can do. Also, it probably wouldn't hurt to add more tests for required modifiers (modreq) and for modifiers as applied to the return type of a method.

@ghost
Copy link

ghost commented Jun 24, 2017

Yes, please. Make sure you use more than one modifier, this is what caught us out.

@stakx
Copy link
Member Author

stakx commented Jun 24, 2017

Will do! 👍 (And before you ask, I'll also make sure not to use palindrome sequences. :-D)

Btw.: Do you think it would make sense to combine my two PRs into one, or should I keep them separate for now?

@ghost
Copy link

ghost commented Jun 24, 2017

Merge them. We know what the issue was.

@stakx
Copy link
Member Author

stakx commented Jun 24, 2017

OK, all done. Let me know if the PR needs any modifications.

@jonorossi
Copy link
Member

Sorry for the silence guys, it's just been one of those weeks. Great work on the getting a fix in with a solid set of unit tests.

Do we want to release this as 4.1.1?

@jonorossi jonorossi modified the milestones: v4.1, vNext Jun 28, 2017
@stakx
Copy link
Member Author

stakx commented Jun 30, 2017

stakx@d5c29bf would possibly fix the failing tests on Mono, but I haven't actually verified the claims made in those comments, so no PR just yet.

@jonorossi
Copy link
Member

jonorossi commented Jun 30, 2017

stakx/Castle.Core@d5c29bf would possibly fix the failing tests on Mono, but I haven't actually verified the claims made in those comments, so no PR just yet.

@stakx we can't (and don't want to) use __MonoCS__ in any library code as we don't ship a separate Mono build.

@stakx
Copy link
Member Author

stakx commented Jun 30, 2017

@jonorossi - understood.

Running some quick experiments using the Mono compiler suggests that Mono actually reverses the modopts, too, so my above commit is invalid, anyway.

I'll get back on this later.

@jonorossi
Copy link
Member

@stakx does order even matter? I can't find any mention of order in ECMA-335. This is the main guts about custom modifiers:

I.9.7 Metadata extensibility
CLI metadata is extensible. There are three reasons this is important:
 The CLS is a specification for conventions that languages and tools agree to support in
a uniform way for better language integration. The CLS constrains parts of the CTS
model, and the CLS introduces higher-level abstractions that are layered over the
CTS. It is important that the metadata be able to capture these sorts of development -
time abstractions that are used by tools even though they are not recognized or
supported explicitly by the CLI.
 It should be possible to represent language-specific abstractions in metadata that are
neither CLI nor CLS language abstractions. For example, it should be possible, over
time, to enable languages like C++ to not require separate headers or IDL files in
order to use types, methods, and data members exported by compiled modules.
 It should be possible, in member signatures, to encode types and type modifiers that are
used in language-specific overloading. For example, to allow C++ to distinguish int
from long even on 32-bit machines where both map to the underlying type int32.
This extensibility comes in the following forms:
 Every metadata object can carry custom attributes, and the metadata APIs provide a
way to declare, enumerate, and retrieve custom attributes. Custom attributes can be
identified by a simple name, where the value encoding is opaque and known only to
the specific tool, language, or service that defined it. Or, custom attributes can be
identified by a type reference, where the structure of the attribute is self-describing
(via data members declared on the type) and any tool including the CLI reflection
services can browse the value encoding.
© Ecma International 2012 57
CLS Rule 34: The CLS only allows a subset of the encodings of custom attributes. The
only types that shall appear in these encodings are (see Partition IV): System.Type,
System.String, System.Char, System.Boolean, System.Byte, System.Int16,
System.Int32, System.Int64, System.Single, System.Double, and any enumeration
type based on a CLS-compliant base integer type.
[Note:
CLS (consumer): Shall be able to read attributes encoded using the restricted scheme.
CLS (extender): Must meet all requirements for CLS consumer and be able to author new
classes and new attributes. Shall be able to attach attributes based on existing attribute
classes to any metadata that is emitted. Shall implement the rules for the
System.AttributeUsageAttribute (see Partition IV).
CLS (framework): Shall externally expose only attributes that are encoded within the
CLS rules and following the conventions specified for System.AttributeUsageAttribute
end note]
 In addition to CTS type extensibility, it is possible to emit custom modifiers into
member signatures (see Types in Partition II). The CLI will honor these modifiers for
purposes of method overloading and hiding, as well as for binding, but will not
enforce any of the language-specific semantics. These modifiers can reference the
return type or any parameter of a method, or the type of a field. They come in two
kinds: required modifiers that anyone using the member must understand in order to
correctly use it, and optional modifiers that can be ignored if the modifier is not
understood.
CLS Rule 35: The CLS does not allow publicly visible required modifiers (modreq, see
Partition II), but does allow optional modifiers (modopt, see Partition II) it does not
understand.
[Note:
CLS (consumer): Shall be able to read metadata containing optional modifiers and
correctly copy signatures that include them. Can ignore these modifiers in type matching
and overload resolution. Can ignore types that become ambiguous when the optional
modifiers are ignored, or that use required modifiers.
CLS (extender): Shall be able to author overrides for inherited methods with signatures
that include optional modifiers. Consequently, an extender must be able to copy such
modifiers from metadata that it imports. There is no requirement to support required
modifiers, nor to author new methods that have any kind of modifier in their signature.
CLS (framework): Shall not use required modifiers in externally visible signatures unless
they are marked as not CLS-compliant. Shall not expose two members on a class that
differ only by the use of optio

II.7.1.1 modreq and modopt
Custom modifiers, defined using modreq (“required modifier”) and modopt (“optional modifier”), are
similar to custom attributes (§II.21) except that modifiers are part of a signature rather than being
attached to a declaration. Each modifer associates a type reference with an item in the signature.
The CLI itself shall treat required and optional modifiers in the same manner. Two signatures that
differ only by the addition of a custom modifier (required or optional) shall not be considered to match.
Custom modifiers have no other effect on the operation of the VES.
[Rationale: The distinction between required and optional modifiers is important to tools other than the
CLI that deal with the metadata, typically compilers and program analysers. A required modifier
indicates that there is a special semantics to the modified item that should not be ignored, while an
optional modifier can simply be ignored.
For example, the const qualifier in the C programming language can be modelled with an optional
modifier since the caller of a method that has a const-qualified parameter need not treat it in any
special way. On the other hand, a parameter that shall be copy-constructed in C++ shall be

@stakx
Copy link
Member Author

stakx commented Jun 30, 2017

does order even matter?

In theory, perhaps not, since it doesn't explicitly say that ordering matters. In practice, at least for the CLR, apparently yes; otherwise we would never have had a bug to fix.

Judging from the CLI standard's perspective, a test checking for a specific modopt / modreq order is perhaps invalid, meaning it should removed. On the other hand, that test documents why we saw the need to reverse the custom modifier order upon method emission. If we just remove the test, how can we be sure whether we need the .Reverse()s during method emission or not...?

@jonorossi
Copy link
Member

In theory, perhaps not. In practice, at least for the CLR, apparently yes; otherwise we would never have had a bug to fix.

I should have been clearer, I was referring to runtime operation, does order matter which is seems like it doesn't, custom modifiers are very much like custom attributes.

Thinking about the exception message from the CLR ("Signature of the body and declaration in a method implementation do not match"), it sounds like it is doing a signature object comparison against its parent, and after trying to dig into the coreclr code looks like that is true in MetaSig::CompareMethodSigs and its callees.

We obviously need to call Reverse so TypeBuilder on the CLR doesn't throw (I wonder if it is just a coincidence that sorting backwards works and if it is actually sorting by something else), however it doesn't seem like Mono had a problem with calling Reverse too, only that the unit tests fail there so we could change AreEqual to AreEquivalent.

@ghost
Copy link

ghost commented Jun 30, 2017

@jonorossi +1

It looks to me like the test is having trouble asserting the output.

<System.Linq.Enumerable+c__Iterator14`1[System.Type]>, actual is <System.Type[0]>

Wondering if we apply ToArray or something to the reverse whether this would work. I will know more once I check it this weekend.

@stakx
Copy link
Member Author

stakx commented Jun 30, 2017

I apologize in advance for the very long post. I'd like to share what I have found regarding how various subsystems deal with modopt ordering. @Fir3pho3nixx, I hope you'll be able to confirm these findings; I'd hate to add to the confusion instead of helping to clear things up.

TL;DR:

I am basing the following findings on two assumptions that I haven't yet verified any further:

  1. Assumption: ILASM and ILDASM do not rely on System.Reflection, but are more like Mono.Cecil in that they directly read and write binary data.
  2. Assumption: Inspecting modopts on method parameters only is representative also for modopts on return types, modreqs on parameters, and modreqs on return types. (I think the unit tests I wrote support this.)

Here is what I found:

  • ILASM and ILDASM. ILASM generates method signatures that have modopts in reverse order (when compared to the original IL source code). ILDASM dumps faithfully show modopts exactly as they are in the binary signatures, but in the GUI they are displayed in reverse order (and therefore in the same order as in the original IL source).

  • Reflection on the CLR. When reading parameter modopts on the CLR using System.Reflection, they are reported in reverse order (e.g. when reading an ILASM-generated DLL, the reported order will match the order in IL source code, since CLR reflection reverses ILASM's reversal.) System.Reflection.Emit writes modopts to a method signature exactly in the order given.

  • Reflection on the Mono VM. When reading parameter modopts on the Mono VM using System.Reflection, they are reported exactly as they have been written to the signature (i.e. in the reverse order when compared to the original IL source code). When emitting parameter modopts on the CLR Mono VM using System.Reflection.Emit, they are emitted into the signature exactly in the given order.

This is then how modopts should be treated in Castle.Core:

  • Never reverse the ordering on Mono, and never reverse ordering when emitting via MethodBuilder.
  • In the library code: Only when executing on the CLR, reverse modopt / modreq order when reading them from the type-to-be-proxied's method via .Get[Optional|Required]CustomModifiers(). (Here's a possible way to determine whether code is executing on Mono vs. the CLR: https://stackoverflow.com/questions/721161/how-to-detect-which-net-runtime-is-being-used-ms-vs-mono)
  • In the unit test suite: Reverse modopt / modreq order only when running on the CLR.

What follows below are the details that led me to the above conclusions.

Compiling with ILASM:

I started by compiling this class with ILASM:

.class interface public abstract auto ansi beforefieldinit ParamModopts
{
  // (a few other methods using modopts go here to "randomize" the modopt types'
  // indices in the TypeRef table.)

  .method public hidebysig newslot abstract virtual 
          instance void IsByValue_IsVolatile_IsVolatile_IsLong_IsConst_IsConst(
            int32
            modopt([mscorlib]System.Runtime.CompilerServices.IsByValue)
            modopt([mscorlib]System.Runtime.CompilerServices.IsVolatile)
            modopt([mscorlib]System.Runtime.CompilerServices.IsVolatile)
            modopt([mscorlib]System.Runtime.CompilerServices.IsLong)
            modopt([mscorlib]System.Runtime.CompilerServices.IsConst) 
            modopt([mscorlib]System.Runtime.CompilerServices.IsConst) 
            A_1) cil managed { }
}

An ILDASM dump of the generated DLL reports the following (abbreviated):

// 	Method #5 (06000005) 
// 	-------------------------------------------------------
// 		MethodName: IsByValue_IsVolatile_IsVolatile_IsLong_IsConst_IsConst (06000005)
// 		Flags     : [Public] [Virtual] [HideBySig] [NewSlot] [Abstract]  (000005c6)
// 		ReturnType: Void
// 		1 Arguments
// 			Argument #1:  CMOD_OPT System.Runtime.CompilerServices.IsConst
//                                    CMOD_OPT System.Runtime.CompilerServices.IsConst
//                                    CMOD_OPT System.Runtime.CompilerServices.IsLong
//                                    CMOD_OPT System.Runtime.CompilerServices.IsVolatile
//                                    CMOD_OPT System.Runtime.CompilerServices.IsVolatile
//                                    CMOD_OPT System.Runtime.CompilerServices.IsByValue
//                                    I4
// 		Signature : 20 01 01 20 0d 20 0d 20  09 20 11 20 11 20 05 08 
// 		1 Parameters
// 			(1) ParamToken : (08000005) Name : A_1 flags: [none] (00000000)
// 
// TypeRef #1 (01000001)
// -------------------------------------------------------
// Token:             0x01000001
// TypeRefName:       System.Runtime.CompilerServices.IsByValue
// 
// TypeRef #2 (01000002)
// -------------------------------------------------------
// Token:             0x01000002
// TypeRefName:       System.Runtime.CompilerServices.IsLong
// 
// TypeRef #3 (01000003)
// -------------------------------------------------------
// Token:             0x01000003
// TypeRefName:       System.Runtime.CompilerServices.IsConst
// 
// TypeRef #4 (01000004)
// -------------------------------------------------------
// Token:             0x01000004
// TypeRefName:       System.Runtime.CompilerServices.IsVolatile

The signature lists the modopts in reverse order when compared to the original IL source code.

I wonder if it is just a coincidence that sorting backwards works and if it is actually sorting by something else

@jonorossi - Let's very briefly consider if the reversal might just be coincidence. Manually decoding the above signature (according to ECMA 335, section II.23.2) shows that the custom modifiers have not been sorted alphabetically, nor according to their index in the TypeRef table:

// 		Signature : 20 01 01 20 0d 20 0d 20  09 20 11 20 11 20 05 08 
  • 20 = certain flags (such as hasthis and the default calling convention)
  • 01 = the number of parameters
  • 01 = the return type, ELEMENT_TYPE_VOID
  • parameter 1's signature:
    • 20 = ELEMENT_TYPE_CMOD_OPT
    • 0d = TypeRef 3 (compressed), i.e. IsConst
    • 20 = ELEMENT_TYPE_CMOD_OPT
    • 0d = TypeRef 3 (compressed), i.e. IsConst
    • 20 = ELEMENT_TYPE_CMOD_OPT
    • 09 = compressed TypeRef 2, i.e. IsLong
    • 20 = ELEMENT_TYPE_CMOD_OPT
    • 11 = compressed TypeRef 4, i.e. IsVolatile
    • 20 = ELEMENT_TYPE_CMOD_OPT
    • 11 = compressed TypeRef 4, i.e. IsVolatile
    • 20 = ELEMENT_TYPE_CMOD_OPT
    • 05 = compressed TypeRef 1, i.e. IsByValue
    • 08 = the actual argument type, ELEMENT_TYPE_I4

Reflecting over the above DLL with System.Reflection:

var assembly = Assembly.LoadFile(@"…\CompiledByILASM.dll");
var method = assembly.GetType("ParamModopts").GetMethod("IsByValue_IsVolatile_IsVolatile_IsLong_IsConst_IsConst");
var modopts = method.GetParameters()[0].GetOptionalCustomModifiers();
foreach (var modopt in modopts)
{
    Console.WriteLine(modopt);
}

When run on the CLR, it will report the same modopt ordering as in the IL source code (i.e. it reverses ILASM's reversal):

System.Runtime.CompilerServices.IsByValue
System.Runtime.CompilerServices.IsVolatile
System.Runtime.CompilerServices.IsVolatile
System.Runtime.CompilerServices.IsLong
System.Runtime.CompilerServices.IsConst
System.Runtime.CompilerServices.IsConst

When run on the Mono VM, it will report the opposite modopt ordering — i.e. the same ordering as in the metadata method signature:

System.Runtime.CompilerServices.IsConst
System.Runtime.CompilerServices.IsConst
System.Runtime.CompilerServices.IsLong
System.Runtime.CompilerServices.IsVolatile
System.Runtime.CompilerServices.IsVolatile
System.Runtime.CompilerServices.IsByValue

Emitting with System.Reflection.Emit

var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
    new AssemblyName("Emitted"), AssemblyBuilderAccess.RunAndSave);

var moduleBuilder = assemblyBuilder.DefineDynamicModule(
    "Emitted", "Emitted.dll");

var typeBuilder = moduleBuilder.DefineType(
    "ParamModopts",
    TypeAttributes.Interface | TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.AutoLayout | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit);

var methodBuilder = typeBuilder.DefineMethod(
    "IsByValue_IsVolatile_IsVolatile_IsLong_IsConst_IsConst",
    MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Abstract | MethodAttributes.Virtual,
    CallingConventions.Standard,
    typeof(void),
    // return type modreqs and modopts:
    null,
    null,
    // parameter types:
    new[] { typeof(int) },
    // parameter modreqs and modopts:
    null,
    new[] { new[] { typeof(IsByValue), typeof(IsVolatile), typeof(IsVolatile), typeof(IsLong), typeof(IsConst), typeof(IsConst) } });

typeBuilder.CreateType();

assemblyBuilder.Save("Emitted.dll");

When run on the CLR, a ILDASM dump of the generated DLL reports the following:

// TypeDef #1 (02000002)
// -------------------------------------------------------
// 	TypDefName: ParamModopts  (02000002)
// 	Flags     : [Public] [AutoLayout] [Interface] [Abstract] [AnsiClass] [BeforeFieldInit]  (001000a1)
// 	Extends   : 01000000 [TypeRef] 
// 	Method #1 (06000001) 
// 	-------------------------------------------------------
// 		MethodName: IsByValue_IsVolatile_IsVolatile_IsLong_IsConst_IsConst (06000001)
// 		Flags     : [Public] [Virtual] [HideBySig] [NewSlot] [Abstract]  (000005c6)
// 		ReturnType: Void
// 		1 Arguments
// 			Argument #1:  CMOD_OPT System.Runtime.CompilerServices.IsByValue
//                                    CMOD_OPT System.Runtime.CompilerServices.IsVolatile
//                                    CMOD_OPT System.Runtime.CompilerServices.IsVolatile
//                                    CMOD_OPT System.Runtime.CompilerServices.IsLong
//                                    CMOD_OPT System.Runtime.CompilerServices.IsConst
//                                    CMOD_OPT System.Runtime.CompilerServices.IsConst I4
// 		Signature : 20 01 01 20 05 20 09 20  09 20 0d 20 11 20 11 08 
// 
// TypeRef #1 (01000001)
// -------------------------------------------------------
// Token:             0x01000001
// TypeRefName:       System.Runtime.CompilerServices.IsByValue
// 
// TypeRef #2 (01000002)
// -------------------------------------------------------
// Token:             0x01000002
// TypeRefName:       System.Runtime.CompilerServices.IsVolatile
// 
// TypeRef #3 (01000003)
// -------------------------------------------------------
// Token:             0x01000003
// TypeRefName:       System.Runtime.CompilerServices.IsLong
// 
// TypeRef #4 (01000004)
// -------------------------------------------------------
// Token:             0x01000004
// TypeRefName:       System.Runtime.CompilerServices.IsConst

Note how the modopts have been written exactly in the order specified.

When run on the Mono VM, a ILDASM dump of the generated DLL reports exactly the same.

@jonorossi
Copy link
Member

@stakx thanks for doing the investigative work and the detailed write up of your findings.

Reflection on the Mono VM. When reading parameter modopts on the Mono VM using System.Reflection, they are reported exactly as they have been written to the signature (i.e. in the reverse order when compared to the original IL source code). When emitting parameter modopts on the CLR using System.Reflection.Emit, they are emitted into the signature exactly in the given order.

Did you mean to write CLR in the last sentence here, or Mono?

This is then how modopts should be treated in Castle.Core:

  • Never reverse the ordering on Mono, and never reverse ordering when emitting via MethodBuilder.
  • In the library code: Only when executing on the CLR, reverse modopt / modreq order when reading them from the type-to-be-proxied's method via .Get[Optional|Required]CustomModifiers().

I'm a little lost by your recommendations, on the CLR should MethodEmitter's call to its MethodBuilder reverse the order or not?

I came across a GitHub issue on the .NET Portability Analyzer that also ran into this ordering problem, referring to "IL syntactic order instead of the metadata order" with a Microsoft employee explaining how they are writing the metadata order: microsoft/dotnet-apiport#236 (comment). It appears to indicate that System.Reflection.Metadata returns IL syntactic order.

When we do make these changes we should change to using Array.Reverse to remove the internal allocation and the ToArray allocation.

@ghost
Copy link

ghost commented Jul 2, 2017

@stakx - Your write up is much appreciated.

@jonorossi - What I am getting is we should reverse the params on windows but not on mono. I have also done some more investigation.

I traced out the parameters going into and out of @stakx test and I also found some rather peculiar behaviour on mono.

Added some trace methods here and here.

When I run it on windows, for the first type I can see the following output:

Bar_AsModoptOnParamType
ModOptParam::Inspecting method 'Foo' on 'Bar_AsModoptOnParamType'
ModOptParam::modReqParams -> 
ModOptParam::modOptParams -> Castle.DynamicProxy.Tests.CustomModifiersTestCase+Bar
ModOptParam::modReqRet -> 
ModOptParam::modOptRet -> 

However when I run it on mono(4.x or 5.x) I dont get any modifiers coming back out.

Bar_AsModoptOnParamType
ModOptParam::Inspecting method 'Foo' on 'Bar_AsModoptOnParamType'
ModOptParam::modReqParams -> 
ModOptParam::modOptParams -> ??? EMPTY ???
ModOptParam::modReqRet -> 
ModOptParam::modOptRet -> 

This happens for everything generated in the test. There is something rather dodgy about how mono reports these modfiers using reflection(they simply just aren't there!).

Busy creating a standalone console app that hopefully will demonstrate the issue.

@ghost
Copy link

ghost commented Jul 2, 2017

After running this windows I get ...

image

But on linux I get:

image

Notice how the modifiers are missing from the output?

@stakx
Copy link
Member Author

stakx commented Jul 2, 2017

@jonorossi:

Did you mean to write CLR in the last sentence here, or Mono?

Sorry, you caught a typo there. I did indeed mean Mono, not CLR.

I'm a little lost by your recommendations, on the CLR should MethodEmitter's call to its MethodBuilder reverse the order or not?

Basically, my current recommendation boils down to this: The only place where custom modifier order should be reversed is when reading them on the CLR with parameterInfo.Get[Optional|Required]CustomModifiers().

@Fir3pho3nixx:

Notice how the modifiers are missing from the output?

I vaguely remember that when I ran my first experiments on Mono with custom modifiers, I came across a situation where not modopts were reported back to me. Then I rewrote my test code, and all of a sudden, the modopts got reported again. I concluded that the error must've been my own, but now that you've noticed the same thing happening, I'm not so sure anymore. (I ran my Mono tests on Windows, btw.)

@ghost
Copy link

ghost commented Jul 2, 2017

My recommendation is we create a separate issue for tracking mono problem and change the test fixture to assume that reflection returns mods like so, perhaps wrapped using an #if MonoCS compilation symbol so we still have a guarantee for it failing properly on windows.

This will bring our travis build back on master, and we can then track the mono issue separately after we have raised my repro as an issue with the right team.

@ghost
Copy link

ghost commented Jul 2, 2017

@stakx - You found one crazy issue! :)

@stakx
Copy link
Member Author

stakx commented Jul 2, 2017

@Fir3pho3nixx: I'm actually feeling somewhat shocked at how this has turned into a convoluted knotty mess... :-)

@jonorossi
Copy link
Member

Basically, my current recommendation boils down to this: The only place where custom modifier order should be reversed is when reading them on the CLR with parameterInfo.Get[Optional|Required]CustomModifiers().

@stakx great, I thought that is what you meant, your statement "never reverse ordering when emitting via MethodBuilder" was what I kept getting caught on since that is what DP does (use a MethodBuilder), but I think you meant when the unit tests use MethodBuilder.

I'm actually feeling somewhat shocked at how this has turned into a convoluted knotty mess... :-)

@stakx yep, this is the world of reflection emit unfortunately, we've had all sorts of painful stuff like this over the years 😄

My recommendation is we create a separate issue for tracking mono problem and change the test

@stakx @Fir3pho3nixx since people are much less likely to use custom modifiers on Mono since there is no C++/CLI, I think @Fir3pho3nixx is right, let's get the build going again and make sure this works on .NET Framework, and either have different asserts on Mono or exclude the tests completely with [Platform(Exclude = "mono", Reason = "Mono has a bug that causes ...")]. This isn't going to be an easy/clean fix and no one is likely going to care on Mono, so I'd be happy to note it in the changelog and move on until it actually affects someone. Thoughts?

@jonorossi
Copy link
Member

Master is now green, closing.

@stakx
Copy link
Member Author

stakx commented Jul 4, 2017

PS: Perhaps the tests in CustomModifiersTestCase.cs that are now skipped on Mono could be added to the list in #94?

@ghost
Copy link

ghost commented Jul 4, 2017

Gentlmen, sorry to sound like a grandma that has wet her diaper, after reading #94 I think we missed out on an opportunity.

In this comment I talked about making this code change to the test. Instead we ignored the entire fixture using the Platform(Exclude) approach.

I liked my suggestion because if mono(massive assumption) ever fixed the problem this test would automagically come alive when we run it on TravisCI after upgrading mono. Excluding it outright is harsh. We reduce it to an NUnit Theory using Assume which exits gracefully and then one days when we get results > 0, tada! the test tells us.

Can we change this code? I will submit the PR. Sorry!

@stakx
Copy link
Member Author

stakx commented Jul 4, 2017

@Fir3pho3nixx:

sorry to sound like a grandma that has wet her diaper

:-D. I guess now that the build is fixed, it would be nice to improve on those tests before we go on and forget about them. 👍

I would also be quite happy to leave the next PRs concerning this up to you, since my Windows-based Mono installation appears to be partially corrupt, which makes reliably building & testing Castle.Core rather difficult.

Regarding your suggestion to replace the test fixture attribute [Platform(Exclude = "Mono")] with:

Assume.That(modopts.Length > 0);

I think it's a great suggestion, but perhaps not sufficient. If Mono does report any custom modifiers, this by itself would make the tests fail again. You might also need to change CollectionAssert.AreEqual to CollectionAssert.AreEquivalent. If that is the case, it would take something important away from the tests: At least for the CLR, .AreEqual documents the fact that exact order matters.

Perhaps Mono needs a whole different set of tests altogether (or at least a few #ifs)?

@ghost
Copy link

ghost commented Jul 4, 2017

@stakx

I want the test to fail again. I am coding conscious living guards into the tests that remind you. Anybody that cares needs to know something changed. It needs to be re-evaluated at that point in time.

@stakx
Copy link
Member Author

stakx commented Jul 4, 2017

@Fir3pho3nixx - sounds awesome. But if I understood correctly, you want the test to fail at some point in the future (when Mono changes), and not right now?

@ghost
Copy link

ghost commented Jul 4, 2017

@stakx - Yes.

@ghost
Copy link

ghost commented Jul 4, 2017

I will submit PR tomorrow night.

@jonorossi - Cool?

@jonorossi
Copy link
Member

Gentlmen, sorry to sound like a grandma that has wet her diaper, after reading #94 I think we missed out on an opportunity.

In this comment I talked about making this code change to the test. Instead we ignored the entire fixture using the Platform(Exclude) approach.

Not at all, I would have preferred we could have run the test even without decent asserts. Thanks for sending through the pull request.

stakx referenced this issue in dotnet/csharplang Dec 9, 2017
…w: Hardware Intrinsics for Intel

Description of the proposal contains specifics about implementation details and formal syntax
proposed to support Constant Parameters feature. It is accompanied by early prototype which
supports const parameters declaration, invocation of methods with constant arguments with value
expressed as constant expression, overload resolution with support for 'const' parameters modifiers.

Still no support for code generation as there is no final decision how to represent 'const' parameters
at the CLI level. No working implementation for Roslyn Constant Parameter feature conditional support,
however, everything is stubbed to support it. Very limited tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants