Entendendo a estrutura zval e vetores - Desenvolvendo Extensões PHP Parte 06

No post anterior trabalhamos com parâmetros e retornos de uma extensão PHP, porem faltou falar um pouco sobre um recurso poderoso do PHP, que são os vetores. Trabalhar com vetores em C na mesma maneira que PHP pode ser algo extremamente complexo, pois temos índices numéricos e literais, de tipos diferentes no mesmo vetor, e graças a API Zend, isso se torna um trabalho tranquilo.

A primeira coisa que temos que fazer, é declarar nossas informações de argumento, e para isso usamos o
ZEND_ARG_ARRAY_INFO
:
ZEND_BEGIN_ARG_INFO_EX(arginfo_myext_test, 0, 0, 1)
 ZEND_ARG_ARRAY_INFO(0, vetor, 0)
ZEND_END_ARG_INFO();
Assim, dizemos ao nosso core que a função receberá um vetor como parâmetro:
$ /opt/php8/bin/php -d extension=./modules/myext.so --re myext
Extension [ <persistent> extension #9 myext version 0.0.1 ] {

 - Functions {
 Function [ <internal:myext> function myext_hello ] {

 - Parameters [1] {
 Parameter #0 [ <required> $name ]
 }
 }
 Function [ <internal:myext> function myext_test ] {

 - Parameters [1] {
 Parameter #0 [ <required> array $vetor ]
 }
 }
 }
}
Note que a função
myext_test
já está recebendo um parâmetro do tipo
array
, agora falta receber o parâmetro dentro de nossa função, e para isso usamos o
Z_PARAM_ARRAY
:
zval *vetor = NULL;

ZEND_PARSE_PARAMETERS_START(1, 1)
 Z_PARAM_ARRAY(vetor)
ZEND_PARSE_PARAMETERS_END();
Caso precise relembrar as macros, não deixe de buscar as referencias no post anterior.

Notou que colocamos nosso vetor numa variável do tipo
zval
? é aqui que o doce azeda, isso por que essa estrutura é responsável pela baixa tipagem do PHP. Ela armazena o nome, tipo e o valor em cada campo correspondente ao seu tipo.

Entendendo o zval

Basicamente é uma estrutura muito simples e muito compreensível, o que complica é que é muito fácil de se perder nessa estrutura. Ela é uma estrutura que possui o valor definida em outra estrutura chamada
zend_value
, e elas são definidas resumidamente como:
typedef union _zend_value {
 zend_long lval; /* long value */
 double dval; /* double value */
 zend_refcounted *counted;
 zend_string *str;
 zend_array *arr;
 zend_object *obj;
 zend_resource *res;
 zend_reference *ref;
 zend_ast_ref *ast;
 zval *zv;
 void *ptr;
 zend_class_entry *ce;
 zend_function *func;
 struct {
 uint32_t w1;
 uint32_t w2;
 } ww;
} zend_value;

typedef struct _zval_struct {
 zend_value value;
 zend_uchar type;
 zend_uchar type_flags;
 uint16_t extra;
 uint32_t reserved;
} zval;
Apesar de parecer complexa, veja que é bem simples.
zval
define o tipo, flags necessários e a estrutura do valor. Então se
zval->type == IS_LONG
,
zend_value
retornará o campo
lval
. Se o
zval->type == IS_STRING
, o
zval->value
retornará o campo
*str
. Não tem nada de complicado certo?

Então para trabalharmos com
array
, cada posição do vetor é um
zval
, assim cada posição pode ser de um tipo.

O que azeda, pelo menos para mim, é que a cada versão
zval
fica mais simples de ser manipulado, e as referencias que procuramos geralmente são antigas e complexas.
zval
é uma estrutura linda, muito bem pensada e definida, mas a documentação do internals é muito vaga, e os escritores escrevem em seu tempo, tornando as referencias antigas. Por exemplo, esse meu texto pode ser muito complexo quando existir o PHP 9 ou 10.

Como você notou, podemos comparar o tipo do
zval
usando a macro
Z_TYPE
:
if(Z_TYPE(zval_var) == IS_STRING) {
 
}
else if(Z_TYPE(zval_var) == IS_LONG) {
 
}
Temos as seguintes constantes definidas:
IS_UNDEF
IS_NULL
IS_FALSE
IS_TRUE
IS_LONG
IS_DOUBLE
IS_STRING
IS_ARRAY
IS_OBJECT
IS_RESOURCE
IS_REFERENCE
IS_CONSTANT_AST
IS_CALLABLE
IS_ITERABLE
IS_VOID
IS_STATIC
IS_MIXED
E para receber o valor, devemos usar as macros, algo como:
if(Z_TYPE(zval_var) == IS_STRING) {
 php_printf("Valor: %s\n", Z_STRVAL(zval_var));
}
else if(Z_TYPE(zval_var) == IS_LONG) {
 php_printf("Valor: %ld\n", Z_LVAL(zval_var));
}

usamos
Z_STRVAL
e
Z_LVAL
nos exemplos, mas temos as macros:
Z_LVAL
Z_DVAL
Z_STRVAL
Z_STRLEN
Z_STRHASH
Z_ARRVAL
Z_OBJ
Z_OBJ_HANDLER
Z_OBJCE
Z_RES
Z_RES_HANDLE
Z_RES_TYPE
Z_REFVAL
Z_FUNC
Z_PTR
Toda macro que trabalha com
zval
possui macros correspondentes para ponteiros com sufixo
_P
, então trabalhamos assim:
// Sem ponteiro
zval var1;
if(Z_TYPE(var1) == IS_STRING) {
 php_printf("valor: %s\n", Z_STRVAL(var1));
}

// Com ponteiro
zval *var2;
if(Z_TYPE_P(var2) == IS_STRING) {
 php_printf("valor: %s\n", Z_STRVAL_P(var2));
}
Então se a gente quiser percorrer todos os valores de nosso vetor, fazemos um for nas posições:
// Cria a função myext_test
PHP_FUNCTION(myext_test)
{
 zval *vetor = NULL;
 
 // Recupera os parametros
 ZEND_PARSE_PARAMETERS_START(1, 1)
 Z_PARAM_ARRAY(vetor)
 ZEND_PARSE_PARAMETERS_END();


 int vetor_size = (int)zend_array_count(Z_ARRVAL_P(vetor));

 php_printf("Tamanho do vetor: %d\n", vetor_size);

 for (int i = 0; i < vetor_size; i++) {
 zval *vetor_elemento = zend_hash_index_find_deref(Z_ARRVAL_P(vetor), i);

 // Valor
 if(Z_TYPE_P(vetor_elemento) == IS_STRING) {
 php_printf("Valor: %s\n", Z_STRVAL_P(vetor_elemento));
 }
 else if(Z_TYPE_P(vetor_elemento) == IS_LONG) {
 php_printf("Valor: %ld\n", Z_LVAL_P(vetor_elemento));
 }
 }
}
Podemos testar nossa função com o código:
<?php

myext_test(["valor 1", "valor 2", 10]);
E teremos o resultado:
$ /opt/php8/bin/php -d extension=./modules/myext.so test.php 
Tamanho do vetor: 3
Valor: valor 1
Valor: valor 2
Valor: 10
Esse código não vai funcionar se a gente tiver uma string como index. Veja que no meu exemplo, as posições do vetor são 0, 1 e 2. Não precisamos tratar quando o vetor é uma string ou um long, pois a Zend Engine novamente nos ajuda nessa tarefa:
// Cria a função myext_test
PHP_FUNCTION(myext_test)
{
 zval *vetor = NULL;
 
 // Recupera os parametros
 ZEND_PARSE_PARAMETERS_START(1, 1)
 Z_PARAM_ARRAY(vetor)
 ZEND_PARSE_PARAMETERS_END();

 // Mostra o tamanho do vetor
 php_printf("Tamanho do vetor: %d\n\n", zend_hash_num_elements(Z_ARR_P(vetor)));

 // Faz um foreach no vetor
 ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(vetor), zend_ulong long_key, zend_string *str_key, zval *val)

 // Verifica se o index é uma string ou um long
 if(!str_key) {
 php_printf("Index: %ld\n", long_key);
 }
 else {
 php_printf("Index: %s\n", str_key->val);
 }

 // Printa o valor dependendo do tipo
 if(Z_TYPE_P(val) == IS_STRING) {
 php_printf("Valor: %s\n", Z_STRVAL_P(val));
 }
 else if(Z_TYPE_P(val) == IS_LONG) {
 php_printf("Valor: %ld\n", Z_LVAL_P(val));
 }

 php_printf("\n");

 ZEND_HASH_FOREACH_END();
}
Notou as macros
ZEND_HASH_FOREACH_KEY_VAL
e
ZEND_HASH_FOREACH_END
? Essas belezinhas nos deixam trabalhar exatamente como no PHP, recebendo o index se for
long
ou
string
, e o valor do index. Se a gente testar agora com o código:
<?php

myext_test(["valor 1", "valor 2", 'Meu Index'=>10]);
Temos a seguinte saída:
$ /opt/php8/bin/php -d extension=./modules/myext.so test.php 
Tamanho do vetor: 3

Index: 0
Valor: valor 1

Index: 1
Valor: valor 2

Index: Meu Index
Valor: 10
Próximo post quero fazer mais alguns exemplo de situações reais com vetores antes de passar a trabalhar com objetos, por que trabalhar com vetor é algo muito comum e muito fácil de se perder.
Pessoal, gostaria de deixar um apelo ai para darem uma força. compartilhem, comentem, deixe seu comentário no post. quem escreve ou faz vídeo, muitas vezes nem faz isso por dinheiro, mas sim para engajar a comunidade de alguma forma. não gostou da forma que eu escrevo? comenta na boa (nem eu gosto também), mas comenta algo, manda pro brodi, bora crescer e centralizar um conteúdo de qualidade
Referencias: https://github.com/php/php-src/blob/master/Zend/zend_types.h, https://github.com/php/php-src/blob/master/Zend/zend_API.h https://github.com/php/php-src/blob/master/Zend/zend.h, https://github.com/php/php-src/blob/master/Zend/zend_hash.h