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