2.3 Conexão Causal

Propaganda
Universidade Federal de Santa Catarina
Instituto de Informática e Estatística
REFLEXÃO COMPUTACIONAL: LINGUAGENS E
APLICAÇÕES
Islon de Souza Scherer
Florianópolis
2004
Índice
1. Introdução..................................................................................................................2
2. Reflexão Computacional............................................................................................3
2.1 Usos da Reflexão.................................................................................................6
2.2 Arquitetura Reflexiva..........................................................................................7
2.3 Conexão Causal...................................................................................................8
2.4 Meta-Domínio.....................................................................................................9
2.5 Metaobjetos.........................................................................................................10
3. OpenC++..................................................................................................................12
3.1 Estrutura do OpenC++......................................................................................13
3.2 Estrutura básica do protocolo............................................................................14
3.3 Compatibilidade com o mundo real..................................................................14
3.4 Outras Questões................................................................................................15
3.5 Extensões de Sintaxe.........................................................................................16
3.6 Sobrecarga do protocolo....................................................................................17
3.7 Meta-circularidade.............................................................................................18
3.8 Trabalhos relacionados......................................................................................19
3.9 Situação atual.....................................................................................................20
4. Javassist.....................................................................................................................21
4.1 Introdução..........................................................................................................21
4.2 Javassist..............................................................................................................22
4.2.1 Uso de Javassist..........................................................................................22
4.2.2 Meta Programação.....................................................................................23
4.2.3 Assistência em Tempo de Execução..........................................................26
4.3 Reflexão em Tempo de Execução......................................................................28
4.4 Questões de Implementação...............................................................................29
4.5 Conclusão ..........................................................................................................30
5. MetaJava...................................................................................................................31
5.1 Introdução..........................................................................................................31
5.2 Modelo Computacional de MetaJava.................................................................31
5.3 Metaobjetos para implementações de sistemas operacionais abertos................34
5.4 Implementação...................................................................................................35
5.4.1 Classes Sombra..........................................................................................35
5.4.2 Implementação do Mecanismo de Eventos...............................................39
5.4.3 Anexamento do Metaobjeto.......................................................................44
5. Lista de Figuras.........................................................................................................46
6. Referências................................................................................................................47
1
1. Introdução
Sistemas computacionais são produzidos com o intuito de fornecer soluções para
determinados problemas, ou ferramentas que auxiliem na solução de um ou mais
problemas. Durante muito tempo os paradigmas de construção de sistemas foram pensados
unicamente para “raciocinarem” no âmbito da solução. Até que as primeiras teorias
envolvendo reflexão computacional viessem à luz essa era a única forma de se pensar em
termos de desenvolvimento de sistemas. Com o advento das primeiras teorias reflexivas
foi desvelada uma nova forma de desenvolver sistemas, não apenas restrita a computar em
cima da solução, mas capaz de introspecção, ou seja, analisar a si próprio, refletir sobre
sua própria estrutura.
Os sistemas modernos são cada vez mais complexos, conforme esta complexidade
aumenta, aumentam as dificuldades na produção dos mesmos. Esta complexidade exige
dos profissionais uma maior gama de conhecimentos para lidarem com tais dificuldades.
Somente conhecimento não é suficiente para acelerar e facilitar o processo de
criação de sistemas complexos. É preciso que novas tecnologias sejam desenvolvidas para
auxiliarem nesta tarefa.
Uma solução interessante para a redução de complexidade é a divisão dos aspectos
funcionais e não funcionais dos sistemas complexos. Entendendo-se por aspectos
funcionais as tarefas que levam a conclusão do objetivo principal do problema, sendo os
aspectos não funcionais, as tarefas de suporte à parte funcional.
2
2. Reflexão Computacional
Um programador que deseje implementar um sistema deve, basicamente, programálo para solucionar um ou mais problemas. O sistema depois de implementado irá resolvelos segundo o modo pelo qual foi construído, ele jamais irá modificar seu modo de
operação para se adaptar ao problema, ou a variação do mesmo, ou seja, ele não irá
raciocinar sobre o problema, isso será feito apenas pelo programador no momento em que
implementa o software.
Sob o ponto de vista da reflexão computacional esse modelo descrito acima se
modifica, o sistema não mais se preocupará unicamente com a solução do problema
segundo os métodos pelo qual foi implementado, mas haverá um novo nível no sistema
que terá “consciência” da tarefa e poderá raciocinar sobre ela, sendo que esse novo nível
(reflexivo) possuirá todos os dados necessários a execução da tarefa. Sempre que esta for
executada o nível reflexivo “reflete” sobre ela, se esse nível decide que algo deve ser
mudado ele repassa as mudanças para o nível base (não reflexivo).
A palavra reflexão possui dois conceitos:
Modificação da direção da propagação de uma onda que incide sobre uma interface
que separa dois meios diferentes, e retorna para o meio inicial. Ato ou efeito de refletir(se). A volta que a consciência ou espírito faz sobre si mesmo, para meditar, usando o
entendimento para conhecer-se com maior profundidade.[8].
No âmbito da reflexão computacional tratamos o termo como uma arquitetura em
que o software possa analisar e mudar seu comportamento, forma de execução e seus
dados para, se necessário, modificar-se visando um maior desempenho na sua execução,
sendo que nesta arquitetura existem dois ou mais níveis: o nível base e o(s) nível(is)
3
reflexivo(s), este último responsável pela reflexão.
Segundo Cysneiros[10], os requisitos não funcionais, ao contrário dos funcionais,
não expressam nenhuma função a ser realizada pelo software, e sim comportamentos e
restrições que este software deve satisfazer. Requisitos não funcionais a muito são
mencionados em métodos de desenvolvimento, nomes como: restrições de software,
condições de contorno, são algumas das denominações utilizadas. Apesar disso, raramente
encontra-se um software em que os requisitos não funcionais tenham sido levados em
consideração ou mesmo listados de maneira adequada.
A separação de domínios, que a reflexão computacional oferece, traz uma nova
possibilidade de modelagem e implementação de características não funcionais ao sistema.
Segundo [11], a idéia básica sobre reflexão computacional está em:
a) separar as funcionalidades básicas das não funcionalidades básicas através de
níveis arquiteturais;
b) as funcionalidades básicas devem ser satisfeitas pelos objetos da aplicação;
c) as não básicas devem ser satisfeitas pelos metaobjetos;
d) as capacidades não funcionais são adicionadas aos objetos da aplicação através de
seus metaobjetos específicos;
e) o objeto base pode ser alterado estruturalmente e em seu comportamento em
tempo de execução, ou compilação.
Atribuir a um sistema tal capacidade, significa dar-lhe, flexibilidade, para alterar e
adaptar dinamicamente sua estrutura e comportamento, redução de complexidade,
separação conceitual, reutilização, transparência e uma maior adaptabilidade.
No paradigma reflexivo os sistemas computacionais são divididos em dois domínios:
o domínio de aplicação e o meta-domínio. O domínio de aplicação é onde ficam as para4
entidades, a parte não reflexiva, responsável por resolver o problema. O meta-domínio é a
parte reflexiva, vinculado ao domínio de aplicação. Este domínio é dividido em níveis,
sendo que o nível mais baixo manipula informação da camada de aplicação enquanto os
subsequentes manipulam informação do nível diretamente inferior ao seu.
Uma questão que deve ser levada em conta no paradigma reflexivo é a conexão causal,
o sistema reflexivo é capaz de automodificação, mas estas mudanças devem garantir que o
sistema depois de modificado seja uma representação equivalente do anterior, senão ele
perderia a fidedignidade, este vínculo foi chamado de conexão causal.
Devido a várias características úteis das linguagens orientadas a objeto(OO) houve um
casamento entre o paradigma OO e o reflexivo de onde surgiram os meta-objetos, ou seja,
objetos do meta-nível juntamente com os objetos de nível base, que são os objetos da
camada de aplicação (para-nível).
O interfaceamento entre o para e o meta-nível é feito através do protocolo de metaobjetos (MOP) que interage com o nível base, coletando informações, modificando-o e
executando-o. Um MOP deve possuir as seguintes características: vinculação, reificação,
execução e modificação; sendo a vinculação o acoplamento do para e do meta-nível,
reificação a coleta de informações do para-nível pelo meta-nível, execução o uso do paranível pelo meta-nível, a modificação é exatamente isso, o meta-nível modificando o paranível.
A possibilidade de um processo computacional manipular a si mesmo existe desde
1945 quando Von Neumann unificou o armazenamento de códigos e dados numa mesma
mídia, mas devido à falta de um dispositivo formal, na época, para se utilizar tal aplicação
essa possibilidade foi descartada pelos cientistas da época.
5
Em meados da década de setenta e oitenta surgiram linguagens que introduziram
certas características reflexivas, mas estas características não foram postas de maneira a
expressar um formalismo, não havia distinção entre para-nível e meta-nível entre outras
dificuldades. Somente com o dialeto 3-Lisp foi adicionado um formalismo apropriado.
Dentre as diversas linguagens que apresentam características reflexivas não formais estão:
Smaltalk, LOOPS, FOL, Actors, etc.
2.1 Usos da Reflexão
A primeira vista, o conceito de reflexão pode parecer um pouco aberto. Mas uma
análise mais criteriosa mostrará que há um substancial valor prático na reflexão. Diversas
funcionalidades na computação requerem reflexão. Vários sistemas comuns exibem tanto
computação objeto (computação sobre o domínio externo do problema) como computação
reflexiva (computação sobre si mesmos). Exemplos de computação reflexiva são: manter
estatísticas de performance, manter informação para propósito de debug, ferramentas com
facilidades de step, tracing (debuggers, compiladores), interfaceamento (saída de gráficos,
entrada de mouse), computação sobre que computação realizar (também chamado de
raciocínio sobre controle), auto-otimização, auto-modificação (sistemas que aprendem) e
auto-ativação (monitores, daemons).
A computação reflexiva não ajuda diretamente a resolver problemas no domínio
externo do sistema. Ao invés disso, ela contribui para a organização interna do mesmo ou
sua interface para o mundo externo. Seu propósito é garantir a efetividade e a
funcionalidade da computação objeto.
6
2.2 Arquitetura Reflexiva
Uma linguagem de programação com uma arquitetura reflexiva reconhece a reflexão
como um conceito fundamental de programação e, assim, provê ferramentas para tratar a
computação reflexiva explicitamente. Isto significa que:
i.
O interpretador de tal linguagem tem que dar a qualquer sistema que esta
rodando, acesso a dados que representam o próprio sistema. Sistemas
implementados em tal linguagem, então, tem a possibilidade para executar
computação reflexiva incluindo código que prescreve como estes dados
devem ser manipulados.
ii.
O interpretador também tem que garantir que a conexão causal entre estes
dados e os aspectos do sistema que ele representa é mantida.
Conseqüentemente, as modificações que esse sistema faz sobre sua autorepresentação são refletidas em sua própria condição e computações.
Arquiteturas reflexivas oferecem um novo paradigma para pensar sobre sistemas
computacionais. Numa arquitetura reflexiva, um sistema computacional é visto como
incorporando uma parte objeto e uma parte reflexiva. A função da computação objeto é
resolver problemas e retornar informação sobre um domínio externo, enquanto que a
função do nível reflexivo é resolver problemas e retornar informação sobre a computação
objeto.
Então, arquiteturas reflexivas provêem uma maneira para se implementar
computação reflexiva de uma maneira mais modular. Como é largamente sabido, mais
modularidade torna o sistema mais gerenciável, mais legível e mais fácil de entender e
modificar. Mas estas não são as únicas vantagens da decomposição. O que é ainda mais
7
importante é que isto torna possível introduzir abstrações que facilitam a programação da
computação reflexiva do mesmo modo que estruturas de controle abstratas como DO e
WHILE facilitam a programação de fluxo de controle.
2.3 Conexão Causal
Sempre que houver uma mudança na estrutura interna do sistema, haverá
automaticamente uma mudança no seu domínio, e, para completar a bivalência, sempre
que houver uma mudança no domínio do sistema a mesma mudança ocorrerá na
representação interna do mesmo. Ou seja, um sistema reflexivo possui conexão causal se e
somente se, sua representação interna se altera em virtude de qualquer alteração do
domínio para sempre representá-lo fidedignamente e, qualquer mudança que ocorra deverá
garantir que as estruturas internas do sistema sejam fiéis a sua representação, como
descrito na formalização do tema por Pattie Maes:
A system is said to be causally connected to its domain if the internal structures
and the domain they represent are linked in such a way that if one of them
changes, that leads to a corresponding effect upon the other. So a causally
connected system always has an accurate representation of its domain and it may
actually cause changes in its domain as mere effect of its computation.
The Causal Connection, Pattie Maes (1987)
Uma forma de garantir a validade da conexão causal em uma dada arquitetura
reflexiva seria fazer o projeto dessa arquitetura de forma que sua auto-representação fosse
8
efetivamente utilizada em sua implementação. Dessa forma, seria automaticamente
garantida a consistência entre o sistema propriamente dito e sua auto-representação. Esta
abordagem apresenta alguns problemas, tais quais: conciliar expressividade e eficiência na
auto-representação. A expressividade reflete o quanto à descrição do sistema dá margem
para análise. A eficiência reflete o desempenho da auto-representação na implementação
do sistema.
Expressividade e eficiência são requisitos quase sempre contraditórios, por isso foi
criada uma outra abordagem para tratar da conexão causal denominada reflexão
declarativa. Na reflexão declarativa, a auto-representação não é uma completa descrição
funcional do sistema, mas sim uma coleção de restrições sobre o estado e comportamento
do mesmo. Nessa abordagem se torna mais fácil conciliar expressividade e eficiência, mas
muito mais difícil assegurar a propriedade de conexão causal. Existem algumas linguagens
que utilizam reflexão declarativa como, por exemplo, GOLUX.
2.4 Meta-Domínio
O conceito de reflexão computacional se difere dos demais, principalmente em um
ponto. Enquanto nos paradigmas tradicionais o sistema como um todo é construído tendo
em vista a solução do problema, na reflexão computacional, este aspecto é apenas um dos,
possivelmente, vários níveis de abstração do sistema.
Nos sistemas reflexivos, estes passam a ser subdivididos em dois domínios: domínio
da aplicação (externo) e meta-domínio (interno). O domínio da aplicação é independente e
irrestrito, se preocupa apenas com a aplicação como em um sistema tradicional, enquanto
o meta-domínio está sempre vinculado com o domínio da aplicação. Essa divisão leva
9
arquiteturas reflexivas a serem organizadas em uma pilha de níveis de abstração. O
primeiro nível é o domínio da aplicação, representado por um único nível sem
características reflexivas denominado nível base.
No meta-domínio ficam um ou mais níveis reflexivos denominados meta-níveis.
Cada meta-nível é responsável por manipular meta-informação do nível imediatamente
inferior a si próprio, podendo tal nível ser o nível base (não reflexivo) ou outro meta-nível
numa espécie de recursão reflexiva, onde cada nível se preocupa com o seu nível inferior.
Figura 1: arquitetura reflexiva básica em OO
2.5 Metaobjetos
10
É possível aplicar o conceito de reflexão computacional a diversos paradigmas de
programação (e na prática existem várias linguagens de diferentes paradigmas que se
utilizam deste conceito), mas por suas diversas características positivas e larga utilização,
a orientação a objeto (OO) é a que melhor se funde com a reflexão computacional. Essa
amálgama é tanto benéfica para o paradigma OO, que se utiliza do baixo acoplamento
oferecido pelas características reflexivas devido a divisão dos requisitos funcionais e
gerenciais entre o meta e o para nível, quanto para a reflexão computacional, onde se torna
mais fácil estruturar o meta e o para-domínio entre outras facilidades devido a estruturação
em classes da OO.
Reflexão computacional está baseada na noção de definir um interpretador de uma
linguagem de programação para a própria linguagem. No paradigma de objetos, isto
significa representar toda a abstração do modelo de objeto em termos do próprio modelo
de objetos.
Um sistema reflexivo precisa obter uma descrição abstrata do sistema tornando-a
suficientemente concreta para permitir operações sobre ela, utilizar esta descrição concreta
para realizar alguma manipulação, modificar a descrição obtida com os resultados da
reflexão computacional, retornando a descrição modificada ao sistema. Para realizar tal
feita é preciso haver meios de comunicar o agente modificador com o objeto modificado,
ou seja, o meta nível e o nível base, comunicação esta feita através de uma interface
definida num MOP (protocolo de metaobjetos).
11
3. OpenC++
Criado por Shigeru Chiba, OpenC++ é um protocolo de metaobjetos (MOP) para
C++. Estruturado de forma que os metaobjetos controlam o programa em tempo de
compilação e não de execução, este MOP se caracteriza por ter, inerentemente, uma boa
performance, visto que não acarreta nenhum overhead em tempo de execução.
OpenC++ controla os seguintes aspectos durante a compilação:

Definição de Classe;

Acesso à Classe;

Invocação de Função Virtual;

Criação de Objetos.
Shigeru Chiba ao desenvolver este MOP, especificamente a versão 2, a qual este
texto trata, se baseou em algumas idéias já existentes como o MOP CLOS, Anibus e
Intrigue, Meta Information Protocol (MIP) assim como na versão 1 do OpenC++.
Este MOP tem como meta prover um meio fácil para programadores escreverem
bibliotecas que fornecem extensões de linguagem transparente e eficientemente.
Analisando de um ponto de vista pragmático, os critérios de Projeto deste MOP são alta
performance e adaptabilidade arbitrária. Em primeiro plano, este MOP não inclui nenhum
overhead em nível de execução, por último o MOP deve ter a habilidade de implementar
extensões comuns de C++, como C++ persistente ou C++ distribuído.
12
3.1 Estrutura do OpenC++
Figura 2: A Estrutura do Protocolo
A arquitetura do OpenC++ é similar a do CLOS[2] em que metaobjetos representam
entidades da linguagem visíveis para o programador. Existem metaobjetos de classe e
metaobjetos de função, o comportamento do programa é controlado por esses metaobjetos.
Uma característica importante do OpenC++ é que ele claramente separa o ambiente
de compilação do ambiente de execução. Objetos normais existem apenas em tempo de
execução, e metaobjetos existem somente em tempo de compilação.
Existindo somente em tempo de compilação os metaobjetos controlam o
comportamento do programa controlando a compilação do mesmo. Os metaobjetos
traduzem apropriadamente definições de alto nível do programa, e, se necessário, incluem
em tempo de execução funções, tipos e dados suplementares ao código traduzido.
Isso implica que este MOP não implica em penalidades de espaço ou velocidade em
tempo de execução, ao contrário do MOP CLOS em que aspectos chave do sistema de
objetos são executados através de invocação de metaobjetos em tempo de execução.O
MOP CLOS, então, necessita de uma implementação sofisticada para alcançar boa
performance.
13
3.2 Estrutura Básica do Protocolo
OpenC++ controla a tradução fonte-para-fonte de OpenC++ para C++.
Primeiramente o código fonte do programa é analisado e dividido em definições de alto
nível para classes e funções. Então um metaobjeto é construído para cada uma dessas
definições. O metaobjeto então traduz a definição de alto nível no código C++ (ou C)
equivalente. O código traduzido é então coletado e montado em um código fonte contínuo.
Figura 3: Visão geral do protocolo
3.3 Compatibilidade com o mundo real
Um MOP é um mecanismo que implementa algo necessário, esta seção apresenta
como o MOP OpenC++ é utilizado para programação prática.
Este MOP pode ser visto como uma ferramenta para implementar bibliotecas que,
tranparentemente e eficientemente, provêm facilidades para o programador. Em Outras
palavras, OpenC++ é um mecanismo para a implementação de bibliotecas. O benefício
para os usuários de bibliotecas não é o MOP por si só, mas a eficiência e transparência da
biblioteca implementada pelo MOP.
14
Alguns exemplos práticos da funcionalidade de OpenC++ para implementar
bibliotecas são:

Biblioteca de objetos persistentes: Usando OpenC++ objetos persistentes
podem ser transparentemente fornecidos para o usuário. Uma nova
metaclasse é usada para encapsular a implementação da extensão da classe
definida pelo usuário.

Biblioteca de matrizes: Usando OpenC++ pode-se eficientemente
implementar uma biblioteca de matrizes. Uma nova metaclasse é usada para
otimizar a implementação de uma classe específica, que é fornecida pela
biblioteca.

Selecionando uma classe concreta em tempo de compilação: mecanismo para
selecionar a mais apropriada classe concreta para uma dada classe abstrata
em tempo de compilação. Com esse mecanismo, os programadores não têm
que instanciar diretamente uma classe concreta específica. Ao invés disso,
eles podem instanciar uma classe abstrata com uma anotação sobre o
requerimento para a implementação da instância.
3.4 Outras questões
Essa seção examina algumas outras questões no MOP OpenC++.
Herança
Uma questão interessante é quando uma subclasse deve herdar a metaclasse de sua
classe base. Isso é importante porque selecionar a metaclasse é o mecanismo principal de
15
customização baseado no MOP que os programadores usam. O MOP OpenC++ permite
que essa questão seja customizada pelo programador.
O MOP seleciona a metaclasse de uma dada classe X com o seguinte algoritmo:
1. Se a classe X tem uma classe base, então chame ComputeMetaclassName()
no metaobjeto para aquela classe base, e selecione o valor do resultado da
metaclasse de X. Por definição essa função retorna a mesma metaclasse da
classe base.
2. Se a metaclasse de X é explicitamente especificada pelo programador com a
declaração metaclass, então selecione essa metaclasse.
3. De outra forma, selecione a metaclasse padrão Class.
Pela primeira regra, as subclasses herdam sua metaclasse de sua classe base. Essa
política de herança pode ser, entretanto, modificada pelo programador. Ele pode redefinir
a função ComputeMetaclassName() para efeitos de customização.
No caso de uma classe ter mais de uma classe base e as classes de metaobjetos para
ela resultam em metaclasses diferentes, o compilador gera um erro de compilação.
3.5 Extensões de Sintaxe
O MOP OpenC++ fornece uma funcionalidade limitada para estender a sintaxe da
linguagem. O programador pode registrar novas palavras-chave, que podem aparecer
somente em certos lugares: o modificador de nomes de tipos, nomes de classes, e o
operador new. Por exemplo, este código esta correto:
distributed class Point { ... };
lightweight Vector v;
16
p = require(“sorted”) new Set;
ditributed, lightweight e require são palavras-chave registradas. Essas palavraschave são passadas para um metaobjeto quando um fragmento de código é traduzido. O
metaobjeto pode usar essas palavras-chave para decidir como traduzir esse fragmento de
código. As palavras-chave podem ser seguidas de algum argumento meta. Por exemplo,
“sorted” é um argumento da palavra-chave require.
De outra forma, a palavra-chave registrada pelo programador deve aparecer como o
nome do componente em uma expressão de acesso ao componente. Nesse caso o parser
reconhece toda a expressão de acesso ao componente como uma declaração definida pelo
usuário.
3.6 Sobrecarga do Protocolo
Sendo que programas traduzidos pelo MOP OpenC++ invocam funções de suporte
em tempo de execução, o MOP pode parecer desenvolver inicialmente penalidades em
tempo de execução. Mas essas penalidades não dão devido ao MOP, mas sim devido ao
esquema de implementação da biblioteca, por exemplo, na biblioteca de objetos
persistentes, o programa traduzido invoca a função Load() em cada acesso ao componente.
Mas a penalidade na performance com a invocação dessa função é inerente ao esquema de
implementação que foi escolhido para a biblioteca. De fato, a invocação dessa função é
necessária mesmo que não usássemos o MOP.
Embora o OpenC++ não envolva penalidades em tempo de execução, ele envolve
penalidades em tempo de compilação sendo que ele move a computação do meta-nível da
17
execução para a compilação. Entretanto essas penalidades podem ser reduzidas por uma
implementação bem elaborada.
3.7 Meta Circularidade
O design do MOP OpenC++ é, assim como outros MOPs, conceitualmente
metacircular. Não há diferenças substanciais entre classes e metaclasses. Uma metaclasse
é simplesmente uma classe que instancia outras classes. A relação entre uma classe e uma
metaclasse é equivalente a relação classe-instância, como mostrado na figura abaixo.
Figura 4: Metaclasse, classe e objeto
Assim, quando um programa inclui definições de metaclasses, o MOP também
constrói classes de metaobjetos para aquelas metaclasses. Os metaobjetos construídos
controlam a compilação das metaclasses.
OpenC++, no entanto, escapa da aparentemente infinita recursão dessa meta
circularidade de maneira parecida com outros MOPs metacirculares. Para compilar uma
classe, sua metaclasse tem de ser compilada primeiramente, e antes de compilar essa
metaclasse, sua metaclasse tem de ser compilada, e assim vai. Mas essa corrente de
18
compilações não infinita porque a raiz de toda corrente de classes-metaclasses é a
metaclasse Class que é a metaclasse de si mesma.
3.8 Trabalhos Relacionados
Algumas da idéias para o OpenC++ vieram de trabalhos anteriores. A idéia de um
MOP em “tempo de compilação” se deve a Anúbis e Intrigue[4, 3]. Esses são MOPs em
tempo de compilação para controlar um compilador Scheme. Nesses MOPs, os
metaobjetos não são apenas entidades de linguagem, mas também representam informação
global como o resultado de análise de fluxo. A arquitetura básica do OpenC++ se deve ao
MOP CLOS[5]. A maior diferença é que os metaobjetos do MOP CLOS são em tempo de
execução (runtime metaobjects) e assim o MOP CLOS requer relativamente um largo
ambiente de execução se é diretamente aplicado em C++. A idéia de uma meta-interface
dos estágios primários da compilação foi também proposta em MPC++[6].
Assim como o MOP CLOS e OpenC++ versão 1, uma variedade de sistemas adotou
metaobjetos em tempo de execução (runtime metaobjects), que representam mecanismos
fundamentais como o interpretador da linguagem e o kernel do Sistema Operacional, e são
responsáveis pelo comportamento em tempo de execução do sistema. Sendo que os
metaobjetos em tempo de execução permitem o usuário tomar várias decisões de política
do sistema, como escalonamento e migração, os usuários podem customizar a performance
do sistema para encaixar em seus requisitos. Um inconveniente dos metaobjetos em tempo
de execução é a sobrecarga em tempo de execução. Umas poucas idéias foram propostas
19
nessa área. Por exemplo, inlining e avaliação parcial (partial evaluation) são técnicas
efetivas para reduzir a sobrecarga. É difícil, entretanto, de recuperar toda a sobrecarga de
uma meta arquitetura em tempo de execução (runtime architecture).
3.9 Situação atual
OpenC++ versão 2 está em processo de desenvolvimento. A metodologia usada é
primeiro desenvolver uma versão simplificada do sistema alvo (C++ no caso), e então
projetar e testar um MOP para aquele sistema simplificado, e, finalmente, portar o MOP
desenvolvido para o sistema alvo. Foi desenvolvido, assim, um sistema de objetos tipoC++ (C++-like), chamado S++, e designado o MOP apresentado aqui para S++. Um
número de exemplos similar a esse apresentado aqui foram implementados para testar o
MOP S++.
20
4. Javassist
Javassist é uma ferramenta de programação feita para Java. Ela possibilita
programadores criarem um programa em meta-nível automatizando certas funções. Além
disso, um variado número de aplicações que utilizam reflexão em tempo de execução
podem ser implementadas através deste sistema.
4.1 Introdução
Uma das características chave das aplicações hoje em dia é a presença de uma
“wizard”, ajudando os usuários a tornar seu trabalho mais produtivo. Programas
conhecidos como o Microsoft Word e o Visual C++ utilizam-se desta ferramenta para
ajudar os usuários a tornarem suas tarefas rotineiras mais simples.
Javassist, que pode ser considerado uma espécie de wizard para Java, automatiza
definições de classes como se fossem mecanicamente derivadas de outras classes.
Por exemplo, sendo que Java não provê um mecanismo para tipos parametrizados
(um mecanismo de template), programadores que querem usar um vetor tendo referências
apenas para objetos String são forçados a usar type casts sempre que um elemento é
retirado daquele vetor:
String s = (String)aVectorOfString.elementAt(3);
De outra forma o programador teria que definir uma nova classe para um vetor de
String:
public class StringVector extends Java.util.Vector {
public void addElement(String s) {
super.addElement(s);
}
21
public String at(int i) {
return (String)elementAt(i);
}
}
Javassist automaticamente define StringVector, ao invés dos programadores terem
que fazê-lo, se o tipo do elemento (String) é especificado por uma anotação simples.
Javassist não só trata apenas de assistência pré-definida, mas permite que o
programador defina uma novo tipo de assistência, que é descrita no próprio Java.
Programadores escrevem um programa de meta-nível utilizando a API de reflexão[15] e a
API de Javassist, e então Javassist presta assistência de acordo com aquele programa de
meta-nível.
4.2 Javassist
Javassist é uma ferramenta de programação para ajudar programadores Java. Iremos
mostrar primeiro um programa usando Javassist para definir automaticamente um
StringVector e então como essa automação é implementada em Java.
4.2.1 Uso de Javassist
Para definir automaticamente uma classe StringVector e usa-la, os programadores
escrevem uma notação simples para uma declaração de import:
import java.util.Vector by VectorAssistant(String);
A anotação começa com by. Isso significa que java.util.Vector é importado com
assistência por uma instância da classe VectorAssistant, que é definida em um programa
de meta-nível. String é um parâmetro para aquela instância.
Então este programa deve ser compilado pelo compilador do Javassist. Supondo que
o nome do programa é foo.j. Então o programador deve escrever:
22
% jc foo.j
Esse comando invoca o compilador e gera o byte code para a classe StringVector
em tmp/StringVector.class. Ele também traduz foo.j para foo.java. Em foo.java, a
declaração de import é modificada para uma declaração tradicional:
import java.util.Vector;
import tmp.StringVector;
A segunda declaração faz a classe StringVector disponível no resto do código fonte.
4.2.2 Meta programação
Esta assistência mostrada acima é feita por um objeto VectorAssistant, que é um
objeto Java puro. Se o compilador Javassist encontra uma declaração import anotada, ele
carrega dinamicamente o objeto assistente especificado e invoca o método assist() naquele
objeto com a classe especificada na declaração import e com os dados parâmetros:
public Class[] assist(class imported, String[] params) throws
CannotCompileException
{
Class[] results = {imported, makeSubclass(CtClass.forName(params[0]))};
return results;
}
Este método retorna um vetor (array) de Class. Todos os objetos classe neste vetor
são importados em um programa compilado. No exemplo acima o primeiro elemento deste
vetor é Vector e o segundo é StringVector.
O método makeSubclass() recebe o objeto classe representando um tipo de
elemento e retorna uma classe vetor para aquele tipo particular:
public Class makesubclass(Class type) throws
CannotCompileException
{
CtClass vec = new CtClass();
vec.setName(CtClass.toSimpleName(type.getName())+”Vector”);
vec.setSuperClass(java.util.Vector.class);
Class[] args1 = {type};
23
vec.addMethod(Void.TYPE, “addElement”, args, null, 1);
Class[] args2 = {Integer.TYPE};
vec.addMethod(type, “at”, args2, null, 2);
vec.setRuntimeMetaobject(MVector.class);
return vec.compile();
}
CtClass (compile-time class) é uma classe fornecida pela API de Javassist. O
método makeSubclass() primeiro cria um objeto CtClass e especifica o nome da classe e
da superclasse. Então ele adiciona dois métodos, addElement() e at() para criar a classe.
Os parâmetros de addMethod() são o tipo de retorno, o nome do método, uma lista de
tipos de parâmetros, uma lista de exceções, e um identificador inteiro. A classe construída
é compilada se o método compile() é invocado neste objeto CtClass. compile() retorna
um objeto Class.
O comportamento dos métodos da classe construída é implementado por metaobjetos
em tempo de execução especificados por setRuntimeMetaobject(). No exemplo acima,
os métodos addMethod() e at() são implementados pela classe MVector (meta vector).
MVector é uma subclasse de Metaobject. Os métodos addMethod() e at() de
StringVector somente delegam uma chamada de método para uma instância de MVector.
Toda instância de StringVector é associada com uma instância distinta de MVector.
Figura 5: A Arquitetura de Meta-nível de Javassist
24
Todas as chamadas de método num objeto StringVector são delegadas para
trapMethodcall(). Este método simula os métodos de StringVector:
public Object trapMethodcall (int identifier, Object[] params)
{
Vector baseobj = (Vector) getObject();
If (identifier == 1) {
baseobj.addElement(params[0]);
Return null;
}
else if (identifier == 2) {
Integer intobj = (Integer) params[0];
int i = intobj.intValue();
return baseobj.elemetAt(i);
}
else
return super.trapMethodcall(identifier, params);
}
identifier é um identificador inteiro especificando o método chamado no nível base.
params é uma lista dos parâmetros atuais. Este método chama addElement() ou
elementAt() no objeto de nível base, mas os métodos invocados são o da superclasse
Vector.getObject() que é parte da API de Javassist; ele retorna o objeto de nível base
associado com este metaobjeto. As classes Object e Integer fazem parte da API de
reflexão sendo parte de Java.
Embora todas as chamadas de método num objeto de nível base são normalmente
redirecionadas para trapMethodcall() em um metaobjeto, os programadores podem
especificar outro métodoao invés de trapMethodcall(). Suponhamos que o programador
adicione outro método em vec em makeSubclass() como segue:
vec.addMethod(Boolean.TYPE, “isEmpty”, null, null, “checkSize”);
Esta expressão adiciona um método cuja assinatura é:
boolean isEmpty()
25
Diferentemente dos outros métodos mostrados no começo, este método isEmpty() é
implementado por checkSize() de MVector. Note que o último parâmetro de
addMethod() não é um inteiro, mas um string especificando o método que implementa.
Se isEmpty() é chamado em um objeto de nível base, ele chama checkSize() no
metaobjeto associado com aquele objeto. O método checkSize() de MVector deve ter a
mesma assinatura de isEmpty():
boolean checkSize() {
Vector baseobj = (Vector) getObject();
return baseobj.size() == 0;
}
O método de nível base isEmpty() returna o valor resultante de checkSize().
Uma singularidade da implementação de isEmpty() é que os parâmetros atuais para
esse método não são convertidos para um vetor de Object. Isto significa que um método
de meta nível não pode implementar um método de nível base de modo genérico. Ele não
pode implementar múltiplos métodos de nível base com diferentes assinaturas. Por outro
lado trapMethodcall() pode implementar métodos de nível base com qualquer tipo de
assinatura desde que os parâmetros incluindo os tipos nativos sejam convertidos para um
tipo canônico: vetor de Object.
4.2.3 Assistência em tempo de execução
Embora a API de Javassist tenha sido criada para uso em tempo de compilação,
programadores também podem usa-la em tempo de execução. Eles podem criar um objeto
CtClass e compilá-lo em tempo de execução. Nenhuma anotação especial para
declarações de import é necessária.
Esta habilidade é útil se uma classe dinamicamente carregada de uma rede requer
uma classe adaptadora sendo que a instância da classe carregada poderá se comunicar com
26
os objetos existentes. Tal classe adaptadora pode ser criada sob demanda usando a API de
Javassist. Assim como também, essa habilidade permite a uma biblioteca de objetos
distribuídos sem um gerador precário. Por exemplo, o sistema RMI da Sun força os
programadores a compilarem classes proxy usando o comando rmic, a API de Javassist
permite classes proxy serem criadas dinamicamente em tempo de execução.
A assistência em tempo de execução de Javassist, entretanto, tem algumas limitações
porque Java é uma linguagem estaticamente tipada. Instâncias de uma classe criada em
tempo de execução com a API de Javassist não podem ser ligadas à variáveis daquele tipo
de classe. Por exemplo, o seguinte programa causa um erro de compilação:
CtClass ct = new CtClass();
ct.setName(“intVector”);
ct.setSuperclass(Vector.class);
Class rt = ct.compile();
intVector v = (intVector)rt.newInstance();
A última linha causa um erro de compilação porque intVector não foi definido ainda
em tempo de compilação. Uma instância de intVector deve ser ligada a uma variável da
superclasse Vector, que existe em tempo de compilação. De outro modo intVector deve
ser introduzido por uma declaração de import. Nesse caso, intVector é criado em tempo
de compilação e normalmente importado, sendo então intVector poderá ocorrer no resto
do programa.
A assistência em tempo de execução é efetiva se o nome de uma classe criada não
precisar ocorrer em um programa. Uma classe proxy para computação distribuída é um
desses exemplos.
27
4.3 Reflexão em tempo de execução
Um número considerável de aplicações típicas de reflexão em tempo de execução
são, também, aplicações de Javassist. O sistema de Javassist inclui um assistente chamado
WrapperAssistant. Esta assistente produz uma classe empacotada de uma dada classe.
Por exemplo:
import sample.Point by WrapperAssistant(VerboseMetaobj);
Esta declaração cria uma classe empacotada da classe sample.Point. Esta classe
empacotada implementa interceptação de chamada de métodos por metaobjetos, que é um
mecanismo fundamental da reflexão em tempo de execução. Os metaobjetos são instâncias
de VerboseMetaobj neste exemplo.
Programadores podem criar a classe empacotada como uma classe independente ou
uma subclasse de sample.Point. Em ambos os casos a classe empacotada provê o mesmo
conjunto de métodos de sample.Point. Estes métodos delegam todas as suas chamadas
para trapMethodcall() em metaobjetos, sendo que os metaobjetos podem interpretar a
chamada dos métodos.
A classe empacotada criada é nomeada wrapper.Point e é importada ao invés da
classe sample.Point. Esta declaração de import é traduzida pelo compilador Javassist em:
import wrapper.Point;
Depois da tradução, wrapper.Point é substituído por sample.Point, sendo que todas
as ocorrências do nome da classe Point no programa aponta para wrapper.Point. Por
conseguinte, sem modificar um programa, os programadores, podem transparentemente
introduzir o mecanismo de reflexão implementado pela classe empacotadora.
28
4.4 Questões de implementação
O sistema Javassist consiste de dois componentes: o compilador Javassist (Javassist
compiler) e a máquina Javassist (Javassist engine). O compilador Javassist processa as
declarações de import anotadas, e a máquina Javassist compila os objetos CtClass. Na
implementação corrente, a máquina Javassist executa um compilador externo como o
javac para compilar o objeto CtClass.
Figura 6: O Sistema Javassist
Em uma versão futura, segundo Chiba[13], a máquina Javassist será um componente
stand-alone não usando um compilador externo. Esta versão alcançará uma compilação
mais rápida porque Javassist não irá requerer a capacidade completa para compilar um
programa Java. Especialmente, Javassist não permite o programador especificar
diretamente um corpo de método. Os métodos adicionados a um objeto CtClass não
fazem nada exceto chamar trapMethodcall() em um metaobjeto. Isso faz com que a
geração de código para corpo de métodos muito simples e rápida. Note que a máquina
Javassist não compila um metaobjeto. Ele é separadamente compilado por um compilador
Java.
29
4.5 Conclusão
Javassist permite ao programador automatizar definições de certos tipos de classes
na linguagem Java. O sistema é baseado na experiência de Chiba com OpenC++[1] e
OpenJava[16]. Entretanto, Javassist não é um sistema reflexivo em tempo de compilação.
Os meta programas de Javassist são executados tanto em tempo de execução quanto em
tempo de compilação. Ao contrário de OpenC++ e OpenJava, um programa de meta-nível
em Javassist não trata diretamente com código fonte ou parse tree. Em princípio ele não
precisa dos códigos fonte das classes processadas. Se essas classes precisam ser
inspecionadas, Javassist usa a API de reflexão de Java. Por outro lado, sendo que Javassist
não faz tradução código-para-código ele não pode tratar de extensões de sintaxe, assim
como OpenC++ e OpenJava fazem.
Javassist não um sistema reflexivo em tempo de execução como MetaXa[14] por
exemplo. Ele provê uma capacidade similar através do WrapperAssistant mas pode ser
usado para outras aplicações como parametrizar tipos, que sistemas em tempo de execução
tradicionais não conseguem implementar. Javassist é um sistema para produzir uma nova
classe sobre demanda, enquanto que sistemas em tempo de execução tradicionais são para
customizar classes existentes.
30
MetaJava
1. Introdução
MetaJava é um interpretador Java estendido que permite reflexão estrutural e
comportamental.
MetaJava foi criado com os seguintes objetivos:

Deve ser possível separar questões funcionais, específicas da aplicação, de
questões não funcionais, como persistência e replicação.
 A arquitetura deve ser geral, significando que diversos problemas como
persistência, replicação, distribuição, sincronização de poder ser resolvidos
usando MetaJava.
2. Modelo computacional de MetaJava
Sistemas tradicionais consistem de um sistema operacional e, no topo dele, um
programa que explora os serviços do sistema operacional usando uma interface de
programa de aplicação (API).
31
Figura 7: Modelo computacional de reflexão comportamental
A aproximação reflexiva de MetaJava é diferente. O sistema consiste do sistema
operacional, o programa de aplicação (o sistema base), e o meta sistema. O programa não
saberá da existência do meta sistema. A computação no sistema base gera eventos. Estes
eventos são entregues ao meta sistema. O meta sistema avalia o evento e reage de uma
maneira específica. Todos os eventos são tratados de maneira síncrona. O nível base é
suspendido enquanto o meta nível processa o evento. Isso da ao meta nível controle
completo sobre a atividade no nível base. Por instância, se o metaobjeto recebe um evento
enter-method, o comportamento padrão será executar o método. Mas o metaobjeto pode
também sincronizar a execução do método com outro método do nível base. Outras
alternativas seriam enfileirar o método para execução com retardo e retornar para o
invocador imediatamente, ou executar o método em um host diferente. O que efetivamente
acontece depende inteiramente do metaobjeto em questão
32
Figura 8: Eventos gerados por computação base
Um objeto base pode também invocar um método do metaobjeto diretamente. Isto é
chamado de meta interação explícita e é usado para controlar o meta nível do nível base.
Nem todo objeto deve ter um metaobjeto amarrado a ele. Metaobjetos podem ser
amarrados dinamicamente a um objeto base em tempo de execução. Isto é especialmente
importante se uma computação distribuída é controlada no meta nível e argumentos
arbitrários de métodos precisam ser reflexivos. Como nenhum metaobjeto é amarrado a
uma aplicação, a meta arquitetura não causa nenhuma sobrecarga. Sendo que as aplicações
só terão que pagar pela funcionalidade reflexiva quanto tiverem que, efetivamente, usa-la.
Até o presente momento metaobjetos podem ser amarrados a referências, objetos e
classes. Se um metaobjeto é amarrado a um objeto, a semântica do objeto é mudada.
Algumas vezes é desejado mudar a semântica de uma referência do objeto apenas, por
exemplo, quando traçando os acessos à referência, ou quando amarrando uma certa
política de segurança à referência. Amarrar um metaobjeto a uma classe faz com que todas
as instâncias daquela classe reflexiva.
33
Para completar suas tarefas o metaobjeto tem acesso a um conjunto de métodos que
podem manipular o estado interno da máquina virtual. Estes métodos são chamados de
interface de meta-nível (MLI) da máquina virtual.
3. Metaobjetos para implementações de sistemas operacionais
abertos
No ambiente Java, muitos mecanismos e políticas que são tradicionalmente
considerados do sistema operacional ou de serviços em tempo de execução do sistema, são
providos pela máquina virtual ou por bibliotecas nativas. Como estes serviços são
implementados em C e a flexibilidade do ambiente Java não está disponível para eles, não
é fácil adaptá-los para necessidades especiais da aplicação ou para transparentemente
adicionar novos serviços. MetaJava provê uma arquitetura para implementações abertas
dos vários mecanismos e políticas que são atualmente uma parte fixa da máquina virtual,
tais como gerenciamento de memória, recolhimento de lixo, gerenciamento e
escalonamento de processos, ou gerenciamento de classes. A máquina virtual deve prover
meramente uma implementação muito primitiva de uns poucos mecanismos básicos, como
comutação de processos, e políticas simples para colocar os primeiros metaobjetos a rodar.
Mecanismos mais complexos e políticas podem ser implementados como metaobjetos de
Java, que podem usar o mecanismo básico via a interface de meta nível.
Se diversas aplicações estão executando dentro de uma máquina Java, metaobjetos
específicos da aplicação levam a uma hierarquia de metaobjetos.
34
Figura 9: Hierarquia dos meta níveis
Metaobjetos globais cedem recursos para aplicações, metaobjetos específicos da
aplicação controlam como estes recursos são usados pela aplicação. Certamente, partes
específicas da aplicação podem ter seus metaobjetos se necessário.
Em adição ao mecanismo descrito acima, uma larga gama de serviços estendidos em
tempo de execução podem ser implementados por metaobjetos: persistência, migração de
objetos, replicação de objetos[18], compilação “just-in-time”, objetos ativos[19],
invocação de métodos assíncronos, várias políticas de segurança, etc.
35
4. Implementação
A versão inicial de MetJava usava uma biblioteca compartilhada para estender a
máquina virtual Java da Sun (JVM). As novas versões requereram várias modificações na
JVM, então foi criada um máquina virtual específica de MetaJava, a MetaJava Virtual
Machine (MJVM). A MJVM é um superconjunto da JVM. Ela usa o mesmo formato de
arquivo de classe e executa o mesmo conjunto de bytes da JVM, mas provê uma interface
de meta nível para a máquina virtual.
A linguagem Java não foi mudada nem o formato do arquivo de classe, então
ferramentas não relacionadas com MetaJava podem ser usadas.
As seguintes modificações foram necessárias para permitir o modelo reflexivo de
MetaJava.
4.1 Classes Sombra (shadow classes)
O propósito de um metaobjeto é mudar a semântica de um metaobjeto. Essa
modificação não deve modificar a semântica de outros objetos da mesma classe.[20]
sugere usar classes para reflexão estrutural e metaobjetos para reflexão comportamental.
Em MetaJava não foi adotado este modelo porque classes (conceitualmente) devem conter
informação completa sobre o objeto. A separação proposta em [20] só parece ser
justificada pelo fato de que classes não são relacionadas a objetos individuais, mas as
todas as instâncias daquela classe. Portanto, o comportamento de objetos individuais não
pode ser mudado modificando classes. MetaJava introduz classes sombra para resolver
esse problema, que é inerente a linguagens baseadas em classes. Uma linguagem baseada
em classes assume que existem vários objetos do mesmo tipo (classe). Linguagens
36
baseadas em protótipos suportam objetos um-de-cada-tipo. É fácil derivar objetos de
outros objetos e mudar seus campos ou métodos do novo objeto sem afetar o original. As
classes sombra de MetaJava são uma aproximação desse comportamento.
Uma classe sombra C’ é uma cópia exata da classe C com as seguintes propriedades:

C e C’ são indestinguiveis no nível base;

C’ é idêntica a C exceto por modificações feitas por um programa de meta
nível;

Atributos e métodos estáticos de C e C’ são compartilhados.
O nível base não consegue diferenciar uma classe de suas classes sombra. Isso faz
com que esse sombreamento seja transparente para o nível base. Uma série de problemas
deve ser considerada para garantir essa transparência.
Consistência dos dados da classe. A consistência entre C e C’ deve ser mantida.
Todos os dados não constantes relacionados à classe devem ser compartilhados entre C e
C’, porque as classes sombra são requeridas quando são mudadas propriedades
relacionadas ao objeto não a classe. A MJVM resolve esse problema compartilhando os
dados (atributos e métodos estáticos) entre a classe sombra e a classe original.
Identidade da classe. Sempre que a MJVM compara classes, ela usa a classe
original apontada pelo link type (figura 10). Essa classe é usada para toda checagem de
tipo, por exemplo

Checagem de compatibilidade de atribuição;

Checar se uma referência pode ser invocada em outra classe ou interface
(checkcast opcode);

Checagem de proteção.
37
Objetos de classe. Em Java, classes são objetos de primeira classe. Testar os objetos
classe de C e C’ por igualdade deve retornar verdadeiro. Em MJVM toda estrutura de
classe contém um ponteiro para p objeto Java que representa esta classe. Estes objetos são
diferentes para C e C’, porque os objetos classe devem conter um link para a estrutura da
classe. Então, quando compara objetos de um tipo de classe, a MJVM realmente compara
o link type.
Figura 10: A criação de uma classe sombra
Porque as classes são objetos de primeira classe é possível usa-los como uma trava
de exclusão mútua. Isto acontece quando o código de monitorenter/monitorexit é
executado ou um método estático sincronizado da classe é chamado. Foi definido que o
sombreamento deve ser transparente para o nível base. Entretanto as travas (locks) de C e
C’ devem ser idênticas. Então a MJVM usa o objeto de classe da estrutura de classe
apontada pelo link type para o travamento.
38
Coleta de Lixo (Garbage Collection). Classes sombra devem ser coletadas se não
são mais usadas, ou seja, quando os metaobjetos são desamarrados dela. O coletor de lixo
segue o link do nível base para marcar as classes na torre de classes sombra (torre de
metaobjetos). Superclasses sombreadas são marcadas como de costume seguindo o link da
superclasse na classe sombra.
Consistência de Código. Algum processo em um objeto de nível base da classe C
enquanto o sombreamento e modificação da sombra acontecem. O sistema então garante
que o código velho é mantido e usado durante a execução do método. Se este método
retorna e é chamado mais uma vez, o novo código é usado. Esta garantia pode ser dada
porque durante a execução de um método toda a informação necessária, é mantida na pilha
Java como parte do ambiente de execução. A experiência irá mostrar se o suporte a
modificação de código afeta a robustez do sistema.
Consumo de Memória. Uma classe sombra C’ é uma cópia superficial de C.
somente blocos de método são copiados em profundidade. Uma classe superficial tem um
tamanho de 80 bytes. Um método tem um tamanho de 52 bytes. Uma entrada na tabela de
métodos é um ponteiro para um método e tem um tamanho de 4 bytes. Portanto, o custo do
sombreamento é um consumo de memória de (80+número_de_métodos*(52+4)) bytes por
classe sombra em MetaJava.
Herança. Durante a criação da classe sombra uma sombra da superclasse é criada
recursivamente. Quando um objeto reverte para sua superclasse com um chamado para
super a superclasse sombreada é usada. Todas as classes sombreadas no caminho de
herança usam o mesmo metaobjeto.
Comportamento Original. Adicionalmente ao link metaobjeto uma classe sombra
em MetaJava também precisa de um link nível-base para a classe original. Ele é
39
importante quando o comportamento original da classe deve ser usado. Em todas as
classes não sombra o link nível-base é nulo. O link nível-base realiza a torre de
metaobjetos. Entretanto há uma diferença importante entre a torre de metaobjetos em
MetaJava e a noção convencional de torre de metaobjetos. Tradicionalmente, uma torre de
metaobjetos é construída sendo que cada metaobjeto reifica a estrutura e o comportamento
do metaobjeto um nível abaixo. Na torre de MetaJava, um metaobjeto pode continuar
(delegar) trabalho para o nível base se ele já terminou suas próprias computações.
4.2 Implementação do mecanismo de evento
Com o que foi mostrado acima é possível transparentemente modificar a estrutura de
classe de objetos individuais. Agora, será mostrado como este mecanismo pode ser usado
para reificação do comportamento do objeto.
Reificação das mensagens que chegam (invocações de método). Falamos sobre
reificação de mensagens que chegam do objeto O (O é da classe C), se a invocação de
O.m() pelo objeto X é tratada pelo metaobjeto de O. Para implementar reificação da
invocação de métodos iremos investigar as seguintes alternativas:
1) Todos os códigos interessantes (invokevirtual e invokespecial) são trocados
por código de reificação (invokevirtual_reflective). Esta é uma maneira limpa
de criar objetos reflexivos mas requer uma extensão do conjunto de código.
2) O processo de interpretação de códigos (bytecodes) interessantes é estendido.
3) O código do método de C.m() é trocado por um pedaço de código que salta
para o meta espaço.
40
A terceira alternativa tem a vantagem de ter apenas efeito local (ao contrário da
primeira) e evitando a chance do interpretador entrar em loop (como a segunda),
possibilitando assim, trabalhar também com processadores Java. Daí foi decidido reificar a
invocação de métodos com invocação de pedaços de código em tempo de execução.
O metaobjeto que implementa uma invocação de método específica precisa
certamente de acesso aos argumentos do método. Uma versão inicial de MetaJava usava
um vetor de objetos para guardar os argumentos do método. Isto era rápido, mas excluía
tipos primitivos como argumentos de método. Na implementação atual de MJVM os
argumentos são passados em um container de argumentos especial que é capaz de mudar o
tipo de um elemento dinamicamente.
Reificação de mensagens que saem. Uma mensagem de saída é causada pelo
opcode invokevirtual, que tem três argumentos: o nome da classe, o nome do método e a
assinatura do método. Quando o evento está sendo registrado, o nome da classe, o nome
do método e a assinatura podem ser especificados. O gerador de código então olha para
todos os opcodes invokevirtual que combina, e cria o seguinte código no lugar do opcode:
1) Criar um objeto evento com nome da classe, nome do método e assinatura;
2) Criar e inicializar o argumento do objeto (TypeAdaptativeContainer);
3) Invocar o evento de envio de função.
Enquanto para reificar as mensagens que chegam, o método completo é trocado por
um pedaço de código, agora é feita injeção de código na origem da chamada do método.
Reificação de acessos a variáveis de instância. A reificação de variáveis de
instância acessíveis globalmente irá restringir a implementação da MJVM a usar uma
função de acesso para todo acesso a variável ou levará a modificação de todo pedaço de
41
código que possivelmente acessará a variável. Por essa razão somente acesso a variáveis
com proteção private protected podem ser reificadas em MetaJava.Variáveis com essa
proteção podem ser acessadas somente em sua classe declaradora ou em suas subclasses.
Mas isso significa que a variável pode ser acessada fora deste objeto, num objeto diferente
da mesma classe, porque a proteção em Java é baseada em classe e não em objetos. Se
esse objeto já usa uma classe sombra, as chances são ruins para se detectar todos os
opcodes putfield/getfield que tem permissão de acessar essa variável em particular. Para
resolver este problema foi sugerida uma nova proteção de variáveis baseada em objetos e
dois novos opcodes putfield_this/get_field_this.
O gerador de pedaços de código (stub generator) para acessos a variáveis de uma
variável com nome N e tipo T em um objeto O da classe C trabalha da seguinte forma:
Encontra todos os opcodes putfield e getfield que acessam a variável da classe C, nome N
e tipo T. Se o opcode é um putfield, remove-se o putfield e insere-se um pedaço de código
com as seguintes funcionalidades:
1) Cria e inicializa um objeto EventDescFieldAccess. O objeto contém
informação se acesso está lendo ou escrevendo. Se está escrevendo, o objeto
de descrição de eventos também contém o pretenso novo valor da variável.
2) Invoca a função estática eventDispatchVoid da classe MetaObject com o
objeto evento como parâmetro.
Opcodes relevantes de getfield são trocados por códigos semelhantes.
Reificação da trava de objeto. Para alguns mecanismos de nível base é ou
impossível ou muito caro reificá-los com injeção de código. Trava de objetos é um destes
mecanismos.O gerador de pedaços de código deve gerar pedaços de código para todos
42
opcodes monitorenter e monitorexit. Além do mais, toda execução de métodos
sincronizados deste objeto deverá ser reificada. Mas isto não garante que todos os acessos
à trava serão reificados no meta nível, porque a JVM ou bibliotecas nativas podem acessar
diretamente a trava.
Toda JVM precisa de uma função que mapeia o objeto trava de Java para uma trava
dependente do sistema. A MJVM usa um ponteiro de função na classe do objeto para
adquirir ou liberar uma trava. Este ponteiro de função normalmente se refere a uma função
que implementa travamento e destravamento. Quando registrando os eventos para
adquirir-trava-de-objeto ou liberar-trava-de-objeto, a MJVM modifica este ponteiro para
apontar para o tratador de eventos do metaobjeto. O metaobjeto responsável pode, agora,
usar algoritmos arbitrários e complexos para gerenciar a trava. Se o metaobjeto decidir
continuar com o protocolo padrão de travamento, ele chama a função de trava na classe
um nível abaixo.
Quando tratando de travamento de objetos devemos ter cautela para não fazermos
armadilhas nem sobreposição reflexiva (reflective overlap). Sobreposição reflexiva[21]
ocorre, se a computação reflexiva influencia o nível base explicita e implicitamente. Para
não influenciar implicitamente a computação do nível base, o tratador de eventos do meta
nível para a trava L não deve usar nenhum método que adquire a trava L.
Reificação do carregamento de classes e da criação de objetos. Carregamento de
classe e criação de objeto é reificada similarmente a trava de objeto. A estrutura de classe
da MJVM contém um ponteiro para a função carregadora de classe/ criadora de objeto. Se
o metaobjeto está interessado no mecanismo, isto é delegado para o metaobjeto.
43
5.3 Anexamento do Metaobjeto
Já foi explicado como alterar o comportamento de objetos individuais sem afetar
outros objetos da mesma classe. Esta seção explica como metaobjetos podem ser anexados
a outros objetos e referências.
Anexando a objetos. A MJVM usa uma reserva de objetos com tratadores de
objetos. Um tratador contém um ponteiro para os dados do objeto e para a classe do objeto.
Quando um metaobjeto é anexado a um objeto, uma classe sombra é criada e o link de
classe deste objeto é redirecionado para a classe sombra.
Um metaobjeto pode ser usado para controlar vários objetos base da mesma classe
de uma maneira similar. Não é necessário criar uma classe sombra para cada objeto.
Classes sombra são criadas em dois passos. No primeiro passo, a classe sombra é criada.
No segundo passo a classe sombra é instalada no objeto. Uma vez que a classe sombra é
criada, ela pode ser instalada em múltiplos objetos.
Anexando a referências. A implementação atual de MetaJava usa uma reserva de
objetos indireta. Se um metaobjeto é anexado a uma referência, a semântica das operações
envolvendo esta referência devem mudar. Assim, o tratador é copiado e o ponteiro de
classe no tratador mudado para apontar para uma classe sombra. Depois de copiar o
tratador, não é mais suficiente comparar tratadores quando checando a identidade dos
objetos. Ao invés disso, os ponteiros de dados dos tratadores são comparados. Isto requer
que os ponteiros de dados sejam únicos. A reserva de metaobjetos de MetaJava garante
isto.
44
Figura 11: Anexando a uma Referência
Amarramento múltiplo. Foi dito que é possível anexar mais de um metaobjeto a
um objeto de nível base. O metaobjeto amarrado mais recentemente é ativado primeiro. Se
esse metaobjeto decide continuar com a ação padrão de nível base, o próximo metaobjeto
de nível baixo na torre de metaobjetos é ativado. Se um metaobjeto não continua com a
ação padrão, metaobjeto que estão abaixo não são ativados.
Fica claro nesta descrição que a ordem dos metaobjetos na torre de metaobjetos é
importante.
45
Lista de Figuras
Figura 1: arquitetura reflexiva básica em OO...................................................................9
Figura 2: A Estrutura do Protocolo..................................................................................12
Figura 3: Visão geral do protocolo..................................................................................13
Figura 4: Metaclasse, classe e objeto...............................................................................17
Figura 5: A Arquitetura de Meta-nível de Javassist.........................................................23
Figura 6: O Sistema Javassist..........................................................................................28
Figura 7: Modelo computacional de reflexão comportamental.......................................30
Figura 8: Eventos gerados por computação base.............................................................31
Figura 9: Hierarquia dos meta níveis...............................................................................33
Figura 10: A criação de uma classe sombra....................................................................36
Figura 11: Anexando a uma Referência...........................................................................43
46
5. Referências
[1] Chiba, S.,”A Metaobject Protocol for C++”, In Proceedings of the ACM Conference
on Object-Oriented Programming Systems, Languages, and Applications (OOPSLA),
página 285-299, Outubro 1995.
[2] G. Kiczales, ed.., “Workshop on Open Implementation’94”, Outubro 1994.
[3] Lamping, J., G. Kiczales, L. Rodriguez, e E. Ruf, “An Architecture for an Open
Compiler”, Int’l Workshop of Reflection and Meta-Level Architecture (A. Yonezawa e B.
C. Smith) pp. 95-106, 1992.
[4] Rodriguez Jr., L. H., “Coarse-Grained Paralelism Using Meta-Object Protocols”
Technical report SSL-91-61, XEROX PARC, Palo Alto, Canadá, 1991.
[5] G. Kiczales, J. des Riviéres, e D. G. Bobrow, “The Art of the Metaobject Protocol”,
The MIT Press, 1991.
[6] Ishikawa, Y., “Meta-Level Architecture for Extendable C++” Technical Report
94024, Real World Computing Partnership, Japão, 1994.
[7]Senra, Rodrigo D. A., “Programação Reflexiva sobre o Protocolo de Meta-Objetos
Guaraná”,Unicamp, 2001.
[8] Dicionário LOGOS <http://www.logosdictionary.com> acessado em 15/03/2004.
[9] Maes, Pattie. The Causal Connection, 1987
[10] CYSNEIROS, L.; LEITE J. Definindo requisitos não funcionais. XI Simpósio
Brasileiro de Engenharia de Software. Fortaleza, CE. Outubro, 1997.
[11] WU, S. Reflective Java: making Java even more reflexible. FTP: Architecture Projects
Management Limited. Cambridge, UK, 1997. Endereço eletrônico: <http://www.ansa.co.uk/>.
Data de aquisição: Fevereiro, 2004.
47
[12] Rajan H., A critical analysis of, “Concepts and Experiments in Computational
Reflection”, by Maes, P.
[13] Chiba, Shigeru. “Javassist – A Reflection-based programming Wizard for Java”.
[14] Kleinöder, J. e M. Golm, “MetaJava: An Efficient Run-Time Meta Architecture
for Java,” em Proc. of the International Workshop on Object Orientation in Operating
Systems (IWOOS'96), IEEE, 1996.
[15] Java Soft, JavaTM Core Reflection API and Specification. Sun Microsystems, Inc.,
1997.
[16] Chiba, S. and M. Tatsubori, "Yet Another java.lang.Class" ECOOP'98 Workshop
on Reflective Object-Oriented Programming and Systems, Julho 1998.
[17] Golm M. e J. Kleinöder, “MetaJava – A Plataform fo Adaptable OperatingSystem Mechanisms”, abril 1997.
[18] G. Kiczales et al. “Aspect-Oriented Programming”. ACM Workshop on Strategic
Directions in Computing Research, MIT, Junho 14-15, 1996.
[19] Michael Golm, Jürgen Kleinöder. “Implementing Real-Time Actors with
MetaJava”, Tech. Report TR-I4-97-09, Universität Erlangen-Nürnberg: IMMD IV, Abril.
1997.
[20] J. Ferber. “Computational Reflection in class based Object-Oriented Languages”.
Conference on Object-Oriented Programming, Systems, Languages, and Applications,
OOPSLA ‘89, New Orleans, La., Outubro. 1989, pp. 317–326.
[21] P. Maes. “Computational Reflection. Technical Report 87_2”, Artificial
Intelligence Laboratory, Vrieje Universiteit Brussel, 1987.
[22] P. Maes. “Issues in Computational Reflection”, 1988
48
Download