Skip to content

Fix attempt of static proxies to call base method for abstract classes #1884

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

Merged
merged 4 commits into from
Oct 27, 2018

Conversation

hazzik
Copy link
Member

@hazzik hazzik commented Oct 24, 2018

Previously it was failing PEVerify similarly to #1728 but for abstract classes (this is possible in case of a polymorphic entities). Unlike interfaces the abstract base class can go into a situation when lazy initializer is not yet available, eg. code in a constructor.

@hazzik
Copy link
Member Author

hazzik commented Oct 24, 2018

@fredericDelaporte what if we move ProxyBuilderHelper.CallDefaultBaseConstructor(IL, parentType); in ImplementConstructor after we set __lazyInitializer and __proxyInfo?

@fredericDelaporte

This comment has been minimized.

{
if (!method.ReturnType.IsValueType)
{
IL.Emit(OpCodes.Ldnull);

This comment was marked as resolved.

@hazzik
Copy link
Member Author

hazzik commented Oct 24, 2018

It looks highly probable to me that the call to the base constructor has to be the first thing done, per the .Net specification.

There is no such a requirement. In fact this is how field initializers are done. First class initializes its fields, then it calls a base constructor:

For this C#:

public class A { }
public class B { int X = -1; public B() { X = 2; } }

Following IL isgenerated:

.class public auto ansi beforefieldinit A
    extends [mscorlib]System.Object
{
    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: nop
        IL_0007: ret
    } // end of method A::.ctor

} // end of class A

.class public auto ansi beforefieldinit B
    extends [mscorlib]System.Object
{
    // Fields
    .field private int32 X

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2059
        // Code size 23 (0x17)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldc.i4.m1
        IL_0002: stfld int32 B::X
        IL_0007: ldarg.0
        IL_0008: call instance void [mscorlib]System.Object::.ctor()
        IL_000d: nop
        IL_000e: nop
        IL_000f: ldarg.0
        IL_0010: ldc.i4.2
        IL_0011: stfld int32 B::X
        IL_0016: ret
    } // end of method B::.ctor

} // end of class B

I think we have to yield default(T) for the abstract case (with of course a special case for void).

This is exactly what I do here. IL_000A-IL_000D just stores the value on a stack to another variable and do short jump to an instruction following the jump, which is completely unnecessary. sharplab shows following:

        IL_0000: ldloca.s 0
        IL_0002: initobj !!T
        IL_0008: ldloc.0
        IL_0009: ret

@hazzik
Copy link
Member Author

hazzik commented Oct 24, 2018

Now I consider pursuing...

Only thing in the PR which is actual are the flags. The code we generate is valid (except the one fixed by this PR). However, even if we fix the flags and make a program "valid" it will still have the issues you have demonstrated before: the code will behave correctly only is the a variable holding the proxy is declared as an interface. And, there could be some potential issues with hidden traps.

@fredericDelaporte
Copy link
Member

fredericDelaporte commented Oct 24, 2018

Ok, so it should be possible to initialize the lazy initializer before calling the base constructor. But then it will be a sizeable possible breaking change: this will cause proxies of classes initializing some of their properties in their constructor to force their initialization on construction, defeating the proxy purpose.

Of course, we can consider it as to be expected, since the class access its state during construction. Having it still lazy and actually ignoring those calls may defeat the constructor intended behavior. But for classes doing some initialization useful for new entities but unneeded for loaded ones, like initializing collections, this would cause them to be no more actually lazy.

@fredericDelaporte
Copy link
Member

fredericDelaporte commented Oct 24, 2018

the code will behave correctly only is the a variable holding the proxy is declared as an interface. And, there could be some potential issues with hidden traps.

I am no more convinced of this, because it looks to me what we actually generate for proxies with #1881 is indeed more like this: not only it will add an explicit implementation, but also an implicit one. That is somewhat duplicated code, but for generated code, should we care? And in such case, there are no issues. We just need to add a test for ascertaining it.

@hazzik
Copy link
Member Author

hazzik commented Oct 24, 2018

Ok, so it should be possible to initialize the lazy initializer before calling the base constructor....

I've thought about this after I wrote my comment. Yes, it will trigger the initialization if a property is accessed in the constructor, and we do not want this (as per what you've just said).

It's fairly common to have following code in the entities:

public class A 
{
  ICollection<B> _children = new List<B>();

  public virtual IEnumerable<B> Children => _children; 
}

Or

public class A 
{
  public virtual IEnumerable<B> Children { get; private set; } = new List<B>();
}

These two examples would behave as intended, however the following code would not:

public class A 
{
  public virtual IEnumerable<B> Children { get; private set; }

  public A()
  {
    Children = new List<B>();
  }
}

But the later code is having a compilation warning "Access of virtual members in a constructor".

@hazzik
Copy link
Member Author

hazzik commented Oct 24, 2018

is indeed more like this

I have implemented this on intention. Exactly because of the case you're describing in "Now I consider pursuing..." It is just easier to implement two methods explicit and implicit in case of interfaces than pick and choose what one kind method to implement.

The case with ILazyCollectionInitializer will still generate semi-valid code which will behave correctly only when the instance is declared as the interface (it cannot be declared as proxy per se).

@fredericDelaporte
Copy link
Member

Still thinking about all that, it seems to me the static proxy lacks tests for #1389, out and ref parameters. The null initializer path has maybe some flaws for these cases too.

@fredericDelaporte
Copy link
Member

fredericDelaporte commented Oct 24, 2018

I have added some tests for the explicit interface case, since the change here is also enough to have it no more failing validation. I have also added checks for constructor calls in other cases, since this PR is about having those calls not failing (in the abstract base class case, but better check the more usual case too).

I do not think I will add tests for out or ref here even if they work (not checked yet), it seems a bit too unrelated.

@hazzik
Copy link
Member Author

hazzik commented Oct 24, 2018

In previous version it fails with NullReferenceException.

Actually, I've applied your last commit onto master and it is even worse than NullReferenceException. Explicit interface fails with StackOverflowException.

@@ -114,7 +158,7 @@ public void VerifyProxyForClassWithInternalInterface()
}

[Test]
public void VerifyProxyForClassWithAdditionalInterface()
public void VerifyProxyOnInterface()
Copy link
Member Author

Choose a reason for hiding this comment

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

Why did you mixed up names? The name was correct.

Copy link
Member

Choose a reason for hiding this comment

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

Not really. This case tests a proxy: IPublic, while the other case tests proxy: TestClass, with TestClass being also TestClass: IPublic.

The names were not allowing to do the distinction. ProxyOnInterface seems to me well more explicit about the fact we test a proxy based on an interface, not a proxy based on a class which also happens to implement some interface.

@hazzik hazzik changed the title Fix attempt to call base method for abstract classes Fix attempt of static proxies to call base method for abstract classes Oct 25, 2018
@fredericDelaporte

This comment has been minimized.

@@ -158,7 +158,7 @@ public void VerifyProxyForClassWithInternalInterface()
}

[Test]
public void VerifyProxyOnInterface()
public void VerifyProxyForClassWithAdditionalInterface()
Copy link
Member

@fredericDelaporte fredericDelaporte Oct 25, 2018

Choose a reason for hiding this comment

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

This test generates a proxy class solely based on the interfaces. It is not actually a proxy of the class. Going to that name is going back to the confusion. Even worse combined with the other renaming.

We now have:

  • VerifyProxyForClassWithInterface: does test a class proxy: PublicInterfaceTestClass, INHibernateProxy, IPublic.
  • VerifyProxyForClassWithAdditionalInterface: does actually test a class proxy: INHibernateProxy, IPublic, without any direct relationship to PublicInterfaceTestClass. Additional is far from telling this.
    The fact that obtaining this requires to add the interface in the interface list of the proxy factory is a (bad) implementation detail. It seems way more important to me to describe the kind of proxy we test, rather than the way we obtain such proxy.

@hazzik

This comment has been minimized.

@hazzik hazzik force-pushed the abstract-base-class branch from 3860bd7 to c1ae5d4 Compare October 25, 2018 23:25
hazzik and others added 2 commits October 26, 2018 12:26
Previously it was failing PEVerify similarly to nhibernate#1728 but for abstract classes (this is possible in case of a polymorphic entities). Unlike interfaces the abstract base class can go into a situation when lazy initializer is not yet available, eg. code in a constructor.
And check access into constructor, and check proxy on type with
interfaces.

To be squashed
@hazzik hazzik force-pushed the abstract-base-class branch from c1ae5d4 to 20fbd03 Compare October 25, 2018 23:26
@hazzik

This comment has been minimized.

@hazzik hazzik changed the title Fix attempt of static proxies to call base method for abstract classes WIP - Fix attempt of static proxies to call base method for abstract classes Oct 26, 2018

IL.Emit(OpCodes.Ret);
}
else
Copy link
Member Author

@hazzik hazzik Oct 26, 2018

Choose a reason for hiding this comment

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

With the current tests this branch is not needed at all. If the if condition replace with if(true) all tests still pass.

I'm considering to make a breaking change and throw exception if __lazyInitializer is null. Because the code in the constructor will not execute properly in any way.

An alternative to an exception would be to set __lazyInitializer before calling base constructor. This way the code will execute properly, but the proxy would be initialized.

@hazzik
Copy link
Member Author

hazzik commented Oct 26, 2018

I finally understood what you have meant, @fredericDelaporte and how the comment differs to the committed tests. What you describe in a comment as a "weird" class in fact could be fairly common code. However, ReSharper complains on such a code with "virtual call in constructor" warning.

Because __lazyInitializer is null only in the constructor calling base class methods does not make sense as the state change will be lost. And no-op should be fine [in some cases]. Consider following example:

public class A
{
  public virtual int X { get; set; }
  public A()
  {
    X = -1;
  }
}

var a = new AProxy();
// a.HibernateLazyInitializer.SetImplementation(new A());
Assert.That(a.X, Is.Equal(-1));

This will pass, because the implementation will have a real constructor.

So far, we have following options for a solution:

  1. always throw exception if __lazyInitializer is null.
  2. throw exception if __lazyInitializer is null and method is not void. Void methods are no-op.
  3. always no-op.
  4. best effort in calling base class (chose an option from 1-3 if base method is not callable).
  5. set __lazyInitializer before calling a base constructor.

I would prefer either 1 or 5.

@fredericDelaporte

This comment has been minimized.

@hazzik

This comment has been minimized.

@fredericDelaporte
Copy link
Member

fredericDelaporte commented Oct 26, 2018

For me, neither 1 or 5 are good. You are absolutely right when you write

the code in the constructor will not execute properly in any way.

Yes, but should we really care? I think no. We are building a proxy for a case who normally does not care at all of the initial state of the instance, because this state is to be replaced later by another one, on initialization.

So I think we should just focus on getting the proxy successfully instantiated, without caring much about its base state, which the proxy does not need to care of. So what does currently this PR, or eventually option 3, seems to me as the way to go.

Option 1 or 5 would be breaking changes without any benefit. It would just restrict what the user can do, without bringing him any benefit in my opinion. What he does in the constructor is still done, and done properly, within the delegated state. The fact it is not done in the base state is in most cases irrelevant, since the proxy behavior depends on the delegated state, not on the base one.

I think that what does currently this PR is still better than option 3, because with option 3 the possible breaking change I have just edited out should be brought back in.

@hazzik
Copy link
Member Author

hazzik commented Oct 26, 2018

With option 3 user can see a NullReferenceException if they try access to the state.

@fredericDelaporte
Copy link
Member

Yes, that is why I consider what does currently this PR is still better.

@fredericDelaporte
Copy link
Member

fredericDelaporte commented Oct 26, 2018

My stance on the subject is valid for the lazy entity proxy case.

But for the lazy property case, that is a bit less well. There is no delegated state for this other kind of proxy, it does care about its base state.
But this other kind of proxy is also used only for loaded entities, not for new ones. Its state after instantiation is anyway overridden by the loading mechanism (excepted non-mapped properties). So in most cases, what does the base constructor does not really matter, provided it does not throw. And for this, this PR current state still seems the most appropriate to me.

It is not ideal. In some special cases, especially for non-mapped properties explicitly implementing an interface, this could result in hard to diagnose behavior for the user. Throwing an explicit exception there would be better for those cases. But it would be worst for other cases, in my opinion more usual, where the user only cares for what does the constructor for new entities, not for loaded ones.

So I think keeping this PR current state is the right, pragmatic decision to take.

Maybe just rename EmitCallBaseIfLazyInitializerIsNull to something more explicit about the intent, like EmitNullLazyInitializerHandling.


IL.Emit(OpCodes.Ret);
}
else if (method.IsPrivate)
Copy link
Member Author

Choose a reason for hiding this comment

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

This is an explicit interface method.

Instead we can emit no-op (same as for the abstract method).

Copy link
Member

Choose a reason for hiding this comment

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

I was not considering to do the effort of calling a base explicit implementation, since that is already not easily doable in C# code, and previous releases were not supporting it anyway. So no-op would have suited me. But now that it is done and tested, why not keeping it. It is still better.

@hazzik hazzik changed the title WIP - Fix attempt of static proxies to call base method for abstract classes Fix attempt of static proxies to call base method for abstract classes Oct 26, 2018

IL.Emit(OpCodes.Ret);
}
else if (method.IsPrivate)
Copy link
Member

Choose a reason for hiding this comment

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

I was not considering to do the effort of calling a base explicit implementation, since that is already not easily doable in C# code, and previous releases were not supporting it anyway. So no-op would have suited me. But now that it is done and tested, why not keeping it. It is still better.

@fredericDelaporte fredericDelaporte merged commit fc76122 into nhibernate:master Oct 27, 2018
@hazzik hazzik deleted the abstract-base-class branch March 15, 2019 01:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants