2

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?

Thex
  • 31
  • 1
  • 5
  • 2
    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 Aug 12 '17 at 21:46
  • 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 it – Thex Aug 12 '17 at 22:06
  • 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 do – Thex Aug 12 '17 at 22:08
  • It's worth noting that a well-behaved add-on should return a library table rather than create global variables. – luther Aug 12 '17 at 23:05
  • I know, that's the point of that little tool :) to help the user remember to do that, and to provide a small framework to store your stuff in a table. What i'd love would be to be able to make it so it catches all your global stuff and silently store it in a table (and give it back to you when trying to call them), so you wouldn't even need to worry about it anymore, but i don't knlw if such a thing is really possible – Thex Aug 13 '17 at 09:11
  • 3
    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 https://www.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 Aug 13 '17 at 11:14
  • Oh jeez ok, i hadn't understood it that way, that's so cool! Thanks a lot! :D – Thex Aug 13 '17 at 19:32

1 Answers1

1

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.

user3125367
  • 2,920
  • 1
  • 17
  • 17