mysqlnd 플러그인 구축 시작하기

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

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

다음을 수행해야 합니다.

  1. C에서 "mysqlnd_plugin_connection" 클래스 작성
  2. "mysqlnd_plugin_set_conn_proxy()"를 통해 프록시 객체 수락 및 등록
  3. C에서 사용자 공간 프록시 메소드 호출(최적화 - zend_interfaces.h)

사용자 공간 개체 메서드는 call_user_function()을 사용하여 호출하거나 Zend 엔진에 더 가까운 수준에서 작동하고 zend_call_method()를 사용할 수 있습니다.

Optimization: calling methods from C using zend_call_method

다음 코드 스니펫은 zend_interfaces.h에서 가져온 zend_call_method 함수의 프로토타입을 보여줍니다.

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 매크로 세트를 만들어 이를 수행할 수 있습니다.

Calling PHP userspace

이 코드 조각은 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);
    /* ... */
  }
  /* ... */
}
                

많은 mysqlnd 메소드의 첫 번째 인수는 C "객체"입니다. 예를 들어, connect() 메소드의 첫 번째 인수는 MYSQLND에 대한 포인터입니다. struct MYSQLND는 mysqlnd 연결 객체를 나타냅니다.

mysqlnd 연결 객체 포인터는 표준 I/O 파일 핸들과 비교할 수 있습니다. 표준 I/O 파일 핸들처럼 mysqlnd 연결 객체는 PHP 리소스 변수 유형을 사용하여 사용자 공간에 연결되어야 합니다.

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 사용자는 덮어쓴 메서드의 부모 구현을 호출할 수 있어야 합니다.

서브클래싱의 결과로 선택한 메서드만 개선할 수 있으며 "사전" 또는 "사후" 후크를 선택할 수 있습니다.

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