読者です 読者をやめる 読者になる 読者になる

PHPに演算子オーバーロードを実装する

PHP演算子オーバーロードを実装してみた。やってみたら思いの外サクッと実装できた。

例えば以下の様なコードが実行できる。オブジェクトが"__add", "__sub", "__mul", "__div" というメソッドを持っていたらそれに対応する演算子オーバーロードされる。

<?php

class Hoge
{
    function __construct($value)
    {
        $this->value = $value;
    }

    function __add(Hoge $right)
    {
        return new Hoge($this->value + $right->value);
    }

    function __sub(Hoge $right)
    {
        return new Hoge($this->value - $right->value);
    }
}

$hoge = new Hoge(1) + new Hoge(2) - new Hoge(5);

echo $hoge->value; // => -2 

以下、ふと思いついてcloneした時のphp-srcリポジトリ対するdiffを一部引用。全部紹介すると長いので、足し算のオーバーロード部分のみ。追加8行。

diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c
index 93dca94..1e8358c 100644
--- a/Zend/zend_operators.c
+++ b/Zend/zend_operators.c
@@ -23,6 +23,7 @@
 
 #include "zend.h"
 #include "zend_operators.h"
+#include "zend_interfaces.h"
 #include "zend_variables.h"
 #include "zend_globals.h"
 #include "zend_list.h"
@@ -794,6 +795,13 @@ ZEND_API int add_function(zval *result, zval *op1, zval *op2 TSRMLS_DC) /* {{{ *
 	zval op1_copy, op2_copy;
 	int converted = 0;
 
+    if (Z_TYPE_P(op1) == IS_OBJECT && zend_hash_exists(&Z_OBJCE_P(op1)->function_table, "__add", strlen("__add") + 1)) {
+        zval* tmp;
+        zend_call_method_with_1_params(&op1, NULL, NULL, "__add", &tmp, op2);
+        ZVAL_ZVAL(result, tmp, 0, 1);
+        return SUCCESS;
+    }
+
 	while (1) {
 		switch (TYPE_PAIR(Z_TYPE_P(op1), Z_TYPE_P(op2))) {
 			case TYPE_PAIR(IS_LONG, IS_LONG): {

道のり

これだけだと面白く無いと思うので、どんな感じで作業したか書いておく。

まずphp-srcをgithubから取ってくる。

$ git clone https://github.com/php/php-src.git
$ cd php-src

buildconfでconfigureファイルを生成してデバッグシンボル付きでビルドする。

$ ./buildconf
$ ./configure --disable-all --enable-debug
$ make

無事にビルドできたらコマンドラインインターフェイスを叩いてみる。

$ sapi/cli/php -v 
PHP 5.5.0-dev (cli) (built: Jul 16 2012 14:26:51) (DEBUG)
Copyright (c) 1997-2012 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2012 Zend Technologies

前準備は済んだ。まず最初に足し算の処理をしている部分を探す。

PHPのコードは、最終的にはZendEngine用のコードに変換されて実行される。足し算もZEND_ADDという命令にコンパイルされる。ZendEngineの中でも実際に処理をする部分が定義してあるのがZend/zend_vm_def.hにある。以下は、その中でもZEND_ADD内の処理を定義したコード。

ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV)
{
    USE_OPLINE
    zend_free_op free_op1, free_op2;

    SAVE_OPLINE();
    fast_add_function(&EX_T(opline->result.var).tmp_var,
        GET_OP1_ZVAL_PTR(BP_VAR_R),
        GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC);
    FREE_OP1();
    FREE_OP2();
    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}

ここを見ると、実際の足し算の処理はfast_add_functionという関数内で実行されていることがわかる。globalで定義元を探すと、Zend/zend_operator.hにあるようだ。

static zend_always_inline int fast_add_function(zval *result, zval *op1, zval *op2 TSRMLS_DC)
{
	if (EXPECTED(Z_TYPE_P(op1) == IS_LONG)) {
		if (EXPECTED(Z_TYPE_P(op2) == IS_LONG)) {
#if defined(__GNUC__) && defined(__i386__)
		__asm__(
			"movl	(%1), %%eax\n\t"
			"addl   (%2), %%eax\n\t"
			"jo     0f\n\t"     
			"movl   %%eax, (%0)\n\t"
			"movb   $0x1,0xc(%0)\n\t"
			"jmp    1f\n"
			"0:\n\t"
			"fildl	(%1)\n\t"
(略)
	}
	return add_function(result, op1, op2 TSRMLS_CC);

中を見ると、long型同士の足し算はインラインアセンプラで書かれていた。fast_add_functionっていう名前から推測するとインラインアセンプラで高速化しているんだろう。よくわからないので飛ばす。よく見ると、インラインアセンブラで高速化できない足し算はadd_function関数内で処理するようだ。

Zend/zend_operators.cのadd_function関数を見る。

ZEND_API int add_function(zval *result, zval *op1, zval *op2 TSRMLS_DC) /* {{{ */
{
	zval op1_copy, op2_copy;
	int converted = 0;

	while (1) {
		switch (TYPE_PAIR(Z_TYPE_P(op1), Z_TYPE_P(op2))) {
			case TYPE_PAIR(IS_LONG, IS_LONG): {
				long lval = Z_LVAL_P(op1) + Z_LVAL_P(op2);

				/* check for overflow by comparing sign bits */
				if ((Z_LVAL_P(op1) & LONG_SIGN_MASK) == (Z_LVAL_P(op2) & LONG_SIGN_MASK)
					&& (Z_LVAL_P(op1) & LONG_SIGN_MASK) != (lval & LONG_SIGN_MASK)) {

					ZVAL_DOUBLE(result, (double) Z_LVAL_P(op1) + (double) Z_LVAL_P(op2));
				} else {
					ZVAL_LONG(result, lval);
				}
				return SUCCESS;
			}

			case TYPE_PAIR(IS_LONG, IS_DOUBLE):
				ZVAL_DOUBLE(result, ((double)Z_LVAL_P(op1)) + Z_DVAL_P(op2));
				return SUCCESS;

			case TYPE_PAIR(IS_DOUBLE, IS_LONG):
				ZVAL_DOUBLE(result, Z_DVAL_P(op1) + ((double)Z_LVAL_P(op2)));
				return SUCCESS;

			case TYPE_PAIR(IS_DOUBLE, IS_DOUBLE):
				ZVAL_DOUBLE(result, Z_DVAL_P(op1) + Z_DVAL_P(op2));
				return SUCCESS;

			case TYPE_PAIR(IS_ARRAY, IS_ARRAY): {
				zval *tmp;

				if ((result == op1) && (result == op2)) {
					/* $a += $a */
					return SUCCESS;
				}
				if (result != op1) {
					*result = *op1;
					zval_copy_ctor(result);
				}
				zend_hash_merge(Z_ARRVAL_P(result), Z_ARRVAL_P(op2), (void (*)(void *pData)) zval_add_ref, (void *) &tmp, sizeof(zval *), 0);
				return SUCCESS;
			}

			default:
				if (!converted) {
					zendi_convert_scalar_to_number(op1, op1_copy, result);
					zendi_convert_scalar_to_number(op2, op2_copy, result);
					converted = 1;
				} else {
					zend_error(E_ERROR, "Unsupported operand types");
					return FAILURE; /* unknown datatype */
				}
		}
	}
}
/* }}} */

switch文の中で型の組み合わせによって処理を変えてるみたいなのでswitch文内に以下のようなcase文を挿入してみる。

case TYPE_PAIR(IS_OBJECT, IS_OBJECT): {
    puts("hoge");
    exit(0);
}
$ make
$ sapi/cli/php -r "new stdClass() + new stdClass();"
hoge

ここで合ってるみたいだ。

次にやることは、オブジェクトが"__add"というメソッドを持っているかどうかを判断すること。PHPのオブジェクトにはクラスエントリが紐付いていて、クラスエントリの中にはそのクラスのメソッドの辞書がある。zval構造体、zend_class_entry構造体、HashTable構造体やそれに関するマクロをglobalgrepを使って調べて、オブジェクトに"__add"というメソッドが生えているかどうかを判断するコードを書く。

case TYPE_PAIR(IS_OBJECT, IS_OBJECT): {
    if (zend_hash_exists(&Z_OBJCE_P(op1)->function_table, "__add", strlen("__add") + 1)) {
        puts("hoge");
        exit(0);
    }
}

Z_OBJCE_Pマクロは、zval構造体からzend_class_entry構造体を取ってくるマクロ。

またmakeして試す。

$ make 
$ sapi/cli/php -r "new stdClass() + new stdClass();"
Notice: Object of class stdClass could not be converted to int in Command line code on line 1

Notice: Object of class stdClass could not be converted to int in Command line code on line 1

__addメソッドがないオブジェクトだと通常のエラー。

$ cat fuga.php
<?php

class Fuga
{
    function __add() { }
}
new Fuga() + new Fuga();

$ sapi/cli/php fuga.php
hoge

__addメソッドをもっているかどうかの判断もうまく動いているようだ。

後はPHPのオブジェクトの__addメソッドを呼び出して結果を返すコードを書いて、終わり。途中でSegmentation faultなどが起こったりメモリリーク警告がでる時は、GDBで動かしながら修正する。

終わり

数年前はC言語よくわからんなーPHPの実装よくわからんなーとか言ってるレベルだったけど最近はやってみると案外サクッとできたりするような部分もあって面白い。

関連記事: