扩展的实现原理

无论是 PHP 扩展还是 Zend 扩展,它们实现的基本原理都是开发者按照扩展规范和 API,实现自己的功能,然后要么以静态编译方式编译到 PHP 的可执行文件,要么以动态编译方式生成动态链接库 .so 文件。加载扩展时,PHP 将动态链接库文件加载到内存,校验其符合规范后,PHP 即可以使用此扩展。

那么这里提到的库是什么呢?库可以看作可复用代码的二进制形式。在计算机世界中,每个程序都要依赖很多基础的底层库。库分为两种:静态链接库和动态链接库。在编译生成可执行程序时,一起被打包到可执行文件的库称为静态链接库,Linux 下一般以 .a 为扩展名(Windows 下为 .lib);而生成可执行文件时并未被打包,在运行时才被载入的库,称为动态链接库,Linux 下一般以 .so 为扩展名(Windows 下为 .dll)。动态链接库使用起来比静态链接库稍微麻烦,但有着非常明显的优势。

  1. 相对于静态链接库,使用动态链接库可以有效地缩小程序体积,节省空间,在同一个运行环境下,不同的程序可以调用相同的库。

  2. 程序更新时,使用了静态链接库的程序需要重新编译整个程序,用户也需要重新下载安装完整的程序,而使用了动态链接库的程序可以只更新库,实现增量更新。

  3. 有助于节省内存。当我们需要某个扩展时,才将其加载到内存中。

  4. 有助于资源共享。这里讲的资源共享,是指在多个进程中实现共享。

下面我们来实现一个简单的动态链接库 libhelloworld.so 并调用:

#include <stdio.h>
void helloworld()
{
    printf("hello, world! \n");
}

代码很简单,只有一个函数——helloworld,调用这个函数,会输出字符串“hello,world!”。我们将其编译生成动态链接库:

$ gcc -shared -fPIC  -o libhelloworld.so helloworld.c
$ ll
-rw-r--r--1 vagrant vagrant   71 1月  25 14:15 helloworld.c
-rwxr-xr-x 1 vagrant vagrant 7.9K 1月  25 14:30 libhelloworld.so

现在我们显式加载此动态链接库:

#include <stdio.h>
#include <dlfcn.h>

int main(int argc, char* argv[])
{
    void* handle = dlopen("./libhelloworld.so", RTLD_LAZY);
    char* error = dlerror();
    if(! handle || error)
    {
        printf("load so error! \n");
        return 1;
    }
    void (*func)() = dlsym(handle, "helloworld");
    if(! func)
    {
        printf("load func error! \n");
        dlclose(handle);
        return 1;
    }
    func();
    dlclose(handle);
    return 0;
}

编译、链接此代码生成可执行程序:

$ gcc test.c -L. -lhelloworld -ldl
$ ll
-rwxr-xr-x 1 vagrant vagrant 8.6K 1月  25 14:34 a.out
-rw-r--r--1 vagrant vagrant   71 1月  25 14:15 helloworld.c
-rwxr-xr-x 1 vagrant vagrant 7.9K 1月  25 14:30 libhelloworld.so
-rw-r--r--1 vagrant vagrant  459 1月  25 14:34 test.c
$ ./a.out
Hello, world!

可以看到已成功生成可执行文件 a.out,运行后输出字符串 “hello, world!”。由此可见,动态链接库 libhelloworld.so 中的函数执行成功。

仔细研究下这段代码,会发现它一共使用了 4 个函数来加载动态链接库并执行其中的函数,它们分别是 dlopendlerrordlsymdlclose,分别用来加载动态链接库、获得相关错误信息、获得函数地址、关闭动态链接库。

PHP 的扩展实现原理和这段代码极其相似,也是用这 4 个函数完成了扩展的加载和函数的调用。当在我们在 PHP 程序中动态通过 dl 函数或通过 php.ini 来动态加载扩展时,PHP 会从 extension_dir 配置项指定的目录加载扩展,这个目录默认为 <install-dir>/lib/php/extensions/<debug-or-not>-<zts-or-not>-ZEND_MODULE_API_NO,如 /usr/local/php/lib/php/extensions/debug-non-zts-20160303/usr/local/php/lib/php/extensions/no-debug-zts-20160303