HashCode

An organizer of symbols

HdInfo and NetInfo

Vou descorrer aqui sobre minha primeira experiência integrando Ruby com o Sistema Operacional chamado de Extending Ruby. Se você veio do Java como eu chamaria de Ruby Native Interface.

Instalação

Para compilação em C utilizamos os pacotes básicos do gcc e para ruby o adicional ruby-dev, portanto segue os abaixo.
(Não testei as versões anteriores do gcc ou do ruby).

gcc-4.1 	#The GNU C compiler
gcc-4.1-base    #The GNU Compiler Collection (base package)
ruby1.8         #Interpreter of object-oriented scripting lan
ruby1.8-dev     #Header files for compiling extension modules

Algumas coisas antes do teste

Serão necessárias conversões de tipos entre C e Ruby para isso utilizaremos as funções:

char * STR2CSTR(String)     #converte uma String Ruby em um ponteiro de char em C (char *)
VALUE rb_str_new2(char *)   #cria uma String em Ruby baseado em uma String em C

retorno dos métodos e variáveis convertidos em Ruby serão tipados para VALUE, contudo para STR2CSTR receberemos um VALUE(uma coisa ruby).

Uma Prévia do Init_(Nome)

Toda classe ou módulo define uma função Init_Nome, onde Nome é o nome da classe/módulo, que será invocada inicialmente antes de quaisquer outras funções/métodos.

Hello Humans

Vamos criar nosso “Hello Humans”. Crie um arquivo hello.c contendo o seguinte trecho.

#include <ruby.h>

static VALUE t_minha_string(VALUE self){

  VALUE r; //declara r como VALUE
  char *str_c = "Hello Humans!!"; //Cria a string em C

  //Cria a string em Ruby e atribui a r
  r=rb_str_new2(str_c);
  return  r;
}
//Declaração da futura classe Hello
VALUE cHello;

void Init_Hello() {
   //Definindo a classe Hello em ruby herdando de Object
  cHello = rb_define_class("Hello", rb_cObject);

  //Mapeando o t_minha_string para to_s em ruby, 0 é a quantidade de parâmetros
  rb_define_method(cHello, "to_s", t_minha_string, 0);

}

Dá pra simplifica t_minha_string em


static VALUE t_minha_string(VALUE self){

  return rb_str_new2("Hello Humans!!");  

}

Observe a declaração cHello ele deverá ser global como no exemplo acima.
Veja que as coisas se encaixam, cHello recebe uma nova referência com o nome de Hello herdando da classe rb_cObject que é uma constante para classe Object em ruby. E t_minha_string em C refletindo em to_s em Ruby.

Make

Vamos criar agora o MAKEFILE. Você deve estar pensando, “nossa que droga! Aquela tripa de parâmetros que sempre dá erro faltando alguma coisa…”. Pare com isso! Em ruby é sempre mais fácil :). Crie um arquivo extconfig.rb com o conteúdo.

require "mkmf"
create_makefile("Hello");

Rode com

ruby extconfig.rb

A mensagem “creating Makefile” aparecerá no output.
Agora dê um

make

Veja a saída:

gcc -fPIC -Wall -g -fno-strict-aliasing -O2  -fPIC  -I. -I/usr/lib/ruby/1.8/i486-linux -I/usr/lib/ruby/1.8/i486-linux -I.   -c hello.c

gcc -shared  -L"/usr/lib" -o Hello.so hello.o  -lruby1.8  -lpthread -ldl -lcrypt -lm   -lc

Linka e já cria o shared object.
Agora com permissão de escria em /usr/lib, instale com

make install

Veja a saída:

/usr/bin/install -c -m 0755 Hello.so /usr/local/lib/site_ruby/1.8/i486-linux

já copia o .so para o diretório correto do ruby.

Testando

Abra seu irb e digite na seqüencia:

require "Hello"
h=Hello.new
puts h.to_s

Ficando assim.

irb(main):001:0> require "Hello"
=> true
irb(main):002:0> h=Hello.new
=> Hello Humans!!
irb(main):003:0> puts h.to_s
Hello Humans!!
=> nil
irb(main):004:0>

Fácil né :).

Acessando informações do HD e das Interfaces de Rede

Criei duas classes uma HdInfo e outra NetInfo em Extend Ruby.

HdInfo

Aqui poderemos demonstrar mais poder em nossas mãos, vamos obter informações sobre nosso hd e depois setar os keys/values em uma classe herdanda de Hash.

serial_no, model, fw_rev são variáveis da biblioteca linux/hdreg.h
Acompanhe pelos comentários.

/*
Shairon Toledo shairon.toledo@gmail.com
http://www.hashcode.eti.br
Seg Abr  2 13:58:17 BRT 2007
*/
#include "ruby.h"
#include <stdio.h>
#include <stdlib.h>
#include <linux/hdreg.h>
#include <fcntl.h>
#include <sys/ioctl.h>

static VALUE t_init(VALUE self,VALUE anObject){

  struct hd_driveid id;
  int disk ;
  int opened;

  //Abre o ponteiro inicial do disco passando
  //paramentro de entrada, ex: /dev/hda, /dev/hdb
  disk=open(STR2CSTR(anObject), O_RDONLY|O_NONBLOCK);
  //seta as props do driver do disco em id
  //opened armazena o estado para tratar erro, a seguir
  opened = ioctl(disk, HDIO_GET_IDENTITY, &id);

  //Cria os key convertendo um (const char *) para RubyString
  VALUE k[4],v[4];
  k[0]=rb_str_new2("serial_no");
  k[1]=rb_str_new2("model");
  k[2]=rb_str_new2("fw_rev");
  k[3]=rb_str_new2("device_name");

  //cria as String baseados nos params do so struct hd_driveid
  v[0]=rb_str_new2((char*) id.serial_no);
  v[1]=rb_str_new2((char*) id.model);
  v[2]=rb_str_new2((char*) id.fw_rev);
  v[3]=anObject;//copia o valor do param de entrada para v[3]

  int i;
  for (i=0;i<4;i++){ //laço de atribuição
    rb_hash_aset(self,k[i],v[i]);

  }

  VALUE err=(opened)?Qtrue:Qfalse;
  //seta o erro. Q(true|false) são constantes equivanete a TrueClass/FalseClass em Ruby
  rb_iv_set(self, "@error",err); 

  return self;

}
//todo para leitura da variável @error
static VALUE t_error(VALUE self, VALUE anObject){
  //Obtem e retorna o valor
  return  rb_iv_get(self, "@error");

}

VALUE cHdInfo;

void Init_HdInfo() {
  //HdInfo < Hash em C
  cHdInfo = rb_define_class("HdInfo", rb_cHash);
  //Mapeia t_init  para initialize do ruby , 1 parametro =>  nome do device
  rb_define_method(cHdInfo, "initialize", t_init, 1);
  //Mapeia t_error para error? em ruby, 0 parametro
  rb_define_method(cHdInfo, "error?", t_error, 0);
}

Para criar o makefile é o mesmo processo do “Hello Humans” mas com a classe HdInfo. Ex. do extconfig.rb para HdInfo:

require "mkmf"
create_makefile("HdInfo");

Rode com

ruby extconfig.rb

depois

make

e

make install

Vamos testar com irb

irb(main):001:0> require "HdInfo"
=> true
irb(main):002:0> hd=HdInfo.new("/dev/hda")
=> {"serial_no"=>"0881J1FX406355", "fw_rev"=>"TW100-11", "device_name"=>"/dev/hda", "model"=>"SAMSUNG SP0411N"}
irb(main):003:0> hd.each{|k,v| p "#{k}=#{v}"}
 "serial_no=0881J1FX406355"
 "fw_rev=TW100-11"
 "device_name=/dev/hda"
 "model=SAMSUNG SP0411N"
=> {"serial_no"=>"0881J1FX406355", "fw_rev"=>"TW100-11", "device_name"=>"/dev/hda", "model"=>"SAMSUNG SP0411N"}
irb(main):004:0> hd.error?
=> false

Lagalzin!

NetInfo

A classe NetInfo segue o mesmo princípio da HdInfo. E a geração do .so da mesma forma, extconfig.rb, make, make install …
Vamos obter as informações de ipaddress, netmask e mac address da interface de rede(lib net/if.h e arpa/inet.h).
Acompanhe pelos comentários.

/*
Shairon Toledo shairon.toledo@gmail.com
http://www.hashcode.eti.br
Seg Abr  2 11:30:26 BRT 2007
*/
#include "ruby.h"
#include &lt;sys/socket.h>
#include &lt;sys/ioctl.h>
#include &lt;net/if.h>
#include &lt;string.h>
#include &lt;stdio.h>
#include &lt;stdlib.h>
#include &lt;arpa/inet.h>
#include &lt;errno.h>
#include &lt;unistd.h>

static VALUE t_init(VALUE self,VALUE anObject){

      int sock, i;
      struct sockaddr_in *sin;
      struct  ifreq  ifdev;
      char *ifname=STR2CSTR(anObject);  //converte anObject(RubyString) em char*

      VALUE k[4]; //Declara k como objetos ruby

      //Nossas chaves
      k[0]=rb_str_new2("netmask");
      k[1]=rb_str_new2("ipaddress");
       k[2]=rb_str_new2("macaddress");
      k[3]=rb_str_new2("ifname");

      //ifname = ao parâmetro de inicialização
      rb_hash_aset(self,k[3],anObject);

      //Cria o socket para obter as informações
      sock = socket(AF_INET, SOCK_DGRAM, 0);
      sprintf(ifdev.ifr_name, "%s", ifname);

      //SIOCGIFNETMASK infomacões de máscara
      ioctl(sock, SIOCGIFNETMASK, &ifdev);
      sin = (struct sockaddr_in *)&ifdev.ifr_broadaddr;
      //Atribui o valor do ip para a chave k[0] convertendo em RubyString
      rb_hash_aset(self,k[0],rb_str_new2(inet_ntoa(sin->sin_addr)));

      //Idem acima mas para IFADDR
      ioctl(sock, SIOCGIFADDR, &ifdev);
      sin = (struct sockaddr_in *)&ifdev.ifr_broadaddr;
      rb_hash_aset(self,k[1],rb_str_new2(inet_ntoa(sin->sin_addr)));

      //Obtem propriedades físicas da interface
      ioctl(sock, SIOCGIFHWADDR, &ifdev);

      char m[2];
      char *mac_addr= malloc(500*sizeof(char));
      for (i=0;i<6;i++){
        //Formatando
        sprintf(m, "%02X", (unsigned char)ifdev.ifr_hwaddr.sa_data[i] );
        strcat(mac_addr,m);
      }
      //Seta o mac address no self usando o key k[2]
      rb_hash_aset(self,k[2],rb_str_new2(mac_addr));

      close(sock);
      free(mac_addr);
      return self;
}

VALUE cNetInfo;

void Init_NetInfo() {

  //NetInfo < Hash em C
  cNetInfo = rb_define_class("NetInfo", rb_cHash);
  //Mapeia t_init para o initialize do ruby, 1 parametro => if_name
  rb_define_method(cNetInfo, "initialize", t_init, 1);

}

Testando no irb

irb(main):001:0> require "NetInfo"
=> true
irb(main):002:0> net=NetInfo.new("eth0")
=> {"ipaddress"=>"192.168.1.1", "netmask"=>"255.255.255.0", "ifname"=>"eth0", "macaddress"=>"00112F3683C9"}

outro teste, criando um teste.rb

puts NetInfo.new("eth0").inspect
puts NetInfo.new("eth1").inspect
puts NetInfo.new("lo").inspect

output

{"ipaddress"=>"192.168.1.1", "netmask"=>"255.255.255.0", "ifname"=>"eth0", "macaddress"=>"00112F3683C9"}
{"ipaddress"=>"10.1.1.101", "netmask"=>"255.0.0.0", "ifname"=>"eth1", "macaddress"=>"00E07DC96741"}
{"ipaddress"=>"127.0.0.1", "netmask"=>"255.0.0.0", "ifname"=>"lo", "macaddress"=>"000000000000"}

Bem é isso aí. Gostei muito disso principalmente pela facilidada da criação do makefile. Os artefatos dos exemplos estão aqui ext_ruby_info_hd_net.tar.gz.

Referências

http://kasperd.net/~kasperd/comp.os.linux.development.faq
http://www.rubycentral.com/book/ext_ruby.html
http://www.ruby-doc.org/core/classes/Hash.html
file:///usr/include/net/if.h
file:///usr/include/arpa/inet.h