Interligaç˜ao entre Racket e Java

Propaganda
Interligação entre Racket e Java
António Menezes Leitão
16 de Junho de 2013
1
Introdução
A interligação de linguagens visa permitir a construção de sistemas cujos constituintes estão escritos em diferentes linguagens.
Uma das formas mais simples de interligação consiste no estabelecimento
de um canal de comunicação bidireccional entre módulos escritos em diferentes
linguagens de programação. Cada módulo executa no seu próprio espaço de endereçamento mas pode enviar e receber dados através do canal de comunicação.
Uma vez que os canais de comunicação apenas permitem a troca de bytes, os
valores a transmitir são codificados à entrada do canal e descodificados à saı́da.
Para facilitar a utilização desta abordagem, permite-se também a invocação
remota, i.e., a possibilidade de um módulo codificar a invocação de funcionalidades providenciadas pelo outro módulo. O outro módulo deverá então descodificar essa invocação, deverá realizá-la e, finalmente, deverá codificar uma
resposta contendo o resultado e enviá-la para o módulo que pediu a invocação.
Este módulo deverá então descodificar a resposta, criando objectos no seu espaço
de endereçamento que representam os valores recebidos.
2
Objectivos
Pretende-se a implementação de uma arquitectura cliente/servidor em que o
servidor está em execução numa JVM e o cliente é escrito em Racket.
O servidor deverá ser implementado pelo método estático startServer (sem
argumentos) definido na classe ist.meic.pa.JCaller que, uma vez invocado,
estabelece um ServerSocket num porto pré-estabelecido e fica à espera da
ligação de um cliente a esse socket. Essa invocação é, portanto, a seguinte:
ist.meic.pa.JCaller.startServer();
Do lado do cliente, a função connect-server (sem argumentos) irá estabelecer a ligação com o servidor no porto pré-estabelecido. Eis um exemplo de
utilização:
> (connect-server)
#t
Uma vez estabelecida a ligação, o servidor entra num modo de processamento
request/response, em que aceita pedidos do cliente, descodifica esses pedidos,
1
avalia-os e codifica uma resposta que é enviada ao cliente. O cliente deverá
então descodificar a resposta e criar os objectos correspondentes no seu lado.
A tı́tulo de exemplo, considere a seguinte expressão avaliada no cliente:
(let ((tokenizer
(jnew (jconstructor (jtype "java.util.StringTokenizer")
(jtype "java.lang.String")
(jtype "java.lang.String"))
(jstring "a>b>c")
(jstring ">"))))
(rstring
(jcall (jmethod (jtype "java.util.StringTokenizer")
"nextToken")
tokenizer)))
Note que a referência às classes Java no lado Racket faz-se usando a função
jtype. Note também que os argumentos a usar na invocação remota do construtor são produzidos pela função jstring que realiza a conversão de strings
Racket para referências para strings Java. Por regra, todas as funções Racket
que fazem a conversão de valores Racket para valores Java tem um nome que
começa pela letra “j” enquanto que as funções que fazem a conversão inversa
(i.e., de Java para Racket) começam pela letra “r.”
Como resultado da avaliação da expressão anterior, a variável local
tokenizer ficará associada a um objecto representando uma instância de
java.util.StringTokenizer capaz de produzir os tokens presentes na string
Java "a>b>c", usando o separador ">". Essa variável é então usada como
argumento da invocação remota do método nextToken (definido na classe
java.util.StringTokenizer), invocação essa que (para este exemplo) devolve
um objecto representando a string Java "a". Finalmente, esse objecto é convertido na string Racket equivalente empregando a função rstring.
Note que existe uma separação total entre valores Java e valores Racket,
sendo que a conversão entre uns e outros tem de ser feita explicitamente pelo
programador.
Naturalmente, deverá ser possı́vel definir funções Racket que “escondam” as
invocações remotas e as conversões de valores. A tı́tulo de exemplo, considere
as duas seguintes definições:
(define tokenizer
(let ((java-string-tokenizer
(jtype "java.util.StringTokenizer"))
(java-string
(jtype "java.lang.String")))
(let ((constructor
(jconstructor java-string-tokenizer
java-string
java-string)))
(lambda (string delimiters)
(jnew constructor
(jstring string)
(jstring delimiters))))))
2
(define next-token
(let ((java-string-tokenizer
(jtype "java.util.StringTokenizer")))
(let ((method
(jmethod java-string-tokenizer "nextToken")))
(lambda (tokenizer)
(rstring (jcall method tokenizer))))))
Usando as funções anteriores, podemos obter os dois primeiros tokens da
string "foo$bar$baz" através de
(let ((tokens (tokenizer "foo$bar$baz" "$"))
(list (next-token tokens) (next-token tokens)))
O resultado da avaliação anterior será ("foo" "bar").
As próximas secções detalham alguns aspectos do projecto a realizar.
2.1
Funções no Cliente
Pretende-se a definição de algumas funções fundamentais:
• jtype: Dada uma string representando o nome de um tipo em Java,
produz um objecto representando esse tipo.
• jconstructor: Dada a representação de um tipo e qualquer número de
representações de outros tipos, produz um objecto representando o construtor do primeiro tipo cuja assinatura coincide com os restantes tipos.
• jnew: Dada a representação de um construtor e dados os argumentos
apropriados para esse construtor, invoca o construtor e produz uma representação do objecto construı́do.
• jmethod: Dada a representação de um tipo, dada uma string e qualquer
número de representações de outros tipos, produz um objecto representando o método definido no primeiro tipo cujo nome é igual à string e
cuja assinatura coincide com os restantes tipos.
• jcall: Dada a representação de um método, dada a representação de um
objecto e dados os argumentos apropriados para esse método, invoca o
método e produz uma representação do objecto resultante construı́do. Se
o método em questão for estático, o segundo argumento é ignorado.
2.2
Valores vs Referências
Uma vez que os tipos de dados pré-definidos diferem de linguagem para linguagem, será necessário que o cliente e o servidor possam chegar a acordo quanto
à correspondência de valores entre um e outro.
Para lidar com este problema, deverá implementar um conjunto de funções
em Racket que, a partir de um valor de Racket, produzem uma representação
do objecto Java pretendido. Essas funções são:
• jbyte: recebe um integer e produz uma representação do byte correspondente em Java.
3
• jshort: recebe um integer e produz uma representação do short correspondente em Java.
• jint: recebe um integer e produz uma representação do int correspondente em Java.
• jlong: recebe um integer e produz uma representação do long correspondente em Java.
• jfloat: recebe um number e produz uma representação do float correspondente em Java.
• jdouble: recebe um number e produz uma representação do double correspondente em Java.
• jboolean: recebe um boolean e produz uma representação do boolean
correspondente em Java.
• jchar: recebe um character e produz uma representação do char correspondente em Java.
• jstring:
recebe uma string e produz uma representação da
java.lang.String correspondente em Java.
• jnull: não recebe argumentos e produz uma representação do null em
Java.
Na grande maioria dos casos, contudo, não existe uma função especı́fica
para a produção de um valor Java. Por exemplo, as instâncias do tipo
java.util.StringTokenizer só podem ser produzidas por obtenção e invocação do construtor apropriado.
Terá ainda de implementar a conversão de valores Java para valores Racket.
Para este projecto apenas se exige a conversão para booleanos, para números,
para caracteres, e para strings:
• rboolean: recebe a representação Racket de um booleano Java e produz
o booleano Racket equivalente.
• rnumber: recebe a representação Racket de um número Java e produz o
número Racket equivalente.
• rcharacter: recebe a representação Racket de um carácter Java e produz
o carácter Racket equivalente.
• rstring: recebe a representação Racket de qualquer objecto Java e produz uma string Racket contendo a mesma sequência de caracteres que é
produzida pela invocação do método Java toString sobre o objecto Java
correspondente.
4
2.3
Identidade vs Igualdade
O conceito de identidade de objectos está presente quer em Java, quer em Racket. Este conceito diz-nos que é possı́vel distinguir objectos (estruturalmente)
iguais através da comparação das referências que temos para eles. Em Java, a
comparação de identidades é feita com o operador ==, enquanto que em Racket
a função eq? cumpre o mesmo efeito.
A tı́tulo de exemplo, considere o seguinte framento de programa Java:
Object foo = "Foo";
if (foo.toString() == foo) {
liveInPeace();
} else {
launchAllMissiles();
}
Se pretendermos implementar o mesmo exemplo em Racket, podemos escrever:
(let ((foo "Foo"))
(let ((java-foo (jstring foo)))
(if (eq? (jcall (jmethod (jtype "java.lang.String")
"toString")
java-foo)
java-foo)
(live-in-peace)
(launch-all-missiles))))
Para que esta expressão possa ser avaliada correctamente, é necessário que
o conceito de identidade seja preservado quer na passagem de valores do Java
para Racket, quer na passagem de valores do Racket para o Java.1
Note que esta propriedade não é garantida quando se utiliza a função
rstring, ou seja, é possı́vel que a avaliação do seguinte fragmento produza
resultados desagradáveis:
(let ((foo "Foo"))
(let ((java-foo (jstring foo)))
(if (eq? (rstring java-foo)
foo)
(live-in-peace)
(launch-all-missiles))))
2.4
Excepções
Sempre que a invocação de uma operação em Java provocar uma excepção, esta
excepção deverá ser propagada para a invocação correspondente em Racket. No
lado Racket, esta excepção deverá ser um subtipo de exn:fail:contract.
1O
padrão de desenho Identity Map poderá dar uma ajuda à solução deste problema.
5
2.5
Recolha de Lixo
Quer Java, quer Racket possuem garbage collection. Isto permite programar
sem procupações quanto à desalocação da memória. Infelizmente, a preservação
do conceito de identidade entre as duas linguagens poderá tornar impossı́vel
que os respectivos mecanismos de recolha de lixo do Racket e do Java possam
realizar o seu trabalho.
Para resolver este problema, considere o uso das capacidades de gestão de
memória do Racket.
2.6
Extensões
Se pretender, pode fazer as extensões que achar apropriadas que possam valorizar ainda mais o seu trabalho. Tenha em conta que essa valorização será, no
máximo, de dois valores a somar à nota que obtiver pela implementação do que
foi pedido nas outras secções.
Algumas das extensões que poderão ser interessantes são:
• Tratamento de arrays
• Operações para obtenção dos valores dos fields dos objectos Java
• Implementação de uma camada de abstracção em Racket que permita uma
invocação de métodos Java empregando argumentos do Racket
• Geração automática de código Racket a partir de classes Java.
3
Código
A parte Racket do seu projecto deverá funcionar na versão 5.3.1.
O uso de sockets está documentado em:
Java
http://docs.oracle.com/javase/tutorial/networking/sockets/
Racket
http://docs.racket-lang.org/reference/tcp.html
A gestão de memória está documentada em:
Racket http://docs.racket-lang.org/reference/memory.html
O código desenvolvido deverá estar escrito no melhor estilo que for possı́vel,
permitindo a sua fácil leitura e dispensando excessivos comentários. É sempre preferı́vel ter código mais claro com poucos comentários do que ter código
obscuro com muitos comentários.
O código deverá ser modular, dividido em funcionalidades com responsabilidades especı́ficas e reduzidas. Cada módulo deverá ter um curto comentário a
descrever o seu objectivo.
6
4
Apresentação
Não se pretende que faça um relatório do seu trabalho mas sim uma apresentação
pública. Esta deverá ser preparada para ter 10 minutos de duração (aproximadamente 5 slides), deverá centrar-se nas opções arquitecturais tomadas e poderá
incluir os detalhes que considere relevantes. Pretende-se que consiga “vender”
a sua solução ao seus colegas e ao corpo docente.
5
Formato da entrega
O projecto é entregue por via electrónica através do Portal Fénix. Cada grupo
deverá entregar um único ficheiro comprimido em formato ZIP, com o nome
jcaller.zip. Este ficheiro deverá conter:
• o código fonte desenvolvido (quer em Java, quer em Racket),
• o ficheiro com os slides da apresentação do trabalho,
• um ficheiro build.xml para compilar o código Java e gerar o jcaller.jar,
• um ficheiro jcaller.rkt para carregar o código Racket.
O formato aceite para os slides de apresentação do trabalho é o PDF. O
ficheiro com a apresentação tem de estar na raı́z do ficheiro ZIP e tem de ter o
nome p.pdf.
O ficheiro build.xml é um ficheiro de Ant (http://ant.apache.org) e deve
produzir na mesma localização o ficheiro jcaller.jar com todo o código
Java necessário para executar o inspector desenvolvido, como descrito anteriormente. O alvo de omissão do build.xml deve efectuar o trabalho descrito, ou
seja, simplesmente executar numa shell
$ ant
deve produzir o resultado esperado (jcaller.jar).
O ficheiro jcaller.rkt é um ficheiro de Racket que, uma vez carregado,
deixa o Racket em condições de avaliar expressões como as exemplificadas neste
documento. Deverá ser suficiente fazer:
$ racket
Welcome to Racket v5.1.3.
> (require "jcaller.rkt")
Para se poder executar os exemplos apresentados neste documento.
6
Avaliação
Os critérios de avaliação incluem:
• A qualidade das soluções desenvolvidas.
• A clareza dos programas desenvolvidos.
• A qualidade da apresentação pública.
Em caso de dúvidas, o corpo docente poderá pedir explicações sobre o funcionamento do projecto desenvolvido, incluı́ndo eventuais demonstrações.
7
7
Plágio
Considera-se plágio o uso de quaisquer fragmentos de programas que não tenham
sido fornecidos pelos docentes da disciplina. Não se considera plágio o uso de
ideias cedidas por terceiros desde que seja feita a devida atribuição.
Esta disciplina segue normas muito rı́gidas relativamente ao plágio. Quaisquer projectos que sejam considerados plagiados serão anulados, independentemente de quem plagiou e de quem tiver sido plagiado, independentemente de o
plágio ter sido autorizado, ou não, pela parte plagiada.
Isto não deverá ser impedimento para a troca salutar de ideias e para a
normal camaradagem e entreajuda que deve existir entre colegas. Contudo,
sugere-se que nunca cedam (ou acedam a) fragmentos de programas sob pena
de quem os recebe não os entender e se limitar a plagiá-los com maior ou menos
esforço de “camuflagem.”
8
Notas Finais
Não se esqueça da Lei de Murphy.
9
Prazos
O código e os slides da apresentação deverão ser entregues via fénix, até às 19:00
do dia 9 de Julho.
As apresentações irão ocorrer no dia 10 de Julho às 15h, no pavilhão de
Informática 2.
8
Download