AceHooks

From WowAce Wiki
Jump to: navigation, search
Ability Vanish.png This addon page is not claimed.
Please see Unclaimed Addon Pages for more details.

Introduction

In the 1.2.5 incarnation of Ace there was a nice and simple implementation of AceHook which worked quite well for most needs. Soon after work began on the next beta release of Ace Turan found some things that he considered vital to the package (handling script hooking, as well as using anonymous handlers for example). At some point later in the process, Iriel happened to ask a question about AceHook in #WoWI-Lounge that prompted a chain reaction of discussion regarding the module.

The concern was how well Ace played with other hooks in the system. In the 1.2.5 release, if Ace did its magical hooking, then a non-ace addon hooked some of the same functions, if you ever unhooked (or :Disabled) the ace hooks, it would completely blow away the non-ace hooks. This issue, and some concerns with efficiency prompted a rewrite of AceHooks and the current version is the result of those conversations.

Terminology

Standard Hook 
This is a hook that is custom written to the situation, and typically will be the most efficient. They can be difficult to manage but work quite wonderfully.
Safe Hook 
This is a hook that takes precautions against destructive unhooking (where you destroy any hooks that happen after you.) This is typically implemented with a switch that "enables" or "disables" the function at unhooking, depending on whether or not the Hook owns the Global Function.
Global Function 
This is the global symbol for the function or method being hooked. When you hook the global function, you replace its symbol with your own.

Design Goals

  • AceHook is designed to be a powerful tool for the developer to hook in an object oriented environment with ease. It it not meant to be a one-stop shopping experience for hooks, because the author in particular firmly believes that the use of any library is situational. If you are casually hooking functions, and not going anywhere near OnUpdate, then perhaps AceHook is for you.
  • The overhead from using the library should be as little possible. This is accomplished with some trickery, but mostly just ways to avoid the overhead of function calls, and the added garbage collection from the use of variable arguments.
  • AceHook's hook calls are limited to 20 arguments. This concept comes from the gentlemen of SeaHooks and Cosmos, who use a single 20 argument table to cut down on the GC in the library.
  • The API must be backwards compatible with the previous versions of AceHook.
  • AceHook should be able to handle the hooking (and setting) of script methods (OnEnter, OnLeave, OnClick, etc).

AceHook API

  • self:Hook("functionName") - Hooks functionName with self.functionName
  • self:Hook("functionName", "handlerName") - Hooks functionName with self.handlerName
  • self:Hook("functionName", handler) - Hooks functionName with the function handler (can be anonymous, or well-defined)
  • self:Hook(Object, "methodName") - Hooks Object["methodName"] with self.methodName
  • self:Hook(Object, "methodName", "handlerName") - Hooks Object["methodName"] with self.handlerName
  • self:Hook(Object, "methodName", handler) - Hooks Object["methodName"] with the function handler (can be anonymous, or well-defined)
  • self:Unhook("functionName") - Unhooks functionName
  • self:Unhook(Object, "methodName") - Unhooks Object["methodName"]
  • self:UnhookAll() - Unhooks all registered hooks, where possible.
  • self:HookReport() - Print a listing of all hooks registered to this source, and their active status.
  • self.Hooks[functionName].orig(...) - Call the original function we've hooked
  • self.Hooks[Object][method].orig(Object, ...) - Call the original method we've hooked

Currently when working through Ace, AceHooks does not provide an API call for HookScript, that is provided by the wrapper module AceScript, which will soon be deprecated. For the time being, that wrapper will exist, but the API when embedded you can use the following:

  • self:HookScript(Object, "method" [,"handlerName" | handler]) - Hook the script "method" of Object if it exists, or set the handler as the script if it doesn't exist.

Until we fully deprecate the nocall options that previous existed (they're just ignored in this version) the API will need to be a wrapper inside of Ace, but this is of no consequence to the Ace developer or user =).

References/Example Code

I've created a small document that outlines some typical hooking reasons and examples at AceHooks Examples.

Troubleshooting

  • Each function can only be hooked from each source ONCE. If you have the need for a double hook from the same module, then we won't be able to help you-- because its a bad idea =)
  • Attempting to double-hook, or double-unhook will fail silently unless you have AceDebug messages enabled for the calling AddOn. While these errors are typically indicators of a larger problem, they are not fatal enough to break addons that end up using these methods (either dispatching hooking via events, etc).
  • The library does everything possible to validate the arguments and find the handlers and methods you specif, and will fatally error out if it can't find what you're looking for.
  • This library will create a table called self.Hooks which is vital to the operation of the library. This means each namespace has their own hook table. At this point hooks aren't mirrored centrally, but in the future we may change that to provide debugging tools.

Embedding

This library is fully embeddable, and will negotiate with any existing AddOns.

  1. Include the AceHook.lua file in your distibution
  2. Include AceHook.lua in your .toc file to ensure the file gets loaded
  3. Include Ace as an OptionalDependency in your AddOn. All this does is ensure that if Ace is going to load (the user already has it) that it loads first, so the versions of AceHook can be negotiated properly. This causes no change/overhead in your AddOn.
  4. Somewhere in the initialization of your addon, have it call AceHook:Embed with your namespace as an argument. This might look like:
MyAddOn = {}
function MyAddOn.Util()
  -- Do something
end

function MyAddOn.OnLoad()
  -- Do some init
  AceHook:Embed(MyAddOn)
end

From that point on, you have access to the following methods:

  • self:Hook
  • self:Unhook
  • self:UnhookAll
  • self:HookReport
  • self:HookScript
  • and the self.Hooks structure for making tail calls.

Between the Ace versions, and the embedded versions of AceHooks, whichever AceHook version has the highest version number will load. If an embedded version overwrites the Ace standard distribution, an error message will be sent to the DEFAULT_CHAT_FRAME.

Download

Right now, AceHook is in final beta and will be made available in standalone form. For right now, you can get the most recent version at the Ace svn:

[1]

Timings

Here are some sample timings for this implementation of AceHooks as compared to a standard hand implemented safe hook, in a variety of forms. Baseline is the function alone, unhooked. StdSafe is a standard safe hook (defined above), FuncSame/Name/Anon are forms of self:Hook("functionName") with a same name handler, named handler and anonymous handlers. The same naming conventions are followed for method hooking.

  • PROFILE: Timing for Baseline took 12.56 seconds (0.05024 average) with 25500 KiB of memory usage
  • PROFILE: Timing for StdSafe took 18.885 seconds (0.07554 average) with 25500 KiB of memory usage
  • PROFILE: Timing for FuncSame took 23.434 seconds (0.093736 average) with 25500 KiB of memory usage
  • PROFILE: Timing for FuncName took 23.405 seconds (0.09362 average) with 25750 KiB of memory usage
  • PROFILE: Timing for FuncAnon took 20.861 seconds (0.083444 average) with 25750 KiB of memory usage
  • PROFILE: Timing for Baseline took 12.56 seconds (0.05024 average) with 25500 KiB of memory usage
  • PROFILE: Timing for MethStd took 19.203 seconds (0.076812 average) with 25500 KiB of memory usage
  • PROFILE: Timing for MethSame took 20.594 seconds (0.082376 average) with 25750 KiB of memory usage
  • PROFILE: Timing for MethName took 21.579 seconds (0.086316 average) with 25750 KiB of memory usage

Conclusions

There is a small amount of overhead associated with using a library such as AceHooks, but we've done everything possible to get that overhead to be as low as possible. In a named method hook, the AceHook implementation only incurs a 12% performance hit over a standard safe implemented hook. This is extremely low, and AceHooks being embeddable and flexible makes it a tool for appropriate situations.

Hopefully the way we've implemented this helps people re-think the way they've been doing their hooks, and help replace this:

local oldOnClick = RaidPullout_OnClick
function newRaidPullout_OnClick 
 -- Do stuff
 -- Do more stuff
end
RaidPullout_OnClick = newRaidPullout_onClick

with

function MyAddon:OnClick
 -- Do stuff
 -- Do more stuff
end

MyAddon:Hook("RaidPullout_OnClick", "OnClick")


Its all personal preference, but AceHook can provide you a strong framework when you're doing a solid amount of hooking.