blog:2020:0610_nervluna_const_and_static_fields

Support for const and static fields in NervLuna

So continuing on our NervLuna binding generator journey, we are now going to consider the support for const and static fields in a given input class.

Handling const fields is going to be relatively easy: these are just ordinary fields that the user can normally read/write in a class, except that we don't want to allow writing to those fields.

Obviously, if we were directly using C++, a “const field” is not even really const anyway: we could still const_cast that field and thus force writing a new value into it. But I feel this is not something that you want to allow in the lua bindings: we should rather respect constness and output an error in case the user tries to perform such an action.

So, we update one of our test class now to contain the following const field:

class ChildClass : public ParentClass
{
public:
    int my_second_int{6};
    bool my_second_bool{false};

    const int my_const_int_value{9};
    
    ParentClass my_parent_field;
    ParentClass* my_second_parent{nullptr};
    ParentClass* my_third_parent{nullptr};

    ChildClass() {
        my_third_parent = new ParentClass();
        my_third_parent->my_int_value = 7;
    }

    ~ChildClass() {
        delete my_third_parent;
    }
};

And we prepare a unit test to ensure that we cannot write into that field:

describe("Access to const fields", function()

  it("Should support read only for const field", function()
    local obj = nvt.ChildClass()

    assert_equal(obj.my_const_int_value, 9)

    -- We should not be able to write to that const field, 
    -- so this should produce an error:
    assert_error(function() 
      obj.my_const_int_value = 11;
    end)
  end)

end)

And now it's time to update the binding generation code, because it will anyway not compile with the code generated in that case for the moment (simply because we are not taking constness into account, so we are just trying to write to that field as an ordinary read/write field).

The actual lua binding code change is done in my FieldWriter class, and is pretty simple: in case the field type is detected to “be const”, we just write an error message instead of the regular field getter logic:

-- #109: if the type is const, then we should not allow writing to it:
  if ftype:isConst() then
    self:writeSubLine("luaL_error(L,\"Cannot write value to const field '${1}'.\");",field:getFullName());
  else
    -- get the value from the stack:
    conv:writeGetter(self, 2, field, true)
  end

And the resulting code we obtain from this is as follow:

// Bind for field my_const_int_value
static int _field_bind_my_const_int_value(lua_State* L) {
	if(!_field_check_my_const_int_value(L)) {
		luaL_error(L, "Invalid stack elements for field my_const_int_value access.");
		return 0;
	}
	int luatop = lua_gettop(L);
	nvt::ChildClass* self = Luna< nvt::ChildClass >::get(L,1);
	if(luatop==2) {
		luaL_error(L,"Cannot write value to const field 'nvt::ChildClass::my_const_int_value'.");
		return 0;
	}
	else {
		lua_pushnumber(L,self->my_const_int_value);
		return 1;
	}
}

⇒ So this seems to be exactly what we want, and indeed, after compilation the unit test is passed. So all good on that point :-).

Now, support for static fields is a bit different: because static fields should not be defined in the “object instances” but rather in the “class” itself. So this means that we should extend the class metatable (ie. the metatable of our metatable), because this is where the __index and __newindex methods will be retrieved from.

This also means that we need to list the “static fields” in a separated array when generating the bindings. And that we will need to update the class init script accordingly of course. So let's get started.

First I'm adding a static field in my test class:

class ChildClass : public ParentClass
{
public:
    int my_second_int{6};
    bool my_second_bool{false};

    const int my_const_int_value{9};

    static int my_static_int;
    
    ParentClass my_parent_field;
    ParentClass* my_second_parent{nullptr};
    ParentClass* my_third_parent{nullptr};

    ChildClass() {
        my_third_parent = new ParentClass();
        my_third_parent->my_int_value = 7;
    }

    ~ChildClass() {
        delete my_third_parent;
    }
};

Of course I define the value of that field in a cpp file afterwards, otherwise my test module will not compile, but that part is not relevant here for the binding generation itself

One interesting point I just noticed is that in that case the libclang library will find that my “my_static_int” value is a cursor of type VarDecl (and not a FieldDecl). I didn't find anything else on how to differenciate between static and non-static fields, so let's just give it a try and use that “VarDecl” type accordingly.

So I add support for a static flag in my reflection “Field” class:

function Class:__init(name, parent)
  Class.__initBases(self, name, parent)
  self:setEntityType(self.EntityType.FIELD)
  self._type = nil
  self._static = false;
end

function Class:setStatic(val)
  self._static = val
end

function Class:isStatic()
  return self._static
end

Then I ensure that I will separate the declaration of the static fields in a dedicated static_fields array (inside the ClassWriter class):

-- #109: write the static fields accessors:
  self:writeSubLine("luna_RegType LunaTraits< ${1} >::static_fields[] = {", fname)
  self:pushIndent()
  
  -- Here we should push the list of all public getters:
  for _,f in ipairs(staticFields) do
    self:writeSubLine("{\"${1}\", &_field_bind_${1}},", f:getName())
  end

And of course we need to handle the “VarDecl” type when processing a class:

elseif ckind == clang.CursorKind.FieldDecl then
  obj = scope:getOrCreateField(cname)
  self:parseField(cur, obj)
elseif ckind == clang.CursorKind.VarDecl then
  -- #109: it seems that the "VarDecl" type is the libclang mechanism to use to detect static fields in a class (?)
  obj = scope:getOrCreateField(cname)
  obj:setStatic(true)
  self:parseField(cur, obj)

And we should update the LunaTraits template to also declare this new static_fields array:

    template <typename T>
    class LunaTraits
    {
    public:
        typedef Luna<T> luna_t;
        static const char className[];
        static const char fullName[];
        static const char *namespaces[];
        static const char *parents[];
        static const StringID baseIDs[];
        static const StringID id;
        static luna_RegType methods[];
        static luna_RegType fields[];
        static luna_RegType static_fields[];
        static luna_ConverterType converters[];
        static luna_RegEnumType enumValues[];
        static T *construct(lua_State *L);
        static void destruct(T *obj);
        typedef T base_t;
        typedef T parent_t;
    };

And last but not least, we should also update the field accessor function in this case to not expect to receive an object. Done

With the lastest changes, here is the kind of accessor that is now generated for a static field:

// Bind for field my_static_int
static int _field_bind_my_static_int(lua_State* L) {
	if(!_field_check_my_static_int(L)) {
		luaL_error(L, "Invalid stack elements for field my_static_int access.");
		return 0;
	}
	int luatop = lua_gettop(L);

	if(luatop==2) {
		nvt::ChildClass::my_static_int = (int32_t)lua_tointeger(L,2);
		return 0;
	}
	else {
		lua_pushnumber(L,nvt::ChildClass::my_static_int);
		return 1;
	}
}

With this code, the compilation will go just fine. But then I also need to upgrade the class generation method to setup the “static_fields” we declared above appropriately, so let's do that:

            lua_newtable(L); // mt for "class metatable"
            int metameta = lua_gettop(L);
            lua_pushliteral(L, "__call");
            lua_pushcfunction(L, new_T);
            lua_settable(L, metameta);

            // #109: Here we should register the static field accessors:
            for (const luna_RegType *l = traits_t::static_fields; l->name; l++)
            {
                lua_pushstring(L, l->name);
                lua_pushlightuserdata(L, (void*)l->mfunc);
                lua_settable(L, metameta);
            }

            // And we also need the __index/__newindex for the static fields access:
            lua_pushliteral(L, "__index");
            lua_pushcfunction(L, static_get_index);
            lua_settable(L, metameta); // metatable.__index=metameta

            lua_pushliteral(L, "__newindex");
            lua_pushcfunction(L, static_set_index);
            lua_settable(L, metameta); // metatable.__index=methods

            // Assign the metameta table:
            lua_setmetatable(L, methods);

In the code above, the functions static_get_index and static_set_index are defined very similarly to the “get_index”/“set_index” function that we use for the regular access to functions/fields in a given object instance. Except that, in the case of a class metatable, we have an actual lua table to work with, so this time we can afford to write a non-existant field into the class itself [note to myself: I should build a unit test to validate this point]

⇒ And at this point, binding generation/compilation is OK and the unit tests are passed. Great :-)!

This should go very smoothly already, but just to be sure, I'm adding a unit tests on static + cont fields access:

describe("Access to static/const fields", function()

  it("Should support read only for const field", function()

    assert_equal(nvt.ChildClass.my_const_static_int, 11)
    
    -- We should not be able to write to that const field, 
    -- so this should produce an error:
    assert_error(function() 
      nvt.ChildClass.my_const_static_int = 12;
    end)
  end)
  
end)

⇒ And this worked just fine. Good.

As already mentioned above, I should also not forget to update the class init script to also copy the static_fields from the parent class metatables… or maybe not ?… If you think about it a static field should really be considered “the property” of a given class itself, not of the classes that will inherit from it (even if in C++ you can access that field using the child class name, I think in lua we should be strict on that point). So for the moment I will not implement this inheritance, and we will see later if this needs to be updated.

While we are working on the support for static elements, we could also push this a little further, and implement the support for static functions too! So here is the extended version of our test class for that:

class ParentClass
{
public:
    int my_int_value{5};
    bool my_bool_value{true};

    static bool isEven(int val) {
        return val%2==0;
    }
};

⇒ And once more, I just realized that support for this is already implemented (“somehow”…): We are currently registering static functions just as regular functions in a class. And thus, we can access them either from an object instance of from the class itself (ie. the metatable for our objects). This also means that those functions will be inherited in derived classes, which seems a bit contradictory to the approach selected just above for the static fields… So maybe we should consider unifying our decision at this level (?). So now I'm changing my mind and I rather think I should inherit the static fields too: let's update the init script with the following content:

-- We should also copy the static fields from that parent:
  local classMT = getmetatable(class)
  local parentMT = getmetatable(parent)

  for k,v in pairs(parentMT) do
    if k~='__index' and k~='__newindex' and classMT[k]==nil then
      classMT[k]=v
    end
  end

And now let's test if our field is indeed inherited (using a new class “ChildClassB”, that will inherit from “ChildClass” below):

it("Should support read/write for inherited static field", function()
    nvt.ChildClass.my_static_int = 10;
    assert_equal(nvt.ChildClassB.my_static_int, 10)
    
    -- We should be able to write to that static field:
    nvt.ChildClassB.my_static_int = 11;
    
    assert_equal(nvt.ChildClassB.my_static_int, 11)
    assert_equal(nvt.ChildClass.my_static_int, 11)
  end)

  it("Should support read only for inherited const field", function()

    assert_equal(nvt.ChildClassB.my_const_static_int, 11)
    
    -- We should not be able to write to that const field, 
    -- so this should produce an error:
    assert_error(function() 
      nvt.ChildClassB.my_const_static_int = 12;
    end)
  end)

⇒ Great! The tests are passed without any trouble. So it seems we are all good once more on this const/static fields development session ;-). Let's call it a day, and next time, we will see if we can generate bindings for an actual subset of the nvCore library that we really need in Lua.

  • blog/2020/0610_nervluna_const_and_static_fields.txt
  • Last modified: 2020/07/10 12:11
  • (external edit)