Nick
Nick

Reputation: 988

Linq Expression tree compiling non-trivial object constants and somehow referring to them

Usually, when compiling an expression tree, I would have thought constants which are not primitives types or strings would be impossible. However, this code:

   public class A
        { public int mint = -1; }

 public static void Main(String[] pArgs)
        {
            //Run(pArgs);

            Action pact = Thing();

            pact();
        }

        public static Action Thing()
        {
            var a = new A();
            a.mint = -1;

            LambdaExpression p =
                Expression.Lambda<Action>(Expression.Assign(Expression.Field(Expression.Constant(a, typeof(A)), Strong.Instance<A>.Field<int>(b => b.mint)), Expression.Constant(3, typeof(int))));

            return ((Expression<Action>)p).Compile();

        }

not only compiles but it actually runs! If you run the compiled method in the Thing() method, then you can actually see the variable a change its field from -1 to 3

I don't see how this makes sense/is possible. How can a method refer to a local variable outside its scope (when inspecting the IL of Thing(), the variable a is just a standard local variable, not on the heap like with a closure). Is there some kind of hidden context past around? How can pact run in Main when the local variable a has presumably been removed from the stack!

Upvotes: 0

Views: 150

Answers (2)

Jon Hanna
Jon Hanna

Reputation: 113302

How can a method refer to a local variable outside its scope

It can't, and it doesn't.

It can sometimes refer to the object the local variable points to though.

Whether it can or not depends on which way the expression is compiled or otherwise used.

There are three ways that Expressions itself will compile an expression into a method:

  1. Compilation with Compile() to IL in a DynamicMethod.
  2. Compilation to IL in with CompileToMethod() (not available in all versions.
  3. Compilation with Compile() into an interpreted set of instructions with a thunk delegate that runs the interpretation.

The first is used if IL compilation is available, unless true is passed to prefer interpretation (on those version with that overload) and interpretation is not also available. Here an array is used for the closure and it is much the same as closing over a local is, in a delegate.

The second is used to write to another assembly and cannot close in this way. Many constants that will work with Compile() will not work with CompileToMethod() for this reason.

The third is used if IL compilation is not available, or true was passed in those versions that have that overload to prefer interpretation. Here a reference to the object is put into an array of "constants" that the interpreter can then refer to.

The other possibility is that something else interprets the expression entirely, e.g. in producing SQL code. Generally this will fail with non-primitive constants other than string though, but if the query processor is aware of the type of the constant (e.g. if it is of a type of entity that it knows about) then code to produce the equivalent to that entity could be produced.

Upvotes: 1

Marc Gravell
Marc Gravell

Reputation: 1063058

It is only a that is a local variable; the actual object (from new A()) was always on the heap. When you used Expression.Constant(a, typeof(A)), it wasn't a that you put in as a constant - it was the value of a, i.e. the object reference. So: the scope of a is irrelevant as far as the tree is concerned. This is actually exactly how captured variables (closures) are usually implemented by the compiler (although you don't see it usually, and the C#-based expression compiler doesn't allow assignment operators), so as far as the expression tree is concerned: this is business as usual.

As a comparable example using the C# expression compiler, see here, where

public void M() {
    int mint = -1;
    Expression<Func<int>> lambda = () => mint;
}

compiles to:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int mint;
}

public void M()
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.mint = -1;
    Expression.Lambda<Func<int>>(Expression.Field(Expression.Constant(<>c__DisplayClass0_, typeof(<>c__DisplayClass0_0)), FieldInfo.GetFieldFromHandle((RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/)), Array.Empty<ParameterExpression>());
}

Upvotes: 0

Related Questions