WoW:Hooking functions: Difference between revisions
No edit summary |
(→Hooking a function: SetItemRef -> ChatFrame_OnHyperlinkShow) |
||
Line 20: | Line 20: | ||
== Hooking a function == | == Hooking a function == | ||
To illustrate the concept, let's create a pre-hook. Pre-hooks are commonly used to override user interface functions that perform an (unprotected) action, potentially adding behavior to existing UI. In this example, hook | To illustrate the concept, let's create a pre-hook. Pre-hooks are commonly used to override user interface functions that perform an (unprotected) action, potentially adding behavior to existing UI. In this example, hook {{api|ChatFrame_OnHyperlinkShow}} (a function called when the user clicks on a chat link) to display a faux tooltip for the {{item|Tinfoil Hat}}, an item currently not in the game. | ||
<div style="max-height: 200px; overflow: auto; border: 2px solid black; padding: 0.5em; margin-left: 0.5em"> | <div style="max-height: 200px; overflow: auto; border: 2px solid black; padding: 0.5em; margin-left: 0.5em"> | ||
<pre style="margin: 0; border: 0"><nowiki>local | <pre style="margin: 0; border: 0"><nowiki>local origChatFrame_OnHyperlinkShow = ChatFrame_OnHyperlinkShow; -- (1) | ||
ChatFrame_OnHyperlinkShow = function(...) -- (2) | |||
local link, text, button = ...; -- (3) | local chatFrame, link, text, button = ...; -- (3) | ||
if type(text) == "string" and text:match("%[Tinfoil Hat%]") and not IsModifiedClick() then --(4) | if type(text) == "string" and text:match("%[Tinfoil Hat%]") and not IsModifiedClick() then --(4) | ||
return ShowTinfoilHat(); -- (5) | return ShowTinfoilHat(); -- (5) | ||
end | end | ||
return | return origChatFrame_OnHyperlinkShow(...); -- (6) | ||
end | end | ||
print("Click me: \124cff0070dd\124Hitem:8191:0:0:0:0:0:0:0:0:1\124h[Tinfoil Hat]\124h\124r"); -- (7) | print("Click me: \124cff0070dd\124Hitem:8191:0:0:0:0:0:0:0:0:1\124h[Tinfoil Hat]\124h\124r"); -- (7) | ||
Line 55: | Line 55: | ||
[[File:Hooking functions demo.png|thumb|right|The resulting tooltip.]] | [[File:Hooking functions demo.png|thumb|right|The resulting tooltip.]] | ||
Explained in detail, the preceding code does: | Explained in detail, the preceding code does: | ||
# Stores the old | # Stores the old ChatFrame_OnHyperlinkShow value (original function) into a local variable | ||
# Changes the value of the | # Changes the value of the ChatFrame_OnHyperlinkShow variable to a new function (the pre-hook). Note the vararg expression (...) in the function signature. | ||
# Extract known arguments from the vararg expression for local use. | # Extract known arguments from the vararg expression for local use. | ||
# Check whether link text is a string, whether it matches [Tinfoil Hat], and whether we should display a tooltip | # Check whether link text is a string, whether it matches [Tinfoil Hat], and whether we should display a tooltip | ||
Line 103: | Line 103: | ||
; Insecure hooking : If you want to alter behavior significantly, it may be easier to call widget:[[API_Frame_GetScript|GetScript]] to get the original handler, and widget:[[API_Frame_SetScript|SetScript]] to set a hooked verson. | ; Insecure hooking : If you want to alter behavior significantly, it may be easier to call widget:[[API_Frame_GetScript|GetScript]] to get the original handler, and widget:[[API_Frame_SetScript|SetScript]] to set a hooked verson. | ||
; Secure hooking: widget:[[API_Frame_HookScript|HookScript]] allows you to set up a secure post-hook that will be called after the original handler, with the same arguments as the original handler. | ; Secure hooking: widget:[[API_Frame_HookScript|HookScript]] allows you to set up a secure post-hook that will be called after the original handler, with the same arguments as the original handler. | ||
== Hooking object methods == | |||
Functions called using the object calling syntax, <code>object:method(...)</code>, can also be hooked; one simply has to remember that the construct is merely syntactic sugar for <code>object.method(object, ...);</code>. To hook a function that was originally declared as: | |||
function AnAddon.Module:FuncName(arg1) | |||
-- Things happen here... | |||
end | |||
We store the original function and replace it with a hook: | |||
local original_AnAddon_Module_FuncName = AnAddon.Module.FuncName; | |||
function AnAddon.Module:FuncName(...) | |||
local arg1 = ...; -- Use the vararg expression in case the function signature changes | |||
if type(arg1) == "number" and arg1 > 0 then | |||
-- Call the original function; because of object calling syntax, | |||
-- pass self as the first argument. | |||
return original_AnAddon_Module_FuncName(self, ...); | |||
end | |||
end | |||
The example hook above will only allow the original function to be called if the first argument is a strictly positive number. | |||
== Removing existing hooks == | == Removing existing hooks == | ||
Removing hooks is generally problematic: there's no way to remove a hooksecurefunc-based post-hook, and insecure hooks may only be removed in some situations. The difficulty arises from the fact that multiple addons may want to hook the same function -- and if any addon but the top-most in the hooking chain wants to unhook, there's no way to tell the hook on top of your function to call the function below. It is therefore recommended to simply make your existing hook pass arguments and return values directly through if you don't want to deal with the data. For example: | Removing hooks is generally problematic: there's no way to remove a hooksecurefunc-based post-hook, and insecure hooks may only be removed in some situations. The difficulty arises from the fact that multiple addons may want to hook the same function -- and if any addon but the top-most in the hooking chain wants to unhook, there's no way to tell the hook on top of your function to call the function below. It is therefore recommended to simply make your existing hook pass arguments and return values directly through if you don't want to deal with the data. For example: | ||
local oldFunction = someGlobalFunc; | local oldFunction, noLongerRelevant = someGlobalFunc, false; | ||
function someGlobalFunc(...) | function someGlobalFunc(...) | ||
if noLongerRelevant then | if noLongerRelevant then | ||
Line 114: | Line 131: | ||
end | end | ||
end | end | ||
Libraries may contain functions that allow you to "unhook" your method -- those operate primarily by moving the ignore switch further up the execution tree: if you | Libraries may contain functions that allow you to "unhook" your method -- those operate primarily by moving the ignore switch further up the execution tree: if you remove your hook, it won't get called, but the library's hook function stays in place. | ||
== Notes == | == Notes == | ||
* Warning! Blizzard has moved some pieces of the UI to be loaded on demand. The functions in those pieces cannot be hooked until they are loaded. | * Warning! Blizzard has moved some pieces of the UI to be loaded on demand. The functions in those pieces cannot be hooked until they are loaded. | ||
* Some libraries, like [[Sea]], [[Ace]], or [[Ace2]] offer functions to assist in hooking. | * Some libraries, like [[Sea]], [[Ace]], or [[Ace2]] offer functions to assist in hooking. |
Revision as of 15:44, 17 September 2010
← HOWTOs
Hooking functions allows you to replace a function value in an accessible environment by a function of your own, calling the original function before or after execution of new code. Hooking enables addons to modify and/or extend the behavior of the default UI and API.
Depending on how the behavior of the original function is be modified, one or more of three hook types may be used:
- Pre-hooks
- New code executes before calling the original function, potentially choosing to modify the parameters passed to it, or not call it at all
- Post-hooks
- New code executes after the original function, potentially using the original function's return values.
- Secure hooks
- Certain functions require a secure execution path to operate correctly; in those cases, one may not replace the original function variable directly (as that would taint the variable and hence the execution path, preventing the original function from doing its job). In this case, a post-hook function can be applied using hooksecurefunc.
Functions as values
Functions are first-class values in Lua: they may be assigned to variables, used as table keys, passed as arguments and returned from functions just like any other value type. Because API and public interface functions reside in the global namespace, and addons rarely use local aliases, it is possible to alter the value of a variable storing a function -- and have existing code call the new function. This makes it possible to modify function behavior by hooking.
The traditional function declaration, shown below, is merely syntactic sugar for the assignment of a function value to the variable specified by the function name. Thus, the following two lines are identical:
function foo(n) return n^2; end foo = function(n) return n^2; end
Because most addons (and the default UI code) call most functions without using a local reference, it is possible to retrieve and change the value stored in a global variable, and get the addons to call your new function rather than the old global one, thereby enabling you to override its behavior. For example:
local ok, err = pcall(error, "Throw this error"); -- results in false, "Throw this error" error = function(...) print(...); end local ok, err = pcall(error, "Throw this error"); -- results in true; "Throw this error" is printed.
In the above example, the original value of error was discarded; in hooking, the original value is typically called by the new function.
Hooking a function
To illustrate the concept, let's create a pre-hook. Pre-hooks are commonly used to override user interface functions that perform an (unprotected) action, potentially adding behavior to existing UI. In this example, hook ChatFrame_OnHyperlinkShow (a function called when the user clicks on a chat link) to display a faux tooltip for the Template:Item, an item currently not in the game.
<nowiki>local origChatFrame_OnHyperlinkShow = ChatFrame_OnHyperlinkShow; -- (1)
ChatFrame_OnHyperlinkShow = function(...) -- (2)
local chatFrame, link, text, button = ...; -- (3)
if type(text) == "string" and text:match("%[Tinfoil Hat%]") and not IsModifiedClick() then --(4)
return ShowTinfoilHat(); -- (5)
end
return origChatFrame_OnHyperlinkShow(...); -- (6)
end
print("Click me: \124cff0070dd\124Hitem:8191:0:0:0:0:0:0:0:0:1\124h[Tinfoil Hat]\124h\124r"); -- (7)
-- The code below deals mostly with the tinfoil hat tooltip and is irrelevant to hooking
local function addLine(a, ...)
if a then
return ItemRefTooltip:AddLine(a,1,1,1,1), addLine(...);
end
end
function ShowTinfoilHat()
ShowUIPanel(ItemRefTooltip);
if (not ItemRefTooltip:IsVisible()) then
ItemRefTooltip:SetOwner(UIParent, "ANCHOR_PRESERVE");
end
ItemRefTooltip:ClearLines();
addLine("\124cff0070ddTinfoil Hat\124r", "Binds when equipped");
ItemRefTooltip:AddDoubleLine("Head","Cloth",1,1,1,1,1,1);
addLine("10 Armor", "-10 Intellect", "+10 Spirit", (UnitLevel("player") < 10) and "\124cffff0000Requires Level 10\124r" or "Requires Level 10");
addLine("\124cff00ff00Equip: Hides the wearer's profile from the Armory.\124r");
addLine("\124cff00ff00Equip: Allows the wearer to see \"the truth.\" May lead to an incontrollable urge to share \"the truth\" with others.\124r");
addLine("\124cff00ff00Use: Grants the wearer immunity to all forms of mind control for the next 10 seconds... or does it?\124r");
addLine("\124cffffd517On behalf of the International Gnomish Conspiracy, I've got to inform you that we're almost out of tinfoil.\124r");
ItemRefTooltip:Show(); ItemRefTooltip:Show();
end</nowiki>
Explained in detail, the preceding code does:
- Stores the old ChatFrame_OnHyperlinkShow value (original function) into a local variable
- Changes the value of the ChatFrame_OnHyperlinkShow variable to a new function (the pre-hook). Note the vararg expression (...) in the function signature.
- Extract known arguments from the vararg expression for local use.
- Check whether link text is a string, whether it matches [Tinfoil Hat], and whether we should display a tooltip
- If all of the preceding conditions are true, call our ShowTinfoilHat() function to construct the faux tooltip, and do not call the original handler.
- Otherwise, call the original handler and let it handle the click.
- Add a faux Tinfoil Hat item link to the chat frame to enable testing of this code.
The example used the vararg expression to make sure that, should the API change, the hook will still pass exactly the arguments it was given (no more, no less) to the original function. An additional sanity check on the type of the link argument in 3 prevents a potential error: if the function was not passed a string as expected (for example, by another addon), we do not want to block the default behavior.
- You can hook a function like "AddonName:Function()" by replaceing the ":" with a "."
Post-hooking
Post-hooks are used to act on an API function call performed by another addon / the user / the default UI, and respond to the performed action when there's no reliable event-based notification. For instance, companions and GetCursorInfo do not function well as of Patch 3.0.9, so it may be desirable to obtain the information about the last companion picked up on the cursor from the PickupCompanion call. So we post-hook:
local lastCompanionType, lastCompanionIndex; -- (1) local function postHook(typeID, index, ...) -- (2) lastCompanionType, lastCompanionIndex = typeID, index; -- (3) return ...; -- (4) end local oldPickupCompanion = PickupCompanion; -- (5) function PickupCompanion(...) -- (6) local typeID, index = ...; -- (7) return postHook(typeID, index, oldPickupCompanion(typeID, index, ...)); --(8) end
The syntax here is a bit convoluted, defining two functions to accomplish create the hook; this construction is used to make sure that all of the arguments (even if a future patch adds/remove additional arguments) are passed to the original function, and all of its return values are returned by the hook. Step-by-step description of this syntax:
- Define two local variables to keep track of the last companion picked up. An addon could then use those variables to determine which companion is on the cursor.
- Define the post-hook function body. The signature is the two known arguments, followed by the original function's return values in a vararg expression.
- Set the local up-values to the arguments passed to the function
- Return all of the original function's return values.
- Save a reference to the original function value
- Change the value of the global PickupCompanion variable to a new function
- Read the two known (expected by the post-hook) arguments from the vararg expression to local variables
- Call the postHook function and the original function.
Hooking secure functions
Secure functions cannot be hooked directly -- as that would taint the variable holding the function, and, as a result, the execution path to the secure function, causing it to fail. Instead, the hooksecurefunc function can be used to apply a post-hook. Unlike the variant above, the values returned by the original function are not supplied.
Let's apply the same hook to PickupCompanion securely:
local lastCompanionType, lastCompanionIndex; local function postHook(typeID, index) lastCompanionType, lastCompanionIndex = typeID, index; end hooksecurefunc("PickupCompanion", postHook);
Hooking widget handlers
You can also hook widget handles (OnClick/OnShow/On... scripts attached to widgets). There are two general mechanics that you could use:
- Insecure hooking
- If you want to alter behavior significantly, it may be easier to call widget:GetScript to get the original handler, and widget:SetScript to set a hooked verson.
- Secure hooking
- widget:HookScript allows you to set up a secure post-hook that will be called after the original handler, with the same arguments as the original handler.
Hooking object methods
Functions called using the object calling syntax, object:method(...)
, can also be hooked; one simply has to remember that the construct is merely syntactic sugar for object.method(object, ...);
. To hook a function that was originally declared as:
function AnAddon.Module:FuncName(arg1) -- Things happen here... end
We store the original function and replace it with a hook:
local original_AnAddon_Module_FuncName = AnAddon.Module.FuncName; function AnAddon.Module:FuncName(...) local arg1 = ...; -- Use the vararg expression in case the function signature changes if type(arg1) == "number" and arg1 > 0 then -- Call the original function; because of object calling syntax, -- pass self as the first argument. return original_AnAddon_Module_FuncName(self, ...); end end
The example hook above will only allow the original function to be called if the first argument is a strictly positive number.
Removing existing hooks
Removing hooks is generally problematic: there's no way to remove a hooksecurefunc-based post-hook, and insecure hooks may only be removed in some situations. The difficulty arises from the fact that multiple addons may want to hook the same function -- and if any addon but the top-most in the hooking chain wants to unhook, there's no way to tell the hook on top of your function to call the function below. It is therefore recommended to simply make your existing hook pass arguments and return values directly through if you don't want to deal with the data. For example:
local oldFunction, noLongerRelevant = someGlobalFunc, false; function someGlobalFunc(...) if noLongerRelevant then return oldFunction(...); else -- Original hook handling code goes here end end
Libraries may contain functions that allow you to "unhook" your method -- those operate primarily by moving the ignore switch further up the execution tree: if you remove your hook, it won't get called, but the library's hook function stays in place.