Maison >développement back-end >tutoriel php >Remplémentation de l'opérateur de plage en PHP

Remplémentation de l'opérateur de plage en PHP

Christopher Nolan
Christopher Nolanoriginal
2025-02-15 09:36:12222parcourir

SitePoint Merveilleux Article Recommandation: Amélioration de la mise en œuvre de l'opérateur de la plage PHP

Cet article est reproduit sur SitePoint avec l'autorisation de l'auteur. Le contenu suivant est écrit par Thomas Punt et introduit la méthode de mise en œuvre améliorée de l'opérateur de plage PHP. Si vous êtes intéressé par les internes PHP et que vous ajoutez des fonctionnalités à vos langages de programmation préférés, c'est le bon moment pour apprendre!

Cet article suppose que les lecteurs peuvent construire PHP à partir du code source. Si ce n'est pas le cas, veuillez d'abord lire le chapitre "Building PHP" du livre du mécanisme interne PHP.

Re-Implementing the Range Operator in PHP


Dans l'article précédent (astuce: assurez-vous de l'avoir lu), j'ai montré un moyen d'implémenter les opérateurs de plage dans PHP. Cependant, les implémentations initiales sont rarement les meilleures, donc cet article vise à explorer comment améliorer les implémentations précédentes.

Merci encore Nikita Popov pour avoir relu cet article!

points clés

  • Thomas Punt réimplique l'opérateur de plage en PHP, en déplaçant la logique de calcul hors de la machine virtuelle Zend, permettant d'utiliser des opérateurs de plage dans le contexte d'expressions constantes.
  • Cette réimplémentation peut être calculée au moment de la compilation (pour les opérandes littéraux) ou au moment de l'exécution (pour les opérandes dynamiques). Cela apporte non seulement un peu d'avantages aux utilisateurs d'Opcache, mais permet également d'utiliser des fonctionnalités d'expression constantes avec les opérateurs de plage.
  • Le processus de réimplémentation implique la mise à jour de la machine virtuelle Lexer, l'analyseur, la phase de compilation et la machine virtuelle Zend. La mise en œuvre de l'analyseur lexical reste la même, tandis que l'implémentation de l'analyseur est la même que la partie précédente. La phase de compilation ne nécessite pas de mise à jour du fichier zend / zend_compile.c, car il contient déjà la logique nécessaire pour gérer les opérations binaires. La machine virtuelle Zend a été mise à jour pour gérer l'exécution de l'opcode Zend_Range à l'exécution.
  • Dans la troisième partie de cette série, Punt prévoit de créer cette implémentation en expliquant comment surcharger cet opérateur. Cela permettra à l'objet d'être utilisé comme opérandes et ajoutera une prise en charge appropriée à la chaîne.

Inconvénients des implémentations précédentes

L'implémentation initiale met toute la logique de l'opérateur de plage dans la machine virtuelle Zend, ce qui oblige le calcul à effectuer uniquement au moment de l'exécution lors de l'exécution de l'opcode Zend_Range. Cela signifie non seulement que pour les opérandes littéraux, les calculs ne peuvent pas être transférés pour compiler le temps, mais signifie également que certaines fonctions ne fonctionnent tout simplement pas.

Dans cette implémentation, nous déplacons la logique de l'opérateur de plage hors de la machine virtuelle Zend pour pouvoir effectuer des calculs au moment de la compilation (pour les opérandes littéraux) ou l'exécution (pour les opérandes dynamiques). Cela apporte non seulement un peu d'avantages aux utilisateurs d'Opcache, mais plus important encore, permet d'utiliser des fonctionnalités d'expression constantes avec les opérateurs de plage.

Exemple:

<code class="language-php">// 作为常量定义
const AN_ARRAY = 1 |> 100;

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

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}</code>

Donc, sans plus tarder, réimplémentons l'opérateur de plage.

Mettre à jour l'analyseur lexical

La mise en œuvre de l'analyseur lexical reste complètement inchangée. Le jeton est d'abord enregistré dans Zend / Zend_Language_Scanner.L (environ 1200 lignes):

<code class="language-c"><st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}</code>

alors déclarez en zend / zend_language_parser.y (environ 220 lignes):

<code class="language-php">// 作为常量定义
const AN_ARRAY = 1 |> 100;

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

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}</code>

L'extension de tokenizer doit être régénérée à nouveau en entrant le répertoire EXT / Tokenizer et en exécutant le fichier tokenizer_data_gen.sh.

Mettre à jour l'analyse

L'implémentation de l'analyseur est la même qu'auparavant. Encore une fois, nous déclarons la priorité et la liaison de l'opérateur en ajoutant le jeton T_Range à la fin de la ligne suivante:

<code class="language-c"><st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}</code>

Ensuite, nous mettons à jour les règles de production expr_without_variable, mais cette fois, l'action sémantique (code à l'intérieur des accolades) sera légèrement différente. Mettez-le à jour avec le code suivant (je le mets sous la règle T_Spaceship, environ 930 lignes):

<code class="language-c">%token T_RANGE           "|> (T_RANGE)"</code>

Cette fois, nous avons utilisé la fonction zend_ast_create_binary_op (plutôt que la fonction zend_ast_create), qui a créé un nœud zend_ast_binary_op pour nous. zend_ast_create_binary_op prend un nom d'opcode qui sera utilisé pour distinguer les opérations binaires pendant la phase de compilation.

Étant donné que nous réutilisons maintenant le type de nœud zend_ast_binary_op, il n'est pas nécessaire de définir un nouveau type de nœud zend_ast_range comme auparavant dans le fichier zend / zend_ast.h.

Mettre à jour la phase de compilation

Cette fois, il n'est pas nécessaire de mettre à jour le fichier zend / zend_compile.c, car il contient déjà la logique nécessaire pour gérer les opérations binaires. Nous avons donc juste besoin de réutiliser cette logique en définissant notre opérateur sur le nœud zend_ast_binary_op.

Ce qui suit est une version simplifiée de la fonction zend_compile_binary_op:

<code class="language-c">%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP T_RANGE</code>

Comme nous pouvons le voir, il est très similaire à la fonction Zend_Compile_Range que nous avons créée la dernière fois. Les deux différences importantes sont de savoir comment obtenir le type d'opcode et ce qui se passe lorsque les deux opérandes sont des littéraux.

Le type d'opcode est pris ce temps à partir du nœud AST (plutôt que de codé en dur comme la dernière fois), car le nœud zend_ast_binary_op stocke cette valeur (comme le montre l'action sémantique de la nouvelle règle de production) pour distinguer les opérations binaires. Lorsque les deux opérandes sont des littéraux, la fonction ZEND_TRY_CT_EVAL_BINALY_OP est appelée. Cette fonction ressemble à ceci:

<code class="language-c">    |   expr T_RANGE expr
            { $$ = zend_ast_create_binary_op(ZEND_RANGE, , ); }</code>

Cette fonction obtient un rappel de la fonction get_binary_op (code source) dans zend / zend_opcode.c en fonction du type d'opcode. Cela signifie que nous devons mettre à jour cette fonction à côté de l'adaptation de l'opcode Zend_Range. Ajoutez l'instruction de cas suivante à la fonction get_binary_op (environ 750 lignes):

<code class="language-c">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);
}
/* }}} */</code>

Maintenant, nous devons définir la fonction Range_Function. Cela se fera dans le fichier Zend / Zend_Operators.C avec tous les autres opérateurs:

<code class="language-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;
}
/* }}} */</code>

Le prototype de fonction contient deux nouvelles macros: zend_api et zend_fastcall. Zend_API est utilisé pour contrôler la visibilité d'une fonction en la mettant à la disposition de la compilation en extension d'un objet partagé. Zend_fastCall est utilisé pour garantir que des conventions d'appels plus efficaces sont utilisées, où les deux premiers paramètres seront passés dans des registres au lieu de piles (plus pertinents pour les constructions 64 bits sur x86 que pour les versions 32 bits).

Le corps de fonction est très similaire à ce que nous avons dans le fichier Zend / Zend_VM_DEF.h dans l'article précédent. Le contenu spécifique à la machine virtuelle n'existe plus, y compris l'appel macro handle_exception (remplacé par une défaillance de retour;), et l'appel de macro zend_vm_next_opcode_check_exception a été complètement supprimé (cette vérification et cette opération doivent être conservées dans la machine virtuelle, donc la macro sera appelée plus tard du code VM). De plus, comme mentionné précédemment, nous évitons d'utiliser le GET_OPN_ZVAL_PTR Pseudo-Macro (plutôt que le get_opn_zval_ptr_deref) pour traiter les références dans la machine virtuelle.

Une autre différence notable est que nous appliquons ZVAL_DEFF aux deux opérandes pour nous assurer que les références sont traitées correctement. Cela a déjà été fait à l'aide du pseudo-macro get_opn_zval_ptr_deref à l'intérieur de la machine virtuelle, mais a maintenant été transféré à cette fonction. Cela n'est pas fait car il doit être compilé à (car pour le traitement du temps de compilation, les deux opérandes doivent être des littéraux et ils ne peuvent pas être référencés), mais parce qu'il permet à Range_Function d'être appelé en toute sécurité ailleurs dans la base de code, sans se soucier du traitement de référence . Par conséquent, la plupart des fonctions de l'opérateur (sauf lorsque les performances sont critiques) effectuent un traitement de référence, plutôt que dans leurs définitions OPCode VM.

Enfin, nous devons ajouter le prototype Range_Function au fichier Zend / Zend_Operators.h:

<code class="language-php">// 作为常量定义
const AN_ARRAY = 1 |> 100;

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

// 作为可选参数的默认值:
function a($a = 1 |> 2)
{
    //
}</code>

Mettre à jour la machine virtuelle Zend

Maintenant, nous devons à nouveau mettre à jour la machine virtuelle Zend pour gérer l'exécution de l'opcode Zend_Range au moment de l'exécution. Mettez le code suivant dans zend / zend_vm_def.h (en bas):

<code class="language-c"><st_in_scripting>"|>" {
</st_in_scripting>    RETURN_TOKEN(T_RANGE);
}</code>

(encore une fois, le numéro d'opcode doit être un plus grand que le numéro d'opcode le plus élevé actuel, qui peut être vu en bas du fichier zend / zend_vm_opcodes.h.)

La définition de cette fois est beaucoup plus courte, car tout le travail est géré dans Range_Function. Nous devons simplement appeler cette fonction et passer dans l'opérande résultat de l'opline actuelle pour enregistrer la valeur calculée. Les vérifications des exceptions supprimées de Range_Function et le passage à l'opcode suivant sont toujours traitées dans la machine virtuelle par un appel à zend_vm_next_opcode_check_exception. De plus, comme mentionné précédemment, nous évitons d'utiliser le GET_OPN_ZVAL_PTR Pseudo-Macro (plutôt que le get_opn_zval_ptr_deref) pour traiter les références dans la machine virtuelle.

régénérez maintenant la machine virtuelle en exécutant le fichier Zend / Zend_VM_Gen.php.

Enfin, la belle imprimante doit à nouveau mettre à jour le fichier zend / zend_ast.c. Mettez à jour le commentaire de la table prioritaire (environ 520 lignes):

<code class="language-c">%token T_RANGE           "|> (T_RANGE)"</code>

Ensuite, insérez une instruction de cas dans la fonction zend_ast_export_ex pour traiter l'opcode Zend_Range (environ 1300 lignes):

<code class="language-c">%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP T_RANGE</code>

Conclusion

Cet article montre une alternative à la mise en œuvre des opérateurs de plage, où la logique de calcul a été déplacée de la machine virtuelle. Cela a l'avantage de pouvoir utiliser des opérateurs de gamme dans le contexte d'expressions constantes.

La troisième partie de cette série d'articles sera construite sur cette implémentation, expliquant comment surcharger cet opérateur. Cela permettra à des objets d'être utilisés comme opérandes (tels que des objets des bibliothèques GMP ou des objets qui implémentent les méthodes de __tostring). Il montrera également comment ajouter approprié le support aux chaînes (contrairement à celles observées dans les fonctions de plage actuelle de PHP). Mais pour l'instant, j'espère que c'est une bonne démonstration de certains aspects plus profonds de la ZE lors de la mise en œuvre des opérateurs en PHP.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn