Kea 2.7.4
|
The hooks framework is a Kea system that simplifies the way that users can write code to modify the behavior of Kea. Instead of altering the Kea source code, they write functions that are compiled and linked into one or more dynamic shared objects, called here (for historical reasons), shared libraries. The library is specified in the Kea configuration and at run time Kea dynamically loads the library into its address space. At various points in the processing, the component "calls out" to functions in the library, passing to them the data is it currently working on. They can examine and modify the data as required.
This guide is aimed at Kea developers who want to write or modify a Kea component to use hooks. It shows how the component should be written to load a shared library at run-time and how to call functions in it.
For information about writing a hooks library containing functions called by Kea during its execution, see the document Hooks Developer's Guide.
In the remainder of this guide, the following terminology is used:
The core of Kea is written in C++ with some remaining legacy parts in Python. While it is the intention to provide the hooks framework for all languages, the initial version is for C++. All examples in this guide are in that language.
From the point of view of the component author, the basic ideas of the hooks framework are quite simple:
Of course, to set up the system the libraries need to be loaded in the first place. The component also needs to:
The following sections will describe these tasks in more detail.
Before any other action takes place, the location of the hook points in the code need to be determined. This, of course, depends on the component but, as a general guideline, hook locations should be located where a callout is able to obtain useful information from Kea and/or affect processing. Typically this means at the start or end of a major step in the processing of a request, at a point where either useful information can be passed to a callout and/or the callout can affect the processing of the component. The latter is achieved in either or both of the following ways:
Once the location of the hook point has been determined, it should be given a name. This name should be unique amongst all hook points and is subject to certain restrictions (see below).
Before the callouts at any hook point are called and any user libraries loaded - so typically during component initialization - the component must register the names of all the hooks. The registration is done using the static method isc::hooks::HooksManager::registerHook()
:
The name of the hook is passed as the sole argument to the registerHook()
method. The value returned is the index of that hook point and should be retained - it is needed to call the callouts attached to that hook.
Note that a hook only needs to be registered once. There is no mechanism for unregistering a hook and there is no need to do so.
In some components, it may be convenient to set up a single initialization function that registers all hooks. For others, it may be more convenient for each module within the component to perform its own initialization. Since the isc::hooks::HooksManager
object is a singleton and is created when first accessed, a useful trick is to automatically register the hooks when the module is loaded.
This technique involves declaring an object outside of any execution unit in the module. When the module is loaded, the object's constructor is run. By placing the hook registration calls in the constructor, the hooks in the module are defined at load time, before any function in the module is run. The code for such an initialization sequence would be similar to:
Hook names are strings and in principle, any string can be used as the name of a hook, even one containing spaces and non-printable characters. However, the following guidelines should be observed:
isc::hooks::DuplicateHook
exception being thrown.Before describing how to call user code at a hook point, we must first consider how to pass data to it.
Each user callout has the signature:
The isc::hooks::CalloutHandle
object is the object used to pass data to and from the callout. This holds the data as a set of name/value pairs, each pair being considered an argument to the callout. If there are multiple callouts attached to a hook, the CalloutHandle
is passed to each in turn. Should a callout modify an argument, the updated data is passed subsequent callouts (each of which could also modify it) before being returned to the component.
Two methods are provided to get and set the arguments passed to the callout called (naturally enough) getArgument
and setArgument
. Their usage is illustrated by the following code snippets.
As can be seen getArgument
is used to retrieve data from the CalloutHandle
, and setArgument
used to put data into it. If a callout wishes to alter data and pass it back to the component, it should retrieve the data with getArgument, modify it, and call setArgument to send it back.
There are a couple points to be aware of:
getArgument
must match the data type of the variable passed to the corresponding setArgument
exactly: using what would normally be considered to be a "compatible" type is not enough. For example, if the callout passed an argument back to the component as an int
and the component attempted to retrieve it as a long
, an exception would be thrown even though any value that can be stored in an int
will fit into a long
. This restriction also applies the "const" attribute but only as applied to data pointed to by pointers, e.g. if an argument is defined as a char*
, an exception will be thrown if an attempt is made to retrieve it into a variable of type const
char*
. (However, if an argument is set as a const
int
, it can be retrieved into an int
.) The documentation of a hook point should detail the exact data type of each argument.Although information is passed back to the component from callouts through CalloutHandle
arguments, a common action for callouts is to inform the component that its flow of control should be altered. For example:
For ease of processing, the CalloutHandle
contains two methods, isc::hooks::CalloutHandle::getStatus()
and isc::hooks::CalloutHandle::setStatus()
. It is only meaningful for the component to use the "get" method. The next step status is cleared (set to the default value of CONTINUE) by the hooks framework when the component requests that callouts be executed, so any value set by the component is lost. Callouts can both inspect the status (it might have been set by callouts earlier in the callout list for the hook) and set it. Note that the setting of the status by a callout does not prevent callouts later in the list from being called: the next step status is just an enum value - the only significance comes from its interpretation by the component.
An example of use could be:
The CalloutHandle
object is linked to the loaded libraries for lifetime reasons (described below). Components should retrieve a isc::hooks::CalloutHandle
using isc::hooks::HooksManager::createCalloutHandle()
:
(isc::hooks::CalloutHandlePtr
is a typedef for a Boost shared pointer to a CalloutHandle.) The CalloutHandle so retrieved may be used for as long as the libraries are loaded.
The handle is deleted by resetting the pointer:
... or by letting the handle pointer go out of scope. The actual deletion occurs when the CallHandle's reference count goes to zero. (The current version of the hooks framework does not maintain any other pointer to the returned CalloutHandle, so it gets destroyed when the shared pointer to it is cleared or destroyed. However, this may change in a future version.)
When the handle is not created locally it is not destroyed so it can keep ownership on arguments. In such case the code must call isc::hooks::CalloutHandle::deleteAllArguments
or simply use the RAII helper isc::hooks::ScopedCalloutHandleState
as in:
Calling the callout is a simple matter of executing the isc::hooks::HooksManager::callCallouts()
method for the hook index in question. For example, with the hook index "pkt_sent" defined as above, the hook can be executed by:
... where "*handle_ptr" is a reference (note: not a pointer) to the isc::hooks::CalloutHandle
object holding the arguments. No status code is returned. If a component needs to get data returned (other than that provided by the next step status), it should define an argument through which the callout can do so.
Most hooks in a component will not have callouts attached to them. To avoid the overhead of setting up arguments in the CalloutHandle
, a component can check for callouts before doing that processing using isc::hooks::HooksManager::calloutsPresent()
. Taking the index of a hook as its sole argument, the function returns true if there are any callouts attached to the hook and false otherwise.
With this check, the code in the component for calling a hook would look something like:
Once hooks are defined, all the hooks code described above will work, even if no libraries are loaded (and even if the library loading method is not called). The CalloutHandle
returned by isc::hooks::HooksManager::createCalloutHandle()
will be valid, isc::hooks::HooksManager::calloutsPresent()
will return false for every index, and isc::hooks::HooksManager::callCallouts()
will be a no-op.
However, if user libraries are specified in the Kea configuration, the component should load them. (Note the term "libraries": the hooks framework allows multiple user libraries to be loaded.) This should take place after the component's configuration has been read, and is achieved by the isc::hooks::HooksManager::loadLibraries()
method. The method is passed a vector of strings, each giving the full file specification of a user library:
loadLibraries()
returns a boolean status which is true if all libraries loaded successfully or false if one or more failed to load. Appropriate error messages will have been logged in the latter case, the status being more to allow the developer to decide whether the execution should proceed in such circumstances.
Before loadLibraries()
can be called a second or subsequent time (as a result of a reconfiguration), all existing libraries must be successfully unloaded. If a library stays in memory from a programming bug in Kea (for instance when no libraries were loaded) or in a library (Memory Management Considerations for Hooks Writer) loadLibraries()
throws a not recoverable error.
Unloading is done in two phases since Kea version 1.7.10:
isc::hooks::HooksManager::prepareUnloadLibraries()
which calls all unload() entry points and deregisters callout points.isc::hooks::HooksManager::unloadLibraries()
even when the prepare failed.If a failure of unloadLibraries()
is ignored any call to loadLibraries()
will throw.
Unloading a shared library works by unmapping the part of the process's virtual address space in which the library lies. This may lead to problems if there are still references to that address space elsewhere in the process.
In many operating systems, heap storage allowed by a shared library will lie in the virtual address allocated to the library. This has implications in the hooks framework because:
CalloutHandle
by a callout in a library may lie in the library's address space.The problem really arises because of the extensive use by Kea of boost smart pointers. When the pointer is destroyed, the pointed-to memory is deallocated. If the pointer points to address space that is unmapped because a library has been unloaded, the deletion causes a segmentation fault.
The hooks framework addresses the issue for CalloutHandles by keeping in that object a shared pointer to the object controlling library unloading. Although a library can be unloaded at any time, it is only when all CalloutHandles that could possibly reference address space in the library have been deleted that the library will actually be unloaded and the address space unmapped.
The hooks framework cannot solve the second issue as the objects in question are under control of the component. It is up to the component developer to ensure that all such objects have been destroyed before libraries are reloaded. In extreme cases this may mean the component suspending all processing of incoming requests until all currently executing requests have completed and data object destroyed, reloading the libraries, then resuming processing.
Since Kea 1.7.10 the unload() entry point is called as the first phase of unloading. This gives more chance to hooks writer to perform necessary cleanup actions so the second phase, memory unmapping can safely happen. The isc::hooks::unloadLibraries()
function was updated too to return false when at least one active callout handle remained.
Previous sections have discussed callout registration by user libraries. It is possible for a component to register its own functions (i.e. within its own address space) as hook callouts. These functions are called in exactly the same way as user callouts, being passed their arguments though a CalloutHandle object. (Guidelines for writing callouts can be found in Hooks Developer's Guide.)
A component can associate with a hook callouts that run either before user-registered callouts or after them. Registration is done via a isc::hooks::LibraryHandle
object, a reference to one being obtained through the methods isc::hooks::HooksManager::preCalloutLibraryHandle()
(for a handle to register callouts to run before the user library callouts) or isc::hooks::HooksManager::postCalloutLibraryHandle()
(for a handle to register callouts to run after the user callouts). Use of the LibraryHandle
to register and deregister callouts is described in The LibraryHandle Object.
Finally, it should be noted that callouts registered in this way only remain registered until the next call to isc::hooks::loadLibraries()
. It is up to the component to re-register the callouts after this method has been called.