Magic interfaces - Comparable¶
Internal interfaces in PHP are very similar to their userland equivalents. The only notable difference is that internal
interfaces have the additional possibility of specifying a handler that is executed when the interface is implemented.
This feature can be used for various purposes like enforcing additional constraints or replacing handlers. We’ll make
use of it to implement a “magic” Comparable
interface, which exposes the internal compare_objects
handler to
userland.
The interface itself will look as follows:
interface Comparable {
static function compare($left, $right);
}
First, let’s register this new interface in MINIT
:
zend_class_entry *comparable_ce;
ZEND_BEGIN_ARG_INFO_EX(arginfo_comparable, 0, 0, 2)
ZEND_ARG_INFO(0, obj1)
ZEND_ARG_INFO(0, obj2)
ZEND_END_ARG_INFO()
const zend_function_entry comparable_functions[] = {
ZEND_FENTRY(
compare, NULL, arginfo_comparable, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT|ZEND_ACC_STATIC
)
PHP_FE_END
};
PHP_MINIT_FUNCTION(comparable)
{
zend_class_entry tmp_ce;
INIT_CLASS_ENTRY(tmp_ce, "Comparable", comparable_functions);
comparable_ce = zend_register_internal_interface(&tmp_ce TSRMLS_CC);
return SUCCESS;
}
Note that in this case we can’t use PHP_ABSTRACT_ME
, because it does not support static abstract methods. Instead
we have to use the low-level ZEND_FENTRY
macro.
Next we implement the interface_gets_implemented
handler:
static int implement_comparable(zend_class_entry *interface, zend_class_entry *ce TSRMLS_DC)
{
if (ce->create_object != NULL) {
zend_error(E_ERROR, "Comparable interface can only be used on userland classes");
}
ce->create_object = comparable_create_object_override;
return SUCCESS;
}
// in MINIT
comparable_ce->interface_gets_implemented = implement_comparable;
When the interface is implemented the implement_comparable
function will be called. In this function we override the
classes create_object
handler. To simplify things we only allow the interface to be used when create_object
was NULL
previously (i.e. it is a “normal” userland class). We could obviously also make this work with arbitrary
classes by backing up the old create_object
handler somewhere.
In our create_object
override we create the object as usual but assign our own handlers structure with a custom
compare_objects
handler:
static zend_object_handlers comparable_handlers;
static zend_object_value comparable_create_object_override(zend_class_entry *ce TSRMLS_DC)
{
zend_object *object;
zend_object_value retval;
retval = zend_objects_new(&object, ce TSRMLS_CC);
object_properties_init(object, ce);
retval.handlers = &comparable_handlers;
return retval;
}
// In MINIT
memcpy(&comparable_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
comparable_handlers.compare_objects = comparable_compare_objects;
Lastly we have to implement the custom comparison handler. It will call the compare
method using the
zend_call_method_with_2_params
macro, which is defined in zend_interfaces.h
. One question that arises is which
class the method should be called on. For this implementation we’ll simply use the first passed object, though this is
just an arbitrary choice. In practice this means that for $left < $right
the class of $left
will be used, but
for $left > $right
the class of $right
is used (because PHP transforms the >
to a <
operation).
#include "zend_interfaces.h"
static int comparable_compare_objects(zval *obj1, zval *obj2 TSRMLS_DC)
{
zval *retval = NULL;
int result;
zend_call_method_with_2_params(NULL, Z_OBJCE_P(obj1), NULL, "compare", &retval, obj1, obj2);
if (!retval || Z_TYPE_P(retval) == IS_NULL) {
if (retval) {
zval_ptr_dtor(&retval);
}
return zend_get_std_object_handlers()->compare_objects(obj1, obj2 TSRMLS_CC);
}
convert_to_long_ex(&retval);
result = ZEND_NORMALIZE_BOOL(Z_LVAL_P(retval));
zval_ptr_dtor(&retval);
return result;
}
The ZEND_NORMALIZE_BOOL
macro used above normalizes the returned integer to -1
, 0
and 1
.
And that’s all it takes. Now we can try out the new interface (sorry if the example doesn’t make particularly much sense):
class Point implements Comparable {
protected $x, $y, $z;
public function __construct($x, $y, $z) {
$this->x = $x; $this->y = $y; $this->z = $z;
}
/* We assume a point is smaller/greater if all its components are smaller/greater */
public static function compare($p1, $p2) {
if ($p1->x == $p2->x && $p1->y == $p2->y && $p1->z == $p2->z) {
return 0;
}
if ($p1->x < $p2->x && $p1->y < $p2->y && $p1->z < $p2->z) {
return -1;
}
if ($p1->x > $p2->x && $p1->y > $p2->y && $p1->z > $p2->z) {
return 1;
}
// not comparable
return 1;
}
}
$p1 = new Point(1, 1, 1);
$p2 = new Point(2, 2, 2);
$p3 = new Point(1, 0, 2);
var_dump($p1 < $p2, $p1 > $p2, $p1 == $p2); // true, false, false
var_dump($p1 == $p1); // true
var_dump($p1 < $p3, $p1 > $p3, $p1 == $p3); // false, false, false