Trabalho Final – Objetos JAVA

André Puel, Gustavo Meira, João Rover, Suzana Pescador

Implementação

Patch para adicionar Java!! ao EPOS

Objetivo

O objetivo deste trabalho é incorporar, dentro do escopo do trabalho final da disciplina INE5424, objetos JAVA no EPOS--. Estes objetos devem incluir suporte a paralelismo e mecanismos de sincronização, de tal modo que uma aplicação exemplo (como o Jantar dos Filósofos) possa ser escrita na linguagem JAVA.

Abordagem

Primeiramente, a idéia foi coletar partes diferentes de JVMs já existentes, e adaptá-las ao contexto do trabalho. Porém, ao analisar algumas JVM, vimos que elas trabalham com formatos internos (e portanto diferentes) de bytecode, impossibilitando essa “mistura” de componentes de diferentes máquinas. Frente a isso, uma solução cogitada pelo grupo foi criar uma nova máquina do zero, porém devido ao fato de não acharmos uma especificação formal do bytecode, não conseguiríamos criar um parser em tempo. Portanto, decidimos por usar uma JVM completa existente, a NanoVM, fazendo as modificações necessárias para integrá-la ao EPOS.

Após a escolha da JVM, o primeiro passo é estudar a estrutura de interfaces nativas utilizadas pela máquina, para que seja possível criar correspondências entre a VM e o EPOS da maneira mais simples possível.




NanoVM

Escolhemos a NanoVM pelos seguintes motivos:

A JVM é escrita na linguagem C e recebe de entrada um formato próprio de bytecode otimizado, que será interpretado pela máquina byte a byte. Todo o processo de ligação com as bibliotecas escritas em Java é feito estaticamente pelo próprio otimizador (NanoVMTool), partindo de um .class Java padrão. Vale a pena salientar que a NanoVM ainda não possui suporte a threads.

Mapeando métodos nativos com NanoVMTool

A ferramenta otimizadora de bytecode necessita de algumas especificações sobre o mapeamento dos métodos. Para esse propósito é necessário fornecer as assinaturas e conjuntos de métodos em texto plano, para que seja feito o mapeamento. No caso inicial, o que desejamos fazer é construir estas especificações para os conjuntos de abstrações do EPOS:

Serão declarados em nanovm/tool/config/EPOS.config os seguintes grupos:

native System

native PrintStream

native Thread

native Semaphore


Esses conjuntos serão enxergados pelo programador como classes Java. Cada um desses conjuntos terá um arquivo .native com as assinaturas dos respectivos métodos. Com esses arquivos, a ferramenta já é capaz de fazer a linkagem e gerar o bytecode otimizado.

System.native:


class java/lang/System 17

field out:Ljava/io/PrintStream; 0

field in:Ljava/io/InputStream; 1


PrintStream.native:


class java/io/PrintStream 18

method println:(Ljava/lang/String;)V 1

method println:(I)V 2

method println:(C)V 3

method print:(Ljava/lang/String;)V 4

method print:(I)V 5

method print:(C)V 6

method format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; 7


Thread.native:


class java/epos/Thread <c>


method <init>: ()

method start:()V <c>

method run: ()V <c>

method sleep: ()V <c> (estático)

method join: ()V <c>


Semaphore.native:


class java/epos/Semaphore <c>


method <init>: ()

method p: ()V <c>

method v: ()V <c>


Interpretando métodos nativos com a NanoVM

Toda invocação nativa no bytecode gerado pela NanoVMTool é representada por uma referência da classe e do método na Constant Pool. Com isso, no momento em que a VM identifica, através destas referências, a chamada de um método nativo ela invoca a sua implementação correspondente em C, obtendo os argumentos (se existentes) da pilha.

Para cada conjunto de funções nativas especificadas na NanoVMTool deverá haver um correspondente na VM da seguinte maneira: um arquivo native_<nome>.h, que conterá a declaração de constantes (se necessárias) e de pelo menos um método chamado native_<nome>_invoke(referência) - além de eventualmente init(), ou outros necessários - , que será responsável por traduzir as referências e executar o método nativo. E para cada .h criado, deverá haver também um arquivo native_<nome>.c que conterá a implementação dos métodos definidos no seu respectivo .h.

As referências dos métodos e das classes serão previamente definidas em native.h. Essas referências serão usadas quando da invocação de um método nativo pela função native_invoke em native_impl.c. Esta função, através das referências acima citadas, irá detectar qual classe está sendo usada, e a partir daí, dentre os métodos previstos para essa classe, executar a ação referenciada conforme native_<classe>.c.


Mapeando threads Java em threads EPOS

Como o mapeamento de threads Java será feito através de métodos nativos, teremos que respeitar a “cadeia de chamadas” explicada acima. Assim sendo, teremos que implementar uma função em C capaz de resolver as constantes definidas por nós que se referem aos métodos e classe Thread.

O método nativo em si, implementado em C, faz uma chamada à construção de uma especialização de Thread EPOS. A rotina desta nova thread deve receber uma referência para as estruturas da NanoVM em execução e um ponto de execução (e.g. uma referência ao método “run”) em perspectiva da VM.

A nova rotina, em concorrência, simula a chamada de uma instrução “invoke”, concluindo assim a instanciação de uma thread do ponto de vista Java.




Fear of the Dark (Box)

Um dos medos que o grupo enfrenta, justamente por não conhecermos de maneira ideal as estruturas da NanoVM, é que o código da máquina virtual cause efeitos colaterais devidos às threads. Tratando-se de C, é muito comum que haja estruturas globais que tornam-se áreas críticas durante a execução concorrente. Desse modo, após a troca de contexto entre threads EPOS, que no fim representam instâncias de threads Java, a máquina virtual pode não estar em um estado consistente (e.g. heap inconsistente, pilha de execução com operandos inválidos, etc.)

Para solucionar este problema, a especialização de threads EPOS deve conter como atributo o contexto da VM. Desse modo não é necessário identificar pontos de entrada que devam ser mutuamente exclusivos, mas sim estruturas que deverão ser privadas a cada thread (o contexto da VM).



Implementação

O primeiro passo de implementação foi compilar a VM no EPOS e rodar um teste simples que apenas mostrava uma mensagem na saída. Para isso, foi necessário a a criação de um wrapper (chamado no nosso trabalho de wrapper_jvm.h), que fazia a interface entre as bibliotecas usadas pela VM e as oferecidas pelo EPOS. Desta forma conseguimos mapear algumas funções básicas para rodar este teste.

O próximo passo então, foi referente ao suporte a threads. Começamos definindo na NanoVMTool as constantes necessárias para o reconhecimento das Threads no arquivo java, para poder gerar o bytecode interpretado pela VM. Criamos também um arquivo de configuração da VM para o EPOS, onde definimos exatamente o que precisaríamos, que foi o suporte a I/O e Threads. Assim, o bytecode recebido pela VM ficou definido corretamente e com um tamanho pequeno.

Dentro da VM, criamos os arquivos acima citados referentes ao suporte a threads (native_thread.h, native_thread.c …) e conseguimos identificar corretamente as referências geradas pela NanoVMTool que dizem respeito às Threads, de dentro da VM. Porém, tivemos alguns problemas inicialmente com a forma de implementação de Threads por Java. Esta linguagem faz a classe estender a classe Thread, e implementar o método abstrato run. Este método run, é chamado pelo método start(), que é o método a ser chamado para iniciar a thread. O nosso primeiro problema foi o suporte à herança de classes nativas da NanoVM, que não existe. Para isso, tivemos que definir os métodos de Thread como estáticos, recebendo como parâmetro a thread em questão, quando esta é necessária (no caso de start(), por exemplo). Ainda nesta linha, criamos a classe StaticThread.java, que declara as funções da Thread como estáticas e nativas, declarando também os argumentos, onde foi necessário. Assim, na implementação destes métodos na classe Thread.java foram chamados os métodos nativos declarados em StaticThread.java.

Outra questão que tivemos que lidar foi a chamada de run() dentro do código C da VM. Para isso, usamos a função vm_run(mref) que a VM usa para começar a execução do código java. Deste modo, na VM , ao identificar que a chamada de método é uma invocação de start(), chama-se a função C native_thread_start() que chama vm_run com a referência para o método run() do objeto referenciado.

Após conseguirmos atingir este objetivo, definimos como próximo passo passar a execução de run() para uma thread EPOS. Mas para podermos rodar mais de uma thread Java em Threads EPOS, precisamos abrir a dark box e caçar variáveis globais da VM, para podermos salvar o contexto da máquina antes de trocar o contexto das Threads EPOS. Conseguimos chegar nas variáveis importantes de serem salvas, que foram refletidas em atributos da classe JavaThread, uma extensão de Thread. Após, definimos que toda Thread EPOS terá uma função virtual prepare_save_context() que estará vazia para threads 'normais', mas para as JavaThreads terá a chamada para a função de salvamento de contexto da máquina. Tais métodos virtuais são invocados em Thread::dispatch, antes da troca de contexto real em CPU::switch_context(), assegurando assim a relação certa das threads às suas respectivas variáveis e pilha.