构造函数与生成器
案例
Store项目某HttpControl基类代码,意图在Controller初始化时异步获取基础信息:
问题现象是多层继承关系下对象属性没有正确初始化。
最终发现在偌大的构造函数中隐藏了一个yield
,导致构造函数成为生成器函数,new操作符调用构造函数仅初始化生成器状态,没有对生成器进行迭代,进而构造函数中逻辑没有执行。
重现
<?php
class P
{
public $foo;
public function __construct($foo)
{
$this->foo = $foo;
}
}
class S extends P
{
public function __construct($foo) {
parent::__construct($foo);
// balabalabala
yield;
}
}
$s = new S(42);
var_dump($s->foo); // null
观察输出结果,hhvm的行为似乎比php要聪明一些,也更合理;
Output for 5.6.0 - 5.6.30, 7.0.0 - 7.1.4
NULL
Output for hhvm-3.12.14 - 3.19.0
Fatal error: Uncaught Error: 'yield' is not allowed in constructor, destructor, or magic methods in...
OP_CODE
通过生成的OP_CODE观察,__construct与普通yield函数一致
<?php
class T
{
public function __construct()
{
yield;
}
public function gen()
{
yield;
}
}
Generated using Vulcan Logic Dumper, using php 7.1.0
Function __construct:
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
5 0 E > GENERATOR_CREATE
6 1 YIELD
7 2 > GENERATOR_RETURN
Function gen:
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
8 0 E > GENERATOR_CREATE
9 1 YIELD
10 2 > GENERATOR_RETURN
关于构造函数
PHP不保证构造函数不可重入,不禁止构造函数return返回值,不禁止构造函数不能为生成器函数
(在不考虑反射情况下)JAVA保证了构造函数不可重入,也保证new操作符返回类实例的语义,JAVA无生成器。
用户通常不会直接调用构造函数,而是依靠new调用构造函数来初始化对象;
无异常情况new操作符总返回类实例而不是构造函数返回值,所以构造函数即使return返回值,也没有意义。
但是,因为PHP构造函数与普通方法无异,既可以重入,也可以返回值;如何在构造函数不重入情况下构造对象并且获取构造函数的返回值下文会提到。
构造函数是普通方法:
<?php
class R
{
public function __construct($a)
{
return $a;
}
}
$r = new R(0);
echo $r->__construct(125); // 125
echo $r->__construct(42); // 42
构造函数可以是生成器:
class R
{
public function __construct($a) {
yield;
echo $a;
}
}
$r = new R("hi"); // no output
foreach ($r->__construct("hello") as $_); // hello
Hack New
注意:简化错误处理
<?php
function _new_($className, ...$args)
{
assert(class_exists($className));
assert(method_exists($className, "__construct"));
$obj = new $className(...$args);
$gen = $obj->__construct(...$args);
assert($gen instanceof \Generator);
foreach($gen as $_);
return $obj;
}
$s = _new_(S::class, 42);
var_dump($s->foo); // 42
or
<?php
function __new($className, ...$args)
{
assert(class_exists($className));
if (method_exists($className, "__construct")) {
$ctor = new \ReflectionMethod($className, "__construct");
assert($ctor->isPublic());
// 根据编译阶段产生的标记位来判断是否是生成器函数
if ($ctor->isGenerator()) {
// 获取未经过构造函数初始化的类实例
$clazz = new \ReflectionClass($className);
$obj = $clazz->newInstanceWithoutConstructor(); // TODO impl
// 直接调用构造函数 并 执行迭代器
/** @var \Generator $gen */
$gen = $ctor->invoke($obj, ...$args);
foreach ($gen as $_); // or iterator_to_array($gen);
return $obj;
}
}
return new $className(...$args);
}
$s = _new_(S::class, 42);
var_dump($s->foo); // 42
如何在Zan中加入对生成器类型构造函数支持
使用上文介绍的方式加入替代new操作符的生成器函数用来实例化对象;
<?php
function __new($className, ...$args)
{
assert(class_exists($className));
if (method_exists($className, "__construct")) {
$ctor = new \ReflectionMethod($className, "__construct");
assert($ctor->isPublic());
if ($ctor->isGenerator()) {
$clazz = new \ReflectionClass($className);
$obj = $clazz->newInstanceWithoutConstructor();
$ignoredRet = (yield $ctor->invoke($obj, ...$args));
// 这里需要特殊处理async
if ($obj instanceof \Zan\Framework\Foundation\Contract\Async) {
yield [$obj];
} else {
yield $obj;
}
return;
}
}
yield new $className(...$args);
}
function _new($className, ...$args)
{
assert(class_exists($className));
if (method_exists($className, "__construct")) {
// 这里没有检查构造函数是否是生成器函数
// 也没有检查特殊的Async
$obj = new $className(...$args);
yield $obj->__construct(...$args);
yield $obj;
} else {
yield new $className(...$args);
}
}
Test
<?php
class A
{
public $code;
public function __construct()
{
$cli = new \Zan\Framework\Network\Common\HttpClient("www.youzan.com", 80);
$r = (yield $cli->get("/"));
/** @var $r \Zan\Framework\Network\Common\Response */
$this->code = $r->getStatusCode();
}
}
class B extends A
{
public function __construct()
{
yield taskSleep(1000);
yield parent::__construct();
}
}
class C extends A implements \Zan\Framework\Foundation\Contract\Async
{
public function __construct()
{
yield parent::__construct();
}
public function execute(callable $callback, $task)
{
swoole_timer_after(1000, function() use($callback) {
$callback(null, null);
});
}
}
测试
<?php
use Zan\Framework\Foundation\Coroutine\Task;
$co = function() {
/** @var B $b */
$b = (yield _new(B::class));
var_dump($b->code);
$b = (yield __new(B::class));
var_dump($b->code);
list($c) = (yield __new(C::class));
var_dump($c->code);
};
Task::execute($co());
执行流程
<?php
class T
{
public function __construct() { yield; }
}
$t = new T;
compiled vars: !0 = $t
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > NOP
7 1 NEW $2 :-5
2 DO_FCALL 0
3 ASSIGN !0, $2
4 > RETURN 1
两点说明:
编译期已经完成的操作(如声明)会替换为NOP,Zend会为每个op-array最后都插入RETURN;
关于opcode handler,可选采处理特定类型的操作数函数,这里类似c++的模板与java的泛型,不过工作不是编译器做的,而是一段php脚本根据样板代码生成的;参见 README.ZEND_VM
OP_CODE: NEW
特化函数: static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_NEW_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
- 根据第一个操作数的类型用不同方式获取到zend_class_entry,
- 从ce初始化一个对象的zval (scope)
- 从zend_object的handler中获取构造函数 (zend_function)
- 有构造函数,压入vm stack,如果没有构造函数,将dummy函数压入;
EG(vm_stack_top) = (zval*)((char*)call + used_stack);
zend_vm_init_call_frame(call, call_info, func, num_args, called_scope, object);
(gdb) print_zst object.ce->name
string(1) "T"
(gdb) print_zstr func.common.function_name
string(11) "__construct"
(gdb) p call
$30 = (zend_execute_data *) 0x7ffff18130d0
OP_CODE: DO_FCALL
特化函数:static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
- 调用构造函数或者dummy函数,注意这里忽略返回值调用__construct
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) {
zend_execute_data *call = EX(call);
zend_function *fbc = call->func;
(gdb) p call
$33 = (zend_execute_data *) 0x7ffff18130d0
(gdb) print_zstr call->This->value->obj->ce->name
string(1) "T"
(gdb) print_zstr fbc->common.function_name
string(11) "__construct"
# static zend_always_inline void i_init_func_execute_data(...
(gdb) p op_array->opcodes->handler
$40 = (const void *) 0x8db600
(gdb) p ZEND_GENERATOR_CREATE_SPEC_HANDLER
$41 = {int (zend_execute_data *)} 0x8db600 <ZEND_GENERATOR_CREATE_SPEC_HANDLER>