HashCode

An organizer of symbols

Archive for September, 2007

Shared Object Híbrido, Java e Ruby

Saturday, September 29th, 2007

Pense em dois softwares, ambos monitoram um dispositivo via driver, aparentemente iguais diferindo apenas pela linguagem em que foram concebidos, um em Java e outro em Ruby. Conseqüentemente, duas bibliotecas fazendo interface entre o driver e o ambiente da linguagem de auto nível.
Resolvi me aventurar nessa idéia e criei uma solução híbrida, uma biblioteca compatível com Ruby e Java.
jni_ruby.png
Testei com um SmartCard, como é algo específico(e você não vai comprar um só pra testar isso :)) usarei a função getenv definida em stdlib.h .

Vamos dividir em três estágios:
- Criar um teste jni(java)
- Criar um teste em ruby(Extending)
- Juntar Java e Ruby com C

Ingredientes(debian packages)
- sun-java6-jdk
- ruby1.8
- ruby1.8-dev
- gcc-4.1

Java

Vamos seguir este fluxo

jni_java_flow.png

Crie um diretório “java” para o teste. Entre no diretório.

1. Criar o arquivo fonte java
Primeiramente crie o arquivo EnviromentVariable.java que depois de compilado será o protótipo para implementação em C.

 class EnviromentVariable {  

  public native String get(String varname);  

 }

2. Gerar o bytecode
Compile

 javac EnviromentVariable.java

3. Gerar os headers

 javah -jni EnviromentVariable

O javah criará um arquivo .h com o conteúdo

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class EnviromentVariable */
#ifndef _Included_EnviromentVariable
#define _Included_EnviromentVariable

#ifdef __cplusplus
extern "C" {
#endif
/*

 * Class:     EnviromentVariable
 * Method:    get
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */

JNIEXPORT jstring JNICALL Java_EnviromentVariable_get(JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

4. Criar o arquivo fonte C
Agora faremos a lógica da biblioteca, portando é necessário a criação do arquivo enviroment_variable_java.c com o conteúdo


#include  <stdio.h>
#include  <stdlib.h> //getenv vem daqui
#include "EnviromentVariable.h"

JNIEXPORT jstring JNICALL Java_EnviromentVariable_get(JNIEnv *jvm, jobject obj, jstring varname){
  //Convertendo uma String Java em C String
  char * c_varname=(char *) (*jvm)->GetStringUTFChars(jvm, varname,NULL);

  //Obtendo o valor da variável e criando uma string para retorno
  return (*jvm)->NewStringUTF(jvm, getenv(c_varname) );
}

5. Gerar o shared object
O home da JVM na minha máquina está em /usr/lib/jvm/java-6-sun, verifique na sua distro onde foi instalada. Pra facilitar export a variável JAVA_HOME

export JAVA_HOME=/usr/lib/jvm/java-6-sun

Agora vamos gerar o libEnviromentVariable.so

gcc enviroment_variable_java.c \\
-shared -o libEnviromentVariable.so\\
-I. -I$JAVA_HOME/include -I$JAVA_HOME/include/linux

6. Testando
Para testarmos, crie um arquivo Main.java com o conteúdo.

class Main{
 static{
   System.loadLibrary("EnviromentVariable");
 }
 static public void main(String args[]){
  System.out.println(new EnviromentVariable().get("PATH"));
 }
}

Observe a cima vamos pegar o valor da variável PATH do seu sistema. Lembrando que a variável tem que existir.
Compile Main.java

javac Main.java

Execute o programa Main com a opção -Djava.library.path=”O diretório onde está o .so” no nosso caso em ‘.’

java -Djava.library.path=. Main

O output semelhante a

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/local/shairon/bin:/usr/lib/jvm/java-6-sun/bin

Depois teste mudando a variável PATH para uma outra variável válida.

Ruby

Vamos seguir o fluxo
jni_ruby_flowx.png
Crie outro diretório no mesmo nível do java mas com o nome de “ruby”. Entre do diretório.

1. Criar o arquivo fonte ruby

Crie o arquivo enviroment_variable_ruby.c conforme abaixo.

#include  <ruby.h>
#include  <stdlib.h>

static VALUE t_get(VALUE self, VALUE varname){
    //Converte String Ruby em C String
   char *c_varname=STR2CSTR(varname);

   //Obtendo o valor da variável e criando uma string para retorno
   return rb_str_new2( getenv(c_varname)); 

}

VALUE cEnviromentVariable;

//Função de inicialização do arquivo
void Init_libEnviromentVariable() {

  //Cria a classe EnviromentVariable
  cEnviromentVariable = rb_define_class("EnviromentVariable", rb_cObject);

  //Associa o método t_get com EnviromentVariable#get
  rb_define_method(cEnviromentVariable, "get", t_get, 1);
}

2. Criar o arquivo para geração do makefile

Vamos criar o arquivo extconfig.rb para geração do makefile. Abaixo

require "mkmf"
create_makefile("libEnviromentVariable")

Execute

ruby extconfig.rb

3. Compile e instale

make

depois

make install

Abrao irb e digite:

require 'libEnviromentVariable'
puts EnviromentVariable.new.get("PATH")

Java e Ruby estão chamando diretamente o getenv, algo fácil pois é só uma chamada de função. Imagine se fosse algo mais complexo? O desenvolvedor iria reutilizar o código em C via copy-paste, nada elegante.
Daqui em diante vamos fazer Ruby e Java usar uma função comum e gerar apenas um Shared Object.

Solução Híbrida

Antes de mais nada observe que o módulo “mkfm” identifica onde estão os headers necessários para a compilação do .so em ruby, durante a compilação aparece o caminho detectado, no meu caso em /usr/lib/ruby/1.8/i486-linux, portanto declarare um variável de ambiente RUBY_HOME para facilitar o entendimento.

export RUBY_HOME=/usr/lib/ruby/1.8/i486-linux

Agora o arquivo que compartilha a mesma função entre Java e Ruby, o arquivo enviroment_variable.c. Verifique a disposição dos seus arquivos e diretório. A estrutura usada é:
1p.png

O conteúdo de enviroment_variable.c, acompanhe pelos comentários.

#include 
#include "java/EnviromentVariable.h"
#include 
#include 

//Função que será compartilhada

char *var(char *varname){
  return getenv(varname);
}

//JAVA SIDE
JNIEXPORT jstring JNICALL Java_EnviromentVariable_get(JNIEnv *jvm, jobject obj, jstring varname){

  char * c_varname=(char *)(*jvm)->GetStringUTFChars(jvm, varname,NULL);

 //Chama a função var e retorna uma String para JVM
  return (*jvm)->NewStringUTF(jvm, var(c_varname) );
}

//RUBY SIDE

static VALUE t_get(VALUE self, VALUE varname){
  char *c_varname=STR2CSTR(varname);
  //Chama a função var e retorna uma String para JVM
  return rb_str_new2( var(c_varname));
}

VALUE cEnviromentVariable;

void Init_libEnviromentVariable() {
  cEnviromentVariable = rb_define_class("EnviromentVariable", rb_cObject);
  rb_define_method(cEnviromentVariable, "get", t_get, 1);
}

Linkando e Compilando

Lembrando que já definimos as variáveis de ambiente $JAVA_HOME e $RUBY_HOME.

Gerando o objeto

gcc -c enviroment_variable.c \
-fPIC -Wall -g -fno-strict-aliasing -O2 \
-I$RUBY_HOME \
-I$JAVA_HOME/include \
-I$JAVA_HOME/include/linux

depois o Shared Object

gcc -L"/usr/lib" \
-shared -o libEnviromentVariable.so enviroment_variable.o \\
-I$RUBY_HOME \\
-I$JAVA_HOME/include \\
-I$JAVA_HOME/include/linux \\
-lruby1.8 -lpthread -ldl -lcrypt -lm -lc

Pronto! Geramos o bacalhau :) o arquivo libEnviromentVariable.so está pronto para ser usado.
Testando em java

java -Djava.library.path=. -cp ./java Main

em Ruby pelo irb

require 'libEnviromentVariable'
puts  EnviromentVariable.new.get("PATH")

Alguns outputs do irb

rb(main):001:0> require 'libEnviromentVariable'
=> true
irb(main):002:0> envs=EnviromentVariable.new
=> #
irb(main):003:0> envs.get("USERNAME")
=> "shairon"
irb(main):004:0> envs.get("TERM")
=> "xterm"
irb(main):005:0> envs.get("SHELL")
=> "/bin/bash"

obs.: depois troque enviroment por environment. :)

Referências

Extending Ruby
Java Native Interface Specification

Instropecção para declaração/atribuição de valores default

Thursday, September 20th, 2007

Sabemos que o attr_accessor cria automaticamente os métodos para ler e escrever em variáveis de instância. Mas nem sempre necessitamos apenas da declaração das variáveis mas também de definir valores iniciais as mesmas. Como abaixo.

class ConfigFile
  attr_accessor :path,:alternative,:append
  def initialize(path='/etc/my.conf',alternative='/usr/local/etc/my.conf',append=false)
    @path,@alternative,@append=path,alternative,append
  end
end

Verificando uma instância sem argumentos

puts ConfigFile.new.inspect
#=> ConfigFile:0xb7a46b60 @append=false, @path="/etc/my.conf", @alternative="/usr/local/etc/my.conf">

Normale. Na classe acima fica de fácil assimilação a atribuição das variáveis de entrada com as variáveis de instância mas é necessário saber a ordem dos paramentos. Observe que escrevi cada variável quatro vezes, nada DRY.

Atribuição por Hash

Então temos o advento da atribuição por Hash(rails-style), portanto para a mesma classe, teríamos algo assim:

class ConfigFile
  attr_accessor :path,:alternative,:append
  def initialize(options={:path => '/etc/my.conf',:alternative=> '/usr/local/etc/my.conf',:append=>false})
    @path,@alternative,@append=options[:path],options[:alternative],options[:append]
  end
end

Vixi, a mesma coisa quatro vezes. Com dois agravante, se passarmos apenas uma entrada no Hash não termos os outros valores

ConfigFile.new :append => false
#=> ConfigFile:0xb7aebea8 @append=false, @path=nil, @alternative=nil

agora suponhamos que sem querer passo “apend” no lugar de “append”, ficaria assim:

ConfigFile.new :apend => "errei!!"
#=> ConfigFile:0xb7ae8320 @append=nil, @path=nil, @alternative=nil

Sem erro informando que “apend” não foi definido.

Módulo AttrDefault

Criei um módulo para facilitar a vida … o AttrDefault.

module AttrDefault
  def initialize(options={})
    _options=self.class.const_get('ATTR_DEFAULT').merge(options)
    instance_eval do
      _options.each do |k,v|
        self.send("#{k}=",v)
      end
    end
  end

  def redo_default(variable)
    send("#{variable}=",self.class.const_get('ATTR_DEFAULT')[variable])
  end

  Object.instance_eval do
    def attr_default(values)
      self.const_set("ATTR_DEFAULT",{}) unless self.const_defined? "ATTR_DEFAULT"
      self.class_eval do
        values.each do |k,v|
          attr_accessor k
          self::ATTR_DEFAULT[k]=v
        end
      end
    end
  end

end

Que

- Declara cada chave do Hash como variável de instância.
- Cria os métodos de acesso via attr_accessor.
- Atribui o valor default a cada variável de instância.
- Ao instanciar um novo objeto, faz override no valor default da variável caso exista, senão existir raise error.
- Redefine o valor se necessário.

Usando
Definição da classe, inclua o módulo AttrDefault e defina os atributos via Hash com attr_default

class ConfigFile
  include AttrDefault
  attr_default :path=>'/etc/my.conf', :alternative=>'/usr/local/etc/my.conf',:append=>true
end

Testando

conf=ConfigFile.new
conf.inspect #=> ConfigFile:0xb7ce3f1c @alternative="/usr/local/etc/my.conf", @path="/etc/my.conf", @append=true

Substituindo valores

conf=ConfigFile.new :path => "/tmp/newpath.conf", :append => false
conf.inspect        #=> ConfigFile:0xb7cbd90c @path="/tmp/newpath.conf", @append=false, @alternative="/usr/local/etc/my.conf"
puts conf.path      #=> /tmp/newpath.conf
conf.path="/novo/path"
puts conf.path      #=> /novo/path

Voltando o valor default

conf.redo_default :path
puts conf.path      #=> /etc/my.conf

Ruby Consumindo Objetos Java via RMI

Wednesday, September 12th, 2007

Demonstrarei uma solução de um programa Ruby consumindo um objeto Java via RMI. Usarei o gem Rjb - Ruby Java Bridge.

Ingredientes
+ debian packages
- sun-java6-jdk
- ruby1.8
- ruby1.8-dev
- gcc-4.1
+ ruby gem
- rjb

Instalação
Instale os pacotes, antes de instalar o gem RJB verifique as variáveis de ambiente JAVA_HOME e LD_LIBRARY_PATH, pois durante instalação do gem será compilado um objeto.

JAVA_HOME apontando para o home da JVM(os paths a seguir são relativos ao Ubuntu, verifique onde está a JVM na sua distro)

export JAVA_HOME=/usr/lib/jvm/java-6-sun

LD_LIBRARY_PATH apontando para os shared objects da JVM

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JAVA_HOME/jre/lib/i386/client

Agora o gem
Instalação normale

gem install rjb

O gem perguntará qual versão você gostaria de instalar, estou usando a 1.0.6.

Bulk updating Gem source index for: http://gems.rubyforge.org
Select which gem to install for your platform (i486-linux)
 1. rjb 1.0.6 (ruby)
 2. rjb 1.0.6 (mswin32)
 3. rjb 1.0.4 (ruby)
 4. rjb 1.0.4 (mswin32)
 5. Skip this gem
 6. Cancel installation
> 1
Building native extensions.  This could take a while...
Successfully installed rjb-1.0.6

Montando a estrutura do RMI
No nosso teste vamos ler um arquivo em Java passando o caminho do arquivo. Vamos aos passos.

Criando a interface

import java.rmi.RemoteException;
import java.rmi.Remote;

public interface AbstractAnyFile extends Remote {
    String get(String filePath) throws RemoteException;
}

Depois a implementação da interface com a lógica de leitura do arquivo.

import java.rmi.RemoteException;
import java.rmi.Naming;
import java.rmi.server.UnicastRemoteObject;
import java.io.*;
public class AnyFile extends UnicastRemoteObject implements AbstractAnyFile {

  public AnyFile() throws RemoteException { }

  public String get(String filePath) {

    try{
      InputStream in = new FileInputStream(new File(filePath));
      StringBuilder ret = new StringBuilder();
      int lidos=0;
      byte b[] = new byte[2048];
      String temp = null;
      while ( (lidos=in.read(b)) != -1){
         temp = new String(b,0,lidos);
         ret.append(temp);
      }
      return ret.toString();
    }catch(Exception e){
       return "Deu pau "+e;
    }
  }

  static void bind(){

    try { 

      Naming.rebind("RemoteFile", new AnyFile() );

    }catch(Exception e){
      System.out.println("Problema ao inicializar " + e.getMessage());
    }
   }

  static public void main(String args[]) {  AnyFile.bind();  }
}

lembranças do passado …
Observe que registramos um objeto AnyFile com o nome de RemoteFile.

Levantando o servidor(Binding)
Copile os .java

javac *.java

Gere o stub

rmic AnyFile

Inicie o rmiregistry(em background)

rmiregistry &

Inicie o servidor

java AnyFile

Do lado Ruby(cliente)
Acompanhe pelos comentários

require 'rubygems'
require 'rjb'

#garantindo a localização da JVM
ENV['JAVA_HOME']='/usr/lib/jvm/java-6-sun'

#Iniciando a JVM
Rjb::load

#Atribuindo a classe Naming a variável naming
naming = Rjb::import('java.rmi.Naming')

#Obtendo o objeto AnyFile pelo nome de RemoteFile(já associado)
myfile = naming.lookup('//127.0.0.1/RemoteFile')

#Obtendo o arquivo remoto '/tmp/teste.txt' através do objeto myfile e imprimindo
puts myfile.get("/tmp/teste.txt")

“Se der tudo certo vai funcionar :)”

Possíveis problemas
Quando for instalar o gem

Building native extensions.  This could take a while...
ERROR:  While executing gem ... (Gem::Installer::ExtensionBuildError)
    ERROR: Failed to build gem native extension.

ruby extconf.rb install rjb
checking for jni.h... no
*** extconf.rb failed ***

Falta de exportar as variáveis JAVA_HOME e LD_LIBRARY_PATH ou não encontrou a JVM.

Em ruby

-:7:in `load': can't create Java VM (RuntimeError)
	from -:7

Também não achou a JVM

Composição de funções em pilha

Tuesday, September 11th, 2007

Lendo e testando o post do Tom Moerte sobre
Composição de funções em Ruby traduzido pelo Akita e refatorado pelo TaQ, a solução de composição de blocos/funções baseai-se em um operador de composição(em Haskell) para geração de uma nova função, simplificando o cálculo e dividindo o problema em partes.

Já em Postscript é implícita tal composição. Por ser uma Linguagem de Programação Orientada a Pilha com a sintaxe Pós-Fixada (Notação Polonesa Invertida) os argumentos e os operadores ficam em um contexto único(pilha), facilitando a execução de cálculos matemáticos sem a necessidade de criar um operador “que junta as funções e passa os parâmetros”. Muito mais simples. Acompanhe.

Primeiramente observe a função

f(x) = X² + 1

Portando para X= 3, temos

f(3) = 3² + 1
f(3) = 10

a mesma função em notação polonesa com operadores postscript

3 2 exp  1 add

quando é executado 3 2 exp o resultado 9 fica na pilha, o valor 1 também está na pilha então 9 1 add resulta em 10. Veja no ghostscript o passo-a-passo(pstack mostra os valores da pilha)

GS>3
GS<1>2
GS<2>pstack
2
3
GS<2>exp
GS<1>pstack
9.0
GS<1>1
GS<2>pstack
1
9.0
GS<2>add
GS<1>pstack
10.0

Vamos criar as funções

/square { 2 exp} def         % eleva ao quadrado o valor corrente
/inc { 1 add } def         %adiciona 1 no valor corrente

testando

GS>3 square
GS<1>pstack
9.0
GS<1>inc
GS<1>pstack
10.0

A composição
Apenas aninhar as funções

/nova{ square inc } def
GS>3 nova
GS<1>pstack
10.0

Programação em pilha tem lá suas vantagens mas não é nada humano.

Testes sem LER

Wednesday, September 5th, 2007

Nossa mãe… toda vez que vou fazer teste tenho que digitar assert_algumacoisa1, assert_algumacoisa2…

Assim

A Classe

require 'test/unit'
class Numero

  def initialize(num)
    @num=num
  end

  def tipo
    (@num%2)?:par:impar
  end

  def valor
    @num
  end

end

O Teste

require 'test/unit'

class TestNumero < Test::Unit::TestCase

  def test_simple
     assert_equal(:par, Numero.new(3).tipo)
     assert_equal(:impar, Numero.new(3).tipo)
     assert_equal(2, Numero.new(3).valor)
     assert_equal(3, Numero.new(3).valor)
  end

end

Resultado

Loaded suite -
Started
F
Finished in 0.027703 seconds.

  1) Failure:
test_simple(TestNumero) [-:24]:
<:impar> expected but was
<:par>.

1 tests, 2 assertions, 1 failures, 0 errors

Cansei…

Recomendo a criação implícita do assert_ via method_missing na classe Test::Unit::TestCase . Exemplo:

Adicionando o facade

require 'test/unit'

class Test::Unit::TestCase
  def method_missing(method_name,*args,&block)
    send("assert_#{method_name}".to_sym, *args,&block)
  end
end

Agora temos o código mais resumido.

class TestNumero < Test::Unit::TestCase

  def test_simple
     equal(:par, Numero.new(3).tipo)
     equal(:impar, Numero.new(3).tipo)
     equal(2, Numero.new(3).valor)
     equal(3, Numero.new(3).valor)
  end
end