Getting started building a mysqlnd plugin


It is important to remember that a mysqlnd plugin is itself a PHP extension.

The following code shows the basic structure of the MINIT function that will be used in the typical mysqlnd plugin:

 /* 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)(/* ... */) {   /* ... */ } 

Task analysis: from C to userspace

  class proxy extends mysqlnd_plugin_connection {   public function connect($host, ...) { .. } } mysqlnd_plugin_set_conn_proxy(new proxy()); 

Process:

  1. PHP: user registers plugin callback

  2. PHP: user calls any PHP MySQL API to connect to MySQL

  3. C: ext/*mysql* calls mysqlnd method

  4. C: mysqlnd ends up in ext/mysqlnd_plugin

  5. C: ext/mysqlnd_plugin

    1. Calls userspace callback

    2. Or original mysqlnd method, if userspace callback not set

You need to carry out the following:

  1. Write a class "mysqlnd_plugin_connection" in C

  2. Accept and register proxy object through "mysqlnd_plugin_set_conn_proxy()"

  3. Call userspace proxy methods from C (optimization - zend_interfaces.h)

Userspace object methods can either be called using call_user_function() or you can operate at a level closer to the Zend Engine and use zend_call_method().

Optimization: calling methods from C using zend_call_method

The following code snippet shows the prototype for the zend_call_method function, taken from 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 supports only two arguments. You may need more, for example:

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

To get around this problem you will need to make a copy of zend_call_method() and add a facility for additional parameters. You can do this by creating a set of MY_ZEND_CALL_METHOD_WRAPPER macros.

Calling PHP userspace

This code snippet shows the optimized method for calling a userspace function from 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; } 

Calling userspace: simple arguments

 /* 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);     /* ... */   }   /* ... */ } 

Calling userspace: structs as arguments

 /* 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);     /* ... */   }   /* ... */ } 

The first argument of many mysqlnd methods is a C "object". For example, the first argument of the connect() method is a pointer to MYSQLND. The struct MYSQLND represents a mysqlnd connection object.

The mysqlnd connection object pointer can be compared to a standard I/O file handle. Like a standard I/O file handle a mysqlnd connection object shall be linked to the userspace using the PHP resource variable type.

From C to userspace and back

  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 users must be able to call the parent implementation of an overwritten method.

As a result of subclassing it is possible to refine only selected methods and you can choose to have "pre" or "post" hooks.

Buildin class: 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; }