请记住,mysqlnd
插件本身就是一个 PHP 扩展。
以下代码展示了典型 mysqlnd
插件中使用的 MINIT 函数的基本结构
/* my_php_mysqlnd_plugin.c */ static PHP_MINIT_FUNCTION(mysqlnd_plugin) { /* globals, ini entries, resources, classes */ /* register mysqlnd plugin */ mysqlnd_plugin_id = mysqlnd_plugin_register(); conn_m = mysqlnd_get_conn_methods(); memcpy(org_conn_m, conn_m, sizeof(struct st_mysqlnd_conn_methods)); conn_m->query = MYSQLND_METHOD(mysqlnd_plugin_conn, query); conn_m->connect = MYSQLND_METHOD(mysqlnd_plugin_conn, connect); }
/* my_mysqlnd_plugin.c */ enum_func_status MYSQLND_METHOD(mysqlnd_plugin_conn, query)(/* ... */) { /* ... */ } enum_func_status MYSQLND_METHOD(mysqlnd_plugin_conn, connect)(/* ... */) { /* ... */ }
任务分析:从 C 到用户空间
class proxy extends mysqlnd_plugin_connection { public function connect($host, ...) { .. } } mysqlnd_plugin_set_conn_proxy(new proxy());
流程
PHP:用户注册插件回调
PHP:用户调用任何 PHP MySQL API 连接到 MySQL
C:ext/*mysql* 调用 mysqlnd 方法
C:mysqlnd 最终进入 ext/mysqlnd_plugin
C:ext/mysqlnd_plugin
调用用户空间回调
或原始 mysqlnd
方法,如果用户空间回调未设置
您需要执行以下操作
用 C 编写一个类“mysqlnd_plugin_connection”
通过“mysqlnd_plugin_set_conn_proxy()”接受和注册代理对象
从 C 调用用户空间代理方法(优化 - zend_interfaces.h)
用户空间对象方法可以使用 call_user_function()
调用,也可以在更接近 Zend 引擎的级别上使用 zend_call_method()
调用。
优化:使用 zend_call_method 从 C 调用方法
以下代码片段展示了 zend_call_method
函数的原型,摘自 zend_interfaces.h。
ZEND_API zval* zend_call_method( zval **object_pp, zend_class_entry *obj_ce, zend_function **fn_proxy, char *function_name, int function_name_len, zval **retval_ptr_ptr, int param_count, zval* arg1, zval* arg2 TSRMLS_DC );
Zend API 仅支持两个参数。您可能需要更多参数,例如
enum_func_status (*func_mysqlnd_conn__connect)( MYSQLND *conn, const char *host, const char * user, const char * passwd, unsigned int passwd_len, const char * db, unsigned int db_len, unsigned int port, const char * socket, unsigned int mysql_flags TSRMLS_DC );
为了解决这个问题,您需要复制 zend_call_method()
并添加一个用于附加参数的功能。您可以通过创建一组 MY_ZEND_CALL_METHOD_WRAPPER
宏来实现。
调用 PHP 用户空间
此代码片段展示了从 C 调用用户空间函数的优化方法
/* my_mysqlnd_plugin.c */ MYSQLND_METHOD(my_conn_class,connect)( MYSQLND *conn, const char *host /* ... */ TSRMLS_DC) { enum_func_status ret = FAIL; zval * global_user_conn_proxy = fetch_userspace_proxy(); if (global_user_conn_proxy) { /* call userspace proxy */ ret = MY_ZEND_CALL_METHOD_WRAPPER(global_user_conn_proxy, host, /*...*/); } else { /* or original mysqlnd method = do nothing, be transparent */ ret = org_methods.connect(conn, host, user, passwd, passwd_len, db, db_len, port, socket, mysql_flags TSRMLS_CC); } return ret; }
调用用户空间:简单参数
/* my_mysqlnd_plugin.c */ MYSQLND_METHOD(my_conn_class,connect)( /* ... */, const char *host, /* ...*/) { /* ... */ if (global_user_conn_proxy) { /* ... */ zval* zv_host; MAKE_STD_ZVAL(zv_host); ZVAL_STRING(zv_host, host, 1); MY_ZEND_CALL_METHOD_WRAPPER(global_user_conn_proxy, zv_retval, zv_host /*, ...*/); zval_ptr_dtor(&zv_host); /* ... */ } /* ... */ }
调用用户空间:结构体作为参数
/* my_mysqlnd_plugin.c */ MYSQLND_METHOD(my_conn_class, connect)( MYSQLND *conn, /* ...*/) { /* ... */ if (global_user_conn_proxy) { /* ... */ zval* zv_conn; ZEND_REGISTER_RESOURCE(zv_conn, (void *)conn, le_mysqlnd_plugin_conn); MY_ZEND_CALL_METHOD_WRAPPER(global_user_conn_proxy, zv_retval, zv_conn, zv_host /*, ...*/); zval_ptr_dtor(&zv_conn); /* ... */ } /* ... */ }
许多 mysqlnd
方法的第一个参数是 C “对象”。例如,connect() 方法的第一个参数是指向 MYSQLND
的指针。结构体 MYSQLND 代表一个 mysqlnd
连接对象。
mysqlnd
连接对象指针可以与标准 I/O 文件句柄进行比较。与标准 I/O 文件句柄类似,mysqlnd
连接对象应使用 PHP 资源变量类型链接到用户空间。
从 C 到用户空间再返回
class proxy extends mysqlnd_plugin_connection { public function connect($conn, $host, ...) { /* "pre" hook */ printf("Connecting to host = '%s'\n", $host); debug_print_backtrace(); return parent::connect($conn); } public function query($conn, $query) { /* "post" hook */ $ret = parent::query($conn, $query); printf("Query = '%s'\n", $query); return $ret; } } mysqlnd_plugin_set_conn_proxy(new proxy());
PHP 用户必须能够调用覆盖方法的父实现。
由于子类化,可以仅细化选定的方法,并且可以选择使用“预”或“后”钩子。
内置类:mysqlnd_plugin_connection::connect()
/* my_mysqlnd_plugin_classes.c */ PHP_METHOD("mysqlnd_plugin_connection", connect) { /* ... simplified! ... */ zval* mysqlnd_rsrc; MYSQLND* conn; char* host; int host_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs", &mysqlnd_rsrc, &host, &host_len) == FAILURE) { RETURN_NULL(); } ZEND_FETCH_RESOURCE(conn, MYSQLND* conn, &mysqlnd_rsrc, -1, "Mysqlnd Connection", le_mysqlnd_plugin_conn); if (PASS == org_methods.connect(conn, host, /* simplified! */ TSRMLS_CC)) RETVAL_TRUE; else RETVAL_FALSE; }