pixelfederation / z-engine
提供直接访问原生PHP结构的库
Requires
- php: 8.2.*
- ext-ffi: *
Requires (Dev)
- phpunit/phpunit: ^8.5.15 || ^9.0.0
This package is auto-updated.
Last update: 2024-09-08 18:53:02 UTC
README
Z-Engine 库
你是否曾梦想过模拟一个最终类或重新定义最终方法?或者可能希望在运行时能够与现有类一起工作?Z-Engine
是一个PHP7.4库,它提供了一个API来访问PHP。忘记所有现有的限制,使用这个库在运行时通过声明新方法、向类添加新接口以及安装自己的系统钩子(如字节码编译、对象初始化等)来转换你的现有代码。
⚠️ 在1.0.0版本之前不要在生产环境中使用!
它是如何工作的?
如你所知,PHP 7.4包含一个名为FFI的新特性。它允许加载共享库(.dll或.so),调用C函数以及在纯PHP中访问C数据结构,无需深入了解Zend扩展API,也无需学习第三“中间”语言。
Z-Engine
使用FFI来访问PHP本身的内部结构。这个想法听起来非常疯狂,但竟然可行!Z-Engine
加载原生PHP结构的定义,如zend_class_entry
、zval
等,并在运行时对其进行操作。当然,这是危险的,因为FFI允许在非常低级别上与结构一起工作。因此,你应该预料到段错误、内存泄漏和其他不良情况。
先决条件和初始化
由于此库依赖于FFI
,它需要PHP>=7.4和启用FFI
扩展。它应该在CLI模式下无任何问题运行,而对于Web模式,应激活preload
模式。此外,当前版本仅限于x64非线程安全的PHP版本。
要安装此库,只需通过composer
添加即可
composer require lisachenko/z-engine
要激活preload
模式,请在您的脚本中添加opcache.preload
指定的Core::preload()
调用。此调用将在服务器预加载期间执行,并由库用于在每个请求期间绕过不必要的C头文件处理。
下一步是使用对Core::init()
的简短调用初始化库本身
use ZEngine\Core; include __DIR__.'/vendor/autoload.php'; Core::init();
现在您可以使用以下示例进行测试
<?php declare(strict_types=1); use ZEngine\Reflection\ReflectionClass; include __DIR__.'/vendor/autoload.php'; final class FinalClass {} $refClass = new ReflectionClass(FinalClass::class); $refClass->setFinal(false); eval('class TestClass extends FinalClass {}'); // Should be created
要了解您可以使用此库做什么,请查看库测试作为示例。
ReflectionClass
库为经典的反射API提供了一个扩展,通过ReflectionClass
来操作类的内部结构
setFinal(bool $isFinal = true): void
将指定的类设置为最终类/非最终类setAbstract(bool $isAbstract = true): void
将指定的类设置为抽象类/非抽象类。即使它包含接口或抽象类中未实现的方法。setStartLine(int $newStartLine): void
更新关于类起始行的元信息setEndLine(int $newEndLine): void
更新关于类结束行的元信息setFileName(string $newFileName): void
为此类设置新的文件名setParent(string $newParent)
[WIP] 为此类配置一个新的父类removeParentClass(): void
[WIP] 移除父类removeTraits(string ...$traitNames): void
[WIP] 从类中移除现有的特性addTraits(string ...$traitNames): void
[WIP] 向类中添加新的特性removeMethods(string ...$methodNames): void
从类中移除一系列方法addMethod(string $methodName, \Closure $method): ReflectionMethod
向类中添加一个新的方法removeInterfaces(string ...$interfaceNames): void
从类中移除一系列接口名称addInterfaces(string ...$interfaceNames): void
向指定类添加一系列接口
除此之外,所有返回 ReflectionMethod
或 ReflectionClass
的方法都被装饰以返回具有对原生结构低级访问的扩展对象。
ReflectionMethod
ReflectionMethods
包含用于处理现有方法定义的方法
setFinal(bool $isFinal = true): void
使指定方法为最终/非最终setAbstract(bool $isAbstract = true): void
使指定方法为抽象/非抽象setPublic(): void
使指定方法为公开setProtected(): void
使指定方法为保护setPrivate(): void
使指定方法为私有setStatic(bool $isStatic = true): void
声明方法为静态/非静态setDeclaringClass(string $className): void
更改此方法的声明类名称setDeprecated(bool $isDeprecated = true): void
声明此方法为已过时/非已过时redefine(\Closure $newCode): void
使用闭包定义重新定义此方法getOpCodes(): iterable
: [WIP] 返回此方法的指令列表
ObjectStore API
PHP中的每个对象都有一个唯一的标识符,可以通过 spl_object_id($object)
获取。有时我们正在寻找通过其标识符获取对象的方法。不幸的是,PHP不提供这样的API,而内部有一个存储在全局 executor_globals
变量(即EG)中的 zend_objects_store
结构实例。
此库通过 Core::$executor->objectStore
提供了一个 ObjectStore
API,该API实现了 ArrayAccess
和 Countable
接口。这意味着您可以通过对象句柄访问此存储来获取任何现有对象
use ZEngine\Core; $instance = new stdClass(); $handle = spl_object_id($instance); $objectEntry = Core::$executor->objectStore[$handle]; var_dump($objectEntry);
Object Extensions API
借助 z-engine
库,可以在不深入了解PHP引擎实现的情况下为您的类重载标准操作符。例如,假设您想定义本机矩阵操作符并使用它们
<?php use ZEngine\ClassExtension\ObjectCastInterface; use ZEngine\ClassExtension\ObjectCompareValuesInterface; use ZEngine\ClassExtension\ObjectCreateInterface; use ZEngine\ClassExtension\ObjectCreateTrait; use ZEngine\ClassExtension\ObjectDoOperationInterface; class Matrix implements ObjectCreateInterface, ObjectCompareValuesInterface, ObjectDoOperationInterface, ObjectCastInterface { use ObjectCreateTrait; // ... } $a = new Matrix([10, 20, 30]); $b = new Matrix([1, 2, 3]); $c = $a + $b; // Matrix([11, 22, 33]) $c *= 2; // Matrix([22, 44, 66])
激活自定义处理程序有两种方式。第一种方式是实现几个系统接口,如 ObjectCastInterface
、ObjectCompareValuesInterface
、ObjectCreateInterface
和 ObjectDoOperationInterface
。之后,应创建此包提供的 ReflectionClass
实例并调用 installExtensionHandlers
方法来安装扩展
use ZEngine\Reflection\ReflectionClass as ReflectionClassEx; // ... initialization logic $refClass = new ReflectionClassEx(Matrix::class); $refClass->installExtensionHandlers();
如果您没有访问代码(例如,供应商),则还可以定义自定义处理程序。您需要显式定义回调作为闭包并将其通过 ReflectionClass
中的 set***Handler()
方法分配。
use ZEngine\ClassExtension\ObjectCreateTrait; use ZEngine\Reflection\ReflectionClass as ReflectionClassEx; $refClass = new ReflectionClassEx(Matrix::class); $handler = Closure::fromCallable([ObjectCreateTrait::class, '__init']); $refClass->setCreateObjectHandler($handler); $refClass->setCompareValuesHandler(function ($left, $right) { if (is_object($left)) { $left = spl_object_id($left); } if (is_object($right)) { $right = spl_object_id($right); } // Just for example, object with bigger object_id is considered bigger that object with smaller object_id return $left <=> $right; });
库提供以下接口
第一个是 ObjectCastInterface
,它提供了处理将类实例转换为标量的钩子。典型示例如下:1)显式 $value = (int) $objectInstance
或隐式:$value = 10 + $objectInstance;
在没有安装 do_operation
处理程序的情况下。请注意,此处理程序不处理转换为 array
类型的转换,因为它以不同的方式实现。
<?php use ZEngine\ClassExtension\Hook\CastObjectHook; /** * Interface ObjectCastInterface allows to cast given object to scalar values, like integer, floats, etc */ interface ObjectCastInterface { /** * Performs casting of given object to another value * * @param CastObjectHook $hook Instance of current hook * * @return mixed Casted value */ public static function __cast(CastObjectHook $hook); }
要获取转换类型,您应该检查 $hook->getCastType()
方法,它将返回类型整数值。可能的值在 ReflectionValue
类中声明为公共常量。例如 ReflectionValue::IS_LONG
。
下一个是 ObjectCompareValuesInterface
接口,用于控制比较逻辑。例如,您可以比较两个对象,甚至比较对象与标量值:if ($object > 10 || $object < $anotherObject)
<?php use ZEngine\ClassExtension\Hook\CompareValuesHook; /** * Interface ObjectCompareValuesInterface allows to perform comparison of objects */ interface ObjectCompareValuesInterface { /** * Performs comparison of given object with another value * * @param CompareValuesHook $hook Instance of current hook * * @return int Result of comparison: 1 is greater, -1 is less, 0 is equal */ public static function __compare(CompareValuesHook $hook): int; }
处理程序应检查可以通过调用 $hook->getFirst()
和 $hook->getSecond()
方法接收到的参数(其中之一应返回你的类的实例),并返回整数结果 -1..1。其中 1 是更大的,-1 是更小的,0 是相等的。
ObjectDoOperationInterface
接口是最强大的,因为它让你可以控制应用于你的对象的数学运算符(如 ADD、SUB、MUL、DIV、POW 等)。
<?php use ZEngine\ClassExtension\Hook\DoOperationHook; /** * Interface ObjectDoOperationInterface allows to perform math operations (aka operator overloading) on object */ interface ObjectDoOperationInterface { /** * Performs an operation on given object * * @param DoOperationHook $hook Instance of current hook * * @return mixed Result of operation value */ public static function __doOperation(DoOperationHook $hook); }
此处理程序通过 $hook->getOpcode()
接收一个操作码(请参阅 OpCode::*
常量)和两个参数(其中之一是类实例)通过 $hook->getFirst()
和 $hook->getSecond()
,并返回该操作的值。在此处理程序中,你可以返回你的对象的新实例以生成不可变对象实例的链。
重要提示:你必须首先安装 create_object
处理程序,然后才能在运行时安装钩子。此外,你不能为内部对象安装 create_object
处理程序。
还有一个名为 setInterfaceGetsImplementedHandler
的额外方法,这对于安装接口的特殊处理程序非常有用。使用与 create_object
处理程序相同的内存槽,interface_gets_implemented
回调将在每次任何类实现此接口时被调用。这为自动类扩展注册提供了有趣的选择,例如,如果一个类实现了 ObjectCreateInterface
,则可以在回调中自动调用 ReflectionClass->installExtensionHandlers()
。
抽象语法树 API
正如你所知,PHP7 使用抽象语法树来处理源代码的抽象模型,以简化语言的未来发展语法。不幸的是,此信息没有返回到用户级。有一些 PHP 扩展,如 nikic/php-ast 和 sgolemon/astkit,它们提供了对底层 AST 结构的低级绑定。Z-Engine 通过 Compiler::parseString(string $source, string $fileName = '')
方法提供对 AST 的访问。此方法将返回实现 NodeInterface
的树顶级节点。PHP 有四种类型的 AST 节点,它们是:声明节点(类、方法等)、列表节点(可以包含任何数量的子节点)、简单节点(包含最多 4 个子节点,具体取决于类型)和特殊值节点类,它可以存储任何值(通常是字符串或数字)。
以下是解析简单 PHP 代码的示例
use ZEngine\Core; $ast = Core::$compiler->parseString('echo "Hello, world!", PHP_EOL;', 'hi.php'); echo $ast->dump();
输出将如下所示
1: AST_STMT_LIST
1: AST_STMT_LIST
1: AST_ECHO
1: AST_ZVAL string('Hello, world!')
1: AST_ECHO
1: AST_CONST
1: AST_ZVAL attrs(0001) string('PHP_EOL')
节点提供简单的 API 通过调用 Node->replaceChild(int $index, ?Node $node)
来变异子节点。你可以在运行时创建自己的节点或使用 Compiler::parseString(string $source, string $fileName = '')
的结果作为你的代码的替换。
修改抽象语法树
当 PHP 7 编译 PHP 代码时,它将其转换为抽象语法树(AST),然后在最终生成持久化在 Opcache 中的 Opcodes 之前。对于每个编译的脚本,都会调用 zend_ast_process
钩子,这允许你在解析和创建 AST 后修改 AST。
要安装 zend_ast_process
钩子,请对 Core::setASTProcessHandler(Closure $callback)
方法进行静态调用,该方法接受一个回调,该回调将在 AST 处理期间被调用,并将接收一个 AstProcessHook $hook
作为参数。你可以通过 $hook->getAST(): NodeInterface
方法访问顶级节点项。
use ZEngine\Core; use ZEngine\System\Hook\AstProcessHook; Core::setASTProcessHandler(function (AstProcessHook $hook) { $ast = $hook->getAST(); echo "Parsed AST:", PHP_EOL, $ast->dump(); // Let's modify Yes to No ) echo $ast->getChild(0)->getChild(0)->getChild(0)->getValue()->setNativeValue('No'); }); eval('echo "Yes";'); // Parsed AST: // 1: AST_STMT_LIST // 1: AST_STMT_LIST // 1: AST_ECHO // 1: AST_ZVAL string('Yes') // No
您可以看到,由于我们在回调中调整了给定的AST,评估结果从"是"变为"否"。但请注意,这是使用最复杂的钩子之一,因为它需要完美理解AST的可能性。在这里创建无效的AST可能会导致奇怪的行为或崩溃。
在运行时创建PHP扩展
Z-Engine库最有趣的部分是使用PHP语言本身创建自己的PHP扩展。您不必花大量时间学习C语言;相反,您可以使用现成的API从PHP本身创建自己的扩展模块!
当然,并不是所有东西都可以在PHP中实现为扩展,例如,更改解析器语法或更改opcache的逻辑——为此,您将不得不深入研究引擎本身的代码。
让我们举一个创建具有全局变量的模块的例子,类似于apcu,这样这些变量在请求完成后不会被清除。人们认为PHP有“无共享”的概念,因此不能跨越请求边界存活,因为当请求完成时,PHP将自动释放对象分配的所有内存。然而,PHP本身可以处理全局变量,它们存储在加载的模块中,通过指针zend_module_entry.globals_ptr
。
因此,如果我们能在PHP中注册模块并为它分配全局内存,PHP将不会清除它,我们的模块将能够跨越请求边界。
从技术角度看,每个模块都由以下结构表示
struct _zend_module_entry {
unsigned short size;
unsigned int zend_api;
unsigned char zend_debug;
unsigned char zts;
const struct _zend_ini_entry *ini_entry;
const struct _zend_module_dep *deps;
const char *name;
const struct _zend_function_entry *functions;
int (*module_startup_func)(int type, int module_number);
int (*module_shutdown_func)(int type, int module_number);
int (*request_startup_func)(int type, int module_number);
int (*request_shutdown_func)(int type, int module_number);
void (*info_func)(zend_module_entry *zend_module);
const char *version;
size_t globals_size;
#ifdef ZTS
ts_rsrc_id* globals_id_ptr;
#endif
#ifndef ZTS
void* globals_ptr;
#endif
void (*globals_ctor)(void *global);
void (*globals_dtor)(void *global);
int (*post_deactivate_func)(void);
int module_started;
unsigned char type;
void *handle;
int module_number;
const char *build_id;
};
您可以看到,我们可以定义几个回调,还有一些包含关于zts、调试、API版本等元信息的字段,这些信息由PHP用来检查此模块是否可以加载到当前环境中。
从PHP方面来看,您应该从包含模块注册和启动的一般逻辑的AbstractModule
类扩展您的模块类,并实现所有来自ModuleInterface
的必需方法。
让我们看看我们的简单模块
use ZEngine\EngineExtension\AbstractModule; class SimpleCountersModule extends AbstractModule { /** * Returns the target thread-safe mode for this module * * Use ZEND_THREAD_SAFE as default if your module does not depend on thread-safe mode. */ public static function targetThreadSafe(): bool { return ZEND_THREAD_SAFE; } /** * Returns the target debug mode for this module * * Use ZEND_DEBUG_BUILD as default if your module does not depend on debug mode. */ public static function targetDebug(): bool { return ZEND_DEBUG_BUILD; } /** * Returns the target API version for this module * * @see zend_modules.h:ZEND_MODULE_API_NO */ public static function targetApiVersion(): int { return 20190902; } /** * Returns true if this module should be persistent or false if temporary */ public static function targetPersistent(): bool { return true; } /** * Returns globals type (if present) or null if module doesn't use global memory */ public static function globalType(): ?string { return 'unsigned int[10]'; } }
我们的SimpleCountersModule
声明它将使用10个无符号整数的数组。它还提供了一些关于所需环境的(调试/zts/API版本)信息。重要的选项是通过从targetPersistent()
方法返回true来标记我们的模块持久化。现在我们准备好注册并使用它了
$module = new SimpleCountersModule(); if (!$module->isModuleRegistered()) { $module->register(); $module->startup(); } $data = $module->getGlobals(); var_dump($data);
注意,在后续请求中模块将被注册,这就是为什么您不应该调用两次register。真正酷的是,模块的全局变量中的任何更改都是真正的全局变量!它们将在请求之间被保留。尝试更新每个项目,以看到我们的数组在请求之间的值是增加的
$index = mt_rand(0, 9); // If you have several workers, you should use worker pid to avoid race conditions $data[$index] = $data[$index] + 1; // We are increasing global counter by one /* Example of var_dump after several requests... object(FFI\CData:uint32_t[10])#35 (10) { [0]=> int(1) [1]=> int(1) [2]=> int(1) [3]=> int(3) [4]=> int(1) [5]=> int(1) [6]=> int(1) [7]=> int(2) [8]=> int(2) [9]=> int(2) }*/
当然,模块可以声明任何复杂的全局结构并按需使用。如果模块需要一些初始化,则可以在您的模块中实现ControlModuleGlobalsInterface
,此回调将在模块启动过程中被调用。这可能对于注册额外的钩子、类扩展等很有用,或者用于全局变量初始化(用预定义的值填充,从数据库/文件系统等恢复状态)
行为准则
本项目遵守贡献者誓言行为准则。通过参与,您应遵守此准则。请报告任何不可接受的行为。
许可协议
本库采用MIT许可证。
创建和维护此库对我来说是一项无休止的艰巨工作。这就是为什么只有一个简单的要求:请向世界回馈一些东西。无论那是一个代码或对项目的财务支持,完全取决于您。