namalzi

← 글 목록

Apache httpd 소스 파헤치기 6: 모듈과 훅 — 모듈 하나가 서버에 끼어드는 방법

Apache httpd · 게시

httpd 의 기능은 거의 전부 모듈이고, 모듈은 훅이라는 정해진 자리에 함수를 걸어 서버에 끼어듭니다. module 구조체, 모듈이 등록되는 길(정적 모듈과 LoadModule), 훅을 만드는 매크로가 실제로 무엇으로 펼쳐지는지, RUN_FIRST 와 RUN_ALL 의 차이, 순서 값과 앞뒤 모듈 이름으로 정하는 실행 순서를 기준 커밋의 소스와 직접 만든 모듈 세 개로 확인합니다.

이 글에서 다루는 것

"Apache httpd 소스 파헤치기" 연재의 여섯 번째 글입니다. 5편에서 main() 이 설정을 읽고 pre_config·post_config 같은 단계를 차례로 부르는 것을 봤습니다. 그때 "단계를 부른다"고만 하고 넘어간 것이 있습니다. ap_run_post_config() 를 부르면 누구의 함수가, 어떤 순서로 불리는가입니다. 이번 글이 그 답입니다.

답하려는 질문은 네 가지입니다.

항목 내용
대상 Apache httpd
버전 2.5.1-dev (trunk)
커밋 e4af30fe6d (2026-10-24)
함께 읽는 라이브러리 APR-util 1.6.3 (태그 1.6.3, 커밋 28327eeca2) — 훅 매크로와 정렬
주로 읽는 파일 include/http_config.h, server/config.c, include/ap_hooks.h, modules/core/mod_so.c, APR-util include/apr_hooks.h·hooks/apr_hooks.c

글에 나오는 줄 번호와 세부 동작은 모두 이 커밋(APR-util 은 위 태그) 기준입니다. 같은 커밋의 소스를 받고 예제용 도커 이미지를 만드는 방법은 1편에 있습니다.

이 글은 처음 소스를 여는 분이 따라올 수 있는 큰 줄기만 골랐습니다. 빼놓은 것은 글 끝의 "더 깊이 들어가면"에 이름과 위치만 적어 두었습니다.

먼저 알아 둘 말

모듈(module)은 httpd 에 기능 하나를 보태는 C 코드 묶음입니다. mod_alias(Alias·Redirect), mod_ssl(HTTPS), mod_proxy(리버스 프록시)처럼 우리가 설정 파일에서 이름을 보는 것들이 모두 모듈이고, 3편에서 봤듯이 코어(core.c)와 HTTP 처리(http_core.c)도 모듈입니다.

훅(hook, 갈고리)은 서버가 일하는 흐름 중간중간에 뚫어 둔 "여기서 다른 함수를 불러 줄 수 있음" 자리입니다. 예를 들어 handler 훅은 "응답 본문을 만들 차례", fixups 훅은 "핸들러를 부르기 직전 마지막 손질" 자리입니다. 모듈은 훅마다 자기 함수를 걸어 두고(등록), 서버는 그 자리에 오면 걸린 함수들을 차례로 부릅니다(실행).

함수 포인터는 "함수가 있는 주소"를 담는 변수입니다. 변수에 담아 두었다가 나중에 그 변수로 함수를 부를 수 있습니다. 훅에 함수를 "건다"는 것은 결국 함수 포인터를 배열에 넣어 둔다는 뜻입니다. 매크로는 컴파일 전에 글자 그대로 다른 코드로 바뀌는 이름입니다(#define). 둘 다 2편에서 자세히 풀었습니다.

DSO(Dynamic Shared Object)는 실행 중인 프로그램이 나중에 불러 올릴 수 있는 라이브러리 파일입니다. 리눅스에서는 .so 파일이고, httpd 에서는 LoadModule 디렉티브가 이것을 싣습니다. 서버 실행 파일 안에 처음부터 묶여 들어간 모듈은 정적(static) 모듈, .so 로 실리는 모듈은 공유(shared) 모듈이라고 부릅니다.

OK·DECLINED·DONE 은 훅에 건 함수가 돌려주는 약속된 값입니다. 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 */

include/httpd.h 461–467행

OK 는 "내가 이 단계를 처리했다", DECLINED 는 "나는 할 일이 없다, 다음 사람에게 넘겨라", DONE 은 "응답을 완전히 끝냈다"입니다. 이 밖에 404·500 같은 HTTP 상태 번호를 돌려주면 오류로 끝내라는 뜻이 됩니다. 이 값들이 흐름을 어떻게 바꾸는지가 4절의 주제입니다.

1. 모듈은 구조체 하나입니다

C 에는 클래스가 없습니다. httpd 는 "모듈이란 이런 것"을 구조체 하나로 정했습니다. include/http_config.h 의 module 입니다. (필드마다 붙은 설명 주석은 모두 지웠고, 코드 줄은 소스 그대로입니다.)

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 347–424행

void *(*create_dir_config) (apr_pool_t *p, char *dir); 처럼 이름 앞에 (* 가 붙은 줄이 함수 포인터 필드입니다. 이 구조체는 크게 네 부분입니다.

  1. 신분증 — version·minor_version(이 모듈이 맞춰 컴파일된 서버 API 판), module_index(설정 벡터의 칸 번호, 아래에서 봅니다), name(소스 파일 이름), magic(진짜 모듈 구조체인지 확인하는 표식), next(모듈끼리 잇는 연결 리스트).
  2. 설정 만들기·합치기 — create_dir_config 등 함수 넷. <Directory> 마다, 가상 호스트마다 이 모듈의 설정 구조체를 만들고 합칩니다. 7편에서 봅니다.
  3. cmds — 이 모듈이 알아듣는 디렉티브의 표. mod_alias 라면 Alias, Redirect 같은 줄을 어떤 함수가 읽는지 적혀 있습니다. 역시 7편에서 봅니다.
  4. register_hooks — 이 글의 주인공. 서버가 이 함수를 한 번 불러 주면, 모듈은 그 안에서 "나는 이 훅, 저 훅에 이 함수를 걸겠다"고 등록합니다.

실제 모듈은 이 구조체를 변수 하나로 만들어 둡니다. 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 783–801행

register_hooks() 는 두 줄입니다. translate_name 훅(URL 을 파일 경로로 바꾸는 단계, 10편)에 translate_alias_redir 를, fixups 훅에 fixup_redir 를 겁니다. 두 번째·세 번째 인자(NULL,aszSucc)와 마지막 인자(APR_HOOK_MIDDLE)는 순서를 정하는 값으로, 5절에서 봅니다.

AP_DECLARE_MODULE(alias) 와 STANDARD20_MODULE_STUFF 는 매크로입니다.

#define AP_DECLARE_MODULE(foo) \
    APLOG_USE_MODULE(foo);                         \
    module AP_MODULE_DECLARE_DATA foo##_module

include/http_config.h 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 489–496행

## 는 매크로 안에서 글자를 이어 붙이는 연산자입니다. 그래서 AP_DECLARE_MODULE(alias) = { ... } 는 module alias_module = { ... } 이라는 전역 변수 정의가 됩니다(앞의 APLOG_USE_MODULE 은 모듈별 LogLevel 을 위한 것으로 16편에서 봅니다). STANDARD20_MODULE_STUFF 는 구조체의 앞 여덟 칸 — version, minor_version, module_index, name, dynamic_load_handle, next, magic, rewrite_args — 을 순서대로 채웁니다. 모듈 작성자는 이 매크로 한 단어 뒤에 나머지 여섯 칸 — 함수 다섯과 디렉티브 표 하나 — 만 적으면 됩니다(마지막 flags 는 적지 않으면 0 입니다).

module 구조체의 네 부분. STANDARD20_MODULE_STUFF 가 채우는 신분증 칸, 설정을 만들고 합치는 함수 네 개, 디렉티브 표 cmds, 훅을 등록하는 register_hooks. 신분증의 module_index 와 설정 함수들은 설정 벡터의 칸 번호로 이어지고, register_hooks 는 훅마다 있는 함수 배열로 이어진다. 노란 상자가 모듈마다 한 칸씩 쓰는 설정 벡터, 파란 상자가 서버의 흐름에 끼어드는 두 통로(디렉티브와 훅)입니다.

module_index — 모듈마다 받는 칸 번호

module_index 는 처음에 -1 이고, 서버에 등록될 때 0, 1, 2… 순서대로 번호를 받습니다(2절). 이 번호는 설정 벡터의 칸 번호입니다. 설정 벡터는 모듈 수만큼 칸이 있는 void * 배열이고, 칸마다 그 모듈의 설정 구조체 주소가 들어 있습니다. 모듈이 자기 설정을 꺼낼 때 쓰는 매크로가 이것을 그대로 보여 줍니다.

#define ap_get_module_config(v,m)       \
    (((void **)(v))[(m)->module_index])

include/http_config.h 553–554행

배열의 module_index 번째 칸을 꺼낼 뿐입니다. 서버는 모듈들의 설정 구조체가 어떻게 생겼는지 전혀 모르고, 각 모듈이 자기 칸을 자기 타입으로 해석합니다. 이 방식의 장단점은 2편에서 다뤘습니다.

헷갈리기 쉬운 곳: 모듈 이름이 세 가지입니다

같은 모듈이 자리에 따라 다른 이름으로 불립니다.

어디서 이름 어디서 오나
LoadModule 첫 인자, <IfModule>, httpd -M alias_module AP_DECLARE_MODULE(alias) 가 만든 변수 이름
httpd -l, 훅 순서의 앞뒤 지정, SHOW_HOOKS 출력 mod_alias.c 구조체의 name 칸, 곧 __FILE__(컴파일한 소스 파일 이름)
LoadModule 둘째 인자 modules/mod_alias.so 빌드가 만든 파일 이름

__FILE__ 에 경로가 붙어 들어오는 컴파일러가 있어서, ap_add_module() 이 마지막 /(윈도 경로라면 \) 뒤만 남기도록 잘라 냅니다(server/config.c 620–629행). 둘째 줄의 이름이 5절에서 다시 중요해집니다.

2. 모듈이 서버에 등록되는 길

모듈 구조체가 있다고 서버가 저절로 아는 것은 아닙니다. 등록되는 길은 두 갈래입니다. 실행 파일에 묶인 정적 모듈은 기동하자마자, .so 로 된 공유 모듈은 httpd.conf 를 읽다가 LoadModule 줄을 만났을 때입니다.

모듈이 등록되는 두 갈래. main() 이 먼저 ap_setup_prelinked_modules() 로 정적 모듈 core, so, http 에 0, 1, 2 번을 주고 ap_add_module() 을 부른다. 이어 ap_read_config() 가 LoadModule 줄을 만나면 mod_so 가 .so 를 열어 모듈 구조체를 찾고 ap_add_loaded_module() 을 거쳐 같은 ap_add_module() 을 부르며, 번호가 -1 인 이 모듈은 거기서 다음 번호를 받는다. ap_add_module() 은 모듈의 register_hooks() 를 부르고, 그 안의 ap_hook_*() 가 훅의 함수 배열에 한 칸씩 넣는다. apr_hook_sort_all() 이 배열을 정렬하고, 요청이 오면 ap_run_handler() 가 정렬된 배열을 차례로 부른다. 두 갈래가 ap_add_module() 에서 만납니다. 노란 상자 — 훅의 함수 배열 — 는 등록 순서대로 쌓이고, 정렬은 ③에서 따로 합니다.

정적 모듈: modules.c 와 ap_setup_prelinked_modules()

어떤 모듈을 실행 파일에 묶을지는 ./configure 때 정해지고, 빌드가 소스 맨 위 폴더에 modules.c 라는 파일을 만들어 냅니다. 저장소에는 없는 파일입니다(저장소의 os/win32/modules.c 같은 것은 다른 운영체제용으로 손으로 적어 둔 판입니다). 예제 컨테이너의 빌드 폴더에서 꺼내 보면 이렇습니다(이 연재의 이미지는 --enable-mods-shared=most 로 빌드해서 정적 모듈이 셋뿐입니다).

### 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
};

모듈 구조체의 주소를 늘어놓은 배열입니다. 같은 파일에 ap_preloaded_modules[](실을 수 있는 모듈)도 있는데 이 빌드에서는 내용이 같습니다. main() 은 기동 초반에 ap_setup_prelinked_modules() 를 부르고(server/main.c 574행), 이 함수가 배열을 돌며 번호를 주고 하나씩 등록합니다.

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;
}

server/config.c 763–816행

설정 벡터의 길이(conf_vector_length)를 "정적 모듈 수 + DYNAMIC_MODULE_LIMIT + 1" 로 미리 잡아 두는 것이 보입니다. 나중에 실릴 공유 모듈의 칸까지 남겨 두는 것입니다. DYNAMIC_MODULE_LIMIT 의 기본값은 256 입니다(include/httpd.h 119–120행).

모든 모듈이 지나는 문: ap_add_module()

정적이든 공유든 등록은 결국 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;
}

server/config.c 558–652행

  1. 판을 확인합니다. 모듈이 컴파일될 때 박힌 version(= 그때의 MODULE_MAGIC_NUMBER_MAJOR)이 지금 서버의 값과 다르면 거절합니다.
  2. 번호를 줍니다. 아직 -1 이면(공유 모듈) 다음 빈 번호를 줍니다. 정적 모듈은 앞 함수에서 이미 받았습니다.
  3. 목록 맨 앞에 붙입니다. ap_top_module 이 가리키는 연결 리스트의 머리에 끼웁니다.
  4. 디렉티브를 등록하고(ap_add_module_commands()) 훅을 등록합니다(ap_register_hooks()).

ap_register_hooks() 는 짧습니다.

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);
    }
}

server/config.c 489–500행

모듈의 register_hooks 함수 포인터를 부르기 직전에 전역 변수 apr_hook_debug_current 에 모듈 이름(mod_alias.c)을 적어 둡니다. 모듈 안의 ap_hook_*() 는 인자로 모듈 이름을 받지 않는데, 등록되는 함수에 "누구의 것"이라는 이름표가 붙는 비결이 이것입니다(3절의 펼친 코드에서 확인합니다). 환경 변수 SHOW_HOOKS 를 켜면 등록과 정렬 과정을 화면에 찍어 주는데, 6절의 예제에서 씁니다.

공유 모듈: mod_so 와 LoadModule

LoadModule 디렉티브는 코어가 아니라 정적 모듈 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 448–450행

TAKE2 는 인자 두 개짜리 디렉티브, EXEC_ON_READ 는 "설정 파일을 읽는 그 자리에서 바로 실행"이라는 뜻입니다. 모듈을 실어야 그 모듈의 디렉티브를 알아들을 수 있으니, 다른 디렉티브처럼 나중에 몰아서 처리할 수 없습니다(디렉티브 처리 순서는 7편). 실제 일은 load_module() 이 합니다. 이미 실렸는지 확인하는 앞부분과 오류 메시지를 만드는 부분을 줄였습니다.

    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 265–322행

  1. 파일을 엽니다. dso_load() 안의 apr_dso_load() 가 .so 파일을 프로세스 안에 올립니다(리눅스라면 그 아래는 dlopen() 입니다).
  2. 이름으로 구조체를 찾습니다. apr_dso_sym() 이 LoadModule 첫 인자 — alias_module — 와 같은 이름의 심벌, 곧 AP_DECLARE_MODULE(alias) 가 만든 전역 변수를 찾아 그 주소를 줍니다. 1절의 "이름이 세 가지" 중 첫째 이름이 여기서 쓰입니다.
  3. 진짜인지 확인합니다. magic 칸이 MODULE_MAGIC_COOKIE 와 같은지 봅니다. 이 커밋의 값은 0x41503235UL /* "AP25" */ 입니다(include/ap_mmn.h 749행).
  4. 등록합니다. ap_add_loaded_module() 이 위의 ap_add_module() 을 부릅니다(server/config.c 703–731행). 여기서 register_hooks() 가 불립니다.
  5. 내릴 준비를 합니다. 설정 풀(pconf)에 cleanup 을 걸어 두어, 재시작이나 종료로 이 풀이 지워질 때 모듈을 목록에서 빼고 .so 를 내립니다. 풀과 cleanup 은 4편에서 봤습니다.
  6. 기본 설정을 만듭니다. ap_single_module_configure() 가 서버 설정과 기본 디렉터리 설정 칸을 채웁니다.

예제로 확인: httpd -l, -M, -L

이 예제는 기준 커밋(e4af30fe6d)을 그대로 빌드한 httpd 2.5.1-dev 로 도커 컨테이너(Ubuntu 24.04)에서 실행했습니다. 6절에서 쓸 모듈 셋(mod_alpha, mod_beta, mod_gamma)을 apxs -c 로 컴파일해 LoadModule 로 싣는 최소 설정입니다. 예제 전체는 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

헷갈리기 쉬운 곳

3. 훅 하나는 매크로 세 개로 만들어집니다

이제 ap_hook_translate_name()·ap_hook_fixups() 같은 함수가 어디서 왔는지 봅니다. 소스를 grep 해 보면 이 이름의 함수 정의가 어디에도 없습니다. 매크로가 만들어 내기 때문입니다. handler 훅 하나를 예로 따라가겠습니다.

① 선언: AP_DECLARE_HOOK

헤더 파일에 이 한 줄이 있습니다.

AP_DECLARE_HOOK(int,handler,(request_rec *r))

include/http_config.h 1399행

"handler 라는 훅이 있고, 여기 걸 함수는 request_rec *r 을 받아 int 를 돌려준다"는 뜻입니다. 이 매크로는 httpd 의 것이지만 하는 일은 APR-util 의 매크로에 이름 접두사를 붙여 넘기는 것뿐입니다.

#define AP_DECLARE_HOOK(ret,name,args) \
        APR_DECLARE_EXTERNAL_HOOK(ap,AP,ret,name,args)

include/ap_hooks.h 74–75행

ap 는 만들어질 함수 이름의 접두사, AP 는 AP_DECLARE 처럼 내보내기 표시 매크로의 접두사입니다. 넘겨받는 쪽이 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 118–132행

ns 에 ap, name 에 handler 를 넣고 ## 로 이어 붙이면 다섯 가지가 생깁니다.

펼쳐진 이름 무엇
ap_HOOK_handler_t 훅에 걸 함수의 모양(타입). int (request_rec *r)
ap_hook_handler() 모듈이 부르는 등록 함수의 선언. 함수 포인터, 앞에 와야 할 모듈들, 뒤에 와야 할 모듈들, 순서 값을 받는다
ap_run_handler() 코어가 부르는 실행 함수의 선언. 인자는 훅과 같은 (request_rec *r)
ap_hook_get_handler() 등록된 배열을 꺼내 보는 함수의 선언
ap_LINK_handler_t 등록 한 건을 담는 구조체. 함수 포인터 + 이름표 + 앞뒤 목록 + 순서 값

헤더에서 생기는 것은 선언뿐입니다. 몸통은 아직 없습니다. 훅에 걸 함수의 모양이 typedef 로 정해져 있어서, 모듈이 모양이 다른 함수를 ap_hook_handler() 에 넘기면 컴파일러가 알아챕니다. 인자가 둘이고 반환값이 없는 함수를 일부러 걸어 예제 컨테이너(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]

오류가 아니라 경고라서 .so 는 그대로 만들어지니 경고를 흘려보내면 안 되지만, C 로 짠 플러그인 구조치고 타입이 꽤 단단히 묶인 편입니다.

② 보관소: APR_HOOK_STRUCT

몸통은 훅을 "가진" 소스 파일 하나에 있습니다. handler 훅의 주인은 server/config.c 입니다. 파일 첫머리에 그 파일이 가진 훅들의 보관소를 만듭니다.

APR_HOOK_STRUCT(
           APR_HOOK_LINK(header_parser)
           /* ... */
           APR_HOOK_LINK(handler)
           /* ... */
)

server/config.c 71–83행

#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 135–140행

펼치면 static struct { apr_array_header_t *link_handler; ... } _hooks; — 훅마다 배열 하나를 가리키는 포인터를 칸으로 가진 구조체 변수입니다. static 이 붙은 전역 변수는 그 .c 파일 안에서만 보입니다. 그래서 파일마다 _hooks 라는 같은 이름을 써도 부딪히지 않습니다. server/request.c 도 자기 _hooks 를 가지고 fixups·translate_name 같은 요청 단계 훅을 보관합니다(server/request.c 61–77행).

③ 구현: AP_IMPLEMENT_HOOK_RUN_FIRST

같은 파일에 이 줄이 몸통을 만듭니다.

AP_IMPLEMENT_HOOK_RUN_FIRST(int, handler, (request_rec *r),
                            (r), DECLINED)

server/config.c 169–170행

인자 넷째 (r) 은 "받은 인자를 다음 함수에 넘길 때 이렇게 적어라", 다섯째 DECLINED 는 "이 값이면 다음으로 넘어가라"입니다. 이 매크로도 ap_hooks.h 에서 APR-util 의 APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST 로 넘어가고(include/ap_hooks.h 137–139행), 그 매크로는 등록 함수를 만드는 APR_IMPLEMENT_EXTERNAL_HOOK_BASE(APR-util include/apr_hooks.h 143–165행)와 실행 함수의 몸통을 이어 붙입니다.

매크로를 매크로로 읽는 것보다 컴파일러가 펼친 결과를 보는 편이 빠릅니다. gcc -E 는 컴파일하지 않고 전처리(매크로 펼치기)까지만 한 결과를 내놓습니다. 위 두 매크로만 적은 작은 파일(ex/u06-hooks/expand.c)을 예제 컨테이너에서 펼쳤습니다. 전처리 결과는 한 줄로 나오므로 run.sh 안의 sed·awk 로 줄 바꿈과 들여쓰기만 넣었고, 글자는 바꾸지 않았습니다(빈 문장 ; 몇 개는 지웠습니다 — 꺼져 있는 디버깅용 probe 매크로가 남긴 것입니다).

### 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) 가 그냥 void 로 펼쳐진 것은 윈도가 아닌 곳에서 AP_DECLARE(type) 이 type 그대로로 정의되어 있기 때문입니다(include/ap_config.h 58–67행). -1 은 DECLINED 가 펼쳐진 값입니다.)

이것이 훅의 전부입니다.

훅 하나를 만드는 매크로들. 헤더의 AP_DECLARE_HOOK 은 함수 모양 typedef, 등록 한 건을 담는 구조체, 함수 선언 셋만 만든다. 소스 config.c 의 APR_HOOK_STRUCT 와 AP_IMPLEMENT_HOOK_RUN_FIRST 가 같은 이름으로 몸통을 만든다. static 배열 _hooks.link_handler 를 사이에 두고 모듈의 register_hooks 는 ap_hook_handler() 로 배열에 한 칸을 넣고, 코어의 ap_invoke_handler 는 ap_run_handler() 로 배열을 돌며 부른다. 노란 상자, 곧 static 배열 하나를 사이에 두고 등록하는 쪽(모듈)과 실행하는 쪽(코어)이 만납니다. 둘은 서로를 모릅니다.

헷갈리기 쉬운 곳

4. RUN_FIRST 와 RUN_ALL — 돌려준 값이 흐름을 바꿉니다

훅을 구현하는 매크로는 세 종류이고, 차이는 언제 멈추느냐입니다.

매크로 실행 함수의 동작 이 커밋의 예
AP_IMPLEMENT_HOOK_VOID 반환값 없음. 걸린 함수를 전부 부른다 child_init, test_config, insert_filter
AP_IMPLEMENT_HOOK_RUN_ALL OK·DECLINED 이면 계속, 그 밖의 값이면 멈추고 그 값을 돌려준다. 끝까지 가면 OK pre_config, post_config, fixups, access_checker
AP_IMPLEMENT_HOOK_RUN_FIRST DECLINED 이면 계속, 그 밖의 값이면 멈추고 그 값을 돌려준다. 끝까지 가면 DECLINED handler, translate_name, map_to_storage, type_checker

예는 server/config.c 85–175행과 server/request.c 79–107행에서 골랐습니다. RUN_ALL 의 몸통에서 RUN_FIRST 와 다른 곳은 반복문 안쪽입니다.

#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 222–250행

if(rv != ok && rv != decline) break; 다음 줄의 rv = ok; 를 보세요. DECLINED 를 돌려준 함수가 있어도 결과는 OK 로 덮입니다. RUN_ALL 훅에서 OK 와 DECLINED 는 흐름상 차이가 없습니다 — 둘 다 "다음 함수로". 멈추게 하는 것은 DONE 이나 HTTP_FORBIDDEN(403) 같은 그 밖의 값뿐입니다. RUN_FIRST 의 같은 자리는 if(rv != decline) break; 한 줄입니다(APR-util include/apr_hooks.h 267–295행). OK 도 멈춤 사유입니다.

RUN_FIRST 와 RUN_ALL 의 반복. RUN_FIRST 는 함수를 부르고 DECLINED 면 다음 함수로, 아니면 OK 든 404 든 그 값을 돌려주고 끝난다. RUN_ALL 은 OK 나 DECLINED 면 다음 함수로, DONE, 403, 500 같은 다른 값이면 그 값을 돌려주고 끝나며, 끝까지 돌면 OK 를 돌려준다. 빨간 상자가 뒤의 함수들이 불리지 않게 되는 곳입니다.

두 방식은 훅이 묻는 질문에 맞춰 골라져 있습니다.

코어는 결과를 어떻게 쓰나

handler 훅을 실제로 부르는 곳은 ap_invoke_handler() 입니다.

    result = ap_run_handler(r);
    /* ... */
    return result == DECLINED ? HTTP_INTERNAL_SERVER_ERROR : result;
}

server/config.c 443–470행

끝까지 아무도 맡지 않으면(DECLINED) 500 입니다. 하지만 실제로는 거의 그럴 일이 없습니다. 코어가 handler 훅의 맨 끝(APR_HOOK_REALLY_LAST)에 파일을 내보내는 default_handler 를 걸어 두었기 때문입니다(server/core.c 6171행). 그 함수는 12편에서 봅니다. fixups 의 결과는 ap_process_request_internal() 의 마지막에서 OK 가 아니면 그대로 돌려줍니다(server/request.c 435–439행). 요청 처리 단계의 전체 순서는 10편에서 봅니다.

헷갈리기 쉬운 곳

5. 순서 정하기 — 순서 값과 앞뒤 모듈 이름

훅에 함수를 걸 때 넘기는 인자 넷 가운데 셋이 순서에 관한 것이었습니다. ap_hook_handler(fn, aszPre, aszSucc, nOrder).

순서 값(nOrder)은 대략적인 자리입니다. 숫자가 작을수록 앞입니다.

    /* 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 297–307행

그냥 정수이므로 APR_HOOK_MIDDLE - 1 처럼 사이 값을 줄 수도 있습니다. handler 훅 선언 위의 주석은 "특정 핸들러 이름에만 반응하는 핸들러는 MIDDLE, 무엇이든 받는 핸들러는 LAST" 를 권합니다(include/http_config.h 1396행).

앞뒤 모듈 이름(aszPre, aszSucc)은 정확한 관계입니다. "mod_include.c 의 함수가 나보다 먼저", "나는 mod_userdir.c 보다 먼저"처럼 모듈 이름의 목록을 NULL 로 끝나는 배열로 넘깁니다. 1절의 mod_alias 가 translate_name 훅에 "mod_userdir.c, mod_vhost_alias.c 보다 앞"이라고 적었고, mod_userdir 쪽도 짝을 맞춰 적어 두었습니다.

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 385–390행

설정 파일에서 Alias /~foo /somewhere 처럼 mod_alias 와 mod_userdir 의 규칙이 겹칠 때 Alias 가 이기는 이유가 이 두 줄입니다. 셋 다 같은 MIDDLE 이지만 이름으로 mod_alias 가 앞에 서고, translate_name 은 RUN_FIRST 라서 mod_alias 가 OK 를 돌려주면 mod_userdir 은 불리지 않습니다.

apr_hook_sort_all() — 언제, 어떻게 정렬하나

등록할 때는 배열 끝에 붙이기만 했습니다. 정렬은 따로 apr_hook_sort_all() 이 합니다. 이 함수는 ap_hook_*() 가 처음 불릴 때 apr_hook_sort_register() 로 올려 둔 배열 목록을 돌며 하나씩 sort_hook() 에 넘깁니다(APR-util hooks/apr_hooks.c 244–259행). 부르는 자리는 ap_setup_prelinked_modules() 의 끝(2절)과, main() 에서 설정을 읽은 직후·설정 트리를 처리한 직후입니다(server/main.c 740행, 758행; 재시작 루프 안에서 설정을 다시 읽을 때도 같은 두 자리, 836행·855행). 설정을 읽는 동안 LoadModule 로 새 모듈이 끼어들었을 수 있으니 그때마다 다시 정렬하는 것입니다.

정렬은 두 단계입니다.

  1. 순서 값으로 대충 줄 세우기. prepare() 가 먼저 C 표준 라이브러리의 qsort() 로 nOrder 순으로 정렬합니다(88행). 비교 함수 이름도 crude_order(대충 순서)이고, 하는 일은 a->nOrder-b->nOrder 한 줄입니다(75–81행). 그다음 aszPre·aszSucc 의 이름을 다른 항목의 szName 과 strcmp 로 맞춰 보며 "누가 누구 앞" 관계를 만듭니다. aszSucc 은 거꾸로 뒤집어 상대 쪽의 "앞에 와야 할 것" 목록에 넣으므로, 결국 관계는 전부 "앞에 와야 할 것" 하나로 통일됩니다(96–125행).
  2. 관계를 지키도록 다시 세우기. tsort()(위상 정렬)가 결과를 하나씩 뽑습니다. 앞에서부터 아직 안 뽑힌 항목을 보다가, 그 항목에 아직 안 뽑힌 "앞에 와야 할 것"이 있으면 그쪽으로 건너가 그것부터 뽑습니다(139–186행). 소스 주석은 이것을 "순서가 어긋난 항목을 앞으로 끌어온다(dragging out-of-order items to the front)"고 설명합니다(130–138행).

2단계 때문에 생기는 결과가 하나 있습니다. 앞뒤 관계가 순서 값보다 셉니다. 순서 값이 LAST 인 함수라도 FIRST 인 함수의 "앞에 와야 할 것"으로 지목되면 그 앞으로 끌려 나옵니다. 6절의 예제가 바로 이 경우입니다.

훅 정렬의 두 단계. 첫째 qsort 는 순서 값만 보고 core.c(-10), mod_gamma.c(0, 앞에 와야 할 것 mod_alpha.c), mod_beta.c(10), mod_alpha.c(20) 순으로 세운다. 둘째 tsort 는 앞에서부터 고르되 앞에 와야 할 것이 남아 있으면 그것부터 뽑아 core.c, mod_alpha.c, mod_gamma.c, mod_beta.c 가 된다. 6절 예제의 fixups 훅. 순서 값으로는 맨 뒤였던 mod_alpha.c 가 mod_gamma.c 에게 끌려 둘째 자리로 옵니다.

6. 예제: 세 모듈이 같은 훅에 걸리면

직접 쓴 모듈 셋이 fixups(RUN_ALL)와 handler(RUN_FIRST)에 함수를 하나씩 겁니다. 각 함수는 불릴 때마다 응답 헤더 X-Trail 에 "나 불렸고 이 값을 돌려준다"를 한 줄씩 남깁니다. 오류 응답에도 남도록 r->err_headers_out 에 적습니다.

모듈 fixups 순서 fixups 반환 handler 순서 handler 반환
mod_alpha.c LAST DECLINED FIRST 언제나 DECLINED
mod_beta.c MIDDLE OK MIDDLE OK, 단 ?pass 면 DECLINED
mod_gamma.c FIRST + 앞에 mod_alpha.c OK, 단 ?stop 이면 403 LAST OK

세 모듈 가운데 가장 많은 것을 보여 주는 mod_gamma.c 입니다. 나머지 둘도 모양이 같습니다(예제 폴더 ex/u06-hooks/).

/* 이 글을 위해 쓴 예제입니다 (httpd 의 소스가 아닙니다) */
#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")) {   /* ?stop 으로 오면 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)
{
    /* 순서 값은 FIRST 지만 "mod_alpha.c 보다 뒤에" 라고 못 박는다.
     * 이름은 모듈 구조체의 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
};

handler 함수의 첫 줄 if (strcmp(r->handler, "hookdemo")) return DECLINED; 가 RUN_FIRST 훅의 예절입니다. 설정의 SetHandler hookdemo 로 정해진 이름이 자기 것이 아니면 곧바로 넘깁니다. 설정은 2절의 LoadModule 셋에 <Location /demo> SetHandler hookdemo </Location> 을 더한 것입니다.

정렬 결과 보기: SHOW_HOOKS

2절에서 본 SHOW_HOOKS 환경 변수를 켜고 설정 검사(-t)만 돌렸습니다. 출력이 길어서 fixups·handler 와 모듈 등록 줄만 grep -n 으로 골랐습니다(앞의 숫자는 원래 출력의 줄 번호입니다).

### 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

읽을 거리가 여럿입니다.

요청을 보내 보기

서버를 띄우고 curl -si 로 세 가지 요청을 보낸 결과입니다(응답 줄, X-Trail 헤더, 본문만 골랐습니다).

### 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
  1. /demo — fixups(RUN_ALL)는 alpha 가 DECLINED 를 돌려줬는데도 gamma, beta 까지 모두 불렸습니다. handler(RUN_FIRST)는 alpha 가 넘기고 beta 가 OK 를 돌려주자 거기서 멈췄습니다. gamma 의 핸들러는 불리지 않았습니다.
  2. /demo?pass — beta 도 DECLINED 로 넘기자 다음 차례인 gamma 가 응답을 만들었습니다. 같은 요청 주소라도 앞 모듈의 대답에 따라 응답을 만드는 모듈이 바뀝니다.
  3. /demo?stop — fixups 에서 gamma 가 403 을 돌려주자 RUN_ALL 이 멈췄습니다. 뒤의 beta 는 불리지 않았고, 핸들러 단계까지 가지도 못하고 403 응답이 나갔습니다.

세 요청의 X-Trail 을 옮긴 그림. /demo 는 200 이고 본문은 mod_beta 가 썼다. fixups 에서 alpha 는 DECLINED, gamma 는 OK, beta 는 OK 로 셋 다 불렸고, handler 에서 alpha 가 DECLINED, beta 가 OK 를 돌려주자 gamma 는 불리지 않았다. /demo?pass 는 200 이고 본문은 mod_gamma 가 썼다. fixups 는 같고, handler 에서 alpha 와 beta 가 DECLINED, gamma 가 OK 다. /demo?stop 은 403 Forbidden 이다. fixups 에서 alpha 가 DECLINED, gamma 가 403 을 돌려주자 beta 는 불리지 않았고 handler 단계까지 가지 못했다 위 curl 출력의 X-Trail 줄을 왼쪽부터 순서대로 옮겼습니다. 회색은 DECLINED, 초록은 OK, 빨강은 오류, 점선 칸은 불리지 않은 함수입니다.

헷갈리기 쉬운 곳: 앞뒤 이름을 틀리면 조용히 무시됩니다

예제를 만들며 확인한 함정입니다. gamma 의 "앞에 와야 할 것"을 "mod_alpha.c" 대신 LoadModule 에 쓰는 이름인 "alpha_module" 로 적어 컴파일하고 같은 검사를 돌렸습니다.

### 8. predecessor written as "alpha_module" instead of "mod_alpha.c"
Sorting fixups: core.c mod_gamma.c mod_beta.c mod_alpha.c

오류도 경고도 없이, 순서 값만으로 정렬된 결과(gamma → beta → alpha)가 나왔습니다. prepare() 는 이름이 맞는 항목을 찾으면 관계를 만들고, 못 찾으면 그냥 넘어갑니다(APR-util hooks/apr_hooks.c 99–111행). 이 성질은 일부러 쓸모 있게 쓰이기도 합니다. mod_alias 가 mod_userdir.c 를 적어 두어도 mod_userdir 을 싣지 않은 서버에서 아무 문제가 없는 것이 그 덕분입니다. 하지만 오타도 똑같이 조용히 무시되므로, 앞뒤 이름은 httpd -l 이나 SHOW_HOOKS 에 나오는 .c 이름으로 적고 정렬 결과를 한 번 눈으로 확인하는 것이 좋습니다.

그 밖에 두 가지를 덧붙입니다.

7. 맛보기: optional function 과 provider

훅은 "여러 모듈이 한 자리에 함수를 거는" 방식입니다. 반대로 한 모듈이 가진 함수를 다른 모듈이 이름으로 찾아 부르고 싶을 때도 있습니다. 그 모듈이 실려 있지 않을 수도 있어서, 보통의 함수 호출로 적으면 링크가 깨집니다. httpd 는 이것을 두 가지로 풉니다.

Optional function 은 함수 하나를 이름으로 등록하고 찾는 장치입니다. mod_so 는 register_hooks() 안에서 APR_REGISTER_OPTIONAL_FN(ap_find_loaded_module_symbol); 한 줄로 자기 함수 하나를 내놓습니다(modules/core/mod_so.c 440–446행). APR_REGISTER_OPTIONAL_FN 은 함수 이름 문자열을 열쇠로 해시 테이블에 함수 포인터를 넣고, 쓰는 쪽은 APR_RETRIEVE_OPTIONAL_FN(이름) 으로 꺼냅니다(APR-util include/apr_optional.h 70–85행). 그 모듈이 없으면 NULL 이 나오므로 쓰는 쪽은 확인만 하면 됩니다. main() 도 ap_signal_server 를 이렇게 찾습니다(server/main.c 785행). 등록이 register_hooks() 안에서 일어난다는 점, 그래서 훅처럼 재시작 때 지워졌다 다시 등록된다는 점(apr_hook_deregister_all() 이 이 해시도 비웁니다, APR-util hooks/apr_hooks.c 266–284행)이 훅과 닮았습니다.

Provider 는 같은 일을 하는 구현 여럿을 "그룹 + 이름 + 판"으로 등록해 두고 고르는 장치입니다. ap_register_provider(pool, 그룹, 이름, 판, 구조체 주소) 로 등록하고 ap_lookup_provider(그룹, 이름, 판) 으로 꺼냅니다(server/provider.c 35–39행, 99–101행). 설정의 AuthBasicProvider file 에서 file 이 바로 이 이름입니다. 인증 모듈들이 이것으로 어떻게 엮이는지는 13편, mod_proxy 의 로드 밸런서가 고르는 방식은 14편에서 봅니다.

두 장치를 나란히 놓은 그림. Optional function 은 함수 하나를 이름으로 찾는 장치다. mod_so 의 register_hooks() 가 APR_REGISTER_OPTIONAL_FN(ap_find_loaded_module_symbol) 로 등록하면, 함수 이름 문자열을 열쇠로 해시 테이블 s_phOptionalFunctions 에 함수 포인터가 들어간다. 쓰는 쪽은 APR_RETRIEVE_OPTIONAL_FN(이름) 으로 꺼내고, 모듈이 없으면 NULL 이 나오므로 확인만 하면 된다. main() 이 ap_signal_server 를 이렇게 찾는다. 재시작 때 apr_hook_deregister_all() 이 이 해시도 비우므로 register_hooks() 에서 다시 등록된다. Provider 는 같은 일을 하는 구현 여럿 중에서 고르는 장치다. ap_register_provider(pool, 그룹, 이름, 판, 구조체 주소) 로 등록하면 그룹, 이름, 판 순서의 세 겹 해시에 구조체가 들어가고, ap_lookup_provider(그룹, 이름, 판) 으로 꺼낸다. AuthBasicProvider file 의 file 이 이 이름이다 해시 이름과 세 겹 구조는 APR-util hooks/apr_hooks.c 378–392행과 server/provider.c 51–71행에서 확인했습니다.

더 깊이 들어가면

이 글은 큰 줄기를 위해 많은 것을 뺐습니다. 이름과 위치만 적어 둡니다.

정리

개념 한 줄 요약
module 구조체 모듈의 전부. 신분증(판·번호·이름·magic) + 설정 함수 넷 + 디렉티브 표 + register_hooks
AP_DECLARE_MODULE(foo) module foo_module 전역 변수를 만든다. LoadModule 첫 인자가 이 변수 이름
module_index 설정 벡터(void * 배열)의 칸 번호. 등록될 때 0 부터 차례로 받는다
ap_add_module() 정적·공유 모듈이 모두 지나는 등록 문. 판 확인 → 번호 → 목록 → 디렉티브 → register_hooks()
mod_so LoadModule 의 주인. .so 를 열고, 이름으로 구조체를 찾고, magic 을 확인하고, ap_add_loaded_module()
AP_DECLARE_HOOK 헤더에서 함수 모양·등록 한 건 구조체·ap_hook_X/ap_run_X 선언을 만든다
AP_IMPLEMENT_HOOK_* .c 에서 static 배열에 넣는 ap_hook_X() 와 배열을 도는 ap_run_X() 의 몸통을 만든다
RUN_FIRST DECLINED 가 아니면 멈춘다. "누가 맡을까" 형 훅(handler, translate_name)
RUN_ALL OK·DECLINED 는 계속, 그 밖의 값이면 멈춘다. "모두 한 번씩" 형 훅(fixups, post_config)
순서 값 REALLY_FIRST(-10) ~ REALLY_LAST(30). qsort() 로 대충 줄 세운다
앞뒤 이름 .c 이름 목록. tsort() 가 지키며, 순서 값보다 세다. 틀린 이름은 조용히 무시된다
optional function / provider 훅의 반대 방향 — 다른 모듈의 함수·구현을 이름으로 찾아 쓴다

다음에 볼 것

모듈이 서버에 끼어드는 두 통로 가운데 훅을 봤습니다. 다른 하나는 1절에서 이름만 보고 넘긴 cmds, 곧 디렉티브입니다. httpd.conf 의 Alias /icons/ "/usr/share/icons/" 한 줄이 어떻게 읽혀 mod_alias 의 함수에 전달되고, <Directory> 와 <VirtualHost> 마다 만들어진 설정 구조체가 어떻게 합쳐지는지 — 이 글의 module_index 와 설정 벡터가 실제로 쓰이는 모습을 7편: 설정 파일과 디렉티브 — httpd.conf 한 줄이 C 구조체가 되기까지에서 봅니다.

Spring 은 같은 문제를 이렇게 풀었습니다. "만드는 흐름에 바깥 코드를 끼워 넣는 자리"라는 생각은 Spring 6편의 BeanPostProcessor 와 같습니다. 등록된 것을 모두 차례로 부르는 방식은 RUN_ALL 과, Ordered 의 숫자가 작을수록 먼저인 규칙은 nOrder 와 닮았습니다. 다른 점은 둘입니다. Spring 은 인터페이스를 구현한 객체를 목록에 넣고, httpd 는 typedef 로 모양을 정한 함수 포인터를 훅마다 따로 둔 배열에 넣습니다. 그리고 httpd 는 숫자 말고도 "mod_alias.c 뒤에"처럼 상대의 이름으로 순서를 적을 수 있습니다. RUN_FIRST 처럼 "처음으로 답하는 하나"를 고르는 방식은 Spring 에서는 21편의 DispatcherServlet 이 HandlerMapping 목록에서 null 이 아닌 첫 답을 쓰는 모습과 닮았습니다.

다른 글