segunda-feira, 1 de março de 2010

Introdução à Ruby/C API

Em Ruby há também formas de estender e embarcar o interpretador. Porém, diferentemente das documentações do Python, a documentação disponível em Ruby sempre se refere aos fatos como "estender o interpretador ruby", ou "ruby c extension", raramente falam exatamente sobre embarcar (embed). Os motivos para usar a Ruby API são os mesmos motivos que os para usar a Python/C API.

O texto abordará os mesmos tópicos do post anterior sobre a Python/C API e referenciará por vezes o post anterior. A versão do interpretador coberta pelo texto é a 1.8.

O básico da Ruby API

Ruby é uma linguagem dinâmica, assim como Python, e não precisa de especificações de tipo. Sendo assim, existe um tipo genérico quando trabalha-se com a a API: VALUE. Diferentemente da Python/C API não é usado um ponteiros pro tipo genérico, é simplesmente VALUE.

Na API não há uma exclusividade de convenção de nomes - nem tudo inicia-se com rb_. Há macros definidas como RSTRING, StringValue e chamadas como rb_define_module. Mas pode-se identificar que todas as chamadas a funções da API começam com rb_ ou ruby_.

Quando a intenção é embarcar o interpretador temos que iniciá-lo e finalizá-lo. Podemos fazer isso através das funções ruby_init e ruby_finalize, que iniciam e finalizam o interpretador ruby, respectivamente.

A Ruby API pode ser acessada através do cabeçalho ruby.h, que varia a localização em diferentes sistemas e eu conheço apenas uma maneira longa de achar o diretório, que é através do módulo rbconfig, que o mkmf usa pra achar os arquivos. O comando poderia ser feito em uma chamada longa ao interpretador diretamente na linha de comando como segue:

hugo@hugo-laptop:~$ ruby -r rbconfig \
> -e "puts Config::MAKEFILE_CONFIG['topdir']"
/usr/lib/ruby/1.8/i486-linux
hugo@hugo-laptop:~$

Isso significa que na minha máquina o arquivo ruby.h está na pasta /usr/lib/ruby/1.8/i486-linux.

Incluir o cabeçalho ruby.h cabeçalho implica em incluir diversos outros, como <stdio.h>, <stdlib.h>, <string.h>, <limits.h> (se disponíveis).

DICA: Inclua sempre o cabeçalho ruby.h antes de qualquer cabeçalho, pois o Ruby define algumas pre-processor definitions nos cabeçalhos padrões (pelo menos no GNU/Linux!).

Exemplo #1 - embarcando o interpretador: usando o método upcase

O exemplo tem a finalidade de transformar uma string minúscula em uma maiúscula, usando o interpretador Ruby pra isso. Em Ruby o método upcase está definido na classe String. Faremos então uma conversão do tipo C pra uma string em Ruby e chamaremos em seguida o método upcase:

#include "ruby.h"

int main() {
VALUE string_convertida,
resultado;
ID upcase_id;
char *string_minuscula = "hugo";
ruby_init();
upcase_id = rb_intern("upcase");

string_convertida = rb_str_new2(string_minuscula);
resultado = rb_funcall(string_convertida, upcase_id, 0);
printf("antes: %s, depois: %s\n", string_minuscula, \
StringValuePtr(resultado));

ruby_finalize();
return 0;

}

O primeiro passo foi incluir o cabeçalho ruby.h pra ter acesso à API. Depois foram definidas duas variáveis do tipo VALUE, onde string_convertida guardará a string convertida de C pra Ruby e resultado guardará o retorno do método upcase.
A variável upcase_id merece uma atenção especial. Na Ruby API pra passar nomes, em muitos dos casos, não pode ser através de uma string, mas através de um ID. Nesse caso, essa variável guardará o ID do nome upcase.
A variável string_minuscula guarda o valor "hugo", em minúsculo, pra ser convertido pra maiúsculo.

A chamada a função ruby_init é pra iniciar o interpretador Ruby pra termos acesso as facilidades do mesmo. A chamada a rb_intern retorna o ID do nome "upcase" caso exista, caso contrário ele criará na tabela de símbolos o id referente ao nome - por isso tem que ser chamado após a inicialização do interpretador.

A função rb_str_new2 cria uma string em ruby, passando como argumento o valor em C.

rb_funcall chama uma função em um determinado objeto. O primeiro parâmetro é o objeto em que reside a função, o segundo é o ID do nome da função, o terceiro o número de parâmetros e daí em diante os parâmetros. No caso acima não era necessário passar parâmetros, assim, o terceiro argumento foi 0 e não houve mais nenhum argumento após este.

A macro StringValuePtr retorna uma string (char *) com o valor da string em ruby. Após tudo isto o interpretador é finalizado, chamando ruby_finalize.

Os protótipos podem ser entendidos como:

ID rb_intern(char *nome);
VALUE rb_str_new2(char *string);
VALUE rb_funcall(VALUE obj, ID nome_id, int argc, ...);
char* StringValuePtr(VALUE string);

O executável pode ser gerado usando o GCC da seguinte maneira:

hugo@hugo-laptop:~/Desktop/posts/rb$ gcc -o upcase upcase.c \
> -I/usr/lib/ruby/1.8/i486-linux -lruby1.8
hugo@hugo-laptop:~/Desktop/posts/rb$ ./upcase
antes: hugo, depois: HUGO
hugo@hugo-laptop:~/Desktop/posts/rb$

A opção -lruby1.8 varia de sistema pra sistema, mas normalmente a opção pra linkar a biblioteca do interpretador é -lruby ou -lrubyVERSAO.

Algo correspondente em Ruby seria:

hugo@hugo-laptop:~/Desktop/posts/rb$ irb
irb(main):001:0> string_minuscula = "hugo"
=> "hugo"
irb(main):002:0> resultado = string_minuscula.upcase
=> "HUGO"
irb(main):003:0> puts "antes: #{string_minuscula}, " \
irb(main):004:0* "depois: #{resultado}"
antes: hugo, depois: HUGO
=> nil
irb(main):005:0>


Exemplo #2 - estendendo o interpretador: Criando um módulo com funções matemáticas

Nesse exemplo criaremos um módulo Matematica que conterá duas funções: fatorial e raiz_quadrada.

Em Ruby todo módulo de extensão precisa de apenas uma coisa: uma função Init_NOMEDOMODULO. O código segue:

#include "ruby.h"
#include <math.h>

int fatorial_c_puro(int n)
{
if (n <= 1)
return 1;
return n * fatorial_c_puro(n-1);
}

VALUE raiz_quadrada(VALUE self, VALUE x)
{
double raiz = sqrt(NUM2DBL(x));
return rb_float_new(raiz);
}

VALUE fatorial(VALUE self, VALUE x)
{
int x_int = NUM2INT(x);
return INT2NUM(fatorial_c_puro(x_int));
}

void Init_matematica()
{
VALUE modulo_matematica;
modulo_matematica = rb_define_module("Matematica");
rb_define_module_function(modulo_matematica, \
"fatorial", \
fatorial, \
1);
rb_define_module_function(modulo_matematica, \
"raiz_quadrada", \
raiz_quadrada, \
1);
}

A abordagem foi idêntica a usada no post sobre a Python/C API, que consiste em ter 1 função em C puro pra calcular fatorial e duas que usam a API.

A macro NUM2DBL converte um objeto numérico de ruby pro tipo double em C. A NUM2INT converte um objeto numérico pra um int e INT2NUM converte um int pra Fixnum ou Bignum.

A função responsável por iniciar o módulo sempre que é feito um require é Init_matematica. Reparem que em Python seria initmatematica, mas em Ruby o prefixo é Init_ ao invés de init.

Dentro da função que inicializa o módulo temos uma variável modulo_matematica que guarda o módulo em si. rb_define_module define um módulo com o valor de Matematica e duas funções fatorial e raiz_quadrada, ambas têm 1 parâmetro. Assim, a função fatorial será acessível da seguinte forma Matematica::fatorial.

Os protótipos podem ser entendidos como:

double NUM2DBL(VALUE n);
int NUM2INT(VALUE n);
VALUE INT2NUM(int n);
VALUE rb_define_module(char *nome);
void rb_define_module_function(VALUE modulo, \
char *nome, \
VALUE(*funcao)(), \
int argc);

Agora precisamos gerar o modulo pra ser usado pelo interpretador Ruby. A maneira mais elegante de se fazer isso é usar o módulo padrão mkmf. Porém, as duas maneiras serão mostradas.

A primeira maneira é através de uma chamada direta ao gcc, que é simples:

hugo@hugo-laptop:~/Desktop/posts/rb$ gcc -shared matematica.c \
> -I/usr/lib/ruby/1.8/i486-linux -o matematica.so -lm
hugo@hugo-laptop:~/Desktop/posts/rb$

A outra maneira é através do mkmf. Essa maneira consiste em criar um script em ruby que é responsável por criar um Makefile que gera o módulo. O arquivo se chamará extconf.rb e o conteúdo será o seguinte:

require 'mkmf'

create_makefile('matematica')

Isso significa que nós criaremos um Makefile que usará o compilador padrão, criando o arquivo matematica.so, matematica.dll ou matematica.bundle - dependo do sistema. No meu caso é um .so. Após criado o arquivo, basta rodá-lo pra criar o Makefile e depois uma simples chamada ao make cria os arquivos.

PS.: O arquivo extconf.rb deve estar no mesmo diretório em que os arquivos escritos em C.

DICA: Veja a documentação da função dir_config do módulo mkmf aqui: http://ruby-doc.org/stdlib/libdoc/mkmf/rdoc/files/mkmf_rb.html#M000549

hugo@hugo-laptop:~/Desktop/posts/rb$ ruby extconf.rb
creating Makefile
hugo@hugo-laptop:~/Desktop/posts/rb$ make
cc -I. -I/usr/lib/ruby/1.8/i486-linux \
-I/usr/lib/ruby/1.8/i486-linux \
-I. -D_FILE_OFFSET_BITS=64 \
-fPIC -fno-strict-aliasing \
-g -g -O2 -fPIC -c matematica.c
cc -shared -o matematica.so upcase.o matematica.o \
-L. -L/usr/lib -L. -Wl,-Bsymbolic-functions \
-rdynamic -Wl,-export-dynamic \
-lruby1.8 -lpthread -ldl -lcrypt -lm -lc
hugo@hugo-laptop:~/Desktop/posts/rb$

É possível testar facilmente se o módulo Matematica está acessível através de um require 'matematica'.

hugo@hugo-laptop:~/Desktop/posts/rb$ irb
irb(main):001:0> require 'matematica'
=> true
irb(main):002:0> Matematica::fatorial 5
=> 120
irb(main):003:0> Matematica::raiz_quadrada 2
=> 1.4142135623731
irb(main):004:0>


Referências

Eu comecei a estudar a Ruby C API através do projeto RubyPython, mas pra entender muita coisa eu usei como referência o livro Programming Ruby, 2nd edition, que tem o capítulo 21 dedicado a API. Também usei uma apresentação de slides com o título de Ruby C Extensions, que tem como autor Mark Volkmann. Eu tentei olhar a documentação no site oficial da linguagem, mas foi difícil de ter acesso e eu recomendo fortemente o livro, tanto pro uso da API quanto pra aprender Ruby e a apresentação mostra muitos pontos rápidamente e com exemplos.

Num futuro próximo escreverei um post sobre como fazer Ruby se comunicar com Python e vice-versa, com interface em ambos lados. Tentarei mostrar um pequeno exemplo, mas talvez nesse meio tempo eu escreva sobre algo diferente ou até mesmo detalhar mais um pouco uma das duas APIs ou aproveitar o feedback recebido e mostrar algo em que os autores dos comentários que chegaram a mim queiram ver ou aprender.

Até o próximo post ;-)

Nenhum comentário:

Postar um comentário