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つの関数を手直しします。まとめると以下のようになります。
- 切替マクロの挿入
- ラッパーコードをマクロで置換する
SWIG_ZTS_SetPointerZval
,SWIG_ZTS_ConvertResourcePtr
,SWIG_ZTS_ConvertPtr
,SWIG_Php_GetModule
,SWIG_Php_SetModule
を置換する
これでPHP5とPHP7に対応したラッパーコードが完成します。
切替マクロの挿入
切替マクロは2か所に挿入します。それぞれ、SWIG_LONG_CONSTANT
とstatic 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; }
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つのコードを別々にして管理するよりは随分楽かと思います。