Search code examples
clualua-apimetatable

Lua - Why are C functions returned as userdata?


I'm working on game scripting for my engine and am using a metatable to redirect functions from a table (which stores custom functions and data for players) to a userdata object (which is the main implementation for my Player class) so that users may use self to refer to both.

This is how I do my binding in C# in the Player class:

        state.NewTable("Player");    // Create Player wrapper table
        state["Player.data"] = this; // Bind Player.data to the Player class
        state.NewTable("mt");        // Create temp table for metatable
        state.DoString(@"mt.__index = function(self,key)
                             local k = self.data[key]
                             if key == 'data' or not k then
                                 return rawget(self, key)
                             elseif type(k) ~= 'function' then
                                 print(type(k))
                                 print(k)
                                 return k
                             else
                                 return function(...)
                                     if self == ... then
                                         return k(self.data, select(2,...))
                                     else
                                         return k(...)
                                     end
                                 end
                             end
                         end");
        state.DoString("setmetatable(Player, mt)"); // Change Player's metatable

For my Player class, I implement a method, bool IsCommandActive(string name). When I need to call this method using self, it needs to use the userdata object, rather than the table, otherwise I get the following error:

NLua.Exceptions.LuaScriptException: 'instance method 'IsCommandActive' requires a non null target object'

For obvious reasons. This is because self refers to the table, not the userdata. So I implemented a metatable so that it may use self to refer to either. The implementation is taken from here, but here is my particular variant (my userdata is stored in an index called data:

mt.__index = function(self,key)
    local k = self.data[key]
        if key == 'data' or not k then
            return rawget(self, key)
        elseif type(k) ~= 'function' then
            print(type(k))
            print(k)
            return k
        else
            return function(...)
                if self == ... then
                    return k(self.data, select(2,...))
                else
                    return k(...)
                end
            end
        end
    end
end

Which I follow by using setmetatable, obviously.

Now to the meat of my question. Notice how I print type(k) and print(k) under the elseif. This is because I noticed that I was still getting the same error, so I wanted to do some debugging. When doing so, I got the following output (which I believe is for IsCommandActive):

userdata: 0BD47190

Shouldn't it be printing 'function'? Why is it printing 'userdata: 0BD47190'? Finally, if that is indeed the case, how can I detect if the value is a C function so I may do the proper redirection?


Solution

  • After lots of reading about metatables, I managed to solve my problem.

    To answer the question in the title, it's apparently what NLua just decides to do and is implementation-specific. In any other bindings, it may very well return as function, but such is apparently not the case for NLua.

    As for how I managed to accomplish what I wanted, I had to define the metatable __index and __newindex functions:

            state.NewTable("Player");
            state["Player.data"] = this;
            state.NewTable("mt");
            state.DoString(@"mt.__index = function(self,key)
                                 local k = self.data[key]
                                 local metatable = getmetatable(k)
                                 if key == 'data' or not k then
                                     return rawget(self, key)
                                 elseif type(k) ~= 'function' and (metatable == nil or metatable.__call == nil) then
                                     return k
                                 else
                                     return function(...)
                                         if self == ... then
                                             return k(self.data, select(2,...))
                                         else
                                             return k(...)
                                         end
                                     end
                                 end
                             end");
            state.DoString(@"mt.__newindex = function(self, key, value)
                                 local c = rawget(self, key, value)
                                 if not c then
                                     local dataHasKey = self.data[key] ~= key
                                     if not dataHasKey then
                                         rawset(self, key, value)
                                     else
                                         self.data[key] = value
                                     end
                                 else
                                     rawset(self, key, value)
                                 end
                             end");
            state.DoString("setmetatable(Player, mt)");
    

    What __index does is override how tables are indexed. In this implementation, if key is not found in the Player wrapper table, then it goes and tries to retrieve it from the userdata in Player.data. If it doesn't exist there, then Lua just does its thing and returns nil.

    And just like that, I could retrieve fields from the userdata! I quickly began to notice, however, that if I set, for instance, self.Pos in Lua, then the Player.Pos would not update in the backing C# code. Just as quickly, I realized that this was because Pos was generating a miss in the Player wrapper table, which meant that it was creating a new Pos field for the table since it actually did not exist!

    This was not the intended behavior, so I had to override __newindex as well. In this particular implementation, it checks if the Player.data (userdata) has the key, and if so, sets the data for that particular key. If it does not exist in the userdata, then it should create it for the Player wrapper table because it should be part of the user's custom Player implementation.