2
votes

I recently learned the existence of methatables in lua, and i was toying around with them until an idea came to my mind: would it be possible to use those to try and avoid "duplicates" in a table? I searched and searched and so far couldn't find what i'm looking for, so here i am.

  • So here's what i'd want to be able to do, and the purpose:

It's to be used in WoW addons programming. I want to make a tool that would prompt a warning whenever creating a variable or function in the global scope (to avoid using it, because of possible naming conflicts that could happen with other addons). Another thing i could want to do from there would be to redirect all transit from and to the _G table. So when the user creates a variable or function in the global scope, the tool would catch that, store it in a table instead of _G, and whenever the users would try to access something from _G, the tool would first look for it in said table; and use _G only as a fallback. That way, the user would be able not to worry about proper encapsulation or namings, the tool would take care of it all for him.

  • What i already managed to do:

I'm setting a __newindex metamethod on _G to catch the global scoped variables and functions, and removing the metamethod at the end of the addon's loading, to avoid it being used by other addons. For the "indirection of _G transit", i already know how i could use __index to try and give the value stored in another table instead before trying to use _G.

  • Issue i'm having:

This works well, but only with variables and functions that don't already exist in _G. Whenever assigning a value to a key that is already in the _G table, it doesn't work (for obvious reasons). I would like to indeed be able to catch those cases, and basically make it impossible to actually overwrite content of _G, and instead using a sort of "overload" (but without the user having to even know that).

  • What i tried:

I tried to hook rawset, to see if it was called automatically, and it appears it is not.

I haven't been able to find much documentation about the _G table in lua, because of the short name mostly. I'm sure something must exist somewhere, and i could probably use the information to get things done the way i want, but currently i'm just kinda lost and running out of ideas. So yeah i'd like to know if there was any way to "catch" all the "implicit calls to rawset" in order to make some checks before letting it do its stuff. I gathered that apparently there is no metamethod for __existingindex or something, so do you know any way to do it please?

1
What you are looking for is known as a proxy table. You need to make a new empty table and assign it to _G, while setting __newindex and __index to functions that search the original _G table, which is captured in some manner. Your __newindex function should not allow redefining existing keys. I've never done such a thing, so I can't tell you how stable such a solution would be. It would certainly be very slow. I can do a full write up if you wish. EDIT: Look at implementations of strict.lua for inspiration.ktb
Thanks for your answer; however i'm not sure how it would solve my issue. Being able to know whether or not a key already exists in _G is not an issue (i could simply have a local _G in my tool's file and check if local_G[key] ~= nil); the issue is that i don't know how to know when an already existing key is being reassigned. My goal for my tool is for it to be "initialize and forget". I would not want having to call a function whenever trying to assign something to the global scope, i would want the tool to "detect" whenever i do it, so it could tell me not to do itThex
If it was somehow possible to make it so every time something is declared as global, it would go into a different table than _G, that would be great. I know that i could do that by getting a local "fake" _G at the beginning of every file, but that's what i'd like to avoid having to doThex
It's worth noting that a well-behaved add-on should return a library table rather than create global variables.luther
For the interpreter, the global assignment a=1 looks like _G['a']=1, thus the first comment is the solution. Also, it is a repetition of the information that is provided here lua.org/pil/13.4.4.html . Also, the referenced book (maybe except C part), it holds answers to your questions about _G. You probably should read it whole.Dimitry

1 Answers

1
votes

Though you've got an answer in comments, there is more deep conception of environments in Lua 5.1. Environment is a table attached to function, where this function redirects its 'global' reads and writes. _G is just a reference to 'global' environment, i.e. the environment of the main thread (main coroutine). It can be cleared to nil with no non-obvious effects, because it is just a variable, something like T = { }; T._T = T.

Specifically, _G == getfenv(0), unless someone changes its meaning (see getfenv() reference for what its argument is). When script is loaded, it is implicitly bound to the global environment. Since Lua's top-level scope (aka main chunk) is just an anonymous function, its environment can be rebound at any time to any other table:

-- x.lua

local T = { }
local shadow = setmetatable({ }, { __index = getfenv(0) })

local mt = {
    __index = shadow,
    __newindex = function (t, k, v)
        -- while T is empty, this will be called every time you 'set global'
        -- do whatever you want here
        shadow[k] = v
        print(tostring(k)..' is set to '..tostring(v))
    end
}

setmetatable(T, mt) -- T[k] goes to shadow, then to _G
setfenv(1, T)       -- change the environment of this module to T

hello = "World"     -- 'hello is set to World'
print(T.hello)      -- 'World'
print(_G.hello)     -- 'nil', because we didn't even touch _G

hello = 3.14        -- 'hello is set to 3.14'
hello = nil         -- 'hello is set to nil'
hello = 2.72        -- 'hello is set to 2.72'

function f()        -- 'f is set to function: 0x804a00'
    print(hello)
end

f()                 -- '2.72'

assert(getfenv(f) == getfenv(1))
assert(getfenv(f) == T)

setfenv(f, _G)      -- return f back to _G
f()                 -- 'nil'

With this approach you can completely hide metatable mechanics from other modules. Note that changes to mt have no effect after setmetatable() call.

Also remember that all functions defined below setfenv() share the same environment T (that doesn't apply to external functions/modules loaded via require or returned from these functions/modules, because environment inheriting is lexical).

Setting __newindex on _G temporarily may work, but remember that any functions that you call in between may try to set globals, and that may interfere with your logic or break theirs in subtle ways. Probability of clash should be low though, because spoiling _G is a bad idea and everyone knows it.