Extending PHP
Wez Furlong
wez@thebrainroom.com
PHP{Con West, Santa Clara, October 2003
Estendendo o PHP
Cedo ou tarde, você irá notar que o PHP não faz algo que você precisa fazer.
-
Não há extensão para uma biblioteca específica
-
Você pode querer integrar o PHP mais em seu sistema (ou vice-versa)
-
Talvez o script PHP tenha performance muito lenta em partes da aplicação
Melhor que abandonar (forma perfeita!) o PHP e procurar por uma linguagem alternativa, poderia facilmente valer a pena estender o PHP para fazer o que você quer.
O que iremos abordar
-
Geração do esqueleto da extensão (ext_skel)
-
config.m4 e o sistema de compilação do PHP
-
Anatomia da Extensão
-
PHP_FUNCTION
-
Resources
-
Passagem de parâmetros
Nós temos 3 horas para esse tutorial interativo; sinta-se livre para perguntar em qualquer momento.
Para nosso foco, eu escolhi uma biblioteca que resolve DNS de forma assíncrona e nós criaremos uma extensão PHP para ela hoje, agora mesmo.
libares
Iniciando com PHP 4.3.0, é possível escrever algumas aplicações de rede multiplexas com stream_select() bem inteligentes.
O único problema é que o padrão de funções DNS gethostbyname() bloqueia todo script até o domínio resolver.
Não seria bom ter um método alternativo para resolver DNS que não cause timeout na socket existente?
Nós iremos usar a libares (escrita por Greg Hudson) como a base da extensão PHP que provê o método alternativo. A libares é distribuída sob licença MIT.
ares C API
Antes de nós podermos escrever a extensão, temos que ter conhecimento do código que nós queremos expor no nosso script PHP.
Focando na substituição da gethostbyname(), ares fornece as seguintes interessantes funções:
interessante API ares
typedef void (*ares_host_callback)(void *arg, int status,
struct hostent *hostent);
int ares_init(ares_channel *channelptr);
void ares_destroy(ares_channel channel);
void ares_gethostbyname(ares_channel channel, const char *name,
int family, ares_host_callback callback, void *arg);
int ares_fds(ares_channel channel, fd_set *read_fds, fd_set *write_fds);
void ares_process(ares_channel channel, fd_set *read_fds,
fd_set *write_fds);
Normalmente o uso destas funções (retirado do exemplo do código ahost na distribuição ares) é algo como:
exemplo da API ares em ação
#include <ares.h>
/* é chamada quando o nome for resolvido */
static void callback(void *arg, int status, struct hostent *host)
{
char *mem, **p;
struct in_addr addr;
if (status != ARES_SUCCESS) {
fprintf(stderr, "%s: %s\n", (char*)arg,
ares_strerror(status, &mem));
ares_free_errmem(mem);
}
for (p = host->h_addr_list; *p; p++) {
memcpy(&addr, *p, sizeof(addr));
printf("%s: %s\n", host->h_name, inet_ntoa(addr));
}
}
int main(int argc, char **argv)
{
ares_channel channel;
char *errmem;
int status, nfds;
fd_set read_fds, write_fds;
struct timeval *tvp, tv;
/* cria/inicializa um canal para comunicações DNS */
status = ares_init(&channel);
if (status != ARES_SUCCESS) {
fprintf(stderr, "ares_init: %s\n",
ares_strerror(status, &errmem));
ares_free_errmem(errmem);
return 1;
}
/* para cada argumento da linha de comando */
for (argv++; *argv; argv++) {
/* look up the domain name */
ares_gethostbyname(channel, *argv, AF_INET, callback, *argv);
}
/* aguarda as queries serem completadas */
while (1) {
FD_ZERO(&read_fds);
FD_ZERO(&write_fds);
nfds = ares_fds(channel, &read_fds, &write_fds);
if (nfds == 0)
break;
tvp = ares_timeout(channel, NULL, &tv);
select(nfds, &read_fds, &write_fds, NULL, tvp);
/* chama o callback como apropriado */
ares_process(channel, &read_fds, &write_fds);
}
/* tudo feito */
ares_destroy(channel);
return 0;
}
funções da extensão
Baseando-se na api C, uma conveniente alternativa em PHP seria algo como isto:
Versão PHP do resolvedor ares
<?php
function gothost($hostname, $status, $hostent)
{
if ($status != ARES_SUCCESS) {
echo "Failed to resolve $hostname: "
. ares_strerror($status) . "\n";
return;
}
foreach ($hostent['addr_list'] as $ip) {
echo "$hostent[name] -> $ip\n";
}
}
/* para cada argumento, resolva-o */
function lookup_hosts()
{
$args = func_get_args();
$resolver = ares_init();
foreach ($args as $arg) {
ares_gethostbyname($resolver, $arg, 'gothost', $arg);
}
// aguarda 2 segundos por host a ser resolvido
while (ares_process_with_timeout($resolver, 2) > 0) {
// tempo expirado (2 segundos se passaram ) - timed out (2 seconds are up)
// pode fazer outra coisa aqui enquanto o está while aguardando
echo ".";
}
ares_destroy($resolver);
}
?>
Nós chegamos então a uma lista de 5 funções que nós necessitamos implementar na nossa extensão PHP:
-
ares_init()
-
ares_gethostbyname()
-
ares_process_with_timeout()
-
ares_destroy()
-
ares_strerror()
Dessas, a ares_process_with_timeout() é especial, já que ela envolve um monte de partes que são difíceis de levar para o espaço do usuário. Depois, nós iremos ver como plugar a ares na stream_select(), mas primeiro vamos dar este passo básico.
ext_skel
Tenha certeza que você tem estas coisas:
-
Último source do PHP 4
-
Cabeçalhos e biblioteca ares
-
Working build environment(!)
Primeira coisa a fazer é gerar um esqueleto de extensão que irá conter todas as coisas que o PHP precisa para ver seu código.
gerando o esqueleto
% cd php4.3.x/ext
% ./ext_skel --extname=ares
ext_skel irá emitir algo como:
saída do ext_skel
Creating directory ares
Creating basic files: config.m4 .cvsignore ares.c php_ares.h CREDITS EXPERIMENTAL
tests/001.phpt ares.php [done].
To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/ares/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-ares
5. $ make
6. $ ./php -f ext/ares/ares.php
7. $ vi ext/ares/ares.c
8. $ make
Repeat steps 3-6 until you are satisfied with ext/ares/config.m4 and
step 6 confirms that your module is compiled into PHP. Then, start writing
code and repeat the last two steps as often as necessary.
Como ele sugere, olhar no arquivo config.m4 é a primeira coisa que nós precisamos fazer - nós queremos que o configure encontre a libares,
então esse é um passo essencial.
config.m4: o sistema de compilação do PHP
O PHP tem um excelente e flexível sistema de compilação. Cada extensão define um config.m4 contendo shell script (e macros m4) para ajudar
a localizar bibliotecas e cabeçalhos que são requeridos.
ext_skel convenientemente gera um template para nós usarmos; tudo que precisamos fazer é descomentar a parte que se aplica
a nossa extensão.
ares config.m4
dnl $Id$
dnl config.m4 for extension ares
PHP_ARG_WITH(ares, for ares support,
[ --with-ares Include ares support])
if test "$PHP_ARES" != "no"; then
# --with-ares -> check with-path
SEARCH_PATH="/usr/local /usr"
SEARCH_FOR="/include/ares.h"
if test -r $PHP_ARES/; then # path given as parameter
ARES_DIR=$PHP_ARES
else # search default path list
AC_MSG_CHECKING([for ares files in default path])
for i in $SEARCH_PATH ; do
if test -r $i/$SEARCH_FOR; then
ARES_DIR=$i
AC_MSG_RESULT(found in $i)
fi
done
fi
if test -z "$ARES_DIR"; then
AC_MSG_RESULT([not found])
AC_MSG_ERROR([Please reinstall the ares distribution])
fi
# --with-ares -> add include path
PHP_ADD_INCLUDE($ARES_DIR/include)
# --with-ares -> check for lib and symbol presence
LIBNAME=ares
LIBSYMBOL=ares_init
PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
[
PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $ARES_DIR/lib, ARES_SHARED_LIBADD)
AC_DEFINE(HAVE_ARESLIB,1,[ ])
],[
AC_MSG_ERROR([wrong ares lib version or lib not found])
],[
-L$ARES_DIR/lib -lm
])
PHP_SUBST(ARES_SHARED_LIBADD)
PHP_NEW_EXTENSION(ares, ares.c, $ext_shared)
fi
Anatomia da Extensão
php_ares.h gerado por ext_skel
/*
+----------------------------------------------------------------------+
| PHP Version 4 |
+----------------------------------------------------------------------+
| Copyright (c) 1997-2003 The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to version 2.02 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available at through the world-wide-web at |
| http://www.php.net/license/2_02.txt. |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Author: |
+----------------------------------------------------------------------+
$Id: header,v 1.10.8.1 2003/07/14 15:59:18 sniper Exp $
*/
#ifndef PHP_ARES_H
#define PHP_ARES_H
extern zend_module_entry ares_module_entry;
#define phpext_ares_ptr &ares_module_entry
#ifdef PHP_WIN32
#define PHP_ARES_API __declspec(dllexport)
#else
#define PHP_ARES_API
#endif
#ifdef ZTS
#include "TSRM.h"
#endif
PHP_MINIT_FUNCTION(ares);
PHP_MSHUTDOWN_FUNCTION(ares);
PHP_RINIT_FUNCTION(ares);
PHP_RSHUTDOWN_FUNCTION(ares);
PHP_MINFO_FUNCTION(ares);
PHP_FUNCTION(confirm_ares_compiled); /* Para teste, remova depois. */
/*
Declare variáveis globais que você precisa entre as macros
BEGIN e END aqui:
ZEND_BEGIN_MODULE_GLOBALS(ares)
long global_value;
char *global_string;
ZEND_END_MODULE_GLOBALS(ares)
*/
/* In every utility function you add that needs to use variables
in php_ares_globals, call TSRM_FETCH(); after declaring other
variables used by that function, or better yet, pass in TSRMLS_CC
after the last function argument and declare your utility function
with TSRMLS_DC after the last declared argument. Always refer to
the globals in your function as ARES_G(variable). You are
encouraged to rename these macros something shorter, see
examples in any other php module directory.
*/
#ifdef ZTS
#define ARES_G(v) TSRMG(ares_globals_id, zend_ares_globals *, v)
#else
#define ARES_G(v) (ares_globals.v)
#endif
#endif /* PHP_ARES_H */
/*
* Local variables:
* tab-width: 4
* c-basic-offset: 4
* indent-tabs-mode: t
* End:
*/
ares.c as generated by ext_skel
/*
+----------------------------------------------------------------------+
| PHP Version 4 |
+----------------------------------------------------------------------+
| Copyright (c) 1997-2003 The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to version 2.02 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available at through the world-wide-web at |
| http://www.php.net/license/2_02.txt. |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Author: |
+----------------------------------------------------------------------+
$Id: header,v 1.10.8.1 2003/07/14 15:59:18 sniper Exp $
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_ares.h"
/* If you declare any globals in php_ares.h uncomment this:
ZEND_DECLARE_MODULE_GLOBALS(ares)
*/
/* True global resources - no need for thread safety here */
static int le_ares;
/* {{{ ares_functions[]
*
* Every user visible function must have an entry in ares_functions[].
*/
function_entry ares_functions[] = {
PHP_FE(confirm_ares_compiled, NULL) /* For testing, remove later. */
{NULL, NULL, NULL} /* Must be the last line in ares_functions[] */
};
/* }}} */
/* {{{ ares_module_entry
*/
zend_module_entry ares_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
"ares",
ares_functions,
PHP_MINIT(ares),
PHP_MSHUTDOWN(ares),
PHP_RINIT(ares), /* Troque para NULL se não há nada para fazer no início da requisição */
PHP_RSHUTDOWN(ares), /* Troque para NULL se não há nada para fazer no término da requisição */
PHP_MINFO(ares),
#if ZEND_MODULE_API_NO >= 20010901
"0.1", /* Troque para o número da versão de sua extensão - Replace with version number for your extension */
#endif
STANDARD_MODULE_PROPERTIES
};
/* }}} */
#ifdef COMPILE_DL_ARES
ZEND_GET_MODULE(ares)
#endif
/* {{{ PHP_INI
*/
/* Remova os comentários e preencha se você precisar ter entradas em php.ini
PHP_INI_BEGIN()
STD_PHP_INI_ENTRY("ares.global_value", "42", PHP_INI_ALL, OnUpdateInt,
global_value, zend_ares_globals, ares_globals)
STD_PHP_INI_ENTRY("ares.global_string", "foobar", PHP_INI_ALL, OnUpdateString,
global_string, zend_ares_globals, ares_globals)
PHP_INI_END()
*/
/* }}} */
/* {{{ php_ares_init_globals
*/
/* Descomente esta função se você tem entradas INI
static void php_ares_init_globals(zend_ares_globals *ares_globals)
{
ares_globals->global_value = 0;
ares_globals->global_string = NULL;
}
*/
/* }}} */
/* {{{ PHP_MINIT_FUNCTION
*/
PHP_MINIT_FUNCTION(ares)
{
/* Se você tem entradas INI, descomente estas linhas
ZEND_INIT_MODULE_GLOBALS(ares, php_ares_init_globals, NULL);
REGISTER_INI_ENTRIES();
*/
return SUCCESS;
}
/* }}} */
/* {{{ PHP_MSHUTDOWN_FUNCTION
*/
PHP_MSHUTDOWN_FUNCTION(ares)
{
/* descomente esta linha se você tem entradas de um INI
UNREGISTER_INI_ENTRIES();
*/
return SUCCESS;
}
/* }}} */
/* Remova se não há nada para fazer no início da requisição */
/* {{{ PHP_RINIT_FUNCTION
*/
PHP_RINIT_FUNCTION(ares)
{
return SUCCESS;
}
/* }}} */
/* Remova se não há nada pra fazer no final da requisição */
/* {{{ PHP_RSHUTDOWN_FUNCTION
*/
PHP_RSHUTDOWN_FUNCTION(ares)
{
return SUCCESS;
}
/* }}} */
/* {{{ PHP_MINFO_FUNCTION
*/
PHP_MINFO_FUNCTION(ares)
{
php_info_print_table_start();
php_info_print_table_header(2, "ares support", "enabled");
php_info_print_table_end();
/* Remove o comentário se você tem entrada em php.ini
DISPLAY_INI_ENTRIES();
*/
}
/* }}} */
/* Remove the following function when you have succesfully modified config.m4
so that your module can be compiled into PHP, it exists only for testing
purposes. */
/* Every user-visible function in PHP should document itself in the source */
/* {{{ proto string confirm_ares_compiled(string arg)
Return a string to confirm that the module is compiled in */
PHP_FUNCTION(confirm_ares_compiled)
{
char *arg = NULL;
int arg_len, len;
char string[256];
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) ==
FAILURE) {
return;
}
len = sprintf(string, "Congratulations! You have successfully modified
ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "ares", arg);
RETURN_STRINGL(string, len, 1);
}
/* }}} */
/* The previous line is meant for vim and emacs, so it can correctly fold and
unfold functions in source code. See the corresponding marks just before
function definition, where the functions purpose is also documented. Please
follow this convention for the convenience of others editing your code.
*/
/*
* Local variables:
* tab-width: 4
* c-basic-offset: 4
* End:
* vim600: noet sw=4 ts=4 fdm=marker
* vim<600: noet sw=4 ts=4
*/
ares_init - resources
A primeira função que nós iremos começar a implementar é a ares_init(). Na biblioteca C,
ares_init inicializa uma estrutura e então todas outras funções operam no ponteiro para esta estrutura. Visto que nós não temos
estruturas e ponteiros em PHP, nós precisamos declarar um tipo resource.
Resource são implementados no PHP usando uma lista linkada global (por requisição) de identificadores de resource. Cada
resource tem associado com ele:
-
O id do "tipo" resource
-
O ponteiro do valor
Para retornar um resource para o script, nós iremos necessitar registrar um tipo resource quando nossa extensão foi carregada.
Nós precisamos fazer isto de modo que a Zend Engine saiba como liberar o resource quando o script terminar - nós registramos o
resource com uma função destrutora.
registrando um resource
/* True global resources - no need for thread safety here */
static int le_ares_channel;
static void ares_channel_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
/* TODO: rsrc->ptr need to be released correctly */
}
/* {{{ PHP_MSHUTDOWN_FUNCTION
*/
PHP_MINIT_FUNCTION(ares)
{
le_ares_channel = zend_register_list_destructors_ex(ares_channel_dtor,
NULL, "ares channel", module_number);
return SUCCESS;
}
*/
Agora nós precisamos escrever a função PHP para o mesmo.
implementando ares_init()
/* {{{ proto resource ares_init()
Creates a DNS resolving communications channel */
PHP_FUNCTION(ares_init)
{
ares_channel channel;
int status;
status = ares_init(&channel);
if (status != ARES_SUCCESS) {
char *errmem;
ares_strerror(status, &errmem);
php_error_docref(NULL TSRMLS_CC, E_WARNING, "failed to init ares channel: %s",
errmem);
ares_free_errmem(errmem);
RETURN_NULL();
}
ZEND_REGISTER_RESOURCE(return_value, channel, le_ares_channel);
}
/* }}} */
• ares_channel é definida como um tipo ponteiro
• Se o channel não puder ser criado, um E_WARNING será emitido, e será retornado NULL.
Por questão de integridade, vamos implementar ares_destroy() agora também. Isto demonstra como aceitar um tipo resource como parâmetro.
ares_destroy()
/* {{{ proto void ares_destroy(resource $channel)
Destroys a DNS resolving channel */
PHP_FUNCTION(ares_destroy)
{
zval *r;
ares_channel channel;
if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
"r", &r)) {
return;
}
ZEND_FETCH_RESOURCE(channel, ares_channel, &r, -1, "ares channel",
le_ares_channel);
zend_list_delete(Z_LVAL_P(r));
}
/* }}} */
• ZEND_FETCH_RESOURCE irá automaticamente emitir um erro e retornar
para a função se o parâmetro $channel não for um resource channel válido (ex: stream ou image)
Quase terminado - agora que nós sabemos como estamos criando o channel, nós
deveríamos preencher aquele dtor para realmente fazer alguma coisa.
registrando um resource
static void ares_channel_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
ares_channel channel = (ares_channel)rsrc->ptr;
ares_destroy(channel);
}
ares_gethostbyname()
Agora nós podemos escrever a real extensão - o resolvedor. Decidimos que a API para a função deve funcionar dessa forma:
protótipo da função para ares_gethostbyname
<?php
ares_gethostbyname($channel, $hostname, $callback, $callbackarg);
?>
Então nós precisamos escrever uma PHP_FUNCTION que aceita um resource, uma string, e dois valores genéricos zval como parâmetro.
ares_gethostbyname() em C
/* {{{ proto void ares_gethostbyname(resource $channel, string $hostname, mixed
$callback, mixed $arg)
Initiate resolution of $hostname; will call $callback with $arg when complete */
PHP_FUNCTION(ares_gethostbyname)
{
zval *zchannel, *zcallback, *zarg;
char *hostname;
long hostname_len;
ares_channel channel;
if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rszz",
&zchannel, &hostname, &hostname_len, &zcallback, &zarg)) {
return;
}
ZEND_FETCH_RESOURCE(channel, ares_channel, &zchannel, -1, "ares channel",
le_ares_channel);
/* more code to go here ... */
}
/* }}} */
Agora nós atingimos um pequeno obstáculo - nós precisamos retornar à engine
quando a resolução completar. É necessário um pouco de mágica, já
que nós não podemos chamar uma PHP_FUNCTION diretamente do C (bem, nós podemos, mas não podemos
chamá-la diretamente a partir deste callback). Nós precisamos definir uma estrutura para ter o
nome da função (ou objeto + nome do método) do callback, assim como o
argumento. Nós os passaremos para a ares e teremos uma pequena função em C para agir
como o verdadeiro callback:
estrutura e callback para gethostbyname
struct php_ares_callback_struct {
zval *callback;
zval *arg;
};
static void php_ares_hostcallback(void *arg, int status, struct hostent *host)
{
struct php_ares_callback_struct *cb = (struct php_ares_callback_struct *)arg;
/* TODO: map hostent to php and call the callback */
}
Vamos voltar para a função gethostbyname e preencher na estrutura do callback:
ares_gethostbyname() continuada
/* {{{ proto void ares_gethostbyname(resource $channel, string $hostname, mixed
$callback, mixed $arg)
Initiate resolution of $hostname; will call $callback with $arg when complete */
PHP_FUNCTION(ares_gethostbyname)
{
zval *zchannel, *zcallback, *zarg;
char *hostname, *callback_name;
long hostname_len;
ares_channel channel;
struct php_ares_callback_struct *cb;
if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rszz",
&zchannel, &hostname, &hostname_len, &zcallback, &zarg)) {
return;
}
ZEND_FETCH_RESOURCE(channel, ares_channel, &zchannel, -1, "ares channel",
le_ares_channel);
/* verify that the callback will work */
if (!zend_is_callable(zcallback, 0, &callback_name)) {
php_error_docref1(NULL TSRMLS_CC, callback_name, E_WARNING,
"3rd argument is not a valid callback");
efree(callback_name);
return;
}
/* copy the values into the structure */
cb = (struct php_ares_callback_struct*)emalloc(sizeof(*cb));
MAKE_STD_ZVAL(cb->callback);
*cb->callback = *zcallback;
zval_copy_ctor(*cb->callback);
MAKE_STD_ZVAL(cb->arg);
*cb->arg = *zarg;
zval_copy_ctor(*cb->arg);
ares_get_hostbyname(channel, hostname, AF_INET,
php_ares_hostcallback, cb);
}
/* }}} */
Nós podemos retornar para o callback agora - temos um outro probleminha -
nós não podemos passar um struct hostent diretamente para um script, então precisamos copiar os
dados dele em uma zval que possa ser passada. Revendo nosso exemplo da API,
escolhemos usar um array com chaves nomeadas, de forma semelhante à estrutura em C:
o exemplo do callback em versão PHP
<?php
function gothost($hostname, $status, $hostent)
{
if ($status != ARES_SUCCESS) {
echo "Failed to resolve $hostname: "
. ares_strerror($status) . "\n";
return;
}
foreach ($hostent['addr_list'] as $ip) {
echo "$hostent[name] -> $ip\n";
}
}
?>
Então a tarefa agora é criar um array e defini-lo como o seguinte:
estrutura do parâmetro hostent
<?php
$hostent = array();
$hostent['name'] = $hostname;
$hostent['addr_list'] = array();
$hostent['addr_list'][] = $ip1;
?>
A tradução C deste código seria como:
criando um array hostent em C
static void php_ares_hostcallback(void *arg, int status, struct hostent *host)
{
struct php_ares_callback_struct *cb = (struct php_ares_callback_struct *)arg;
struct in_addr addr;
char **p;
zval *hearray, *addr_list;
MAKE_STD_ZVAL(hearray);
array_init(hearray);
add_assoc_string(hearray, "name", host->h_name, 1);
MAKE_STD_ZVAL(addr_list);
array_init(addr_list);
for (p = host->h_addr_list; *p; p++) {
memcpy(&addr, *p, sizeof(addr));
add_next_index_string(addr_list, inet_ntoa(addr), 1);
}
add_assoc_zval(hearray, "addr_list", addr_list);
}
Agora nós chegamos à parte mais importante - chamar o callback.
usando call_user_function para chamar o callback
static void php_ares_hostcallback(void *arg, int status, struct hostent *host)
{
zval retval;
zval *arguments[3];
struct php_ares_callback_struct *cb = (struct php_ares_callback_struct *)arg;
/* insert code for mapping the hostent to an array (from above) here */
arguments[0] = cb->arg;
MAKE_STD_ZVAL(arguments[1]);
ZVAL_LONG(arguments[1], status);
arguments[1] = hearray;
if (call_user_function(EG(function_table), NULL,
cb->callback, &retval, 3, arguments TSRMLS_CC) == SUCCESS) {
/* release any returned zval - it's not important to us */
zval_dtor(&retval);
}
FREE_ZVAL(arguments[1]);
/* and clean up the structure */
zval_dtor(cb->callback);
zval_dtor(cb->arg);
zval_dtor(hearray);
efree(cb);
}
ares_process_with_timeout
Esta função provê uma maneira fácil para verificar o status da resolução do DNS,
e chama o callback se tiver completado. Por conveniência nós podemos especificar
um timeout então chamamos "sleep" enquanto esperamos.
Vamos ver novamente o pedaço de código do exemplo da ahost:
pedaço da ahost
int main(int argc, char **argv)
{
ares_channel channel;
int status, nfds;
fd_set read_fds, write_fds;
struct timeval *tvp, tv;
/* ... */
/* aguarda as queries serem completadas - wait for the queries to complete */
while (1) {
FD_ZERO(&read_fds);
FD_ZERO(&write_fds);
nfds = ares_fds(channel, &read_fds, &write_fds);
if (nfds == 0)
break;
tvp = ares_timeout(channel, NULL, &tv);
select(nfds, &read_fds, &write_fds, NULL, tvp);
/* chama o callback apropriado - calls the callback as appropriate */
ares_process(channel, &read_fds, &write_fds);
}
/* ... */
}
Nós faremos um pouco diferente - iremos especificar nosso próprio timeout.
Implementando a função
/* {{{ proto long ares_process_with_timeout(resource $channel, long $secs)
verifica se a resolução do DNS foi completada e chama callbacks. Retorna o número de requisições. */
PHP_FUNCTION(ares_process_with_timeout)
{
zval *zchannel;
ares_channel channel;
long timeout_secs;
struct timeval tv;
int status, nfds;
fd_set read_fds, write_fds;
if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rl",
&zchannel, &timeout_secs)) {
return;
}
ZEND_FETCH_RESOURCE(channel, ares_channel, &zchannel, -1, "ares channel",
le_ares_channel);
FD_ZERO(&read_fds);
FD_ZERO(&write_fds);
nfds = ares_fds(channel, &read_fds, &write_fds);
if (nfds) {
tv.tv_secs = timeout_secs;
tv.tv_usec = 0;
if (select(nfds, &read_fds, &write_fds, NULL, &tv) > 0) {
/* calls the callback as appropriate */
ares_process(channel, &read_fds, &write_fds);
}
RETURN_LONG(nfds);
} else {
RETURN_LONG(0);
}
}
/* }}} */
Boas coisas para manuseamento de erros
Se a resolução do DNS falha por alguma razão, nós podemos querer notificar o usuário
da mensagem de erro, ou querer agir de acordo com o código do erro e fazer alguma ação alternativa.
Para essas coisas nós precisamos ter uma função para recuperar o texto da mensagem de erro, e também seria
legal ter algumas constantes simbólicas para descrever os códigos de erro.
implementando ares_strerror
/* {{{ proto string ares_strerror(long $statuscode)
retorna uma string com a descrição de um código de erro */
PHP_FUNCTION(ares_strerror)
{
long statuscode;
char *errmem = NULL;
if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l",
&statuscode)) {
return;
}
ares_strerror(status, &errmem);
RETVAL_STRING(return_value, errmem, 1);
ares_free_errmem(errmem);
}
/* }}} */
A outra coisa boa de ter são constantes simbólicas. Podemos fazer o equivalente da define() quando nossa extensão é carregada.
Fazemos isto na função do módulo init (MINIT), logo depois do ponto onde nós registramos nosso tipo resource.
códigos de erro definidos pela libares
#define ARES_SUCCESS 0
/* Server error codes (ARES_ENODATA indicates no relevant answer) */
#define ARES_ENODATA 1
#define ARES_EFORMERR 2
#define ARES_ESERVFAIL 3
#define ARES_ENOTFOUND 4
#define ARES_ENOTIMP 5
#define ARES_EREFUSED 6
/* Locally generated error codes */
#define ARES_EBADQUERY 7
#define ARES_EBADNAME 8
#define ARES_EBADFAMILY 9
#define ARES_EBADRESP 10
#define ARES_ECONNREFUSED 11
#define ARES_ETIMEOUT 12
#define ARES_EOF 13
#define ARES_EFILE 14
#define ARES_ENOMEM 15
#define ARES_EDESTRUCTION 16
Para essas funções estarem no espaço do usuário:
registrando um resource
/* {{{ PHP_MSHUTDOWN_FUNCTION
*/
PHP_MINIT_FUNCTION(ares)
{
le_ares_channel = zend_register_list_destructors_ex(ares_channel_dtor,
NULL, "ares channel", module_number);
REGISTER_LONG_CONSTANT("ARES_SUCCESS", ARES_SUCCESS, CONST_CS|CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("ARES_ENODATA", ARES_ENODATA, CONST_CS|CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("ARES_EFORMERR", ARES_EFORMERR, CONST_CS|CONST_PERSISTENT);
/* ... */
return SUCCESS;
}
*/
Resumo
Nós podemos agora criar requisições de resolução de DNS assíncronas, e até executá-las paralelamente.
Aprendemos sobre as principais partes da criação de uma extensão PHP:
-
config.m4
-
estrutura geral
-
PHP_FUNCTION
-
zend_parse_parameters
-
criar callbacks
-
registrar resources
-
registrar constantes
-
retornar valores para o script
como usar com stream_select()
<?php
$s1 = fsockopen($host1, $port);
$s2 = fsockopen($host2, $port);
$chan = ares_init();
ares_gethostbyname($chan, 'sample.com', 'sample.com');
/* aguarda por algo para voltar - wait for things to come back */
while (true) {
/* não aguarda nada - retorna imediatamente - don't wait for anything - return immediately */
ares_process_with_timeout($chan, 0);
/* aguarda 5 segundos por dados no sockets - wait 5 seconds for data on the sockets */
$r = array($s1, $s2);
$n = stream_select($r, $w = null, $e = null, 5);
if ($n) {
/* faz alguma coisa aqui - do something with it */
}
}
?>