目录
关键要点
先前实现的缺点
更新词法分析器
更新解析器
更新编译阶段
更新Zend虚拟机
结论
首页 后端开发 php教程 重新实现PHP中的范围运算符

重新实现PHP中的范围运算符

Feb 15, 2025 am 09:36 AM

SitePoint精彩文章推荐:改进后的PHP范围运算符实现

本文经作者授权转载于SitePoint。以下内容由Thomas Punt撰写,介绍了PHP范围运算符的改进实现方法。如果您对PHP内部机制和为喜爱的编程语言添加功能感兴趣,那么现在正是学习的好时机!

本文假设读者能够从源代码构建PHP。如果不是这样,请先阅读PHP内部机制书籍的“构建PHP”章节。

Re-Implementing the Range Operator in PHP


在本文的前篇(提示:请确保您已阅读),我展示了一种在PHP中实现范围运算符的方法。然而,最初的实现很少是最好的,因此本文旨在探讨如何改进之前的实现。

再次感谢Nikita Popov校对本文!

关键要点

  • Thomas Punt重新实现了PHP中的范围运算符,将计算逻辑从Zend虚拟机中移出,从而允许在常量表达式上下文中使用范围运算符。
  • 此重新实现能够在编译时(对于字面量操作数)或运行时(对于动态操作数)进行计算。这不仅为Opcache用户带来了一点好处,而且允许将常量表达式功能与范围运算符一起使用。
  • 重新实现过程涉及更新词法分析器、解析器、编译阶段和Zend虚拟机。词法分析器实现保持不变,而解析器实现与之前部分相同。编译阶段不需要更新Zend/zend_compile.c文件,因为它已经包含处理二元运算的必要逻辑。Zend虚拟机已更新为在运行时处理ZEND_RANGE操作码的执行。
  • 在本系列的第三部分中,Punt计划通过介绍如何重载此运算符来构建此实现。这将使对象能够用作操作数,并为字符串添加适当的支持。

先前实现的缺点

最初的实现将范围运算符的所有逻辑都放在Zend虚拟机中,这迫使计算在执行ZEND_RANGE操作码时纯粹在运行时进行。这不仅意味着对于字面量操作数,计算不能转移到编译时,而且还意味着某些功能根本无法工作。

在此实现中,我们将范围运算符逻辑从Zend虚拟机中移出,以便能够在编译时(对于字面量操作数)或运行时(对于动态操作数)进行计算。这不仅为Opcache用户带来了一点好处,更重要的是允许将常量表达式功能与范围运算符一起使用。

例如:

// 作为常量定义
const AN_ARRAY = 1 |> 100;

// 作为初始属性定义
class A
{
    private $a = 1 |> 2;
}

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}
登录后复制
登录后复制
登录后复制

因此,事不宜迟,让我们重新实现范围运算符。

更新词法分析器

词法分析器实现保持完全不变。令牌首先在Zend/zend_language_scanner.l(约1200行)中注册:

<st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}
登录后复制
登录后复制
登录后复制

然后在Zend/zend_language_parser.y(约220行)中声明:

// 作为常量定义
const AN_ARRAY = 1 |> 100;

// 作为初始属性定义
class A
{
    private $a = 1 |> 2;
}

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}
登录后复制
登录后复制
登录后复制

必须再次通过进入ext/tokenizer目录并执行tokenizer_data_gen.sh文件来重新生成标记器扩展。

更新解析器

解析器实现与之前部分相同。我们再次通过将T_RANGE令牌添加到以下行的末尾来声明运算符的优先级和结合性(约70行):

<st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}
登录后复制
登录后复制
登录后复制

然后,我们再次更新expr_without_variable产生式规则,但这次语义动作(花括号内的代码)将略有不同。使用以下代码更新它(我将其放在T_SPACESHIP规则下方,约930行):

%token T_RANGE           "|> (T_RANGE)"
登录后复制
登录后复制

这次,我们使用了zend_ast_create_binary_op函数(而不是zend_ast_create函数),它为我们创建了一个ZEND_AST_BINARY_OP节点。zend_ast_create_binary_op采用一个操作码名称,该名称将在编译阶段用于区分二元运算。

由于我们现在正在重用ZEND_AST_BINARY_OP节点类型,因此无需像之前在Zend/zend_ast.h文件中那样定义新的ZEND_AST_RANGE节点类型。

更新编译阶段

这次,无需更新Zend/zend_compile.c文件,因为它已经包含处理二元运算的必要逻辑。因此,我们只需通过将我们的运算符设为ZEND_AST_BINARY_OP节点来重用此逻辑。

以下是zend_compile_binary_op函数的简化版本:

%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP T_RANGE
登录后复制
登录后复制

正如我们所看到的,它与我们上次创建的zend_compile_range函数非常相似。两个重要的区别在于如何获取操作码类型以及当两个操作数都是字面量时会发生什么。

操作码类型这次是从AST节点获取的(而不是像上次那样硬编码),因为ZEND_AST_BINARY_OP节点存储此值(如新的产生式规则的语义动作所示)以区分二元运算。当两个操作数都是字面量时,将调用zend_try_ct_eval_binary_op函数。此函数如下所示:

    |   expr T_RANGE expr
            { $$ = zend_ast_create_binary_op(ZEND_RANGE, , ); }
登录后复制

该函数根据操作码类型从Zend/zend_opcode.c中的get_binary_op函数(源代码)获取回调。这意味着我们需要接下来更新此函数以适应ZEND_RANGE操作码。将以下case语句添加到get_binary_op函数(约750行):

void zend_compile_binary_op(znode *result, zend_ast *ast) /* {{{ */
{
    zend_ast *left_ast = ast->child[0];
    zend_ast *right_ast = ast->child[1];
    uint32_t opcode = ast->attr;

    znode left_node, right_node;
    zend_compile_expr(&left_node, left_ast);
    zend_compile_expr(&right_node, right_ast);

    if (left_node.op_type == IS_CONST && right_node.op_type == IS_CONST) {
        if (zend_try_ct_eval_binary_op(&result->u.constant, opcode,
                &left_node.u.constant, &right_node.u.constant)
        ) {
            result->op_type = IS_CONST;
            zval_ptr_dtor(&left_node.u.constant);
            zval_ptr_dtor(&right_node.u.constant);
            return;
        }
    }

    do {
        // redacted code
        zend_emit_op_tmp(result, opcode, &left_node, &right_node);
    } while (0);
}
/* }}} */
登录后复制

现在我们必须定义range_function函数。这将在Zend/zend_operators.c文件中与所有其他运算符一起完成:

static inline zend_bool zend_try_ct_eval_binary_op(zval *result, uint32_t opcode, zval *op1, zval *op2) /* {{{ */
{
    binary_op_type fn = get_binary_op(opcode);

    /* don't evaluate division by zero at compile-time */
    if ((opcode == ZEND_DIV || opcode == ZEND_MOD) &&
        zval_get_long(op2) == 0) {
        return 0;
    } else if ((opcode == ZEND_SL || opcode == ZEND_SR) &&
        zval_get_long(op2)      return 0;
    }

    fn(result, op1, op2);
    return 1;
}
/* }}} */
登录后复制

函数原型包含两个新的宏:ZEND_API和ZEND_FASTCALL。ZEND_API用于通过使函数可用于编译为共享对象的扩展来控制函数的可见性。ZEND_FASTCALL用于确保使用更高效的调用约定,其中前两个参数将使用寄存器而不是堆栈传递(对于x86上的64位构建比32位构建更相关)。

函数体与我们在上一篇文章中的Zend/zend_vm_def.h文件中所拥有的非常相似。VM特定的内容不再存在,包括HANDLE_EXCEPTION宏调用(已替换为return FAILURE;),并且ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION宏调用已被完全删除(此检查和操作需要保留在VM中,因此宏将稍后从VM代码中调用)。此外,如前所述,我们避免使用GET_OPn_ZVAL_PTR伪宏(而不是GET_OPn_ZVAL_PTR_DEREF)在VM中处理引用。

另一个值得注意的区别是我们正在对两个操作数应用ZVAL_DEFEF以确保正确处理引用。这以前是在VM内部使用伪宏GET_OPn_ZVAL_PTR_DEREF完成的,但现在已转移到此函数中。这样做不是因为它在编译时需要(因为对于编译时处理,两个操作数都必须是字面量,并且它们不能被引用),而是因为它使代码库中的其他位置能够安全地调用range_function,而无需担心引用处理。因此,大多数运算符函数(除了性能至关重要的地方)都执行引用处理,而不是在其VM操作码定义中执行。

最后,我们必须将range_function原型添加到Zend/zend_operators.h文件:

// 作为常量定义
const AN_ARRAY = 1 |> 100;

// 作为初始属性定义
class A
{
    private $a = 1 |> 2;
}

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}
登录后复制
登录后复制
登录后复制

更新Zend虚拟机

现在我们必须再次更新Zend虚拟机以在运行时处理ZEND_RANGE操作码的执行。将以下代码放在Zend/zend_vm_def.h(底部):

<st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}
登录后复制
登录后复制
登录后复制

(同样,操作码编号必须比当前最高操作码编号大一,这可以在Zend/zend_vm_opcodes.h文件的底部看到。)

这次的定义要短得多,因为所有工作都在range_function中处理。我们只需调用此函数,传入当前opline的结果操作数即可保存计算值。从range_function中删除的异常检查和跳到下一个操作码仍在VM中由对ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION的调用处理。此外,如前所述,我们避免使用GET_OPn_ZVAL_PTR伪宏(而不是GET_OPn_ZVAL_PTR_DEREF)在VM中处理引用。

现在通过执行Zend/zend_vm_gen.php文件重新生成VM。

最后,漂亮打印机需要再次更新Zend/zend_ast.c文件。通过指定新运算符的优先级为170来更新优先级表注释(约520行):

%token T_RANGE           "|> (T_RANGE)"
登录后复制
登录后复制

然后,在zend_ast_export_ex函数中插入一个case语句,以在ZEND_AST_BINARY_OP case语句中处理ZEND_RANGE操作码(约1300行):

%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP T_RANGE
登录后复制
登录后复制

结论

本文展示了一种实现范围运算符的替代方法,其中计算逻辑已从VM中移出。这具有能够在常量表达式上下文中使用范围运算符的优点。

本系列文章的第三部分将在此实现的基础上构建,介绍如何重载此运算符。这将允许对象用作操作数(例如来自GMP库的对象或实现__toString方法的对象)。它还将展示如何为字符串添加适当的支持(不像PHP当前范围函数中看到的支持)。但就目前而言,我希望这能很好地演示ZE在将运算符实现到PHP中时的一些更深层次的方面。

以上是重新实现PHP中的范围运算符的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

<🎜>:泡泡胶模拟器无穷大 - 如何获取和使用皇家钥匙
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系统,解释
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆树的耳语 - 如何解锁抓钩
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1670
14
CakePHP 教程
1428
52
Laravel 教程
1329
25
PHP教程
1276
29
C# 教程
1256
24
说明PHP中的安全密码散列(例如,password_hash,password_verify)。为什么不使用MD5或SHA1? 说明PHP中的安全密码散列(例如,password_hash,password_verify)。为什么不使用MD5或SHA1? Apr 17, 2025 am 12:06 AM

在PHP中,应使用password_hash和password_verify函数实现安全的密码哈希处理,不应使用MD5或SHA1。1)password_hash生成包含盐值的哈希,增强安全性。2)password_verify验证密码,通过比较哈希值确保安全。3)MD5和SHA1易受攻击且缺乏盐值,不适合现代密码安全。

PHP和Python:比较两种流行的编程语言 PHP和Python:比较两种流行的编程语言 Apr 14, 2025 am 12:13 AM

PHP和Python各有优势,选择依据项目需求。1.PHP适合web开发,尤其快速开发和维护网站。2.Python适用于数据科学、机器学习和人工智能,语法简洁,适合初学者。

PHP行动:现实世界中的示例和应用程序 PHP行动:现实世界中的示例和应用程序 Apr 14, 2025 am 12:19 AM

PHP在电子商务、内容管理系统和API开发中广泛应用。1)电子商务:用于购物车功能和支付处理。2)内容管理系统:用于动态内容生成和用户管理。3)API开发:用于RESTfulAPI开发和API安全性。通过性能优化和最佳实践,PHP应用的效率和可维护性得以提升。

PHP类型提示如何起作用,包括标量类型,返回类型,联合类型和无效类型? PHP类型提示如何起作用,包括标量类型,返回类型,联合类型和无效类型? Apr 17, 2025 am 12:25 AM

PHP类型提示提升代码质量和可读性。1)标量类型提示:自PHP7.0起,允许在函数参数中指定基本数据类型,如int、float等。2)返回类型提示:确保函数返回值类型的一致性。3)联合类型提示:自PHP8.0起,允许在函数参数或返回值中指定多个类型。4)可空类型提示:允许包含null值,处理可能返回空值的函数。

PHP的持久相关性:它还活着吗? PHP的持久相关性:它还活着吗? Apr 14, 2025 am 12:12 AM

PHP仍然具有活力,其在现代编程领域中依然占据重要地位。1)PHP的简单易学和强大社区支持使其在Web开发中广泛应用;2)其灵活性和稳定性使其在处理Web表单、数据库操作和文件处理等方面表现出色;3)PHP不断进化和优化,适用于初学者和经验丰富的开发者。

PHP和Python:解释了不同的范例 PHP和Python:解释了不同的范例 Apr 18, 2025 am 12:26 AM

PHP主要是过程式编程,但也支持面向对象编程(OOP);Python支持多种范式,包括OOP、函数式和过程式编程。PHP适合web开发,Python适用于多种应用,如数据分析和机器学习。

您如何防止PHP中的SQL注入? (准备的陈述,PDO) 您如何防止PHP中的SQL注入? (准备的陈述,PDO) Apr 15, 2025 am 12:15 AM

在PHP中使用预处理语句和PDO可以有效防范SQL注入攻击。1)使用PDO连接数据库并设置错误模式。2)通过prepare方法创建预处理语句,使用占位符和execute方法传递数据。3)处理查询结果并确保代码的安全性和性能。

PHP和Python:代码示例和比较 PHP和Python:代码示例和比较 Apr 15, 2025 am 12:07 AM

PHP和Python各有优劣,选择取决于项目需求和个人偏好。1.PHP适合快速开发和维护大型Web应用。2.Python在数据科学和机器学习领域占据主导地位。

See all articles