BizStationブログ

ビズステーション株式会社の公式ブログです。

PHP7 ExtensionをSWIGで作る (Building a PHP7 Extension with SWIG)

Transactd 3.0をリリースしました。また、PHPクライアントはPHP7に対応しました。(Transactd 3.0についてはまた改めて書きたいと思います)

TransactdのPHPクライアントは、SWIGで生成したラッパーコードをベースに構築されています。
PHP7では、基本的な変数の型や関数の引数型などに変更があり、PHP5系で使用していたコードのままではExtensionがコンパイルできません。SWIGでPHP7のExtensionが作成できればいいのですが、まだPHP7には対応していません。また、Transactdではラッパーをベースに関数のオーバーロードなどを変更しているため、現状のコードをPHP7に対応させる必要がありました。今回はその内容を備忘録的に書いておきたいと思います。

以降は、OneソースでPHP5とPHP7のどちらでもコンパイル・正常動作させるためのSWIGコードの変更手順です。TransactdのPHPクライアントに限らず、SWIGで生成されたPHP Extension共通で使用できると思います。

コードの変更の技術的な内容は、下記のPHPのマニュアルを基に行っています。 
Upgrading PHP extensions from PHP5 to NG

変更の概要

OneソースでPHP5とPHP7の両方で動作するようにするため、ZEND_ENGINEマクロのバージョンを調べて関数や型をマクロで切り替えます。次に、ラッパーコードをこのマクロを使用するように置換します。最後に組み込みの5つの関数を手直しします。まとめると以下のようになります。

  1. 切替マクロの挿入
  2. ラッパーコードをマクロで置換する
  3. SWIG_ZTS_SetPointerZval, SWIG_ZTS_ConvertResourcePtr, SWIG_ZTS_ConvertPtr, SWIG_Php_GetModule, SWIG_Php_SetModuleを置換する

これでPHP5とPHP7に対応したラッパーコードが完成します。

切替マクロの挿入

切替マクロは2か所に挿入します。それぞれ、SWIG_LONG_CONSTANTstatic ZEND_RSRC_DTOR_FUNCが出現する位置に、元のコードを削除して挿入します。

マクロ1

#ifdef ZEND_ENGINE_3
#define SIZE_OFFSET 1
#else
#define SIZE_OFFSET 0
#endif

#define SWIG_LONG_CONSTANT(N, V) zend_register_long_constant((char*)#N, sizeof(#N) - SIZE_OFFSET, V, CONST_CS | CONST_PERSISTENT, module_number TSRMLS_CC)
#define SWIG_DOUBLE_CONSTANT(N, V) zend_register_double_constant((char*)#N, sizeof(#N) - SIZE_OFFSET, V, CONST_CS | CONST_PERSISTENT, module_number TSRMLS_CC)
#define SWIG_STRING_CONSTANT(N, V) zend_register_stringl_constant((char*)#N, sizeof(#N) - SIZE_OFFSET, (char*)(V), strlen(V), CONST_CS | CONST_PERSISTENT, module_number TSRMLS_CC)
#define SWIG_CHAR_CONSTANT(N, V) do {\
    static char swig_char = (V);\
    zend_register_stringl_constant((char*)#N, sizeof(#N) - SIZE_OFFSET, &swig_char, 1, CONST_CS | CONST_PERSISTENT, module_number TSRMLS_CC);\
} while (0)

マクロ2

#ifdef ZEND_ENGINE_3
static ZEND_RSRC_DTOR_FUNC(SWIG_landfill) { zend_resource *rsrc = res; (void)rsrc; }

//Replace 
typedef zval zval_args_type;
#define ZVAL_ARGS &args
#define ZVAL_ARGS_ARRAY args
#define ZVAL_ARGV_ARRAY argv
#define CONV_to_double_ex(zv) 
#define CONV_to_long_ex(zv)
#define CONV_to_string_ex(zv)  convert_to_string(&zv)
#define CONV_to_boolean_ex(zv) ZVAL_LONG(&zv, (Z_TYPE(zv) == IS_TRUE) ? 1 : 0);
#define Z_TYPE_AGRS(N) Z_TYPE_P(&args[N])
#define Z_TYPE_ARGV(Z) Z_TYPE_P(&Z)
#define ZVAL_ARGS_P(N) (&argv[N])
#define ARGV_IS_BOOL(N) (Z_TYPE_ARGV(argv[N]) == IS_TRUE || Z_TYPE_ARGV(argv[N]) == IS_FALSE)
#define RSRC res
#define LIST_FIND(ZVAL) value = (swig_object_wrapper *)Z_RES_P(ZVAL)->ptr;
#define ARGS_IS_TRUE(N) zval_is_true(&args[N])

//Append compatible type and define
typedef zend_resource zend_rsrc_list_entry;
#define Z_DVAL_PP(zv) zv.value.dval
#define Z_LVAL_PP(zv) zv.value.lval
#define Z_STRVAL_PP(zv) Z_STRVAL_P(&zv)

#undef  ZVAL_STRING
#define ZVAL_STRING(z, s, dummy)                                 \
    do {                                                         \
    const char *_s = (s);                                        \
        ZVAL_NEW_STR(z, zend_string_init(_s, strlen(_s), 0));    \
  } while (0)

#undef ZVAL_STRINGL
#define ZVAL_STRINGL(z, s, l, dummy)                             \
    do {                                                         \
    const char *_s = (s);                                        \
      ZVAL_NEW_STR(z, zend_string_init(_s, l, 0));               \
  } while (0)


#else

static ZEND_RSRC_DTOR_FUNC(SWIG_landfill) { (void)rsrc; }
typedef long zend_long;
typedef zval** zval_args_type;
#define ZVAL_ARGS *args
#define ZVAL_ARGS_ARRAY args
#define ZVAL_ARGV_ARRAY argv
#define CONV_to_double_ex(zv) convert_to_double_ex(zv)
#define CONV_to_long_ex(zv) convert_to_long_ex(zv)
#define CONV_to_string_ex(zv) convert_to_string_ex(zv)
#define CONV_to_boolean_ex(zv) convert_to_boolean_ex(zv)
#define Z_TYPE_AGRS(N) (ZVAL_ARGS[N])->type
#define Z_TYPE_ARGV(Z) Z_TYPE_PP((Z))
#define ZVAL_ARGS_P(N) (*(argv[N]))
#define ARGV_IS_BOOL(N) (Z_TYPE_ARGV(argv[N]) == IS_BOOL)
#define RSRC rsrc
#define LIST_FIND(ZVAL) (swig_object_wrapper *)zend_list_find((*ZVAL[0])->value.lval, &type)
#define ARGS_IS_TRUE(N) zval_is_true(*args[N])

#endif

ラッパーコードをマクロで置換する

次に、ラッパーコードを以下のパターンですべて置換します。左側のパターンのコードをの右側のコードにします。nパラメータの実際は整数の値です。_xxxxはクラス名で、残りは実際のラッパーコードの記述そのものです。

zval **args[n]                               --> zval_args_type args[n]
zend_get_parameters_array_ex(n, args)         --> zend_get_parameters_array_ex(n, ZVAL_ARGS_ARRAY)
zend_get_parameters_array_ex(argc,argv)       --> zend_get_parameters_array_ex(argc, ZVAL_ARGV_ARRAY)
SWIG_ConvertPtr(*args[n]                    --> SWIG_ConvertPtr(ZVAL_ARGS[n]
(*args[n])->type                              --> Z_TYPE_AGRS(n)
convert_to_string_ex                          --> CONV_to_string_ex
convert_to_long_ex                            --> CONV_to_long_ex
convert_to_double_ex                          --> CONV_to_double_ex 
convert_to_boolean_ex                         --> CONV_to_boolean_ex
(Z_TYPE_PP(argv[n]) == IS_BOOL)               --> ARGV_IS_BOOL(n)
zend_list_find((*args[n])->value.lval, &type) --> LIST_FIND(args)
__wrap_delete_xxxx(rsrc,                      --> __wrap_delete_xxxx(RSRC,
efree(rsrc->ptr)                              --> efree(RSRC->ptr)
zval_is_true(*args[n])                        --> ARGS_IS_TRUE(n) 

この置換作業は、今後C++のインタフェースに変更があって関数の追加や変更をする際、SWIGで生成したものを使用する場合にも必要です。

組み込み関数の変更

最後に5つのSWIGの組み込み関数を置換します。

SWIG_ZTS_SetPointerZval

static void
SWIG_ZTS_SetPointerZval(zval *z, void *ptr, swig_type_info *type, int newobject TSRMLS_DC) {
  /*
   * First test for Null pointers.  Return those as PHP native NULL
   */
  if (!ptr) {
    ZVAL_NULL(z);
    return;
  }
  if (type->clientdata) {
    swig_object_wrapper *value;
    if (!(*(int *)(type->clientdata)))
      zend_error(E_ERROR, "Type: %s failed to register with zend", type->name);
    value = (swig_object_wrapper *)emalloc(sizeof(swig_object_wrapper));
    value->ptr = ptr;
    value->newobject = (newobject & 1);
    if ((newobject & 2) == 0) {
      /* Just register the pointer as a resource. */
      #ifdef ZEND_ENGINE_3
      ZVAL_RES(z, zend_register_resource(value, *(int *)(type->clientdata)));
      #else
      ZEND_REGISTER_RESOURCE(z, value, *(int *)(type->clientdata));
      #endif
    } else {
      /*
       * Wrap the resource in an object, the resource will be accessible
       * via the "_cPtr" member. This is currently only used by
       * directorin typemaps.
       */
      #ifdef ZEND_ENGINE_3
      zval resource;
      zend_class_entry *ce = NULL;
      #else
      zval *resource;
      zend_class_entry **ce = NULL;
      int result;
      #endif
      const char *type_name = type->name + 3; /* +3 so: _p_Foo -> Foo */
      size_t type_name_len;
      const char * p;
      /* Namespace__Foo -> Foo */
      /* FIXME: ugly and goes wrong for classes with __ in their names. */
      while ((p = strstr(type_name, "__")) != NULL) {
        type_name = p + 2;
      }
      type_name_len = strlen(type_name);
      #ifdef ZEND_ENGINE_3
      zend_string* tn = zend_string_init(type_name, type_name_len, 0);
      ZVAL_RES(&resource, zend_register_resource(value, *(int *)(type->clientdata)));
      #else
      MAKE_STD_ZVAL(resource);
      ZEND_REGISTER_RESOURCE(resource, value, *(int *)(type->clientdata));
      #endif
      if (SWIG_PREFIX_LEN > 0) {
        char * classname = (char*)emalloc(SWIG_PREFIX_LEN + type_name_len + 1);
        strcpy(classname, SWIG_PREFIX);
        strcpy(classname + SWIG_PREFIX_LEN, type_name);
        #ifdef ZEND_ENGINE_3
        zend_string* zp = zend_string_init(classname, SWIG_PREFIX_LEN + type_name_len, 0);
        ce = zend_lookup_class(zp);
        zend_string_release(zp);
        #else
        result = zend_lookup_class(classname, SWIG_PREFIX_LEN + type_name_len, &ce TSRMLS_CC);
        #endif
        efree(classname);
      } else {
        #ifdef ZEND_ENGINE_3
        ce = zend_lookup_class(tn);
        #else
        result = zend_lookup_class((char *)type_name, type_name_len, &ce TSRMLS_CC);
        #endif
      }
      #ifdef ZEND_ENGINE_3
      if (!ce) {
      #else
      if (result != SUCCESS) {
      #endif
        /* class does not exist */
        object_init(z);
      } else {
      #ifdef ZEND_ENGINE_3
        object_init_ex(z, ce);
      #else
        object_init_ex(z, *ce);
      #endif
      }
      Z_SET_REFCOUNT_P(z, 1);
      #ifdef ZEND_ENGINE_3
      ZVAL_MAKE_REF(z);
      zend_hash_str_update(HASH_OF(z),  "_cPtr", sizeof("_cPtr") - 1, &resource);
      zend_string_release(tn);
      #else
      Z_SET_ISREF_P(z);
      zend_hash_update(HASH_OF(z), (char*)"_cPtr", sizeof("_cPtr"), (void*)&resource, sizeof(zval), NULL);
      #endif
    }
    return;
  }
  zend_error(E_ERROR, "Type: %s not registered with zend",type->name);
}

SWIG_ZTS_ConvertResourcePtr

static void *
SWIG_ZTS_ConvertResourcePtr(zval *z, swig_type_info *ty, int flags TSRMLS_DC) {
  swig_object_wrapper *value;
  void *p;
  int type;
  const char *type_name;

#ifdef ZEND_ENGINE_3
  type = Z_RES_P(z)->type;
  value = (swig_object_wrapper *)Z_RES_P(z)->ptr;
#else
  value = (swig_object_wrapper *) zend_list_find(z->value.lval, &type);
#endif

  if (type==-1) return NULL;
  if (flags & SWIG_POINTER_DISOWN) {
    value->newobject = 0;
  }
  p = value->ptr;
#ifdef ZEND_ENGINE_3
  type_name = zend_rsrc_list_get_rsrc_type(Z_RES_P(z));
#else
  type_name = zend_rsrc_list_get_rsrc_type(z->value.lval TSRMLS_CC);
#endif
  return SWIG_ZTS_ConvertResourceData(p, type_name, ty TSRMLS_CC);
}

SWIG_ZTS_ConvertPtr

static int
SWIG_ZTS_ConvertPtr(zval *z, void **ptr, swig_type_info *ty, int flags TSRMLS_DC) {
  if (z == NULL) {
    *ptr = 0;
    return 0;
  }
  switch (Z_TYPE_P(z)) {
    case IS_OBJECT: {
      //find z->_cPtr
#ifdef ZEND_ENGINE_3
    zval* _cPtr;
    if ((_cPtr = zend_hash_str_find(HASH_OF(z), "_cPtr",  sizeof("_cPtr")- 1)) != NULL) {
      if (Z_TYPE_P(_cPtr) == IS_INDIRECT)
        _cPtr = Z_INDIRECT_P(_cPtr);
      if (Z_TYPE_P(_cPtr)==IS_RESOURCE) {
      *ptr = SWIG_ZTS_ConvertResourcePtr(_cPtr, ty, flags TSRMLS_CC);
      return (*ptr == NULL ? -1 : 0);
      }
      }
#else
      zval **_cPtr;
    if (zend_hash_find(HASH_OF(z), (char*)"_cPtr", sizeof("_cPtr"), (void**)&_cPtr) == SUCCESS) {
      if ((*_cPtr)->type == IS_RESOURCE) {
        *ptr = SWIG_ZTS_ConvertResourcePtr(*_cPtr, ty, flags TSRMLS_CC);
        return (*ptr == NULL ? -1 : 0);
      }
    }
#endif
      break;
    }
    case IS_RESOURCE:
      *ptr = SWIG_ZTS_ConvertResourcePtr(z, ty, flags TSRMLS_CC);
      return (*ptr == NULL ? -1 : 0);
    case IS_NULL:
      *ptr = 0;
      return 0;
  }

  return -1;
}

SWIG_Php_GetModule

static swig_module_info *SWIG_Php_GetModule() {
  zval *pointer;
  swig_module_info *ret = 0;
  TSRMLS_FETCH();

#ifdef ZEND_ENGINE_3
  if (pointer = zend_get_constant_str(const_name, sizeof(const_name) - 1)) {
    if (Z_TYPE_P(pointer) == IS_LONG) {
      ret = (swig_module_info *) pointer->value.lval;
    }
  } 
#else
  MAKE_STD_ZVAL(pointer);
  if (zend_get_constant(const_name, sizeof(const_name) - 1, pointer TSRMLS_CC)) {
    if (pointer->type == IS_LONG) {
      ret = (swig_module_info *)pointer->value.lval;
    }
  }
  FREE_ZVAL(pointer);
#endif

  return ret; 
}

SWIG_Php_SetModule

static void SWIG_Php_SetModule(swig_module_info *pointer) {
  TSRMLS_FETCH();
  REGISTER_MAIN_LONG_CONSTANT(const_name, (zend_long) pointer, CONST_PERSISTENT | CONST_CS);
}

Windowsでのビルドエラー

Windowsの場合、PHP7はVisual Studio 2015でのビルドが必要です。
VS2015でstd::stringを使用すると、stringヘッダーのinline namespace literalsコンパイルエラーが発生することがあります。その時は、#include <string>をコードの最初にインクルードすると解消できます。

最後に

Transactdではこの変更でPHP用のテストすべてにパスしています。
Transactd用のラッパーコードは
transactd/tdclphp_wrap.cpp at master · bizstation/transactd · GitHub
で確認できます。ZEND_ENGINE_3で検索すればPHP7用の修正が列挙できます。(ここに記載したコードはRefactoringしてあるので少し異なりますが基本同じです)

マクロでの切替はコードが読みづらく必ずしもベストではありませんが、PHP5とPHP7はExtension構造が基本的に同じなので、2つのコードを別々にして管理するよりは随分楽かと思います。