Posts: 3
Threads: 2
Joined: Oct 2017
Thanks: 6
Given 0 thank(s) in 0 post(s)
10-23-2017, 03:31 PM
(This post was last modified: 10-23-2017, 03:31 PM by dusk.)
I am about to write my first plugin. After reading over the documentation, I decided to try and be creative with the syntax for setting hooks, because I felt like the "official" recommended way was going to be really difficult for me to read and follow. So I tried setting hooks this way, outside of the Initialize function and using anonymous functions for the function argument, and behold, it works
One thing I noticed was that code outside of any function runs before the Initialize function is called. So I'm wondering, are there any negative side effects to connecting these events before the plugin has been technically initialized?
Posts: 6,485
Threads: 176
Joined: Jan 2012
Thanks: 131
Given 1075 thank(s) in 852 post(s)
10-23-2017, 05:15 PM
(This post was last modified: 10-23-2017, 05:18 PM by xoft.)
Yes, it is possible to register hooks that way, and your insight that the registrations execute before the Initialize function is correct, and is central to Lua. When (any) Lua interpreter reads code, it first parses it into an internal representation, and then executes *all* of it. It doesn't mean it calls all functions, but rather, the execution sees "function Initialize() ... end" and knows "aha, this code creates a new function and names it Initialize", so it internally registers a new function of the name and points it to the block of executable code. Then it sees some function calls to AddHook, so it does those immediately.
There might be some negative points to registering hooks this way. I'll describe how Cuberite loads plugins, and you'll see them.
First Cuberite makes a list of all .lua files in the plugin folder, and it sorts the list by name [*]. Then, it loads each file (load = reading and executing the code, as described above). Then it asks Lua to execute the global function Initialize. So what could go wrong? As long as your plugin is within just 1 file, not much, really. But consider this:
File a.lua:
-- At global scope, outside all functions:
cPluginManager.AddHook(cPluginManager.HOOK_PLAYER_MOVING, OnPlayerMoving)
File b.lua:
function OnPlayerMoving(...)
-- The actual implementation
end
In this case, the plugin is loaded, file-by-file, so first the file a.lua is loaded and executed first. The hook registration fails, because OnPlayerMoving is unknown at this point (thus nil, by Lua definition). If the file names were swapped, then everything would work just fine, which is quite unfortunate, because it hides the possible problem until someone refactors the code and renames the files again. For this reason, we do encourage people to use the Initialize function as the place where all initialization should be done - when Cuberite calls that function, you can be sure that whatever the file load order, your global symbols are already defined correctly.
[*]: Special exception: the Info.lua file is always loaded as the last one, so that the function references inside it work properly.
Posts: 3
Threads: 2
Joined: Oct 2017
Thanks: 6
Given 0 thank(s) in 0 post(s)
10-23-2017, 07:47 PM
(This post was last modified: 10-23-2017, 07:57 PM by dusk.)
(10-23-2017, 05:15 PM)xoft Wrote: Yes, it is possible to register hooks that way, and your [...]
Thanks for the detailed description
I'm really fond of Cuberite, it's a wonderful project, and I can see that it has passionate devs.
I can see where the problem would arise. Would I be correct in assuming that setting hooks outside of any function, in the global scope, would always be safe from this problem only provided that an anonymous function is passed as the function argument? e.g.:
cPluginManager.AddHook(cPluginManager.HOOK_WHATEVER, function(...)
--[[...]]
end)
Instead of:
cPluginManager.AddHook(cPluginManager.HOOK_WHATEVER, HookWhateverHandler)
Posts: 4,628
Threads: 115
Joined: Dec 2011
Thanks: 693
Given 494 thank(s) in 423 post(s)
If you use anonymous functions I don't see how it could go wrong.
Posts: 6,485
Threads: 176
Joined: Jan 2012
Thanks: 131
Given 1075 thank(s) in 852 post(s)
An anonymous function will be alright. A global function will be alright as long as you know it will be defined before the registration (i.e. same file, function above registration). All other usages should be considered unsafe and avoided.
Posts: 6,485
Threads: 176
Joined: Jan 2012
Thanks: 131
Given 1075 thank(s) in 852 post(s)
By the way, the fact that hook registrations outside of function calls work is a side effect of what we've been trying to do once, a feature that would allow multiple plugins to be merged into a single plugin, simply by copying the files over (assuming that the filenames had no conflict). This was abandoned, mostly because of the Info.lua special file and because it didn't bring as many benefits as expected, but the underlying features were left over.
Posts: 4,628
Threads: 115
Joined: Dec 2011
Thanks: 693
Given 494 thank(s) in 423 post(s)
(10-23-2017, 11:42 PM)xoft Wrote: An anonymous function will be alright. A global function will be alright as long as you know it will be defined before the registration (i.e. same file, function above registration). All other usages should be considered unsafe and avoided.
Didn't we also have a problem previously where an OS with case-sensitive filenames ordered the files different compared to an OS with case-insensitive filenames or has that been fixed ?
Posts: 6,485
Threads: 176
Joined: Jan 2012
Thanks: 131
Given 1075 thank(s) in 852 post(s)
Still there, Cuberite uses case-sensitive sort, but if the OS / filesystem doesn't preserve the filename case, it's a problem.
And I'm not intending to fix that, because case-insensitivizing is a terrible amount of work when coupled with internationalization.
Posts: 7
Threads: 2
Joined: Jan 2018
Thanks: 1
Given 0 thank(s) in 0 post(s)
01-09-2018, 03:15 AM
(This post was last modified: 01-09-2018, 03:17 AM by TC1061.)
There is another solution. You can store all callbacks in a hooks table, and register all callbacks from this table, for example:
Code: hooks={}
function hooks.tick(e,d) -- Will be registered as HOOK_TICK hook.
-- Do something cool
return ... -- Return something cool
end
function Initialize(Plugin)
Plugin:SetName("SomeCoolPlugin")
Plugin:SetVersion(1)
if hooks then
for k,v in pairs(hooks) do
cPluginManager.AddHook(cPluginManager["HOOK_"..string.upper(k)], v)
end
end
PLUGIN = Plugin -- NOTE: only needed if you want OnDisable() to use GetName() or something like that
if commands then
for k,v in pairs(commands) do
cPluginManager.BindCommand(unpack(v))
end
end
LOG("Initialised " .. Plugin:GetName() .. " v." .. Plugin:GetVersion())
return true
end
function OnDisable()
LOG(PLUGIN:GetName() .. " is shutting down...")
end
Posts: 4,628
Threads: 115
Joined: Dec 2011
Thanks: 693
Given 494 thank(s) in 423 post(s)
That's a neat way of storing the hooks, but I'd probably use something like this instead:
Code: local hooks = {
[cPluginManager.HOOK_TICK] = function(TimeDelta)
end
}
function Initialize(Plugin)
Plugin:SetName("SomeCoolPlugin")
Plugin:SetVersion(1)
if hooks then
for hook, callback in pairs(hooks) do
cPluginManager:AddHook(hook, callback)
end
end
return true;
end
|