HashCode

An organizer of symbols

Archive for the ‘java’ Category

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

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