Skip to content

Conversation

@ProductiveRage
Copy link
Contributor

@ProductiveRage ProductiveRage commented Aug 31, 2020

Hopefully I've included enough information in issue #987 and in the comment for the second changeset in this PR to explain the issue (when an int member is serialized and then used to populate an object constructor argument in the deserialization process, this is seen as value as the type object is assignable from the type int but the value itself needs to be boxed otherwise the generated IL is invalid, hence the terrifying-sounding ExecutionEngineException).

Fixes #987

…Type

This was previously checking whether the declaring type of the member was a value type, not the member itself. This property doesn't seem to be used anywhere and so fixing it shouldn't break anything else (it doesn't impact the unit test results at all).
…to ctor args as type object

If the value of a property of type int is used to populate a constructor argument of type object in deserialization then this is allowed by the constructor-matching code because it simply uses IsAssignableFrom as a check for compatability (and object IsAssignable from int) but when the IL is generated to pass an int value as an object, it has to be boxed in order for the code to be valid.

See notes at MessagePack-CSharp#987 (comment) that demonstrate how to use the reproduce case to generate a dll that PEVerify can be run against to show that it was invalid before this change.

I've added unit tests in this changeset for the fix but I suspect that the test runner would have difficulties executing the tests WITHOUT the fix as the ExecutionEngineError could tear down the process before the test runner could record it as a failure. However, if you were to use Debug Test on one of the tests that I've added but comment out the fix that I put into the DynamicObjectResolver then you would be able to see the ExecutionEngineError be caught by the debugger.
Copy link
Collaborator

@AArnott AArnott left a comment

Choose a reason for hiding this comment

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

The fix looks well written, but I wonder whether supporting the scenario that requires boxing is the right direction to go here. Why not throw a meaningful exception indicating that the code author should fix their constructor parameter type to match the member type? That would avoid the boxing which avoids unnecessary GC pressure.
@neuecc thoughts?

@neuecc
Copy link
Member

neuecc commented Sep 2, 2020

System.Text.Json throws InvalidOperationExcetpion.

System.InvalidOperationException: Each parameter in constructor 'Void .ctor(System.Object)' on type 'ConsoleApp36.MyClass' must bind to an object member on deserialization. Each parameter name must be the camel case equivalent of an object member named with the pascal case naming convention.

Newtonsoft.Json throws InvalidCastException(because object is deserialized to long)

Unable to cast object of type 'System.Int64' to type 'System.Int32'.

Exception is good.
But if you think about things like, for example, how do you think about upcast (which is OK, right?) I also think that boxing is a more consistent behavior.

@AArnott
Copy link
Collaborator

AArnott commented Sep 3, 2020

Upcasting is OK, although that would be weird that the constructor would have to downcast its argument to match the member type.
@neuecc, when you say boxing is more consistent, I wonder what it's consistent with? You already identified that System.Text.Json and Newtonsoft.Json throw instead of boxing.

@neuecc
Copy link
Member

neuecc commented Sep 3, 2020

@AArnott

The serializer doesn't get involved with the body of the constructor.

binary -> deserialize field(int) -> constructor(object)

It causes only upcast, int -> object.

It's more like JSON.NET.
JSON.NET passes a deserialized value (long) to the constructor(object).
Fail reason, long -> int cast is the body of ctor.
(If it is a long field, the deserialization succeeds).

MessagePack has a higher chance of successful deserialization than JSON.NET because it is more strictly type-preserving than JSON, so int is int.

@AArnott
Copy link
Collaborator

AArnott commented Sep 3, 2020

Ok, so it sounds like you're in favor of this change. Very well. :)

@AArnott
Copy link
Collaborator

AArnott commented Sep 3, 2020

@ProductiveRage Can you add a test for a ctor parameter type of int when the member type is long? I wonder if we still have CLR exceptions thrown when the value types don't match.

@ProductiveRage
Copy link
Contributor Author

@AArnott Sorry it's taken me a few days. I expected that it would work with the scenario that you proposed because I presumed that int would be assignable-from long but that is not the case and the following class:

[MessagePackObject]
public class SourceWithLongMemberAndIntCtorArg
{
    public SourceWithLongMemberAndIntCtorArg(int value) => Value = value;

    [Key(0)]
    public long Value { get; }
}

.. will fail at the line var serialised = MessagePackSerializer.Serialize(source); in the code below -

var source = new SourceWithLongMemberAndIntCtorArg(10);
var serialised = MessagePackSerializer.Serialize(source);
var clone = MessagePackSerializer.Deserialize<SourceWithLongMemberAndIntCtorArg>(serialised);
Console.WriteLine(clone.Value);

The error (as expected) is:

MessagePackDynamicObjectResolverException: can't find matched constructor. type:Tester.SourceWithLongMemberAndIntCtorArg

The same error occurs if the constructor argument is a long and the member and int because long is not assignable from int, nor is int assignable from long (they may be cast from one to another but IsAssignableFrom returns false for both directions).

Do you think that it makes sense to add any more code for scenarios similar to this in order for my original change to make sense?

@AArnott
Copy link
Collaborator

AArnott commented Sep 10, 2020

Thanks for testing. That exception is good. I don't mind unequal value types failing, but I was hoping it was not a CLR engine exception and your report is that it is not. So that's good. :)

@AArnott AArnott added this to the v2.1 milestone Sep 10, 2020
@AArnott AArnott changed the title Fix issue #987 - box value types if passed to ctor args as type object Box value types if passed to ctor args as type object Sep 10, 2020
@AArnott AArnott merged commit 86ea879 into MessagePack-CSharp:master Sep 10, 2020
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.

System.ExecutionEngineException encountered that should be a MessagePackSerializationException

3 participants