仿照PHP的实现简单的扩展动态加载

PHP通过扩展机制,可以方便的实现对PHP的动态扩展,在PHP扩展加载过程中简单的分析了PHP扩展的加载过程,下面可以继续根据PHP的相关实现,实现一个自己的简单的支持扩展的程序。

在PHP中,有个非常重要的数据结构zend_module_entry,可以先看一下它的定义:

typedef struct _zend_module_entry zend_module_entry;

struct _zend_module_entry {
    unsigned short size;
    unsigned int zend_api;
    unsigned char zend_debug;
    unsigned char zts;
    const struct _zend_ini_entry *ini_entry;
    const struct _zend_module_dep *deps;
    const char *name;
    const struct _zend_function_entry *functions;
    int (*module_startup_func)(INIT_FUNC_ARGS);
    int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
    int (*request_startup_func)(INIT_FUNC_ARGS);
    int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
    void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
    const char *version;
    size_t globals_size;
#ifdef ZTS
    ts_rsrc_id* globals_id_ptr;
#else
    void* globals_ptr;
#endif
    void (*globals_ctor)(void *global TSRMLS_DC);
    void (*globals_dtor)(void *global TSRMLS_DC);
    int (*post_deactivate_func)(void);
    int module_started;
    unsigned char type;
    void *handle;
    int module_number;
    char *build_id;
};

比较复杂,其中最重要的,也是真正编写扩展中会接触到的,主要包括name,functions,module_startup_func,module_shutdown_func,request_startup_func,request_shutdown_func这几个成员,其中name标明了扩展的名字,functions表明了该扩展拥有的函数,以及4个startup/shutdown相关函数。

在编写扩展时,需要指定一个zend_module_entry,如下:

    zend_module_entry counter_module_entry = {
        STANDARD_MODULE_HEADER,
        "counter",
        counter_functions,
        PHP_MINIT(counter),
        PHP_MSHUTDOWN(counter),
        PHP_RINIT(counter),        /* Replace with NULL if there's nothing to do at request start */
        PHP_RSHUTDOWN(counter),    /* Replace with NULL if there's nothing to do at request end */
        PHP_MINFO(counter),
        "0.1", /* Replace with version number for your extension */
        STANDARD_MODULE_PROPERTIES
};

可以看到每个扩展都会有相关的一个zend_module_entry,并在这个entry里指明扩展的相关信息。

在zend_module_entry中,functions是一个_zend_function_entry类型的指针,这是一个函数类型,看看这个类型的定义:

typedef struct _zend_function_entry {
    const char *fname;
    void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
    const struct _zend_arg_info *arg_info;
    zend_uint num_args;
    zend_uint flags;
} zend_function_entry;

其中fname是函数的名字,handler是指向函数的指针,需要注意的是,INTERNAL_FUNCTION_PARAMETERS是一个宏,展开后是int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC,需要说明的是,所有的PHP扩展函数的参数都是这些,而实现的函数的参数,是通过zend_parse_parameters函数获取的。

有了这两个结构体,我们就可以”山寨”一个自己的动态扩展机制了。
首先,需要定义一个函数结构体,就叫function_entry吧。

typedef struct _function_entry {
    const char *fname;
    void (*handler)(void);
} function_entry;

这个function_entry比较简单,只有一个名字还有一个函数指针,为了简单起见,暂时所有的扩展函数都是没有参数,同时也没有返回值的吧。

有了函数结构体,就可以定义一下标志扩展的结构体了,就叫ext_entry吧。

typedef struct _ext_entry {
    const char *name;
    const int func_nums;
    const struct _function_entry *functions;
} ext_entry;

同样的非常简单,只有扩展名,扩展所拥有的函数个数,已经保存扩展函数的一个数组指针。

接下来便是实现扩展加载,以及扩展调用的部分了:

#include <dlfcn.h>
#include <string.h>
#include "ext.h"

#define MAX_EXT_NUM 10

ext_entry* exts[MAX_EXT_NUM] = {0};

int ext_num = 0;


int load_ext(const char *path)
{
    if (ext_num == MAX_EXT_NUM) {
        return -1;
    }

    void *handle;
    ext_entry* entry;
    ext_entry* (*get_ext_entry)(void);
    
    handle = dlopen(path, RTLD_LAZY | RTLD_GLOBAL);
    if (!handle) {
        return -2;
    }
    
    get_ext_entry = (ext_entry *(*)(void)) dlsym(handle, "get_ext_entry");
    if (!get_ext_entry) {
        dlclose(handle);
        return -3;
    }

    entry = get_ext_entry();
    exts[ext_num++] = entry;

    return ext_num;
}


int call_ext_func(const char *extname, const char *func)
{
    for (int i=0; i<ext_num; i++)
    {
        if (strcmp(extname, exts[i]->name) == 0) {
            for (int j=0; j<exts[i]->func_nums; j++) {
                if (strcmp(func, exts[i]->functions[j].fname) == 0) {
                    exts[i]->functions[j].handler();
                    return 0;
                }
            }
        }
    }
    return -1;
}

一共只有两个函数,load_ext以及call_ext_func,分别用来加载一个扩展,以及调用某个扩展的某个函数。需要说明的是,在扩展中,必须实现get_ext_entry函数,这个函数返回一个ext_entry的指针,load_ext函数根据扩展返回的ext_entry指针进行加载。

再看一个示例的扩展

#include <stdio.h>
#include "ext.h"

void print()
{
    printf("print in myext");
}

function_entry functions[1] = { {"print", print} };

ext_entry myext_entry = {
    "myext",
    1,
    functions
};

ext_entry* get_ext_entry()
{
    return &myext_entry;
}

以及main

#include <stdio.h>
#include "ext.h"

int main(int argc, char *argv[])
{
    char path[100] = {0};
    char ext[100] = {0};
    char func[100] = {0};
    scanf("%s", path);
    printf("%d\n", load_ext(path));
    scanf("%s", ext);
    scanf("%s", func);

    call_ext_func(ext, func);
    return 0;
}

编译的时候,首先编译主程序,再将手动写的扩展编译成so动态库,然后,就可以调用了。

最后调用的结果如下:

$ ./ext_skel                                       
/path/to/myext.so
1
myext
print
print in myext

以上代码在https://github.com/C0reFast/ext_skel