swoole-bundle/z-engine

提供对原生PHP结构的直接访问的库

8.3.0 2023-12-21 00:09 UTC

README

Z-Engine 库

Build Status GitHub release Minimum PHP Version License

你是否曾梦想过模拟一个final类或重新定义final方法?或者也许你希望在运行时能够处理现有的类?Z-Engine 是一个PHP7.4库,它为PHP提供了一个API。忘记所有现有的限制,并使用这个库在运行时通过声明新方法、向类添加新接口以及安装自己的系统钩子(如opcode编译、对象初始化等)来转换你的现有代码。

⚠️ 在1.0.0版本之前不要在生产环境中使用!

它是如何工作的?

正如你所知,PHP 7.4版本包含一个新功能,称为 FFI。它允许加载共享库(.dll或.so),调用C函数以及在纯PHP中访问C数据结构,无需深入了解Zend扩展API,也无需学习第三种“中间”语言。

Z-Engine 使用FFI来访问PHP自身的内部结构。这个想法尝试起来非常疯狂,但它成功了!Z-Engine 加载了原生PHP结构(如zend_class_entryzval等)的定义,并在运行时对其进行操作。当然,这是危险的,因为FFI允许在非常低级别上处理结构。因此,你应该预料到段错误、内存泄漏和其他不好的事情。

预先要求和初始化

由于这个库依赖于FFI,它需要PHP>=7.4和启用FFI扩展。它应该在没有任何问题的情况下在CLI模式下工作,而对于Web模式,应该激活preload模式。此外,当前版本仅限于x64非线程安全版本的PHP。

要安装此库,只需通过composer添加即可

composer require lisachenko/z-engine

要激活preload模式,请在你的脚本中添加Core::preload()调用,指定为opcache.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 将指定类设置为final/非final
  • 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 向指定的类添加一系列接口。

除此之外,所有返回 ReflectionMethodReflectionClass 的方法都被装饰,以返回具有对本地结构的低级别访问的扩展对象。

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,而内部有一个 zend_objects_store 结构的实例存储在全局变量 executor_globals 中(即 EG)。

此库通过 Core::$executor->objectStore 提供了 ObjectStore API,它实现了 ArrayAccessCountable 接口。这意味着您可以通过对象句柄访问此存储来获取任何现有对象。

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])

有两种方法可以激活自定义处理程序。第一种方法是实现几个系统接口,如 ObjectCastInterfaceObjectCompareValuesInterfaceObjectCreateInterfaceObjectDoOperationInterface。之后,您应该创建此包提供的 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 的额外方法,用于安装接口的特殊处理程序。interface_gets_implemented 回调使用与 create_object 处理程序相同的内存槽,并在任何类实现此接口时被调用。这为自动类扩展注册提供了有趣的选项,例如,如果一个类实现了 ObjectCreateInterface,则在回调中自动调用 ReflectionClass->installExtensionHandlers()

抽象语法树 API

如您所知,PHP7 使用抽象语法树来简化对源代码抽象模型的操作,从而简化语言语法的未来开发。不幸的是,此信息未返回到用户级别。有几个 PHP 扩展,如 nikic/php-astsgolemon/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

您可以看到,评估结果已从 "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);

注意,在后续请求中模块将被注册,这就是为什么您不应该调用两次注册。真正酷的是,模块的全局变量是真正的全局变量!它们将在请求之间被保留。尝试更新每个项以查看我们的数组中的值在请求之间是否增加

$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许可证

创建和维护此库对我来说是一项无尽的艰巨工作。这就是为什么只有一个简单的要求:请向世界回馈一些东西。无论是代码还是对项目的财务支持,完全取决于您。