Back-End

14 abr, 2016

Como criar uma extensão PHP C para manipular arrays – Parte 01: extensão básica de uma classe array

726 visualizações
Publicidade

Em projetos PHP, arrays são usados ​​em todos os lugares, porque são úteis e flexíveis para armazenar todos os tipos de estruturas de dados.

No entanto, quando você precisa maximizar o desempenho da manipulação de arrays para fins específicos, você pode conseguir grandes ganhos se implementar uma extensão PHP escrita na linguagem C.

Leia este artigo para aprender como construir sua própria extensão para manipulação básica de array em C.

Introdução

Nesta parte do artigo, vamos considerar o exemplo de uma classe array simples e todos os conceitos básicos que você precisa saber sobre a forma de construir uma extensão para acessá-la.

Vamos começar com uma lista de recursos úteis à qual faremos referências:

Eu escrevi o código no Ubuntu Linux, então, para outras distribuições Linux (e OS X) você pode precisar de alterações mínimas para fazê-lo funcionar, como a mudança de apt-get para rpm ou qualquer seja o pacote instalado utilizado na sua distribuição. Se você quer escrever no Windows, precisa fazer mais mudanças não previstas neste artigo.

Construindo PHP direto da fonte

Para começar, você precisa baixar e instalar o código-fonte do PHP para que você possa compilá-lo com suporte de depuração habilitada. Você certamente pode escrever uma extensão com a versão PHP regular com suporte de depuração desativada, mas a definição de sinalizadores de depuração torna-se mais útil para entender e corrigir quaisquer problemas que você tem em seu código de extensão C.

Então abra o seu console shell, vá para o diretório onde você vai obter o código-fonte do PHP, por exemplo ~/dev/c/), e recupere o código PHP do seu repositório Git.

git clone http://git.php.net/repository/php-src.git
cd php-src

Recupere a última versão do código-fonte do PHP 5.

git checkout PHP-5.6

Instale alguns requerimentos para construir ferramentas se você não os tiver instalados.

sudo apt-get install build-essential autoconf automake libtool

Agora temos que instalar o bison. A versão 14 do Ubuntu vem com bison versão 3, e o PHP não lida bem com isso. Precisamos da versão 2.7.

wget http://launchpadlibrarian.net/ 140087283/libbison-dev_2.7.1.dfsg-1_amd64.deb
wget http://launchpadlibrarian.net/ 140087282/bison_2.7.1.dfsg-1_amd64.deb

sudo dpkg -i libbison-dev_2.7.1.dfsg-1_amd64.deb
sudo dpkg -i bison_2.7.1.dfsg-1_amd64.deb

Como vamos construir PHP sem extensões por padrão, não precisamos do libxml2. Caso contrário, você precisa instalar a biblioteca libxml2-dev também.

sudo apt-get install libxml2-dev

Configure o PHP build para habilitar o suporte de depuração e sem quaisquer extensões. O parâmetro –prefix especifica o diretório no qual vamos instalar o PHP.

./buildconf
./configure --disable-all --enable-debug --prefix=$HOME/dev/bin/php
make && make install

Ok, o PHP está pronto para ser executado. Execute com o sinalizador -v e certifique-se de que nós construímos o que você precisa e onde você quer.

~/dev/bin/php/bin/php -v

Construindo uma extensão PHP

Esqueletos da extensão podem ser rapidamente geradas usando ext_skel, que se encontra no diretório de código de origem PHP. Nós não iremos usar ext_skel, porque ele está cheio de centenas de comentários desnecessários nos arquivos gerados.

Se você ainda quer usar ext_skel, então precisa executá-lo com os seguintes parâmetros: –extname para especificar o nome da extensão, e caminho para a pasta esqueleto usando –skel.

~/dev/c/php-src/ext/ext_skel --extname=slobel --skel=$HOME/dev/c/php-src/ext/skeleton/

De qualquer forma, você deve ter um diretório com os seguintes arquivos:

slobel/
    .gitignore
    config.m4
    config.w32
    slobel.c
    php_slobel.h

Abra o arquivo config.m4 e digite:

if test "$PHP_SLOBEL" = "yes"; then
  AC_DEFINE(HAVE_SLOBEL, 1, [Whether you have Slobel])
  PHP_NEW_EXTENSION(slobel, slobel.c, $ext_shared)
fi

Indo mais longe no config.m4, só vamos tocar a linha com PHP_NEW_EXTENSION, adicionando aos novos arquivos.

Agora vamos escrever a nossa principal extensão de arquivo de cabeçalho: php_slobel.h. Ela deve necessariamente ser chamada de php_%nameextension%.h

#ifndef PHP_SLOBEL_H
#define PHP_SLOBEL_H 1

extern zend_module_entry slobel_module_entry;
#define phpext_slobel_ptr &slobel_module_entry

//If we compile a thread-safe version, connect the appropriate header file.
#ifdef ZTS
#include "TSRM.h"
#endif

#endif

Nesse arquivo, declaramos uma variável de tipo zend_module_entry com informações sobre nossa expansão. O nome da variável deve ser %nameextension%_module_entry.

Abra slobel.c  e escreva o código como a seguir:

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_slobel.h"

// Define a test function.
PHP_FUNCTION(hello_from_slobel)
{
    // The second parameter specifies that we want to copy the string in memory.
    RETURN_STRING("SLOBEL ENABLED! YEY!", 1);
}

// We give PHP aware of our function, indicating its function table module.
const zend_function_entry slobel_functions[] = {
    PHP_FE(hello_from_slobel,  NULL)
    PHP_FE_END
};

// We define a function that will cause php when connecting our expansion.
PHP_MINIT_FUNCTION( slobel_init )
{
    return SUCCESS;
}


zend_module_entry slobel_module_entry = {
    STANDARD_MODULE_HEADER,
    "slobel", // the name of the extension.
    slobel_functions,
    PHP_MINIT(slobel_init),
    NULL, // MSHUTDOWN
    NULL, // RINIT
    NULL, // RSHUTDOWN
    NULL, // MINFO
    "0.1", //version of the extension.
    STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_SLOBEL
ZEND_GET_MODULE(slobel)
#endif

A questão principal aqui é a definição de uma variável com informações sobre esse módulo no qual temos função tabela, iniciar a função e outros dados necessários, ou a configuração, nesse caso, de campos NULL não específicos. A macro ZEND_GET_MODULE apenas cria a função get_module para a nossa biblioteca, que retorna a variável slobel_module_entry.

Ok, agora estamos prontos para construir nossa extensão. Execute phpize, que irá gerar para nós os arquivos de configuração para as configurações do garbage collector para a expansão.

~/dev/bin/php/bin/phpize

E então construa a extensão. A opção de arquivo –with-php-config especifica o caminho para o comando php-config que temos quando compilamos o PHP com suporte para depuração habilitada.

./configure --with-php-config= $HOME/dev/bin/php/bin/php-config
make && make install

Se tudo for construído sem erros, em seguida execute com o PHP com extensão. Se não, o código ainda será executado.

~/dev/bin/php/bin/php -dextension=slobel.so --r "hello_from_slobel();"
SLOBEL ENABLED! YEY!

Breve introdução a zval e funções

Antes de chegarmos a classes, vamos dar uma breve olhada no que funções e variáveis do ​​PHP fornecem.

Para declarar uma função, use a macro PHP_FUNCTION, PHP_NAMED_FUNCTION ou PHP_METHOD. A única diferença é o nome da função resultante.

void prefix_name(int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used void ***tsrm_ls)

Onde:

  • ht – número de argumentos com que a função é chamada;
  • return_value – um ponteiro para uma variável para qual o resultado é escrito;
  • return_value_ptr – um ponteiro para um ponteiro para a variável de saída (no caso de você precisar retornar o resultado por referência);
  • this_ptr – um ponteiro para o objeto, se o método é chamado;
  • return_value_is_used – um sinalizador que indica se a variável é então reciclada;
  • tsrm_ls – Thread Safe Resourse Manager Local Storage! Um ponteiro para uma variável.

Argumentos de funções são definidos usando macros ZEND_ARG_INFO_ *.

//| name of the variable | _ | Does returns Link | number of mandatory variables |
ZEND_BEGIN_ARG_INFO_EX(arginfo_construct, 0, 0, 1)
//| Whether the link is passed | argument name |
ZEND_ARG_INFO(0, var1)
ZEND_ARG_INFO(0, var2)
ZEND_END_ARG_INFO()

/*
static const zend_arg_info arginfo_construct[] = {
    { NULL, 0, NULL, 2, 0, 0, 0, 0 },
    { "var1", sizeof("var1")-1, NULL, 0, 0, 0, 0, 0 },
    { "var2", sizeof("var2")-1, NULL, 0, 0, 0, 0, 0 },
}
*/

ZEND_BEGIN_ARG_INFO_EX, ZEND_ARG_INFO ZEND_END_ARG_INFO e irá resultar em um array de estruturas zend_arg_info. Além disso, o primeiro elemento do array é do tipo zend_internal_function_info.

Outras funções definidas pelas macros PHP_FE, PHP_ME, PHP_ME_MAPPING estão listadas na tabela de função dos elementos module/class, tais como zend_function_entry.

typedef struct _zend_function_entry {
    const char *fname; // Name functions available in PHP.
    void (*handler)(INTERNAL_FUNCTION_PARAMETERS); // A pointer to a function.
    const struct _zend_arg_info *arg_info; // A pointer to an array of arguments.
    zend_uint num_args; // the number of arguments (in the array pointed to arg_info).
    zend_uint flags; // Various flags.
} zend_function_entry

Quando você registrar um módulo, as funções são inseridas na tabela de função global (function_table). Quando você registrar uma classe, você adiciona recursos de classe para a tabela.

Para obter os argumentos usados ​​pela função zend_parse_parameters, que é chamada com os seguintes parâmetros.

  • num_args – o número de argumentos.
  • tsrm_ls – descrito acima.
  • type_spec – uma string que especifica os tipos de argumentos.
  • … – Mais listas de ponteiros para variáveis ​​onde serão gravados os argumentos resultantes.

Para trabalhar com variáveis ​​usando zval PHP, que armazena um valor em seu zvalue_value, seu tipo, a contagem de referência e uma flag indicando que ele é usado aqui.

struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};

Para alocar memória para zval, deve-se usar macros ALLOC_ZVAL (apenas aloca memória), MAKE_STD_ZVAL (ALLOC_ZVAL + valores de inicialização) ou outros.

Isso deve ser feito porque em vez zval ALLOC_ZVAL alocar memória para _zval_gc_info, que armazena mais informações para encontrar referências circulares.

Para remover um zval, é usada a função zval_ptr_dtor. Ao contrário, zval_dtor, zval_ptr_dtor primeiro diminui a contagem de referência e exclui zval somente se o contador se tornar zero.

Também é necessário considerar que, para todos os valores zvalue_value, números complexos são armazenados em ponteiros. Portanto, se você tem duas referências zval para a mesma string na memória, a remoção de uma delas não vai fazê-la apontar para um local de memória inválido.

Mais sobre o zval pode ser encontrado no livro PHP Internals, e sobre as referências circulares no Guia para PHP.

Definindo uma classe em nossa extensão

Voltando para a nossa extensão, vamos adicionar a primeira classe. Crie um arquivo e escreva no slobel_darray.h o seguinte:

#ifndef PHP_SLOBEL_DARRAY_H
#define PHP_SLOBEL_DARRAY_H 1

extern zend_class_entry *slobel_darray_ce;

void slobel_darray_init(TSRMLS_D);

#endif

Nós acabamos de declarar a variável de classe tipo slobel_darray_ce zend_class_entry e a função para inicializar.

Agora crie um arquivo slobel_darray.c.

#include "php.h"
#include "slobel_darray.h"

zend_class_entry *slobel_darray_ce;

PHP_METHOD(slobel_darray, sayHello)
{
    RETURN_STRING("Hello from darray!", 1);
}

const zend_function_entry slobel_darray_functions[] = {
    // class name, function name, arginfo, flags.
    PHP_ME(slobel_darray, sayHello, NULL, ZEND_ACC_PUBLIC)
    PHP_FE_END
};

void slobel_darray_init(TSRMLS_D)
{
    zend_class_entry tmp_ce;
    INIT_CLASS_ENTRY(tmp_ce, "SLOBEL\\DArray", slobel_darray_functions);

    slobel_darray_ce = zend_register_internal_class( &tmp_ce TSRMLS_CC);

    return;
}

Há apenas uma função interessante no slobel_darray_init. Primeiro criamos uma estrutura temporária para a nossa classe tmp_ce e a preenchemos com INIT_CLASS_ENTRY. O segundo parâmetro é o nome da classe, que será disponibilizado para PHP, incluindo o seu namespace.

Use a função zend_register_internal_class, que registra a nossa classe na tabela de classe (class_table).

Agora adicione a chamada de função para a função slobel_darray_init slobel_init (arquivo slobel.h).

PHP_MINIT_FUNCTION(slobel_init)
{
    slobel_darray_init(TSRMLS_C);
    return SUCCESS;
}

E adicione um novo arquivo no slobel_darray.c config.m4 (uma lista de arquivos especificada sem vírgulas).

PHP_NEW_EXTENSION( slobel, slobel.c slobel_darray.c, $ext_shared)

Como mudamos o arquivo config.m4, precisamos executar mais uma vez o phpize.

~/dev/bin/php/bin/phpize --clean
~/dev/bin/php/bin/phpize


./configure --with-php-config= $HOME/dev/bin/php/bin/php-config
make && make install

Fazemos um script php para testar a nossa extensão (nome original: slobel.php)

<?php
$darray = new \SLOBEL\DArray();

echo $darray->sayHello() . PHP_EOL;
?>

E execute o script com a nossa extensão:

~/dev/bin/php/bin/php -dextension= slobel.so slobel.php
Hello from darray!

D para Dynamic

Com esta classe, que só sabe dizer “Olá”, não é possível ir muito longe. Especialmente se ela foi concebida para funcionar como um array. É hora de considerar esse array e escrever o código.

Crie um diretório ds e adicione-o ao arquivo darray.h, que declara a estrutura e função do nosso array.

#ifndef PHP_SLOBEL_DS_DARRAY_H
#define PHP_SLOBEL_DS_DARRAY_H 1

#include "php.h"

typedef  struct slobel_ds_darray {
    size_t count; // the number of non-NULL elements.
    size_t length; // the current size of the array
    size_t min_length; // the minimum size of the array
    size_t capacity; // Power - how much will increase the size
    void *elements; // an array of elements (zval)
} slobel_ds_darray;

slobel_ds_darray *slobel_ds_darray_create(size_t size, size_t capacity);
void slobel_ds_darray_destroy(slobel_ds_darray *array);

#define slobel_ds_darray_length(array) ((array)->length)
#define slobel_ds_darray_min_length(array) ((array)->min_length)
#define slobel_ds_darray_capacity(array) ((array)->capacity)
#define slobel_ds_darray_count(array) ((array)->count)
#define slobel_ds_darray_first(array) ((zval *)(array)->elements)

#endif

Agora, no arquivo ds/darray.c, defina as funções declaradas acima. Por enquanto, é apenas para criação e remoção de estruturas.

#include "ds/darray.h"
#include "php.h"

slobel_ds_darray *slobel_ds_darray_create( size_t size, size_t capacity) {
    slobel_ds_darray *array = emalloc(sizeof(slobel_ds_darray));
    if (!array) {
        return NULL;
    }

    array->count = 0;
    array->length = 0;
    array->min_length = size;
    array->capacity = capacity;
    array->elements = NULL;


    return array;
}


void slobel_ds_darray_destroy( slobel_ds_darray *array) {
    if (!array) {
        return;
    }

    efree(array);
}

Nós temos uma classe, há um array, e precisamos amarrar de alguma forma um conceito para o outro. Primeiro, vamos detalhar como o PHP funciona com objetos.

Para armazenar objetos em estrutura de variáveis ​​(com zval), é usado zend_object_value, que tem os seguintes campos.

typedef struct _zend_object_value {
    zend_object_handle handle;
    const zend_object_handlers *handlers;
} zend_object_value;
  • handlers – estrutura com função de ponteiros, que admitem PHP quando fazemos algo com os objetos (ler propriedade, chamar o método, e assim por diante). Mais informações sobre zend_object_handlers serão explicadas mais tarde.
  • handle – é um inteiro regular, objeto «id» no armazenamento de objeto object_store. Quando um objeto é criado, o PHP o coloca na estrutura do zend_object object_store e nos retorna um identificador inteiro. Assim, se uma classe tem uma função create_object, ela é chamada durante a criação de uma zend_object. Mais informações podem ser encontradas no livro PHP internals.

Então, tudo o que precisamos agora é definir a estrutura que estende o zend_object. Para fazer isso, escreva sua própria função create_object e uma função para liberar a memória alocada para a estrutura. Adicione-a após a declaração de slobel_darray_ce.

zend_object_handlers slobel_darray_handlers;

typedef struct slobel_darray {
    zend_object std;
    slobel_ds_darray *array;
} slobel_darray;

static void slobel_darray_free_object_storage( slobel_darray *intern TSRMLS_DC)
{
    zend_object_std_dtor( &intern->std TSRMLS_CC);

    if (intern->array) {
        slobel_ds_darray_destroy(intern->array);
    }

    efree(intern);
}

zend_object_value slobel_darray_create_object( zend_class_entry *class_type TSRMLS_DC) {
    zend_object_value retval;

    slobel_darray *intern = emalloc( sizeof(slobel_darray));
    memset(intern, 0, sizeof(slobel_darray));

    // Initializes the object: indicates reference to the class, and resets the other fields.
    zend_object_std_init( &intern->std, class_type TSRMLS_CC);
    // Copy the properties of the class object.
    object_properties_init( &intern->std, class_type);

    // Adds an object to the object store (object_store).
    retval.handle = zend_objects_store_put(
        intern,
        // Standard object's destructor.
        (zend_objects_store_dtor_t) zend_objects_destroy_object,
        //our function to release memory object.
        (zend_objects_free_object_storage_t) slobel_darray_free_object_storage, 
        NULL // copy function, we do not need.
        TSRMLS_CC
    );

    //refers to a structure with a handler function for objects.
    retval.handlers = &slobel_darray_handlers;

    return retval;
}

zend_objects_store_put tem três funções:

  • dtor (zend_objects_destroy_object) – destructor do objeto a ser chamado quando a contagem de referência no objeto se torna 0, ou no final do script. O destructor também é responsável pela execução de código personalizado (__destruct). Ao mesmo tempo, em um caso particular, o PHP pode omitir a chamada destructor (por exemplo, se quando você chamou __destruct foi jogada uma exceção ou chamada exit())
  • free_storage (zend_objects_free_object_storage) – uma função que libera a memória alocada para o objeto (zend_object). Chamado sempre quando a contagem de referência sobre o objecto torna-se 0, ou no final do script
  • clone – uma função que é chamada quando um objeto é copiado. Por padrão, ela é ignorada, e isso significa que você deve explicitamente chamar a função cópia da função handler zend_objects_store_clone_obj. É mais fácil apenas pegar e usar a própria função imediatamente. Portanto, em 99% dos casos apenas defina clone como NULL

Na função slobel_darray_init, adicione as seguintes linhas:

// Specify your own function to create zend_object
slobel_darray_ce->create_object = slobel_darray_create_object;

// Copy the default handlers for objects
memcpy( &slobel_darray_handlers, zend_get_std_object_handlers(), sizeof( zend_object_handlers ));

Mas onde está o array? Vamos criar o array no construtor.

slobel_darray.c:

PHP_METHOD(slobel_darray, __construct)
{
    slobel_darray *intern;
    long size = 0;
    long capacity = 0;

    zend_error_handling error_handling;

    // Replace error handler so that when an error obtaining constructor arguments were thrown exception.
    zend_replace_error_handling(EH_THROW, NULL, &error_handling TSRMLS_CC);

    // The third parameter specifies the types of variables (l - long), the next - pointers to variables.
    if (zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "ll", &size, &capacity) == FAILURE) {
        zend_restore_error_handling( &error_handling TSRMLS_CC);
        return;
    }

    // Restores the default error handler.
    zend_restore_error_handling( &error_handling TSRMLS_CC);

    if (size <= 0) {
        zend_throw_exception(NULL, "Array size must be positive", 0 TSRMLS_CC);
        return;
    }

    if (capacity < 0) {
        zend_throw_exception(NULL, "Array capacity must be positive or 0", 0 TSRMLS_CC);
        return;
    }

    // We get the facility handle.
    intern = zend_object_store_get_object( getThis() TSRMLS_CC);

    intern->array = slobel_ds_darray_create( (size_t)size, (size_t)capacity);
    if (!intern->array) {
        zend_throw_exception(NULL, "Failed to allocate array", 0 TSRMLS_CC);
    }

    return;
}

Adicione __construct na função tabela:

ZEND_BEGIN_ARG_INFO_EX(arginfo_construct, 0, 0, 1)
// if passed by reference, the argument name.
ZEND_ARG_INFO(0, size)
ZEND_ARG_INFO(0, capacity)
ZEND_END_ARG_INFO()

const zend_function_entry slobel_darray_functions[] = {
    PHP_ME( slobel_darray, __construct, arginfo_construct, ZEND_ACC_PUBLIC)
    PHP_ME( slobel_darray, sayHello, arginfo_void, ZEND_ACC_PUBLIC)
    PHP_FE_END
};

Agora é hora de construir a extensão e ter certeza de que tudo está compilado normalmente. Execute phpize, para pegar as mudanças no config.m4. Eu prometo que esta é a última vez.

~/dev/bin/php/bin/phpize --clean
~/dev/bin/php/bin/phpize

./configure --with-php-config= $HOME/dev/bin/php/bin/php-config
make && make install

E execute o script de teste

~/dev/bin/php/bin/php -dextension=slobel.so slobel.php

Conclusão

Na primeira parte deste artigo, nós aprendemos como criar extensões do PHP em C e como implementar classes e métodos.

Na próxima parte, vamos concluir, aprendendo a implementar interfaces típicas que fazem as classes PHP serem tratadas como arrays regulares, como ArrayAccess e Traversable.

***

Dmitry Mamontov faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://www.phpclasses.org/blog/post/304-How-to-Create-a-PHP-C-Extension-to-Manipulate-Arrays–Part-1-Basic-Array-Class-Extension.html