What this post covers
This is the sixth post in the "Digging into Apache httpd's Source" series. In Part 5 we watched main() read the configuration and call stages such as pre_config and post_config one after another. Something was skipped there with a quick "it calls the stage": when ap_run_post_config() is called, whose functions run, and in what order? This post answers that.
There are four questions.
- What is a module, seen as C code? Where in the source does the line
LoadModule alias_module modules/mod_alias.soinhttpd.conflead? - How is a hook — the thing a module registers with to say "call my function at this stage" — built? What do macros like
AP_DECLARE_HOOKexpand to? - When several modules hang on the same hook, who is called first, and whose return value stops the rest?
- How does a
.sofile get loaded into a running server?
| Item | Details |
|---|---|
| Target | Apache httpd |
| Version | 2.5.1-dev (trunk) |
| Commit | e4af30fe6d (2026-10-24) |
| Library read alongside | APR-util 1.6.3 (tag 1.6.3, commit 28327eeca2) — hook macros and sorting |
| Files read most | include/http_config.h, server/config.c, include/ap_hooks.h, modules/core/mod_so.c, APR-util include/apr_hooks.h and hooks/apr_hooks.c |
Every line number and detail in this post refers to that commit (and, for APR-util, that tag). Part 1 explains how to get the source at the same commit and build the Docker image used for the examples.
This post keeps only the main thread that someone opening the source for the first time can follow. What was left out is named, with its location, under "If you want to go deeper" at the end.
Words to know first
A module is a bundle of C code that adds one feature to httpd. mod_alias (Alias, Redirect), mod_ssl (HTTPS) and mod_proxy (reverse proxy) — the names you see in config files — are all modules, and as we saw in Part 3, so are the core (core.c) and HTTP handling (http_core.c).
A hook is a spot opened up in the middle of the server's work that says "other functions may be called here". For example, the handler hook means "time to produce the response body", and the fixups hook means "last touches just before the handler runs". Modules attach their functions to hooks (registration), and when the server reaches that spot it calls the attached functions in turn (running).
A function pointer is a variable that holds "the address of a function". You can keep it in a variable and later call the function through that variable. "Attaching a function to a hook" ends up meaning putting a function pointer into an array. A macro is a name that is replaced, letter for letter, by other code before compilation (#define). Both are explained in detail in Part 2.
A DSO (Dynamic Shared Object) is a library file that a running program can load later. On Linux it is a .so file, and in httpd the LoadModule directive loads it. A module built into the server executable from the start is called a static module; one loaded from a .so is a shared module.
OK, DECLINED and DONE are the agreed values that a function on a hook returns. They are defined in include/httpd.h.
#define OK 0 /**< Module has handled this stage. */
#define DECLINED -1 /**< Module declines to handle */
#define DONE -2 /**< Module has served the response completely
* - it's safe to die() with no more output
*/
#define SUSPENDED -3 /**< Module will handle the remainder of the request.
* The core will never invoke the request again */
OK means "I handled this stage", DECLINED means "nothing for me here, pass it on", and DONE means "the response is completely finished". Returning an HTTP status number such as 404 or 500 instead means "end this with an error". How these values change the flow is the subject of section 4.
1. A module is one struct
C has no classes. httpd defined "what a module is" as a single struct: module in include/http_config.h. (Every field's explanatory comment has been removed; the code lines are exactly as in the source.)
typedef struct module_struct module;
struct module_struct {
int version;
int minor_version;
int module_index;
const char *name;
void *dynamic_load_handle;
struct module_struct *next;
unsigned long magic;
void (*rewrite_args) (process_rec *process);
void *(*create_dir_config) (apr_pool_t *p, char *dir);
void *(*merge_dir_config) (apr_pool_t *p, void *base_conf, void *new_conf);
void *(*create_server_config) (apr_pool_t *p, server_rec *s);
void *(*merge_server_config) (apr_pool_t *p, void *base_conf,
void *new_conf);
const command_rec *cmds;
void (*register_hooks) (apr_pool_t *p);
int flags;
};
include/http_config.h lines 347–424
Lines like void *(*create_dir_config) (apr_pool_t *p, char *dir);, with (* before the name, are function pointer fields. The struct has four main parts.
- ID card —
versionandminor_version(the server API version the module was compiled against),module_index(its slot number in the config vector, explained below),name(the source file name),magic(a mark proving it really is a module struct), andnext(a linked list joining the modules). - Creating and merging config — four functions such as
create_dir_config. They create and merge this module's config struct for every<Directory>and every virtual host. We look at them in Part 7. cmds— the table of directives this module understands. Formod_aliasit says which function reads lines likeAliasandRedirect. Also in Part 7.register_hooks— the star of this post. The server calls this function once, and inside it the module registers "I'll attach this function to this hook and that one to that hook".
A real module creates this struct as a single variable. Here is the end of mod_alias.
static void register_hooks(apr_pool_t *p)
{
static const char * const aszSucc[]={ "mod_userdir.c",
"mod_vhost_alias.c",NULL };
ap_hook_translate_name(translate_alias_redir,NULL,aszSucc,APR_HOOK_MIDDLE);
ap_hook_fixups(fixup_redir,NULL,NULL,APR_HOOK_MIDDLE);
}
AP_DECLARE_MODULE(alias) =
{
STANDARD20_MODULE_STUFF,
create_alias_dir_config, /* dir config creater */
merge_alias_dir_config, /* dir merger --- default is to override */
create_alias_config, /* server config */
merge_alias_config, /* merge server configs */
alias_cmds, /* command apr_table_t */
register_hooks /* register hooks */
};
modules/mappers/mod_alias.c lines 783–801
register_hooks() is two lines. It attaches translate_alias_redir to the translate_name hook (the stage that turns a URL into a file path, Part 10) and fixup_redir to the fixups hook. The second and third arguments (NULL,aszSucc) and the last one (APR_HOOK_MIDDLE) decide the order; we get to them in section 5.
AP_DECLARE_MODULE(alias) and STANDARD20_MODULE_STUFF are macros.
#define AP_DECLARE_MODULE(foo) \
APLOG_USE_MODULE(foo); \
module AP_MODULE_DECLARE_DATA foo##_module
include/http_config.h lines 467–469
#define STANDARD20_MODULE_STUFF MODULE_MAGIC_NUMBER_MAJOR, \
MODULE_MAGIC_NUMBER_MINOR, \
-1, \
__FILE__, \
NULL, \
NULL, \
MODULE_MAGIC_COOKIE, \
NULL /* rewrite args spot */
include/http_config.h lines 489–496
## is the operator that glues text together inside a macro. So AP_DECLARE_MODULE(alias) = { ... } becomes module alias_module = { ... }, the definition of a global variable (the APLOG_USE_MODULE in front is for per-module LogLevel, covered in Part 16). STANDARD20_MODULE_STUFF fills the first eight slots of the struct in order — version, minor_version, module_index, name, dynamic_load_handle, next, magic, rewrite_args. After that one word, a module author only has to write the remaining six slots — five functions and one directive table (the final flags is 0 if left out).
The yellow box is the config vector, where each module uses one slot; the blue boxes are the two channels into the server's flow (directives and hooks).
module_index — each module's slot number
module_index starts as -1, and when the module is registered with the server it gets 0, 1, 2 … in turn (section 2). This number is a slot number in the config vector. A config vector is a void * array with one slot per module, and each slot holds the address of that module's config struct. The macro a module uses to fetch its own config shows exactly this.
#define ap_get_module_config(v,m) \
(((void **)(v))[(m)->module_index])
include/http_config.h lines 553–554
It just takes the module_index-th slot of the array. The server has no idea what the modules' config structs look like; each module interprets its own slot as its own type. The pros and cons of this approach are discussed in Part 2.
Easy to get wrong: a module has three names
The same module goes by different names depending on where you look.
| Where | Name | Comes from |
|---|---|---|
First argument of LoadModule, <IfModule>, httpd -M |
alias_module |
The variable name created by AP_DECLARE_MODULE(alias) |
httpd -l, before/after lists in hook ordering, SHOW_HOOKS output |
mod_alias.c |
The struct's name slot, i.e. __FILE__ (the name of the compiled source file) |
Second argument of LoadModule |
modules/mod_alias.so |
The file name the build produced |
Some compilers put the full path into __FILE__, so ap_add_module() trims it to whatever follows the last / (or \ for a Windows path) (server/config.c lines 620–629). The name in the second row becomes important again in section 5.
2. How a module gets registered with the server
Having a module struct does not mean the server knows about it. There are two routes to registration: a static module, built into the executable, is registered right at startup; a shared module, in a .so, is registered when reading httpd.conf reaches its LoadModule line.
The two routes meet in ap_add_module(). The yellow box — a hook's function array — fills up in registration order; sorting happens separately, at ③.
Static modules: modules.c and ap_setup_prelinked_modules()
Which modules are built into the executable is decided at ./configure time, and the build generates a file called modules.c in the top-level source folder. It is not in the repository (things like os/win32/modules.c in the repository are hand-written versions for other operating systems). Here it is, taken from the build folder in the example container (the image for this series was built with --enable-mods-shared=most, so there are only three static modules).
### 0. /usr/src/httpd/modules.c (generated at build time), lines 21-26
AP_DECLARE_DATA module *ap_prelinked_modules[] = {
&core_module,
&so_module,
&http_module,
NULL
};
It is an array of module struct addresses. The same file also has ap_preloaded_modules[] (the modules available for loading), whose contents are identical in this build. Early in startup main() calls ap_setup_prelinked_modules() (server/main.c line 574), which walks the array, hands out numbers, and registers the modules one by one.
AP_DECLARE(const char *) ap_setup_prelinked_modules(process_rec *process)
{
/* ... */
apr_hook_global_pool=process->pconf;
/* ... */
total_modules = 0;
for (m = ap_preloaded_modules; *m != NULL; m++)
(*m)->module_index = total_modules++;
max_modules = total_modules + DYNAMIC_MODULE_LIMIT + 1;
conf_vector_length = max_modules;
/* ... */
for (m = ap_prelinked_modules; *m != NULL; m++) {
error = ap_add_module(*m, process->pconf, NULL);
if (error) {
return error;
}
}
apr_hook_sort_all();
return NULL;
}
Notice that the config vector length (conf_vector_length) is set up front to "number of static modules + DYNAMIC_MODULE_LIMIT + 1". It leaves room for the shared modules that will be loaded later. The default DYNAMIC_MODULE_LIMIT is 256 (include/httpd.h lines 119–120).
The door every module walks through: ap_add_module()
Static or shared, registration always ends up in ap_add_module().
AP_DECLARE(const char *) ap_add_module(module *m, apr_pool_t *p,
const char *sym_name)
{
/* ... */
if (m->version != MODULE_MAGIC_NUMBER_MAJOR) {
return apr_psprintf(p, "Module \"%s\" is not compatible with this "
"version of Apache (found %d, need %d). Please "
"contact the vendor for the correct version.",
m->name, m->version, MODULE_MAGIC_NUMBER_MAJOR);
}
if (m->module_index == -1) {
/* ... */
m->module_index = total_modules++;
dynamic_modules++;
}
/* ... */
if (m->next == NULL) {
m->next = ap_top_module;
ap_top_module = m;
}
/* ... */
ap_add_module_commands(m, p);
/* ... */
ap_register_hooks(m, p);
return NULL;
}
- Check the version. If the
versionbaked in when the module was compiled (theMODULE_MAGIC_NUMBER_MAJORof that time) differs from the current server's, the module is refused. - Hand out a number. If it is still
-1(a shared module), it gets the next free number. Static modules already got theirs in the previous function. - Put it at the head of the list. It is linked in at the front of the list that
ap_top_modulepoints to. - Register its directives (
ap_add_module_commands()) and its hooks (ap_register_hooks()).
ap_register_hooks() is short.
AP_DECLARE(void) ap_register_hooks(module *m, apr_pool_t *p)
{
if (m->register_hooks) {
if (getenv("SHOW_HOOKS")) {
printf("Registering hooks for %s\n", m->name);
apr_hook_debug_enabled = 1;
}
apr_hook_debug_current = m->name;
m->register_hooks(p);
}
}
Right before calling the module's register_hooks function pointer, it writes the module's name (mod_alias.c) into the global variable apr_hook_debug_current. The ap_hook_*() calls inside the module take no module name as an argument; this is the trick that still labels each registered function with "whose it is" (we confirm it in the expanded code in section 3). Setting the environment variable SHOW_HOOKS prints registration and sorting to the screen; we use it in the example in section 6.
Shared modules: mod_so and LoadModule
The LoadModule directive belongs not to the core but to the static module mod_so.
static const command_rec so_cmds[] = {
AP_INIT_TAKE2("LoadModule", load_module, NULL, RSRC_CONF | EXEC_ON_READ,
"a module name and the name of a shared object file to load it from"),
modules/core/mod_so.c lines 448–450
TAKE2 means a directive with two arguments, and EXEC_ON_READ means "run it right where it is read in the config file". The module has to be loaded before its directives can be understood, so this one cannot be put off and handled later like other directives (the order in which directives are processed is in Part 7). The real work is done by load_module(). The opening part that checks whether the module is already loaded, and the parts that build error messages, are cut.
error = dso_load(cmd, &modhandle, filename, &module_file);
if (error)
return error;
/* ... */
if (apr_dso_sym(&modsym, modhandle, modname) != APR_SUCCESS) {
/* ... */
}
modp = (module*) modsym;
modp->dynamic_load_handle = (apr_dso_handle_t *)modhandle;
modi->modp = modp;
/* ... */
if (modp->magic != MODULE_MAGIC_COOKIE) {
return apr_psprintf(cmd->pool, "API module structure '%s' in file %s "
"is garbled - expected signature %08lx but saw "
/* ... */);
}
/* ... */
error = ap_add_loaded_module(modp, cmd->pool, modname);
if (error) {
return error;
}
/* ... */
apr_pool_cleanup_register(cmd->pool, modi, unload_module, apr_pool_cleanup_null);
/* ... */
ap_single_module_configure(cmd->pool, cmd->server, modp);
return NULL;
}
modules/core/mod_so.c lines 265–322
- Open the file.
apr_dso_load(), insidedso_load(), loads the.sofile into the process (on Linux,dlopen()sits underneath). - Find the struct by name.
apr_dso_sym()looks up the symbol with the same name as the firstLoadModuleargument —alias_module— that is, the global variable created byAP_DECLARE_MODULE(alias), and returns its address. This is where the first of the "three names" from section 1 is used. - Check it is genuine. It checks that the
magicslot equalsMODULE_MAGIC_COOKIE. At this commit the value is0x41503235UL /* "AP25" */(include/ap_mmn.h line 749). - Register it.
ap_add_loaded_module()calls theap_add_module()we just saw (server/config.c lines 703–731). That is whereregister_hooks()gets called. - Prepare for unloading. A cleanup is registered on the config pool (
pconf), so when that pool is cleared by a restart or shutdown, the module is taken off the list and the.sois unloaded. Pools and cleanups were covered in Part 4. - Create default config.
ap_single_module_configure()fills in the server config slot and the default directory config slot.
Checking with an example: httpd -l, -M, -L
This example was run in a Docker container (Ubuntu 24.04) using httpd 2.5.1-dev built unchanged from the pinned commit (e4af30fe6d). It is a minimal config that compiles the three modules used in section 6 (mod_alpha, mod_beta, mod_gamma) with apxs -c and loads them with LoadModule. The whole example is in ex/u06-hooks/run.sh.
### 1. httpd -l
Compiled in modules:
core.c
mod_so.c
http_core.c
### 2. httpd -M
Loaded Modules:
core_module (static)
so_module (static)
http_module (static)
mpm_event_module (shared)
unixd_module (shared)
authz_core_module (shared)
alpha_module (shared)
beta_module (shared)
gamma_module (shared)
### 3. httpd -L | grep -A1 ^LoadModule
LoadModule (mod_so.c)
a module name and the name of a shared object file to load it from
-lshows only static modules, and by theirnameslot (core.c), becauseap_show_modules()printsap_loaded_modules[n]->name(server/config.c lines 2507–2514). It finishes before the config file is read, so shared modules do not appear (server/main.c lines 647–649).-Mshows them by variable name (alpha_module) after the whole config has been read. The function that prints this list is itself one thatmod_soattached to thetest_confighook (modules/core/mod_so.c lines 378–419, registered at line 444).-Lprints each directive with the module that owns it and its description. You can see thatLoadModulebelongs tomod_so.c, and that the description is exactly the string in theso_cmdstable above.
Easy to get wrong
- A module built for 2.4 will not load into the server at this commit. On the 2.4.x branch
MODULE_MAGIC_COOKIEis"AP24"(0x41503234UL) andMODULE_MAGIC_NUMBER_MAJORis20120211; at this commit they are"AP25"and20211221(include/ap_mmn.h lines 749–752). Such a module is stopped by themagiccheck inload_module()before it ever reaches the version check inap_add_module(). Modules must be recompiled against the headers of the server they will run in. register_hooks()is not a function that is called only once. When the config pool is cleared, a pre-cleanup wipes every hook registration (server/main.c lines 321–327, registered at line 339), and at the top of the loop that re-reads the config, the static modules'register_hooks()is called again (server/main.c lines 816–818). Shared modules are registered again when the re-read meets theirLoadModulelines. This repeats with the "config is read twice" startup from Part 5 and with every restart, so it is safest forregister_hooks()to register hooks and nothing else — no other state.
3. A hook is built from three macros
Now let's see where functions like ap_hook_translate_name() and ap_hook_fixups() come from. If you grep the source, there is no definition with that name anywhere. Macros produce them. We'll follow one hook, handler, as the example.
① Declaration: AP_DECLARE_HOOK
A header file has this one line.
AP_DECLARE_HOOK(int,handler,(request_rec *r))
include/http_config.h line 1399
It says: "there is a hook called handler, and a function attached to it takes a request_rec *r and returns an int". The macro belongs to httpd, but all it does is add name prefixes and hand over to an APR-util macro.
#define AP_DECLARE_HOOK(ret,name,args) \
APR_DECLARE_EXTERNAL_HOOK(ap,AP,ret,name,args)
include/ap_hooks.h lines 74–75
ap is the prefix of the function names to be generated, and AP is the prefix of export-marking macros like AP_DECLARE. The receiving end is the real thing in APR-util.
#define APR_DECLARE_EXTERNAL_HOOK(ns,link,ret,name,args) \
typedef ret ns##_HOOK_##name##_t args; \
link##_DECLARE(void) ns##_hook_##name(ns##_HOOK_##name##_t *pf, \
const char * const *aszPre, \
const char * const *aszSucc, int nOrder); \
link##_DECLARE(ret) ns##_run_##name args; \
APR_IMPLEMENT_HOOK_GET_PROTO(ns,link,name); \
typedef struct ns##_LINK_##name##_t \
{ \
ns##_HOOK_##name##_t *pFunc; \
const char *szName; \
const char * const *aszPredecessors; \
const char * const *aszSuccessors; \
int nOrder; \
} ns##_LINK_##name##_t;
APR-util include/apr_hooks.h lines 118–132
Put ap into ns and handler into name, glue with ##, and five things appear.
| Expanded name | What it is |
|---|---|
ap_HOOK_handler_t |
The shape (type) of a function attached to the hook: int (request_rec *r) |
ap_hook_handler() |
Declaration of the registration function a module calls. Takes a function pointer, the modules that must come before, the modules that must come after, and an order value |
ap_run_handler() |
Declaration of the run function the core calls. Same arguments as the hook: (request_rec *r) |
ap_hook_get_handler() |
Declaration of a function that returns the registered array |
ap_LINK_handler_t |
A struct holding one registration: function pointer + name tag + before/after lists + order value |
The header produces declarations only. There are no bodies yet. Because the shape of an attachable function is fixed by a typedef, the compiler notices if a module passes a function of a different shape to ap_hook_handler(). I deliberately attached a function with two arguments and no return value and compiled it in the example container (gcc 13.3) (ex/u06-hooks/mod_badtype.c).
### 10. a function of the wrong shape passed to ap_hook_handler()
mod_badtype.c:10:21: warning: passing argument 1 of 'ap_hook_handler' from incompatible pointer type [-Wincompatible-pointer-types]
It is a warning rather than an error, so the build goes on and you must not let such warnings slide — but for a plugin system written in C, the types are tied down fairly firmly.
② Storage: APR_HOOK_STRUCT
The bodies live in the one source file that "owns" the hook. The handler hook is owned by server/config.c. At the top of that file, storage for the hooks it owns is created.
APR_HOOK_STRUCT(
APR_HOOK_LINK(header_parser)
/* ... */
APR_HOOK_LINK(handler)
/* ... */
)
#define APR_HOOK_STRUCT(members) \
static struct { members } _hooks;
/** macro to link the hook structure */
#define APR_HOOK_LINK(name) \
apr_array_header_t *link_##name;
APR-util include/apr_hooks.h lines 135–140
Expanded, this is static struct { apr_array_header_t *link_handler; ... } _hooks; — a struct variable with one slot per hook, each slot a pointer to an array. A global variable marked static is visible only inside its own .c file, so every file can use the same name _hooks without clashing. server/request.c has its own _hooks too, holding request-stage hooks such as fixups and translate_name (server/request.c lines 61–77).
③ Implementation: AP_IMPLEMENT_HOOK_RUN_FIRST
In the same file, this line creates the bodies.
AP_IMPLEMENT_HOOK_RUN_FIRST(int, handler, (request_rec *r),
(r), DECLINED)
The fourth argument (r) says "this is how to write the received arguments when passing them to the next function", and the fifth, DECLINED, says "move on to the next one if you get this value". This macro also goes through ap_hooks.h to APR-util's APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST (include/ap_hooks.h lines 137–139), which joins APR_IMPLEMENT_EXTERNAL_HOOK_BASE — the part that builds the registration function (APR-util include/apr_hooks.h lines 143–165) — to the body of the run function.
Rather than reading macros as macros, it is quicker to look at what the compiler expands them to. gcc -E stops after preprocessing (macro expansion) without compiling. I expanded a tiny file containing just the two macros above (ex/u06-hooks/expand.c) in the example container. The preprocessor output comes out as one long line, so the sed and awk in run.sh only add line breaks and indentation; no characters were changed (a few empty ; statements were removed — leftovers from debugging probe macros that are switched off).
### 9. gcc -E: what AP_IMPLEMENT_HOOK_RUN_FIRST(handler) becomes
static struct {
apr_array_header_t *link_handler;
}
_hooks;
void ap_hook_handler(ap_HOOK_handler_t *pf,const char * const *aszPre, const char * const *aszSucc,int nOrder) {
ap_LINK_handler_t *pHook;
if(!_hooks.link_handler) {
_hooks.link_handler=apr_array_make(apr_hook_global_pool,1,sizeof(ap_LINK_handler_t));
apr_hook_sort_register("handler",&_hooks.link_handler);
}
pHook=apr_array_push(_hooks.link_handler);
pHook->pFunc=pf;
pHook->aszPredecessors=aszPre;
pHook->aszSuccessors=aszSucc;
pHook->nOrder=nOrder;
pHook->szName=apr_hook_debug_current;
if(apr_hook_debug_enabled) apr_hook_debug_show("handler",aszPre,aszSucc);
}
apr_array_header_t * ap_hook_get_handler(void) {
return _hooks.link_handler;
}
int ap_run_handler (request_rec *r) {
ap_LINK_handler_t *pHook;
int n;
int rv = -1;
if(_hooks.link_handler) {
pHook=(ap_LINK_handler_t *)_hooks.link_handler->elts;
for(n=0 ; n < _hooks.link_handler->nelts ; ++n) {
rv=pHook[n].pFunc (r);
if(rv != -1) break;
}
}
return rv;
}
(AP_DECLARE(void) became plain void because on non-Windows platforms AP_DECLARE(type) is defined as just type (include/ap_config.h lines 58–67). -1 is what DECLINED expands to.)
That is all a hook is.
ap_hook_handler()— the function a module calls insideregister_hooks(). On the first call it creates the array and puts it on the "arrays to sort" list withapr_hook_sort_register(). After that it only appends one entry (ap_LINK_handler_t) to the end of the array and writes in the function pointer, the before/after lists and the order value. The name tagszNamecomes from the global variableapr_hook_debug_currentwe saw in section 2 — the name of the module whoseregister_hooks()is being called right now.ap_run_handler()— the function the core calls. It walks the array from the front, calling each function through its pointer, and stops as soon as a value other thanDECLINEDcomes back.
The registering side (modules) and the running side (the core) meet across one static array — the yellow box. They do not know about each other.
Easy to get wrong
- Grepping for
ap_hook_handlerfinds no definition. As shown above, a singleAP_IMPLEMENT_HOOK_*line produces it. To find a hook's body, search forAP_IMPLEMENT_HOOK_[A-Z_]*(.*, *hookname.handleris inserver/config.c,fixupsinserver/request.c. - There is old hand-expanded code inside a comment. Right below the
post_configimplementation inserver/config.c(lines 102–105) there is code that expands thepost_confighook by hand, but it is an old record sitting entirely inside a comment (/* ... */) (server/config.c lines 107–158). It differs from today's macros in places (for example, a function that should return anintends withreturn;), so trust thegcc -Eoutput above for the real expansion.
4. RUN_FIRST and RUN_ALL — the return value changes the flow
There are three kinds of macro for implementing a hook, and the difference is when they stop.
| Macro | What the run function does | Examples at this commit |
|---|---|---|
AP_IMPLEMENT_HOOK_VOID |
No return value. Calls every attached function | child_init, test_config, insert_filter |
AP_IMPLEMENT_HOOK_RUN_ALL |
Continues on OK or DECLINED; on any other value, stops and returns it. Returns OK if it reaches the end |
pre_config, post_config, fixups, access_checker |
AP_IMPLEMENT_HOOK_RUN_FIRST |
Continues on DECLINED; on any other value, stops and returns it. Returns DECLINED if it reaches the end |
handler, translate_name, map_to_storage, type_checker |
The examples come from server/config.c lines 85–175 and server/request.c lines 79–107. Where RUN_ALL's body differs from RUN_FIRST's is inside the loop.
#define APR_IMPLEMENT_EXTERNAL_HOOK_RUN_ALL(ns,link,ret,name,args_decl,args_use,ok,decline) \
/* ... */
ret rv = ok; \
/* ... */
for(n=0 ; n < _hooks.link_##name->nelts ; ++n) \
{ \
/* ... */
rv=pHook[n].pFunc args_use; \
/* ... */
if(rv != ok && rv != decline) \
break; \
rv = ok; \
} \
/* ... */
return rv; \
}
APR-util include/apr_hooks.h lines 222–250
Look at rv = ok; right after if(rv != ok && rv != decline) break;. Even if a function returned DECLINED, the result is overwritten with OK. In a RUN_ALL hook, OK and DECLINED make no difference to the flow — both mean "next function". Only other values such as DONE or HTTP_FORBIDDEN (403) stop it. In RUN_FIRST the same spot is the single line if(rv != decline) break; (APR-util include/apr_hooks.h lines 267–295). There, OK is also a reason to stop.
The red boxes are where the remaining functions stop being called.
Each hook gets the style that fits the question it asks.
handlerasks "who will produce the response to this request?" One answer is enough, so it is RUN_FIRST. Each module passes withDECLINEDif the job is not its own (ifr->handleris not its name), and the first module to returnOKtakes the response.fixupssays "if each of you has something to adjust before the handler, do it now". Everyone must get a chance, so it is RUN_ALL. But if someone says "this request must be cut off here" (403 and so on), it stops there.
How the core uses the result
The place that actually calls the handler hook is ap_invoke_handler().
result = ap_run_handler(r);
/* ... */
return result == DECLINED ? HTTP_INTERNAL_SERVER_ERROR : result;
}
If nobody takes it all the way to the end (DECLINED), the result is 500. In practice that almost never happens, because the core has attached default_handler, which serves files, at the very end of the handler hook (APR_HOOK_REALLY_LAST) (server/core.c line 6171). We look at that function in Part 12. The result of fixups is returned as-is at the end of ap_process_request_internal() if it is not OK (server/request.c lines 435–439). The full order of request-processing stages is in Part 10.
Easy to get wrong
- RUN_ALL returns
OKeven when nothing is attached, becauservstarts asok. APR-util's source still carries a FIXME saying "this returns ok when nothing is run. I suspect it should really return decline, but that breaks Apache currently" (APR-util include/apr_hooks.h lines 204–206). - Returning
DECLINEDfrom a RUN_ALL hook does not mean "opting out". As we saw, it moves on exactly likeOK. Conversely, in a RUN_FIRST hook, if you did some work but want other modules to get their turn, returnDECLINEDrather thanOK. The moment you returnOK, the modules after you are not called. - You cannot tell the kind of hook from the header alone. The
AP_DECLARE_HOOKdeclaration does not say RUN_FIRST or RUN_ALL; only theAP_IMPLEMENT_HOOK_*line in the.cfile does. Before attaching a function to an unfamiliar hook, find its implementation line first.
5. Deciding the order — order values and the names of other modules
Of the four arguments passed when attaching a function, three were about order: ap_hook_handler(fn, aszPre, aszSucc, nOrder).
The order value (nOrder) is a rough position. Smaller numbers come first.
/* Hook orderings */
/** run this hook first, before ANYTHING */
#define APR_HOOK_REALLY_FIRST (-10)
/** run this hook first */
#define APR_HOOK_FIRST 0
/** run this hook somewhere */
#define APR_HOOK_MIDDLE 10
/** run this hook after every other hook which is defined*/
#define APR_HOOK_LAST 20
/** run this hook last, after EVERYTHING */
#define APR_HOOK_REALLY_LAST 30
APR-util include/apr_hooks.h lines 297–307
They are plain integers, so values in between such as APR_HOOK_MIDDLE - 1 are allowed. The comment above the handler hook declaration recommends MIDDLE for handlers that respond only to a specific handler name and LAST for handlers that take anything (include/http_config.h line 1396).
The names of other modules (aszPre, aszSucc) express exact relations. You pass a list of module names as a NULL-terminated array: "the function from mod_include.c must come before me", "I must come before mod_userdir.c". In section 1, mod_alias said on the translate_name hook "before mod_userdir.c and mod_vhost_alias.c", and mod_userdir writes the matching half.
static void register_hooks(apr_pool_t *p)
{
static const char * const aszPre[]={ "mod_alias.c",NULL };
static const char * const aszSucc[]={ "mod_vhost_alias.c",NULL };
ap_hook_translate_name(translate_userdir,aszPre,aszSucc,APR_HOOK_MIDDLE);
modules/mappers/mod_userdir.c lines 385–390
When rules from mod_alias and mod_userdir overlap in a config — say Alias /~foo /somewhere — these two lines are why Alias wins. All three are MIDDLE, but the names put mod_alias first, and since translate_name is RUN_FIRST, once mod_alias returns OK, mod_userdir is not called.
apr_hook_sort_all() — when and how sorting happens
Registration only appended to the end of the array. Sorting is done separately by apr_hook_sort_all(). It walks the list of arrays that apr_hook_sort_register() recorded on each hook's first ap_hook_*() call and passes them one by one to sort_hook() (APR-util hooks/apr_hooks.c lines 244–259). It is called at the end of ap_setup_prelinked_modules() (section 2) and, in main(), right after reading the config and right after processing the config tree (server/main.c line 740, line 758; when the restart loop re-reads the config, the same two spots again at line 836 and line 855). New modules may have joined via LoadModule while the config was read, so it sorts again each time.
Sorting has two steps.
- Line up roughly by order value.
prepare()first sorts bynOrderwith the C standard library'sqsort()(line 88). The comparison function is even calledcrude_order, and all it does isa->nOrder-b->nOrder(lines 75–81). Then it matches the names inaszPreandaszSuccagainst the other entries'szNamewithstrcmpto build "who comes before whom" relations.aszSuccis turned around and added to the other side's "must come before" list, so in the end every relation is expressed as a single kind, "must come before" (lines 96–125). - Re-line them up to respect the relations.
tsort()(topological sort) picks the result one entry at a time. It looks from the front for an entry not yet picked, and if that entry has a "must come before" entry not yet picked, it jumps over to that one and picks it first (lines 139–186). The source comment describes this as "dragging out-of-order items to the front" (lines 130–138).
Step 2 has one consequence: relations beat order values. Even a function with order value LAST, once named as a "must come before" of a FIRST function, is pulled forward in front of it. The example in section 6 is exactly this case.
The fixups hook from the section 6 example. mod_alpha.c, last by order value, is pulled to second place by mod_gamma.c.
6. Example: three modules on the same hook
Three modules written for this post each attach one function to fixups (RUN_ALL) and one to handler (RUN_FIRST). Each time a function is called it leaves a line in the response header X-Trail saying "I was called and I'm returning this value". It writes to r->err_headers_out so the line survives on error responses too.
| Module | fixups order |
fixups returns |
handler order |
handler returns |
|---|---|---|---|---|
mod_alpha.c |
LAST |
DECLINED |
FIRST |
always DECLINED |
mod_beta.c |
MIDDLE |
OK |
MIDDLE |
OK, but DECLINED for ?pass |
mod_gamma.c |
FIRST + mod_alpha.c before it |
OK, but 403 for ?stop |
LAST |
OK |
Here is mod_gamma.c, which shows the most of the three. The other two have the same shape (example folder ex/u06-hooks/).
/* Example written for this post (not httpd source code) */
#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_request.h"
static int gamma_fixups(request_rec *r)
{
if (r->args && !strcmp(r->args, "stop")) { /* on ?stop, cut the request off with 403 */
apr_table_add(r->err_headers_out, "X-Trail", "fixups: gamma -> 403");
return HTTP_FORBIDDEN;
}
apr_table_add(r->err_headers_out, "X-Trail", "fixups: gamma -> OK");
return OK;
}
static int gamma_handler(request_rec *r)
{
if (strcmp(r->handler, "hookdemo")) return DECLINED;
apr_table_add(r->err_headers_out, "X-Trail", "handler: gamma -> OK");
ap_set_content_type(r, "text/plain");
ap_rputs("response written by mod_gamma\n", r);
return OK;
}
static void register_hooks(apr_pool_t *p)
{
/* The order value is FIRST, but we pin "after mod_alpha.c".
* The name is the module struct's name, i.e. the source file name. */
static const char * const pre[] = { "mod_alpha.c", NULL };
ap_hook_fixups(gamma_fixups, pre, NULL, APR_HOOK_FIRST);
ap_hook_handler(gamma_handler, NULL, NULL, APR_HOOK_LAST);
}
AP_DECLARE_MODULE(gamma) = {
STANDARD20_MODULE_STUFF,
NULL, NULL, NULL, NULL, NULL,
register_hooks
};
The first line of the handler function, if (strcmp(r->handler, "hookdemo")) return DECLINED;, is the etiquette of a RUN_FIRST hook: if the name set by SetHandler hookdemo in the config is not yours, pass immediately. The config is the three LoadModule lines from section 2 plus <Location /demo> SetHandler hookdemo </Location>.
Watching the sort: SHOW_HOOKS
With the SHOW_HOOKS environment variable from section 2 turned on, I ran only the config check (-t). The output is long, so grep -n picks out only the fixups and handler lines and the module registration lines (the leading numbers are line numbers in the original output).
### 4. SHOW_HOOKS=1 httpd -t | grep (fixups, handler)
1:Registering hooks for core.c
20: Hooked handler
21: Hooked handler
23: Hooked fixups
36:Registering hooks for mod_so.c
38:Registering hooks for http_core.c
61:Sorting handler: core.c core.c
63:Sorting fixups: core.c
81:Registering hooks for event.c
97:Registering hooks for mod_unixd.c
101:Registering hooks for mod_authz_core.c
106:Registering hooks for mod_alpha.c
107: Hooked fixups
108: Hooked handler
109:Registering hooks for mod_beta.c
110: Hooked fixups
111: Hooked handler
112:Registering hooks for mod_gamma.c
113: Hooked fixups pre(mod_alpha.c)
114: Hooked handler
128:Sorting handler: core.c mod_alpha.c mod_beta.c mod_gamma.c core.c
130:Sorting fixups: core.c mod_alpha.c mod_gamma.c mod_beta.c
173:Sorting handler: core.c mod_alpha.c mod_beta.c mod_gamma.c core.c
175:Sorting fixups: core.c mod_alpha.c mod_gamma.c mod_beta.c
There is a lot to read here.
- Sorting happens three times (lines 61/63, 128/130, 173/175). These are the three places from section 5 — right after the static modules are registered, right after the config is read, and right after the config tree is processed. At the first sort only the three static modules exist, so there is just
core.c. - Both ends of
handlerbelong to the core. The leadingcore.ciscore_upgrade_handler, attached withREALLY_FIRST, and the trailingcore.cisdefault_handlerwithREALLY_LAST(server/core.c lines 6170–6171). Our three stand between them in order-value order:FIRST(alpha) →MIDDLE(beta) →LAST(gamma). fixupsis not in order-value order. By order value alone it should be gamma (FIRST) → beta (MIDDLE) → alpha (LAST), but the result is alpha → gamma → beta. Just as in the section 5 figure, alpha, gamma's "must come before", was pulled forward. Note that alpha even ended up ahead of beta, with which it has no relation at all.
Sending requests
With the server running, here are three requests sent with curl -si (only the status line, the X-Trail headers and the body are kept).
### 5. curl /demo
HTTP/1.1 200 OK
X-Trail: fixups: alpha -> DECLINED
X-Trail: fixups: gamma -> OK
X-Trail: fixups: beta -> OK
X-Trail: handler: alpha -> DECLINED
X-Trail: handler: beta -> OK
response written by mod_beta
### 6. curl /demo?pass
HTTP/1.1 200 OK
X-Trail: fixups: alpha -> DECLINED
X-Trail: fixups: gamma -> OK
X-Trail: fixups: beta -> OK
X-Trail: handler: alpha -> DECLINED
X-Trail: handler: beta -> DECLINED
X-Trail: handler: gamma -> OK
response written by mod_gamma
### 7. curl /demo?stop
HTTP/1.1 403 Forbidden
X-Trail: fixups: alpha -> DECLINED
X-Trail: fixups: gamma -> 403
/demo— infixups(RUN_ALL), gamma and beta were all called even though alpha returnedDECLINED. Inhandler(RUN_FIRST), alpha passed, beta returnedOK, and it stopped there. gamma's handler was not called./demo?pass— when beta also passed withDECLINED, gamma, next in line, produced the response. The same URL can be answered by a different module depending on what the earlier modules say./demo?stop— gamma returned 403 infixups, and RUN_ALL stopped. beta, after it, was not called, and the 403 response went out without ever reaching the handler stage.
The X-Trail lines of the curl output above, left to right in order. Grey is DECLINED, green is OK, red is an error, and dashed boxes are functions that were not called.
Easy to get wrong: a wrong name in the before/after lists is silently ignored
This is a trap I checked while building the example. I compiled gamma with its "must come before" written as "alpha_module" — the name used in LoadModule — instead of "mod_alpha.c", and ran the same check.
### 8. predecessor written as "alpha_module" instead of "mod_alpha.c"
Sorting fixups: core.c mod_gamma.c mod_beta.c mod_alpha.c
No error, no warning — the result is sorted by order value alone (gamma → beta → alpha). prepare() builds a relation when it finds an entry with a matching name, and just moves on when it does not (APR-util hooks/apr_hooks.c lines 99–111). This behaviour is useful on purpose: it is why mod_alias can name mod_userdir.c without causing trouble on a server that does not load mod_userdir. But typos are ignored just as quietly, so write before/after names as the .c names that httpd -l or SHOW_HOOKS shows, and check the sorted result with your own eyes once.
Two more points.
- Do not rely on the order among functions with the same order value. Step 1 is
qsort(), and the C standard does not promise thatqsort()keeps the original order of equal elements. That is why this example gave the threehandlerfunctions different order values. If an order really matters, pin it with names. - If the before/after relations form a cycle, they cannot be sorted.
tsort()is written to hitassert(0); /* we have a loop... */when it meets one (APR-util hooks/apr_hooks.c lines 149–150). I did not run an example with a cycle, so I have not checked how a real server ends in that case.
7. A taste: optional functions and providers
A hook is the way "many modules attach functions to one spot". Sometimes you want the reverse: one module wants to find, by name, a function another module owns, and call it. That other module may not be loaded, so writing it as an ordinary function call would break linking. httpd solves this in two ways.
An optional function is a mechanism for registering and finding a single function by name. mod_so offers one of its own functions with the single line APR_REGISTER_OPTIONAL_FN(ap_find_loaded_module_symbol); inside register_hooks() (modules/core/mod_so.c lines 440–446). APR_REGISTER_OPTIONAL_FN puts the function pointer into a hash table keyed by the function name as a string, and the user takes it out with APR_RETRIEVE_OPTIONAL_FN(name) (APR-util include/apr_optional.h lines 70–85). If that module is absent you get NULL, so the user only needs to check. main() finds ap_signal_server this way too (server/main.c line 785). It resembles hooks in that registration happens inside register_hooks(), and that, like hooks, it is wiped and registered again on restart (apr_hook_deregister_all() empties this hash as well, APR-util hooks/apr_hooks.c lines 266–284).
A provider is a mechanism for registering several implementations of the same job under "group + name + version" and choosing one. You register with ap_register_provider(pool, group, name, version, struct address) and look up with ap_lookup_provider(group, name, version) (server/provider.c lines 35–39, lines 99–101). The file in a config line like AuthBasicProvider file is exactly such a name. How authentication modules are tied together this way is in Part 13, and how mod_proxy's load balancer picks its method is in Part 14.
The hash name and the three-level structure were checked in APR-util hooks/apr_hooks.c lines 378–392 and server/provider.c lines 51–71.
If you want to go deeper
This post left a lot out to keep to the main thread. Here are names and locations only.
- Optional hooks — hooks you can attach to even when the module owning the hook is not loaded.
AP_OPTIONAL_HOOK(include/ap_hooks.h lines 169–170), the core attaching to amod_proxyhook (server/core.c lines 6176–6177), and the storageapr_optional_hook_add()(APR-util hooks/apr_hooks.c lines 339–369). - Hook probes — tracing macros wrapped around hook calls, switched on with
APR_HOOK_PROBES_ENABLED(APR-util include/apr_hooks.h lines 52–109), and the spot that turns them off for argument-less hooks (server/config.c lines 181–195). - Removing modules —
ap_remove_module()andap_remove_loaded_module()(server/config.c lines 662–761), andmod_so'sunload_module()(modules/core/mod_so.c lines 129–144). - Adjusting the config vector length —
conf_vector_length_pre_config(), which shrinks the vector to the real number of modules after the config is read, andap_reserve_module_slots(), which reserves slots in advance (server/config.c lines 2269–2292, lines 2556–2559). -D DUMP_MODULE_DATA— a feature at this commit that prints the-Minformation in TOML format (modules/core/mod_so.c lines 364–376). The 2.4.x branch'smod_so.cdoes not have it.ap_get_module_configunderAP_DEBUG— where the macro is replaced by a function (include/http_config.h lines 551–558).
Recap
| Concept | One-line summary |
|---|---|
module struct |
Everything a module is. ID card (version, number, name, magic) + four config functions + directive table + register_hooks |
AP_DECLARE_MODULE(foo) |
Creates the global variable module foo_module. The first LoadModule argument is this variable's name |
module_index |
Slot number in the config vector (a void * array). Handed out in order from 0 at registration |
ap_add_module() |
The registration door for both static and shared modules. Version check → number → list → directives → register_hooks() |
mod_so |
Owner of LoadModule. Opens the .so, finds the struct by name, checks magic, then ap_add_loaded_module() |
AP_DECLARE_HOOK |
In a header, creates the function shape, the one-registration struct, and the ap_hook_X/ap_run_X declarations |
AP_IMPLEMENT_HOOK_* |
In a .c file, creates the bodies of ap_hook_X(), which pushes onto a static array, and ap_run_X(), which walks it |
| RUN_FIRST | Stops on anything but DECLINED. "Who takes this?" hooks (handler, translate_name) |
| RUN_ALL | Continues on OK/DECLINED, stops on anything else. "Everyone once" hooks (fixups, post_config) |
| Order value | REALLY_FIRST (-10) to REALLY_LAST (30). qsort() lines things up roughly |
| Before/after names | Lists of .c names. tsort() respects them, and they beat order values. Wrong names are silently ignored |
| Optional function / provider | The reverse direction of hooks — find another module's function or implementation by name |
What to look at next
Of the two channels a module uses to plug into the server, we have seen hooks. The other is cmds from section 1 — directives, which we only named. How the line Alias /icons/ "/usr/share/icons/" in httpd.conf is read and handed to a mod_alias function, and how the config structs created for each <Directory> and <VirtualHost> are merged — where this post's module_index and config vector are actually used — is in Part 7: The Config File and Directives — How One Line of httpd.conf Becomes a C Struct.
Here is how Spring solved the same problem. The idea of "spots where outside code is inserted into the creation flow" is the same as BeanPostProcessor in Spring Part 6. Calling every registered one in turn resembles RUN_ALL, and the rule that a smaller Ordered number comes first resembles nOrder. There are two differences. Spring puts objects that implement an interface into a list, while httpd puts function pointers whose shape is fixed by a typedef into a separate array per hook. And besides numbers, httpd can express order by the other party's name, as in "after mod_alias.c". Picking "the first one that answers", as RUN_FIRST does, resembles how DispatcherServlet in Part 21 takes the first non-null answer from its list of HandlerMappings.
- Part 1: How I Read It with AI, and the Map
- Part 2: Before Reading a Server Written in C — Structs, Function Pointers, Macros
- Part 3: The Big Picture — Core, APR, MPM, Modules and the Three Big Structs
- Part 5: main() and Startup — Why the Config Is Read Twice, and Restarts
- Part 10: Request Processing — From a URL to a File and a Handler