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:
- Você tem uma ou mais fontes em
C/C++
a partir das quais você faz uma biblioteca compartilhada. - Ao criar uma extensão usando o Rice, é necessário vincular o código a libruby.a e librice.a.
- Copie a biblioteca compartilhada para alguma pasta e faça com que ela faça parte da variável de ambiente RUBYLIB.
- Utilize o carregamento usual baseado em
require
no prompt do Interactive Ruby (irb)/script de ruby. Se a biblioteca compartilhada tem o nome rubytest.so, quando se digita simplesmenterequire '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
- O site rice.rubyforge.org é a página do Rice, que vem com informações excelentes no estilo Doxygen.
- Popularmente conhecido como o livro Pickaxe, Programming Ruby: The Pragmatic Programmers’ Guide (Dave Thomas, Chad Fowler e Andy Hunt; 2ª edição) é uma leitura obrigatória sobre o Ruby.
- Outro recurso importante sobre Ruby é The Ruby Programming Language de Yukihiro “Matz” Matsumoto (criador do Ruby) e David Flanagan (O’Reilly, 2008).
- To Ruby From C and C++ é um excelente site para programadores de
C/C++
que querem aprender sobre o Ruby. - O operador área de Software Livre no developerWorks fornece uma ampla gama de informações sobre ferramentas de software livre e sobre o uso de tecnologias de software livre.
- Fique atualizado com os eventos técnicos e webcasts do developerWorks com ênfase em uma variedade de produtos IBM e assuntos do segmento de mercado de TI.
- Participe de um resumo de instruções gratuito no briefing ao vivo e gratuito do developerWorks Live! para se informar sobre os produtos e ferramentas IBM, além das tendências do mercado de TI.
- Acompanhe as demos sob demanda do developerWorks , que abrangem desde demos de instalação e configuração de produtos para iniciantes até funcionalidades avançadas para desenvolvedores experientes.
- Siga os tweets do developerWorks no Twitter.
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
- Confira o Blogs do developerWorks e participe da Comunidade do developerWorks.
- Participe da Comunidade do developerWorks. Conecte-se com outros usuários do developerWorks ao mesmo tempo que explora blogs, fóruns, grupos e wikis orientados a desenvolvedor.
***
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