在协程环境中使用 PHP Lua 拓展
项目迁移中遇到的最大问题是由 PHP Lua 引起的不兼容和内存泄漏问题。之前大量使用了 Lua 拓展的 registerCallback
方法来注册验证自定义逻辑的辅助方法,大致逻辑如下:
class Matcher
{
protected $lua;
public function __construct()
{
$this->lua = new Lua();
$this->lua->registerCallback('validate', [$this, 'validate']);
}
protected function validate()
{
// logic codes
}
public function __destruct()
{
unset($this->lua);
}
}
从定义上看,registerCallback
就是将 PHP 方法保存 $this->lua
对象中。但并非如此,实际上是保存到了 Lua::$_callbacks
的静态变量里:
/* php-lua/lua.c line 864*/
zend_declare_property_null(lua_ce, ZEND_STRL("_callbacks"), ZEND_ACC_STATIC|ZEND_ACC_PRIVATE);
/* php-lua/lua.c line 761 */
PHP_METHOD(lua, registerCallback) {
char *name;
size_t len;
zval *func;
lua_State *L;
zval* callbacks;
if (zend_parse_parameters(ZEND_NUM_ARGS(),"sz", &name, &len, &func) == FAILURE) {
return;
}
L = (Z_LUAVAL_P(getThis()))->L;
/* 获取 _callbacks 变量 */
callbacks = zend_read_static_property(lua_ce, ZEND_STRL("_callbacks"), 1);
/* 初始化 _callbacks 数组 */
if (ZVAL_IS_NULL(callbacks)) {
array_init(callbacks);
}
/* 在 lua_State 里创建一个闭包,upvalue(1) 的值即为 callback 在数组中的索引 */
if (zend_is_callable(func, 0, NULL)) {
lua_pushnumber(L, zend_hash_num_elements(Z_ARRVAL_P(callbacks)));
lua_pushcclosure(L, php_lua_call_callback, 1);
lua_setglobal(L, name);
} else {
zend_throw_exception_ex(lua_exception_ce, 0, "invalid php callback");
RETURN_FALSE;
}
/* 将 callback 加入到数组中 */
zval_add_ref(func);
add_next_index_zval(callbacks, func);
RETURN_ZVAL(getThis(), 1, 0);
}
在协程环境中,由于静态变量跨请求长期存在,_callbacks
无法被正确释放,导致严重的内存泄漏。如果将注册的方法全部静态化,可以避免重复注册的问题,但是所有带状态的参数都需要显示传递,不仅会在 lua 中暴露更多细节,还让业务代码更加复杂,违背了引入 Lua 的初衷。
为了保证业务代码的兼容性,最后决定修改 Lua 拓展,将辅助方法直接保存到 Lua 的实例中。一开始是考虑直接去掉 _callbacks
静态属性,然后在注册和调用时临时替换 lua_object_handlers
的 write_property
和 read_property
为正常的函数指针。
但最后并没有采用,而是使用了 custom_object
的方式,单独保存 _callbacks
数组:
struct _php_lua_object {
lua_State *L;
zval _callbacks; /* 新增 */
zend_object obj;
};
由于在 php_lua_call_callback
中没有直接获取 _callbacks
变量的方法,我用了一个非常 dirty 的方式,直接将 php_lua_object *
指针通过 lua_pushlightuserdata
保存到了 lua 中:
/* ... */
php_lua_object *p_lua_object = Z_LUAVAL_P(getThis());
L = p_lua_object->L;
callbacks = &(p_lua_object->_callbacks);
/* ... */
lua_pushlightuserdata(L, p_lua_object); /* 指针 */
lua_pushnumber(L, zend_hash_num_elements(Z_ARRVAL_P(callbacks)));
lua_pushcclosure(L, php_lua_call_callback, 2);
lua_setglobal(L, name);
/* ... */
c closure 执行时,直接从 upvalues 里恢复指针:
/* ... */
php_lua_object *p_lua_object = lua_touserdata(L, lua_upvalueindex(1)); /* 恢复指针 */
order = lua_tonumber(L, lua_upvalueindex(2));
/* ... */
callbacks = &(p_lua_object->_callbacks);
/* ... */
func = zend_hash_index_find(Z_ARRVAL_P(callbacks), order); /* 获取 callback */
/* ... */
将 zend_object
中的 handle 保存到闭包应该也能达到这个目标:
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
};
不过由于 PHP 开发者多次提到 EG(objects_store) 已经没有什么太大的用处,上面的注释也可以证明,还是放弃使用为妙。另外,如果将 _callback
指针直接保存到 lua_State 中也可以考虑,这样就不用像我这样修改 send_zval 的参数了。
具体的修改记录可以访问 php-lua 查看,但是切勿用于生成环境,后面有空后会考虑更好的实现方式。使用修改后的拓展也需要注意手动释放 Lua 对象。比如在文章开头的使用方式中,由于 Lua 对象和 Matcher 对象存在循环引用,单独 unset($matcher)
并不会立刻触发 Matcher 的析构函数,同样存在内存泄漏风险。