A linguagem MIR, proposta por Tirelo, Oliveira et alii [Tir00, Oli04], e aqui modicada, provê a expressividade necessária à especicação de programas segundo o paradigma de máquinas de estado abstratas. MIR se mostra adequada, portanto, à utilização como linguagem a ser entendida pelo arcabouço kℓar para a otimização de especicações ASM, uma vez que foi espe- cicamente projetada para permitir otimizações especícas deste modelo. Além disso, recursos da linguagem tais como a presença de módulos, de escopos locais e globais e a composição de módulos garantem a escalabilidade da linguagem, permitindo que problemas maiores possam ser decompostos em partes, abordados e então recombinados.
A nossa contribuição à linguagem MIR consiste no fato desta prover a capacidade de se especicar sistemas multi-agentes. Este agentes possuem as propriedades de autonomia, sensibilidade ao contexto e proatividade, conforme apresentadas por Zambonelli et al [ZJW03]. Do ponto de vista da sintaxe, utilizou-se uma representação textual baseada em XML para a representação de especicações de programas em linguagem MIR. Esta representação baseada em XML é adequada para uma representação intermediária, conforme os motivos apresentados oportunamente na Seção 4.4. Outras vantagens que merecem ser destacadas são a possibilidade de se vericar a sintaxe de um programa por meio do XML Schema Denition (XSD) fornecido junto com o arcabouço3 e a existência de várias ferramentas para a edição
de arquivos XML que poderiam ser utilizados na programação nesta linguagem.
representadas em memória, e como estas são compiladas para o código C++ correspondente.
5.1 Mapeamento de ASM para OOP
Considerando-se que o gerador de código produz um programa em C++ para reetir a se- mântica de uma especicação ASM, é preciso deixar explícito a abordagem utilizada para o mapeamento de elementos ASM em entidades da programação orientada por objetos (OOP). Os primeiros elementos a se considerar da linguagem MIR são os módulos e os agentes. Para a representação em memória de módulos de programas MIR, foram implementadas as classes da Figura 5.1. Lá estão apresentadas também as classes que representam os elementos do ambiente de um módulo, que são as diversas tabelas de funções, abstrações de regras e semáforos. É percorrendo esta hierarquia que o visitor de geração de código é capaz de gerar o código C++ equivalente. As assinaturas importadas e referenciadas são apresentadas na Figura 5.2.
Um ambiente, por sua vez, é representado em memória por uma classe chamada Envi- ronment, cujos componentes são as tabelas das entidades que compõem um ambiente. Estas tabelas são representadas pelas classes StaticAndDerivedFunctionTable, DynamicFunctionTable, DerivedFunctionTable, ExternalFunctionTable, ActionTable, SubMachineTable e SemaphoreTable, que nada mais são do que tabelas das entidades correspondentes a cada uma das classes. Cabe ressaltar que as funções foram organizadas em uma hierarquia tal que funcionalidades comuns são sempre agrupadas em classes abstratas ancestrais a serem derivadas pelas classes que necessitam de tal funcionalidade, evitando, assim, duplicação desnecessária de código.
Todas as classes em questão possuem também o método accept(Visitor* v). Este método é necessário à utilização do padrão de projeto Visitor, apresentado na Seção 3.3.7. É por meio deste padrão que as funcionalidades de persistência e geração de código, apresentadas neste capítulo, são implementadas. A presença deste padrão também abre a possibilidade de que novas funcionalidades sejam adicionadas de maneira modular.
As listas de referências e importações são apresentadas na Figura 5.2. Basicamente, estas 81
listas são constituídas de assinaturas de funções, abstrações de regras e semáforos, agrupa- das pelo módulo de origem. A funcionalidade comum das referências e das importações são agrupadas em uma classe abstrata ancestral comum.
Uma especicação MAS (MIR Agents Starter) é representada por um objeto da classe MASSpecication, apresentado na Figura 5.3. Esta classe contém, basicamente, um conjunto de agentes a serem instanciados.
No processo de tradução para C++, a compilação de um módulo dá origem a uma classe. Os componentes do módulo, ou seja, as tabelas com as funções, abstrações de regras e semáforos são traduzidos nos membros desta classe, conforme detalhado adiante. A regra de transição do módulo é um método de nome especial, executeTransitionRule. A Figura 5.5 mostra um exemplo de interface da classe resultante da compilação de um módulo. Toda classe que representa um módulo deve herdar de Module, apresentado na Figura 5.7. Esta classe dene o método executeTransitionRule como um método puramente virtual. Desta forma, agentes podem ser uniformemente tratados como objetos da classe Module, e cada agente executará sua regra de transição corretamente. Em seguida é denida uma função de assinatura e tipo de retorno void *runModule(void *a) que cria um agente e então o dispara. Esta função é necessária porque agentes são executados concorrentemente, por meio de threads, e uma thread na biblioteca de concorrência utilizada é uma função com tal assinatura e tipo de retorno.1 A implementação
desta função pode ser vista na Figura 5.4.
Um agente, por sua vez, é um objeto do módulo do seu tipo. O disparo de um agente é a execução de seu método executeTransitionRule por meio de uma thread. A compilação de um disparador de agentes, anteriormente chamado de MAS, dá origem a uma classe que cria as instâncias dos agentes especicados de acordo com seu tipo e então dispara estes agentes. As Figuras 5.8 e 5.9 mostram um exemplo da tradução de um disparador de agentes. No construtor da classe, MAS::MAS() na Figura 5.9, são criados os agentes como objetos dos módulos correspondentes, e então estes são inseridos em uma tabela hash que associa cada agente criado a seu nome. Este ambiente onde os agentes habitam é acessível por qualquer agente que nela esteja, o que torna os agentes acessíveis entre si por meio de seu nome. Por sua vez, os agentes, após criados, são disparados no método run desta mesma classe. Para cada agente criado, é associada uma thread que recebe o agente como parâmetro, assim como uma referência à função runModule. Esta função está denida junto com a classe de base Module, como pode ser visto na Figura 5.4, e sua função basicamente é executar, dentro da thread, o método executeTransitionRule do módulo.
A compilação de um arquivo que representa um disparador de agentes (de extensão mas) resulta em um par de arquivos de extensão h e cc de mesmo nome, conforme os modelos apre- sentados nas Figuras 5.8 e 5.9, respectivamente. Após a inclusão dos cabeçalhos dos módulos utilizados, o arquivo de extensão h dene a classe MAS, cujo papel é instanciar e disparar os agentes especicados no disparador de agentes, e então servir de contexto para estes agentes. A classe segue o padrão Singleton, denido na Seção 3.3.4, e por isso nenhum construtor público está disponível. No lugar disto, a referência a um objeto desta classe é obtida por meio do método estático getInstance(). No momento em que um objeto desta classe é instanciado, os agentes são criados. O disparo destes agentes ocorre por meio da chamada ao método run(). O arquivo de extensão cc é o arquivo principal que dará origem ao executável, pois é nele que é denida a função main. Esta função obtém uma instância do contexto dos agentes e então 1A biblioteca utilizada é a pthread, que é a biblioteca de threads padrão de sistemas operacionais unix-like.
Figura 5.2: Diagrama de classes: referências e importações.
Figura 5.3: Diagrama de classes: especicação MAS.
#include "Module.h"
// The function that starts a specific agent, used by the thread void *runModule(void* a) {
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); Module* agent = (Module*) a;
agent->executeTransitionRule(); return 0; }
// where the other agents can be found.
Module1(std::map<std::string, Module*>* agents, std::string agName); // The imports
Module2* __Modulo2; Module3* __Modulo3; ...
ModuleN* __ModuloN; // The global environment ...
// The local environment ...
// The transition rule
virtual void executeTransitionRule(); private:
// The update list and some auxiliary methods ... };
#else
class Module1 : public Module; #endif
Figura 5.5: Exemplo de uma classe criada para representar um módulo genérico.
dispara estes agentes. Note que na criação dos agentes no construtor privado MAS::MAS(), cada agente recebe como parâmetro, no momento de sua criação, o contexto no qual ele é inserido, de modo que é possível a um agente acessar as funções dinâmicas de outro agente, o que caracteriza a memória compartilhada. O método run() dispara a execução da regra de transição dos agentes criados como threads independentes.
Um módulo é compilado em código C++ por meio da classe CodeGenerator. Esta classe implementa os métodos da classe abstrata Visitor, de modo que a geração de código em si se dá por meio da passagem de um objeto do tipo CodeGenerator para o método visit(Visitor* v) do objeto que representa o módulo a ser compilado. Para cada componente deste módulo, o código correspondente é gerado, e esta seção tem por objetivo apresentar os critérios utilizados para a geração de código em cada elemento do módulo.
A compilação de um módulo dá origem a um par de arquivos de extensões h e cc, cu- jas estruturas são semelhantes àquelas apresentadas nas Figuras 5.5 e 5.6, respectivamente. No arquivo com extensão h, a denição da classe é cercada apropriadamente por expressões de compilação condicional, de modo a permitir denições de classes mutuamente recursivas.
#include "Module1.h"
//--- Module1::Module1(std::map<std::string, Module*>* a) {
__Modulo2 = new Module2(a); __Modulo3 = new Module3(a); ...
__ModuloN = new ModuleN(a); // regra init
// ... }
//--- void Module1::executeTransitionRule() {
bool __stop = false;
while (!__stop) { // The transition rule... } }
//---
Figura 5.6: Exemplo de uma classe criada para representar um módulo.
Dentro destas expressões, os primeiros elementos da denição são os include's necessários à compilação deste módulo. Estes include's consistem nos arquivos de cabeçalho dos módulos referenciados ou importados por este módulo, e esta informação é dada pelas listas de módulos referenciados e importados que cada módulo possui em sua denição.
A seguir é iniciada a denição da classe em si que representa o módulo. O nome desta classe é o nome do módulo, e esta herda da classe abstrata Module, presente no ambiente de execução, que provê comportamentos comuns a qualquer módulo. Além disso, esta estratégia também fornece ao módulo uma interface mínima comum a todos os módulos. Agentes são objetos desta classe. O primeiro método a ser denido na interface da classe é o construtor, que recebe como argumento o contexto no qual o agente ora criado será inserido e o nome do agente a ser criado. Este contexto serve também ao propósito de permitir que outros agentes estejam acessíveis.
A composição com outros módulos é feita por meio da agregação. Exemplicando, seja A um módulo que importa os módulos B e C. Então a classe gerada a partir da denição de A possui um campo cujo tipo é a classe gerada a partir da denição do módulo B e um campo cujo tipo é a classe gerada a partir da denição do módulo C.
O ambiente global do módulo é então declarado. Abstrações de regras são declaradas como métodos que retornam void, enquanto funções são declaradas como métodos de retorno dife- rente de void. Semáforos são declarados como estruturas sem_t da biblioteca semaphore utili- zada para prover os semáforos e as operações sobre estes. Os detalhes de como cada um destes métodos é implementado de fato para cada tipo de elemento do ambiente é apresentado mais à frente, quando da explicação do arquivo de extensão cc associado a um módulo. O ambiente local do módulo é denido a seguir, à semelhança do ambiente global. A regra de transição do módulo é compilada para um método de nome especial, a saber, void executeTransitionRule(). Após a geração de código de cada módulo utilizado e a geração de código do disparador de agentes, deve-se compilar cada módulo utilizado e então compilar o código do disparador dos agentes. A compilação é direta, e um makefile de exemplo é apresentado na Figura 5.10. A Figura 5.11 apresenta a forma geral de um construtor de um módulo. Em um primeiro momento são inicializados os módulos que porventura constituem este módulo em si. Neste
protected:
// the name of the agent std::string agentName; };
//--- // The function that starts a specific agent, used by the thread
void *runModule(void* a);
//--- #else
class Module;
void *runModule(void* a); #endif
Figura 5.7: Classe de base de um módulo.
exemplo, o módulo MyModule é composto por dois outros módulos, a saber, A e B. Em seguida, funções dinâmicas locais de aridade 0 são inicializadas. Esta inicialização é necessária apenas para funções 0-árias, pois para funções de aridade superior é utilizada a classe std::map junto com um método acessor que garante o valor padrão para pontos ainda não denidos. A mesma inicialização existe para funções dinâmicas 0-árias globais, porém esta inicialização não é feita no construtor, pois trata-se de componentes estáticos da classe do módulo. Finalmente, o código correspondente à regra de inicialização init é gerado. Após este código, a função flush() é chamada, que levará a termo as atualizações eventualmente geradas por regras de atualização.