Search code examples
debuggingluaupvalue

Manually setting upvalue variables for 'load'ed LUA chunk


I'm rolling out my own LUA debugger for a C++ & LUA project and I just hit a bit of a hurdle. I'm trying to follow MobDebug's implementation for setting expression watches, which after removing all the bells and whistles essentially boils down to string-concatenating the expression you want to watch inside a return(...) statement and running that. The exact line is:

local func, res = mobdebug.loadstring("return(" .. exp .. ")")

This works perfectly as long as you're just debugging a single script and most of your variables are just in the global environment, but for my own purposes I'm trying to restrict / scope down the evaluation of these return(...) expressions to certain tables and their fields.

More specifically, I'm working with some LUA "classes" using the setmetatable / __index pattern, and, given access to the table (let's call it SomeClass) I'd like to be able to evaluate expressions such as

self.mSomeVariable + self.mSomeOtherVariable - self:someOperation(...)

where self is referring to SomeClass (i.e. there exists SomeClass:someOperation, etc).

I'm really at my wits end on how to approach this. At first I tried the following, and it most definitely didn't work.

> SomeClass = {}
> SomeClass.DebugEval = function(self, chunk_str) return load("return(" .. chunk_str .. ")")() end
> SomeClass.DebugEval(SomeClass, "print(1)") // 1 nil
> SomeClass.DebugEval(SomeClass, "print(SomeClass)") // table: 0000000000CBB5C0 nil
> SomeClass.DebugEval(SomeClass, "print(self)") // nil nil

The fact that I cannot even reference the "class" via self by passing it in directly to the wrapping function's argument makes me suspicious that this might be a upvalue problem. That is, load(...) is creating a closure for the execution of this chunk that has no way of reaching the self argument... but why? It's quite literally there???

In any case, my debugger backend is already on C++, so I thought "no problem, I'll just manually set the upvalue. Again, I tried doing the following using lua_setupvalue, but this also didn't work. I'm getting a runtime error on the pcall.

luaL_dostring(L, "SomeClass = {}"); // just for convenience; I could've done this manually
luaL_loadstring(L, "print(1)"); // [ ... , loadstring_closure ]
lua_getglobal(L, "SomeClass"); // [ ... , loadstring_closure, reference table]
lua_setupvalue(L, -2, 1); // [ ... , loadstring_closure] this returns '_ENV'
lua_pcall(L, 0, 0, 0); // getting LUA_ERRRUN

What am I missing here? Perhaps I'm approaching this in a totally incorrect way? My end goal is simply being able to execute these debugger watches but restricted to certain class instances, so I can enable my watches on a per-instance basis and inspect them. And yes, I absolutely need to roll out my own debugger. It's a long story. Any and all help is appreciated.


Solution

  • The doc to load and lua_load functions in the Lua manual mentions:

    load: When you load a main chunk, the resulting function will always have exactly one upvalue, the _ENV variable.

    lua_load: When loading main chunks, this upvalue will be the _ENV variable.

    In summary, the loaded chunk only has one upvalue, which is _ENV.

    Since Lua 5.2, SomeClass = {} is equivalent to _ENV.SomeClass = {}, and print(self) is equivalent to print(_ENV.self). Because SomeClass is set in the global scope, the loaded chunk can also see this entry in the _ENV table, but there is no place will automatically set _ENV.self.

    You may expect the function to work like this:

    function expect(self)
        return (function()
            return print(self)
        end)()
    end
    

    In fact it will work like this:

    function fact(self)
        return (function()
            return print(_ENV.self)
        end)()
    end
    

    For the "expect" function, the lua compiler will add self as an upvalue for you, but for the "fact" function, it will do nothing.

    And your C++ code completely overwritten _ENV with SomeClass, which is going too far.