Search code examples
lambdavala

Confused how Delegates and Closures behave in Vala


I've created a minimal example which reproduces a strange Vala behaviour, which I do not understand and would like to have explained.

The constructor of class Test takes a Func and uses it to initialize its class member f:

public class Test
{
    public delegate int Func();
    public static Func FUNC_0 = () => { return 0; };

    public Func f;

    public Test( Func f )
    {
        this.f = f;   // line 10
    }
}

I instantiate a Test object using the Func defined in Test.FUNC_0, and do a few tests:

public static void main()
{
    assert( Test.FUNC_0 != null );   // first assert
    var t = new Test( Test.FUNC_0 );
    assert( t.f != null );           // second assert
}

Now what's strange about this?

  • First of all, it turns out that Test.FUNC_0 is null. How can that be?!
  • valac gives me a warning that "copying delegates is not supported", but in line 10, which is the this.f = f assignment, so this warning doesn't regard the Test.FUNC_0 field.
  • If I remove the first assert and replace the Test.FUNC_0 argument of new Test by () => { return 0; }, then the second assert passes. So what's wrong with this.f = f in line 10? Is the closure in line 10 copied or not?
  • And if it is, how would I tweak the code to keep just a reference as a class member in Test?

I'd really appreciate to see this explained. The valac version is 0.28.1.


Solution

  • Your problem is actually nothing to do with delegate and everything to do with singly-owned instances. All non-primitive types in Vala are either owned or unowned. Some classes (including the ones that derive from GLib.Object) can have multiple owners. When a copy of the class is needed, the reference count on he target class is incremented. Other classes (including string) and structs can only have a single owner but come with a copy function that allows generating a duplicate of that class. Delegates and certain classes (like FileStream) also only have a single owner, but can't be copied.

    The reason delegates can't be copied is that a delegate is three pieces of information: a callback function, some context data, and, maybe, a destructor for the context data. There's no copy function.

    Since parameters are unowned by default, this.f = f, is trying to copy a delegate, which it does not own, into a reference that it does. This would be memory unsafe since it would either hold the reference past the life cycle of the object or the destructor could be called twice.

    You have two options:

    public class Test
    {
        public delegate int Func();
        public static Func FUNC_0 = () => { return 0; };
    
        public Func f;
    
        public Test( owned Func f )
        {
            this.f = (owned) f;   // you are just moving the delegate, not copying it.
        }
    }
    

    alternatively:

    public class Test
    {
        public delegate int Func();
        public static Func FUNC_0 = () => { return 0; };
    
        public unowned Func f;
    
        public Test( Func f )
        {
            this.f = f;   // You are copying an unowned reference to another
            // unowned reference, which is fine. Memory management is now your
            // job and not Vala's.
        }
    }
    

    This second one is a bit dangerous. For instance, the following code will compile and corrupt memory:

    Test? t = null;
    if (true) {
      int x = 5;
      Func f = () => { return x; };
      t = new Test(f);
    }
    t.f();