Back-End

4 out, 2012

Desenvolvendo extensões do Ruby em C++ usando o Rice

Publicidade

Um dos recursos mais legais do Ruby é a possibilidade de ampliá-lo com uma interface de programação de aplicativos (API) definida em C/C++. O Ruby fornece o cabeçalho C ruby.h, que vem com uma ampla variedade de funções para criar classes, módulos, etc. de Ruby. Além do cabeçalho fornecido pelo Ruby, várias outras abstrações de alto nível estão disponíveis para estender o Ruby, que foram desenvolvidas com base no ruby.h nativo— uma das abstrações que este artigo aborda é a Ruby Interface for C++ Extensions conhecida como Rice..

Criando uma extensão do Ruby

Antes de passar a uma extensão do Rice ou API de C do Ruby, eu quero descrever claramente o processo padrão de criação da extensão:

  1. Você tem uma ou mais fontes em C/C++ a partir das quais você faz uma biblioteca compartilhada.
  2. Ao criar uma extensão usando o Rice, é necessário vincular o código a libruby.a e librice.a.
  3. Copie a biblioteca compartilhada para alguma pasta e faça com que ela faça parte da variável de ambiente RUBYLIB.
  4. Utilize o carregamento usual baseado emrequire no prompt do Interactive Ruby (irb)/script de ruby. Se a biblioteca compartilhada tem o nome rubytest.so, quando se digita simplesmente require 'rubytest' , ela é carregada.

Suponha que o cabeçalho ruby.h reside em /usr/lib/ruby/1.8/include, os cabeçalhos do Rice residem em /usr/local/include/rice/include e o código de extensão está no arquivo rubytest.cpp. A Listagem 1 mostra como compilar e carregar o código:

bash# g++ -c rubytest.cpp –g –Wall -I/usr/lib/ruby/1.8/include  \
-I/usr/local/include/rice/include
bash# g++ -shared –o rubytest.so rubytest.o -L/usr/lib/ruby/1.8/lib \
-L/usr/local/lib/rice/lib  -lruby –lrice –ldl  -lpthread
bash# cp rubytest.so /opt/test
bash# export RUBYLIB=$RUBYLIB:/opt/test
bash# irb
irb> require 'rubytest'
=> true

O programa Hello World

Agora você está pronto para criar o seu primeiro Hello World usando o Rice. Você cria uma classe usando a API do Rice chamada Test com um método hello que exibe a cadeia de caracteres “Hello, World!” Quando o interpretador do Ruby carrega a extensão, ele chama a função Init_<shared library name>. Na extensão rubytest da Listagem 1, essa chamada implica que rubytest.cpp tem uma função Init_rubytest definida. O Rice permite criar a sua própria classe usando a API define_class. A Listagem 2 mostra o código.

#include "rice/Class.hpp"
extern "C"
void Init_rubytest( ) {
Class tmp_ = define_class("Test");
}

Quando você compila e carrega o código da Listagem 2 no irb, deve obter a saída da Listagem 3.

irb> require ‘rubytest’
=> true
irb> a = Test.new
=> #<Test:0x1084a3928>
irb> a.methods
=> ["inspect", "tap", "clone", "public_methods", "__send__",
"instance_variable_defined?", "equal?", "freeze", …]

Observe que há vários métodos de classes predefinidas disponíveis, como inspect . Isso acontece porque a classe Test que você definiu é derivada implicitamente a partir da classe Object (todas as classes do Ruby são derivadas de Object; de fato, tudo no Ruby — inclusive os números — é um objeto que tem Object como classe base).

Agora, inclua um método à classe Test . A Listagem 4 mostra o código.

void hello() {
std::cout << "Hello World!";
}
extern "C"
void Init_rubytest() {
Class test_ = define_class("Test")
.define_method("hello", &hello);
}

A Listagem 4 utiliza a API define_method para incluir um método na classe Test . Observe que define_class é uma função que retorna um objeto do tipo Class; define_method é uma função do membro da classe Module_Impl, que é a classe base de Class. Este é um teste do Ruby que verifica se tudo está realmente correto:

irb> require ‘rubytest’
=> true
irb> Test.new.hello
Hello, World!
=> nil

Passando argumentos do Ruby para o código C/C++

Agora que o programa Hello World está funcionando, tente passar um argumento do Ruby para a função hello e faça a função exibir a mesma coisa para a saída padrão (sdtout). A forma mais simples de fazer isso é incluir um argumento de cadeia de caracteres na função hello :

void hello(std::string args) {
std::cout << args << std::endl;
}
extern "C"
void Init_rubytest() {
Class test_ = define_class("Test")
.define_method("hello", &hello);
}

No mundo do Ruby, é assim que se chama a função hello :

irb> a = Test.new
<Test:0x0145e42112>
irb> a.hello "Hello World in Ruby"
Hello World in Ruby
=> nil

A maior vantagem do uso do Rice é o fato de não precisar fazer nada específico para converter uma cadeia de caracteres do Ruby para std::string.

Agora, tente usar um array de cadeias de caracteres na função hello e, em seguida, veja como você passaria informações do Ruby para o código C++ . A forma mais simples de fazer isso é usar o tipo de dados Array que o Rice fornece. Definido no cabeçalho rice/Array.hpp, o uso do Rice::Array é semelhante ao uso de um contêiner de Standard Template Library (STL). Os iteradores usuais no estilo da STL e coisas semelhantes são definidos como parte da interface de Array . A Listagem 5 mostra a rotina count , que toma como argumento um Array do Rice.

#include "rice/Array.hpp"

void Array_Print (Array a)   {
Array::iterator aI = a.begin();
Array::iterator aE = a.end();
while (aI != aE) {
std::cout << "Array has " << *aI << std::endl;
++aI;
}
}

Esta é a beleza dessa solução: suponha que você tenha um std::vector<std::string> como o argumento Array_Print . Este é o erro que o Ruby lança:

>> t = Test.new
=> #<Test:0x100494688>
>> t.Array_Print ["g", "ggh1", "hh1"]
ArgumentError: Unable to convert Array to std::vector<std::string,
std::allocator<std::string> >
from (irb):3:in `hello'
from (irb):3

No entanto, com a rotina Array_Print mostrada aqui, o Rice se encarrega da conversão de um array de Ruby para o tipo de array de C++ Array . Esta é uma amostra de execução:

>> t = Test.new
=> #<Test:0x100494688>
>>  t.Array_Print ["hello", "world", "ruby"]
Array has hello
Array has world
Array has ruby
=> nil

Agora tente fazer o contrário — passar um array do C++ para o mundo do Ruby. Observe que, no Ruby, os elementos de array podem não ser do mesmo tipo. A Listagem 6 mostra o código.

#include "rice/String.hpp"
#include "rice/Array.hpp"
using namespace rice;

Array return_array (Array a)  {
Array tmp_;
tmp_.push(1);
tmp_.push(2.3);
tmp_.push(String("hello"));
return tmp_;
}

A Listagem 6 mostra claramente que é possível criar um array de Ruby com tipos diferentes dentro do C++. Este é o código de teste no Ruby:

>> x = t.return_array
=> [1, 2.3, "hello"]
>> x[0].class
=> Fixnum
>> x[1].class
=> Float
>> x[2].class
=> String

E se você não tem a flexibilidade de alterar uma lista de argumentos de C++?

Normalmente, a interface do Ruby se destina a converter as funções de C++ cuja assinatura não é possível mudar. Por exemplo, considere um caso no qual é necessário passar um array de cadeias de caracteres do Ruby para o C++. A assinatura da função C++ é assim:

void print_array(std::vector<std::string> args)

Na verdade, aqui você está procurando algum tipo de função from_ruby que toma um array de Ruby e o converte para std::vector<std::string>. É exatamente isso que o Rice fornece— uma função from_ruby com a assinatura a seguir:

template <typename T>
T from_ruby(Object );

Para cada tipo de dados que deve ser convertido para o tipo C++ , é necessário especializar para o modelo a rotina from_ruby . Por exemplo, se você passa o array de Ruby para a função de processo mostrada acima, a Listagem 7 mostra como você deve definir a função from_ruby .

template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o)   {
Array a(o);
std::vector<std::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(((String)*aI).str());
return v;
}

Observe que a função from_ruby não precisa ser chamada explicitamente. Quando um array de string é passado como argumento da função a partir do mundo do Ruby, from_ruby o converte para std::vector<std::string>. Entretanto, o código da Listagem 7 não é perfeito. Já vimos que os arrays no Ruby podem ter tipos diferentes. Por outro lado, você fez uma chamada a ((String)*aI).str() para obter um std::string a partir de Rice::String. (str é um método de Rice::String: confira String.hpp para ver mais detalhes). Se você tivesse que lidar com o caso mais genérico, a Listagem 8 mostra como o código ficaria.

template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o)   {
Array a(o);
std::vector<std::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(from_ruby<std::string> (*aI));
return v;
}

Como cada elemento do array de Ruby também é um objeto Ruby do tipo String e você está apostando que o Rice tem um método from_ruby definido para converter esse tipo para std::string, não é necessário fazer mais nada. Caso contrário, será necessário fornecer um método from_ruby para a conversão. Este é o método from_ruby de to_from_ruby.ipp nas fontes do Rice:

template<>
inline std::string from_ruby<std::string>(Rice::Object x) {
return Rice::String(x).str();
}

Teste esse código do mundo do Ruby. Comece passando um array de todas as cadeias de caracteres, como mostra a Listagem 9.

>> t = Test.new
=> #<Test:0x10e71c5c8>
>> t.print_array ["aa", "bb"]
aa bb
=> nil
>> t.print_array ["aa", "bb", 111]
TypeError: wrong argument type Fixnum (expected String)
from (irb):4:in `print_array'
from (irb):4

Conforme o esperado, a primeira chamada de print_array deu certo. Já que não há um método from_ruby para converter Fixnum para std::string, a segunda chamada faz com que o interpretador de Ruby lance um TypeError. Há várias formas de corrigir esse erro — por exemplo, durante a chamada do Ruby, passar somente as cadeias de caracteres como parte do array (como t.print_array["aa", "bb", 111.to_s]), ou dentro do código C++ , fazer uma chamada para Object.to_s. O método to_s faz parte da interface Rice::Object e retorna Rice::String, que tem um método predefinido str que retorna um std::string. A Listagem 10 usa a abordagem C++ .

template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o)   {
Array a(o);
std::vector<std::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(aI->to_s().str());
return v;
}

Em geral, o código da Listagem 10 será mais envolvido, já que será necessário manipular as representações de cadeia de caracteres customizadas para as classes definidas pelo usuário.

Criando uma classe completa com variáveis usando C++

Você já viu como criar uma classe Ruby e associou funções dentro do código de C++ . Para uma classe mais genérica, é necessária uma forma de definir variáveis de instância e fornecer um método initialize . Para configurar e obter os valores das variáveis de instância de um objeto do Ruby, você usa os métodos Rice::Object::iv_set e Rice::Object::iv_get , respectivamente. A Listagem 11 mostra o código.

void init(Object self) {
self.iv_set("@intvar", 121);
self.iv_set("@stringvar", String("testing"));
}
Class cTest = define_class("Test").
define_method("initialize", &init);

Quando uma função C++ é declarada como um método de classes do Ruby usando a API define_method , você tem a opção de declarar o primeiro argumento da função C++ como Object. Ao fazer isso, o Ruby preenche esse Object com uma referência à instância que realiza a chamada. Em seguida, você chama iv_set no Object para configurar as variáveis de instância. Esta é a aparência da interface no mundo do Ruby:

>> require 'rubytest'
=> true
>> t = Test.new
=> #<Test:0x1010fe400 @stringvar="testing", @intvar=121>

Da mesma forma, para retornar uma variável de instância, a função que retorna precisa tomar um Object que faz referência ao objeto no Ruby e chamar iv_get nele. A Listagem 12 mostra um fragmento.

void init(Object self) {
self.iv_set("@intvar", 121);
self.iv_set("@stringvar", String("testing"));
}
int getvalue(Object self) {
return self.iv_get("@intvar");
}
Class cTest = define_class("Test").
define_method("initialize", &init).
define_method("getint", &getvalue);

Transformando uma classe de C++ em um tipo do Ruby

Até agora, você encapou funções livres (ou seja, métodos que não são de classes) como métodos de classes de Ruby. Você passou referências para o objeto Ruby declarando as funções em C com o primeiro argumento Object. Essa abordagem funciona, mas não basta encapar uma classe de C++ com um objeto de Ruby. Para encapar uma classe de C++ , você também usa o método define_class , mas pode padronizá-lo com o tipo de classe C++ . O código na Listagem 13 encapa uma classe de C++ como um tipo do Ruby.

class cppType {
    public:
      void print(String args) {
        std::cout << args.str() << endl;
      }
};
Class rb_cTest =
        define_class<cppType>("Test")
         .define_method("print", &cppType::print);

Observe que define_class está modelizado, conforme o que já foi explicado. Entretanto, nem tudo nessa classe está correto. Este é o log do interpretador do Ruby quando você tenta instanciar um objeto do tipo Test:

>> t = Test.new
TypeError: allocator undefined for Test
	from (irb):3:in `new'
	from (irb):3

O que aconteceu? Bem, é necessário ligar o construtor explicitamente a um tipo do Ruby. (É uma das esquisitices do Rice.) O Rice fornece um método define_constructor para associar um construtor ao tipo C++ . Também é necessário incluir o cabeçalho Constructor.hpp. Observe que é necessário fazer isso mesmo se você não tem um construtor explícito no código. A Listagem 14 fornece o código de amostra.

#include "rice/Constructor.hpp"
#include "rice/String.hpp"
class cppType {
public:
void print(String args) {
std::cout << args.str() << endl;
}
};

Class rb_cTest =
define_class<cppType>("Test")
.define_constructor(Constructor<cppType>())
.define_method("print", &cppType::print);

Também é possível associar um construtor a uma lista de argumentos usando o método define_constructor . A forma de fazer isso no Rice é incluir os tipos de argumento na lista modelo. Por exemplo, se cppType tem um construtor que aceita um número inteiro, é necessário chamar define_constructor como define_constructor(Constructor<cppType, int>()). Aviso: os tipos do Ruby não têm construtores diversos. Portanto, se você tem um tipo de C++ com construtores diversos e associa todos eles usando define_constructor, no mundo do Ruby, é possível instanciar o tipo com argumentos (ou não), conforme o definido pelo último define_constructor no código fonte. A Listagem 15 explica tudo o que acabamos de mencionar.

class cppType {
public:
cppType(int m) {
std::cout << m << std::endl;
}
cppType(Array a) {
std::cout << a.size() << std::endl;
}
void print(String args) {
std::cout << args.str() << endl;
}
};
Class rb_cTest =
define_class<cppType>("Test")
.define_constructor(Constructor<cppType, int>())
.define_constructor(Constructor<cppType, Array>())
.define_method("print", &cppType::print);

Este é o log do mundo do Ruby. Observe que o Ruby entende o construtor que foi associado por último:

>> t = Test.new 2
TypeError: wrong argument type Fixnum (expected Array)
from (irb):2:in `initialize'
from (irb):2:in `new'
from (irb):2
>> t = Test.new [1, 2]
2
=> #<Test:0x10d52cf48>

Definindo um novo tipo do Ruby como parte de um módulo

A definição de um novo módulo do Ruby a partir do C++ se resume a uma chamada a define_module. Para definir uma classe disponível somente como uma parte desse módulo, você usa define_class_under em vez do método define_class usual. O primeiro argumento para define_class_under é o objeto do módulo. A partir da Listagem 14, se você fosse definir cppType como parte de um módulo do Ruby chamado types, A Listagem 16 mostra como isso seria feito.

#include "rice/Constructor.hpp"
#include "rice/String.hpp"
class cppType {
public:
void print(String args) {
std::cout << args.str() << endl;
}
};

Module rb_cModule = define_module("Types");
Class rb_cTest =
define_class_under<cppType>(rb_cModule, "Test")
.define_constructor(Constructor<cppType>())
.define_method("print", &cppType::print);

É assim que você usaria a mesma coisa no Ruby:

>> include Types
=> Object
>> y = Types::Test.new [1, 1, 1]
3
=> #<Types::Test:0x1058efbd8>

Observe que, no Ruby, os nomes dos módulos e das classes devem começar com letra maiúscula. O Rice não dá erro se, por exemplo, você dá ao módulo o nome types em vez de Types.

Criando uma estrutura de Ruby com o código C++

Você usa a criação struct no Ruby para criar rapidamente uma classe de Ruby padrão. A Listagem 17 mostra a forma típica do Ruby de criar uma nova classe do tipo NewClass com três variáveis chamadas a, ab e aab.

>> NewClass = Struct.new(:a, :ab, :aab)
=> NewClass
>> NewClass.class
=> Class
>> a = NewClass.new
=> #<struct NewClass a=nil, ab=nil, aab=nil>
>> a.a = 1
=> 1
>> a.ab = "test"
=> "test"
>> a.aab = 2.33
=> 2.33
>> a
=> #<struct NewClass a=1, ab="test", aab=2.33>
>> a.a.class
=> Fixnum
>> a.ab.class
=> String
>> a.aab.class
=> Float

Para codificar o equivalente da Listagem 17 no C++, é necessário usar a API define_struct( ) declarada no cabeçalho rice/Struct.hpp. Essa API retorna um Rice::Struct. Você associa a classe de Ruby que esse struct cria e o módulo do qual a classe fará parte. É para isso que o método initialize serve. Os membros da classe individuais são definidos usando a chamada de função define_member . Observe que você criou um novo tipo do Ruby, mas não associou nenhum tipo ou função do C++ a ele. Este é o código para criar uma classe chamada NewClass:

#include "rice/Struct.hpp"
…
Module rb1 = define_module("Types");
define_struct().
define_member("a").
define_member("ab").
define_member("aab").
initialize(rb1, "NewClass");

Conclusão

Este artigo mostrou como —criar objetos do Ruby no código de C++ , associar funções no estilo C como métodos de objeto do Ruby, converter tipos de dados entre o Ruby e o C++, criar variáveis de instância e encapar uma classe de C++ como um tipo do Ruby. Pode-se fazer tudo isso usando o cabeçalho ruby.h e libruby, mas você teria que fazer muita codificação padrão para fazer tudo funcionar. O Rice facilita todo esse trabalho. Espero que você se divirta muito criando novas extensões em C++ para o mundo do Ruby!

***

O IBM® Tivoli® Monitoring ajuda a otimizar o desempenho e a disponibilidade da infraestrutura de TI. Use o software Tivoli Monitoring para gerenciar sistemas operacionais, bancos de dados e servidores em ambientes distribuídos e de host.

Recursos

Aprender

Obter produtos e tecnologias

  • Avalie produtos IBM da maneira que for melhor para você: faça download da versão de teste de um produto, avalie um produto online, use-o em um ambiente de nuvem ou passe algumas horas no no Ambiente de Simulação da SOA para saber mais sobre como implementar arquitetura orientada a serviço (SOA) de maneira eficiente.

Discutir

***

Sobre o autor: Arpan Sen é engenheiro líder que trabalha no desenvolvimento de software no segmento de mercado da automação de design eletrônico. Ele trabalhou em diversos tipos de UNIX, incluindo Solaris, SunOS, HP-UX e IRIX, além de Linux e Microsoft Windows, por muitos anos. Possui um grande interesse por técnicas de otimização do desempenho de software, teoria de gráfico e computação paralela. Arpan possui pós-doutorado em sistemas de software.

***

Artigo original disponível em: http://www.ibm.com/developerworks/br/library/os-extendruby/index.html