Suporte para o Desenvolvimento de Ferramentas de Programaç˜ao
Transcrição
Suporte para o Desenvolvimento de Ferramentas de Programaç˜ao
Suporte para o Desenvolvimento de Ferramentas de Programação Interactiva Luı́s Manuel Pinto da Rocha Afonso Carriço Tese de Mestrado Eng. Electrotécnica e de Computadores Área de Computadores 31 de Julho de 1991 Suporte para o Desenvolvimento de Ferramentas de Programação Interactiva Luı́s Manuel Pinto da Rocha Afonso Carriço Tese submetida para provas de mestrado Departamento de Engenharia Electrotécnica e de Computadores Instituto Superior Técnico Lisboa 31 de Julho de 1991 Tese realizada sob a orientação do Prof. Doutor José Manuel da Costa Alves Marques Professor Catedrático do Departamento de Engenharia Electrotécnica Instituto Superior Técnico Nome: Luı́s Manuel Pinto da Rocha Afonso Carriço Tı́tulo: Suporte para o Desenvolvimento de Ferramentas de Programação Interactiva. Palavras chave Ferramentas de Programação Interactiva Linguagens de Programação Programação Orientada para Objectos Keywords Interactive Programming Tools Programming Languages Object-oriented programming Para a Paula. Suporte para o Desenvolvimento de Ferramentas de Programação Interactiva Luı́s Manuel Pinto da Rocha Afonso Carriço1 IST2 - INESC3 - JNICT4 22 de Agosto de 2008 1 [email protected] 2 Instituto Superior Técnico de Engenharia de Sistemas e Computadores 4 Junta Nacional de Investigação Ciêntifica e Tecnologica 3 Instituto Resumo Este trabalho dedica-se à concepção e realização de um conjunto de serviços, que possam ser usados como suporte ao desenvolvimento de ferramentas de programação interactiva. Adopta-se a linguagem C++ de modo a assegurar, desde logo, grande compatibilidade com sistemas já desenvolvidos, assumindo, mesmo assim, uma abordagem de programação orientada para objectos. Considerando essa metodologia de programação, introduz-se uma sistematização de alguns conceitos nela envolvidos, classificando em seguida o modelo de tipos da linguagem que se adoptou. É também analisado o suporte que outras linguagens e algumas bibliotecas oferecem às necessidades requeridas pelas ferramentas de programação interactiva, com o intuito de dar uma panorâmica dos trabalhos relacionados com o ICE, o sistema que aqui se irá apresentar. Discutem-se então as caracterı́sticas do modelo e das estruturas de suporte em execução, que devem ser incluı́das no ICE, como forma preencher as caracterı́sticas dos serviços que deve oferecer. Estes englobam um mecanismo de invocação interpretada de operações, incluindo invocação de objectos por mensagem, criação de instâncias em tempo de execução e identificação de objectos por nome, e um serviço de salvaguarda e recuperação de objectos. Abordam-se finalmente as questões de concretização. i ii Abstract This work is about the conception and implementation of a set of services that can be used to support the development of interactive programming tools. It adopts the C++ language in order to ensure the desired openness and compatibility with other systems and still include the advantages of an object oriented programming approach. A set of concepts used in this programming methodology are defined and the adopted language is described and classified accordingly. Its also analyzed the support that other languages and some libraries offer to the referred programming tools, providing this way an overview on the work related with ICE, the system hereby presented. The ICE model and run-time support structures are discussed as a consequence of the characteristics required for the services it should offer. These include a mechanism for interpreted invocation of operations, comprehending object invocation by message, run-time instance creation and name identification, and a service for object storage and retrieval. Its implementation is then described. iii iv Agradecimentos Ao meu orientador, Professor José Alves Marques, a quem desejo expressar o meu reconhecimento pela sua crı́tica exigente, que sempre manifestou ao longo destes anos. Aos Engos .Nuno Guimarães e Pedro Antunes que se dispuseram a ler esta tese e com quem tive longas discussões. Sem dúvida, as suas opiniões representaram uma valiosa contribuição para este trabalho. À Enga .Paula Pereira, Engo .Ricardo Nunes e a todos os que utilizaram este trabalho, pela contribuição que deram, com as crı́ticas decorrentes dessa sua experiência. A todos os que se prontificaram a ler a primeira versão desta tese, e em particular aos elementos do projecto COMANDOS do INESC, Engos .Paulo Ferreira, André Zúquete, Pedro Sousa e Manuel Sequeira, pelas interessantes discussões tidas, que me permitiram esclarecer as relações entre ambos os trabalhos. Aos meus colegas de mestrado, Engos .Luı́s Rodrigues, Mário Baptista e José Pereira, com os quais, durante a parte escolar, realizei vários trabalhos em grupo. Ao INESC, onde encontrei os meios técnicos e a possibilidade de inserção num projecto, no qual me foi possı́vel enquadrar esta tese. v Aos meus pais, Manuel e Natália, e à minha esposa, Ana Paula, pela compreensão demonstrada às minhas súbitas mudanças de humor, e em particular ao meu pai, pelo encorajamento que sempre me deu. Desejo ainda expressar o meu especial agradecimento à minha mãe e à minha esposa pela dedicada e paciente revisão que fizeram a este texto. Lisboa, 31 de Julho de 1991 Luı́s Manuel Pinto da Rocha Afonso Carriço vi Índice Resumo i Abstract iii Agradecimentos v Índice vii Lista das figuras xii Lista das tabelas xiii 1 Introdução 1 1.1 Ferramentas de Programação Interactiva . . . . . . . . . . . . . . . . 2 1.2 Contexto e trabalho de base . . . . . . . . . . . . . . . . . . . . . . . 3 1.2.1 O 4D . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.2.2 A INGRID . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.3 Objectivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1.4 Estrutura da Tese . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2 Panorâmica 2.1 11 Conceitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.1.1 Classes e Protótipos . . . . . . . . . . . . . . . . . . . . . . . 12 2.1.1.1 Mensagens, Métodos e Funções Membro . . . . . . . 13 2.1.1.2 Tipos e Tipificação . . . . . . . . . . . . . . . . . . . 14 2.1.2 Herança e Delegação . . . . . . . . . . . . . . . . . . . . . . . 14 2.1.3 Encapsulamento . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.1.4 Polimorfismo e Classes abstractas . . . . . . . . . . . . . . . . 16 2.1.4.1 Formas de polimorfismo . . . . . . . . . . . . . . . . 17 vii 2.1.4.2 2.2 C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.2.1 Caracterı́sticas gerais . . . . . . . . . . . . . . . . . . . . . . . 20 2.2.2 O modelo de tipos . . . . . . . . . . . . . . . . . . . . . . . . 21 2.2.3 2.3 2.3.2 2.5 2.2.2.1 Classes e Herança . . . . . . . . . . . . . . . . . . . . 21 2.2.2.2 Encapsulamento . . . . . . . . . . . . . . . . . . . . 23 2.2.2.3 Polimorfismo . . . . . . . . . . . . . . . . . . . . . . 24 Aspectos de concretização . . . . . . . . . . . . . . . . . . . . 25 2.2.3.1 Discriminação de métodos . . . . . . . . . . . . . . . 25 2.2.3.2 Tabela de métodos virtuais . . . . . . . . . . . . . . 26 Suporte aos modelos em tempo de execução . . . . . . . . . . . . . . 28 2.3.1 2.4 Tipos Conformes . . . . . . . . . . . . . . . . . . . . 19 O Smalltalk . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 2.3.1.1 Modelo de objectos . . . . . . . . . . . . . . . . . . . 29 2.3.1.2 A primitiva de invocação . . . . . . . . . . . . . . . . 30 2.3.1.3 Optimizações . . . . . . . . . . . . . . . . . . . . . . 31 O Objective-C . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2.3.2.1 Modelo de objectos . . . . . . . . . . . . . . . . . . . 33 2.3.2.2 A primitiva de invocação . . . . . . . . . . . . . . . . 33 2.3.3 O suporte de execução do COMANDOS . . . . . . . . . . . . 35 2.3.4 Bibliotecas C++ . . . . . . . . . . . . . . . . . . . . . . . . . 36 2.3.5 Outras linguagens . . . . . . . . . . . . . . . . . . . . . . . . . 37 Salvaguarda e recuperação de objectos . . . . . . . . . . . . . . . . . 37 2.4.1 O Smalltalk . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 2.4.2 O Eiffel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 2.4.3 O Objective-C . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 2.4.4 O IK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 2.4.5 O OOPS e o ET++ . . . . . . . . . . . . . . . . . . . . . . . 43 Sı́ntese . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 3 Suporte à interpretação 3.1 45 Invocação por mensagens . . . . . . . . . . . . . . . . . . . . . . . . . 46 3.1.1 Sobreposição de nomes . . . . . . . . . . . . . . . . . . . . . . 49 3.1.1.1 Selector da mensagem . . . . . . . . . . . . . . . . . 49 3.1.1.2 Identificação de tipo para os argumentos . . . . . . . 51 3.1.1.3 Identificação dos argumentos . . . . . . . . . . . . . 53 viii 3.1.2 Definição completa de funções membro . . . . . . . . . . . . . 55 3.1.3 Funções membro especiais . . . . . . . . . . . . . . . . . . . . 57 3.1.4 3.2 3.3 3.1.3.1 Operadores . . . . . . . . . . . . . . . . . . . . . . . 58 3.1.3.2 Conversores . . . . . . . . . . . . . . . . . . . . . . . 58 3.1.3.3 Destrutor e operador delete . . . . . . . . . . . . . 59 Generalização da invocação por mensagem . . . . . . . . . . . 60 Criação de objectos em tempo de execução . . . . . . . . . . . . . . . 63 3.2.1 A primitiva de criação de objectos . . . . . . . . . . . . . . . . 65 3.2.2 Integração com a invocação por mensagens . . . . . . . . . . . 68 3.2.3 Funções membro estáticas . . . . . . . . . . . . . . . . . . . . 69 3.2.4 Generalização a todos os tipos C++ . . . . . . . . . . . . . . 70 Serviço de nomes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 4 Salvaguarda e recuperação de objectos 77 4.1 Versatilidade na representação externa . . . . . . . . . . . . . . . . . 78 4.2 Salvaguarda e recuperação automáticas . . . . . . . . . . . . . . . . . 82 4.2.1 4.3 4.2.1.1 Localização das variáveis membro no objecto . . . . 85 4.2.1.2 Identificação do tipo das variáveis membro . . . . . . 86 4.2.2 Primitivas que definem a SR automática . . . . . . . . . . . . 87 4.2.3 Mecanismo de redefinição da SR automática . . . . . . . . . . 88 4.2.3.1 As primitivas envolvidas na redefinição . . . . . . . . 89 4.2.3.2 As funções especı́ficas . . . . . . . . . . . . . . . . . 90 As primitivas de salvaguarda e recuperação . . . . . . . . . . . . . . . 90 4.3.1 4.3.2 4.3.3 4.4 Descrição das instâncias no modelo de suporte . . . . . . . . . 84 Utilização da informação do tipo do objecto . . . . . . . . . . 90 4.3.1.1 Resolução de tipos na salvaguarda . . . . . . . . . . 92 4.3.1.2 Resolução de tipos na recuperação . . . . . . . . . . 92 Criação de objectos na recuperação . . . . . . . . . . . . . . . 93 4.3.2.1 Integração com o serviço de nomes . . . . . . . . . . 93 4.3.2.2 Reserva do espaço de memória . . . . . . . . . . . . 94 Sintaxe das representações e meios de salvaguarda . . . . . . . 95 Operações sobre o conjunto-de-salvaguarda . . . . . . . . . . . . . . . 96 4.4.1 Detecção de objectos já guardados ou recuperados . . . . . . . 96 4.4.1.1 4.4.2 Referências para variáveis membro . . . . . . . . . . 97 Limitação do número objectos envolvidos . . . . . . . . . . . . 99 ix 4.5 A geração de código . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 5 O ICE 5.1 Interface comum aos objectos . . . . . . . . . . . . . . . . . . . . . . 107 5.1.1 5.1.2 5.2 IObject . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 5.1.1.1 Interface ao serviço de nomes . . . . . . . . . . . . . 108 5.1.1.2 Interface de acesso à informação de tipo . . . . . . . 108 5.1.1.3 Interface ao serviço de invocação por mensagem . . . 109 5.1.1.4 Interface ao serviço de salvaguarda e recuperação de objectos . . . . . . . . . . . . . . . . . . . . . . . . . 111 IOID . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 5.1.2.1 Criação de identificadores-de-objecto . . . . . . . . . 112 5.1.2.2 Utilização dos identificadores-de-objecto . . . . . . . 112 5.1.2.3 Redefinição do protocolo herdado de IObject . . . . 115 Os objectos-de-tipo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 5.2.1 5.2.2 5.2.3 5.3 107 Interface comum aos objectos-de-tipo . . . . . . . . . . . . . . 116 5.2.1.1 Acesso por nome . . . . . . . . . . . . . . . . . . . . 116 5.2.1.2 Teste da relação entre tipos . . . . . . . . . . . . . . 116 5.2.1.3 Criação de objectos . . . . . . . . . . . . . . . . . . . 117 5.2.1.4 Invocação de métodos por mensagem . . . . . . . . . 118 Os tipos primitivos . . . . . . . . . . . . . . . . . . . . . . . . 120 5.2.2.1 Tipos fundamentais . . . . . . . . . . . . . . . . . . 120 5.2.2.2 Apontadores . . . . . . . . . . . . . . . . . . . . . . 121 5.2.2.3 Funções . . . . . . . . . . . . . . . . . . . . . . . . . 122 Os tipos definidos pelo utilizador . . . . . . . . . . . . . . . . 122 5.2.3.1 Relações entre classes . . . . . . . . . . . . . . . . . 123 5.2.3.2 Criação de objectos e invocação por mensagem . . . 124 5.2.3.3 Interface à salvaguarda e recuperação . . . . . . . . . 125 5.2.3.4 Parametrização de objectos-de-classe . . . . . . . . . 128 Os objectos-de-método . . . . . . . . . . . . . . . . . . . . . . . . . . 128 5.3.1 Os selectores dos métodos . . . . . . . . . . . . . . . . . . . . 129 5.3.2 Interface comum aos objectos-de-método . . . . . . . . . . . . 130 5.3.3 5.3.2.1 Verificação da validade de uma mensagem . . . . . . 131 5.3.2.2 Execução do código associado ao objecto-de-método 132 Os objectos-de-método para código compilado . . . . . . . . . 133 x 5.3.4 5.4 5.5 Definição dos gestores de métodos 5.3.3.2 Resolução das invocações virtuais para métodos compilados . . . . . . . . . . . . . . . . . . . . . . . 135 Os objectos-de-método para código interpretado . . . . . . . . 136 5.4.1 Regras da utilização de nomes . . . . . . . . . . . . . . . . . . 139 5.4.2 Concretização . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 O serviço de nomes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 Interface ao serviço de nomes . . . . . . . . . . . . . . . . . . 141 Os objectos-de-E/S . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142 5.6.1 Interface comum aos objectos-de-E/S . . . . . . . . . . . . . . 142 5.6.1.1 5.6.2 Primitivas de salvaguarda e recuperação . . . . . . . 144 Objectos especı́ficos . . . . . . . . . . . . . . . . . . . . . . . . 144 6 Conclusão 6.1 . . . . . . . . . . 133 A geração automática de objectos-de-tipo . . . . . . . . . . . . . . . . 137 5.5.1 5.6 5.3.3.1 147 Trabalho Futuro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 6.1.1 Evolução Funcional . . . . . . . . . . . . . . . . . . . . . . . . 148 6.1.2 Integração com o IK . . . . . . . . . . . . . . . . . . . . . . . 150 6.1.2.1 Suporte comum à execução dos serviços . . . . . . . 150 6.1.2.2 Integração de serviços . . . . . . . . . . . . . . . . . 152 6.1.3 Perspectivas de exploração . . . . . . . . . . . . . . . . . . . . 153 Bibliografia 155 xi Lista de Figuras 1.1 INGRID . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1 2.2 2.3 Tabelas de métodos virtuais . . . . . . . . . . . . . . . . . . . . . . . 27 Relações entre instâncias, classes e metaclasses no Smalltalk . . . . . 29 Salvaguarda e recuperação de objectos . . . . . . . . . . . . . . . . . 38 3.1 3.2 3.3 3.4 3.5 3.6 3.7 Objectos de classe como suporte à invocação por mensagens . . . . . Identificador de objectos . . . . . . . . . . . . . . . . . . . . . . . . . Objectos de classe e de método como suporte à invocação por mensagens Generalização da invocação por mensagem. . . . . . . . . . . . . . . . Acesso aos construtores . . . . . . . . . . . . . . . . . . . . . . . . . . Mensagens sobre objectos-de-classe. . . . . . . . . . . . . . . . . . . . Integração dos serviços de suporte à interpretação . . . . . . . . . . . 48 54 56 63 66 70 73 4.1 4.2 4.3 4.4 4.5 4.6 Modelo de Entradas/Saı́das ou salvaguarda e recuperação de objectos Salvaguarda e recuperação automática de objectos. . . . . . . . . . . Redefinição da salvaguarda e recuperação automática de objectos . . Referência a variáveis membro não resolvida . . . . . . . . . . . . . . Utilização do mecanismo de SR num editor de interfaces. . . . . . . . Utilização do mecanismo de SR na concretização de um clipboard. . . 81 87 88 98 99 101 5.1 5.2 A hierarquia de metaclasses do ICE . . . . . . . . . . . . . . . . . . 116 Ligações entre objectos, objectos-de-classe e objectos-de-metaclasse . 124 xii 6 Lista de Tabelas 5.1 Tempos de invocação para cada serviço. . . . . . . . . . . . . . . . . . 136 xiii xiv Capı́tulo 1 Introdução A crescente complexidade dos problemas apresentados hoje em dia, aos sistemas computacionais programados (software) e a sua grande divulgação, criou a necessidade de encontrar mecanismos mais eficazes no desenvolvimento e manutenção desses sistemas. A adopção de metodologias de programação adequadas, com o objectivo de sistematizar a concretização dos sistemas programados, é, sem dúvida, um desses mecanismos. Neste contexto, as aproximações de programação Orientada para Objectos (OO) têm provado, nos últimos anos, a sua eficiência como forma de introduzir modularidade nos programas e promover a reutilização de código. Por outro lado, o aparecimento de ferramentas e ambientes integrados, auxiliando o programador em todas as fases do processo de programação, veio permitir um aumento considerável da produtividade, contribuindo igualmente para a homogeneidade e a robustez dos seus programas. Também a evolução ocorrida nos mecanismos de interacção Homem-Máquina, com o aparecimento de sistemas gráficos, dispositivos de selecção directa, etc, influenciou decisivamente a forma de criação das aplicações. De facto, a introdução desses mecanismos, ao elevar o nı́vel de abstracção na interacção com o utilizador, veio dificultar o desenvolvimento das aplicações, acrescentando ainda problemas ergonómicos, normalmente ignorados pelos programadores. Em contrapartida, permitiu a evolução das formas de interacção nas próprias ferramentas de programação, contribuindo para aumentar a eficiência das pessoas que as usam e alargando mesmo o universo de utilizadores a pessoas não especializadas em programação. 1 2 CAPı́TULO 1. INTRODUÇÃO 1.1 Ferramentas de Programação Interactiva A utilização de ferramentas e ambientes de programação interactiva [Shu 88] vem sendo, desde à muito, comum em diversas áreas cientı́ficas, nomeadamente em inteligência artificial, onde os dialectos de LISP e os seus ambientes de execução tomam especial significado. Nesta área, a intrı́nseca indefinição dos problemas a resolver e consequentemente a dificuldade da sua resolução, levou, desde cedo, à adopção desses ambientes (e.g. Interlisp [Teitelman 86], Interlisp-D [Sheil 86]), segundo uma perspectiva de programação experimental [Sheil 86], em que o programador restrutura e testa frequentemente o seu programa, antes de chegar a um resultado minimamente razoável. No entanto, esta forma de programação não é de todo exclusiva do universo da inteligência artificial. Por exemplo, a evolução dos sistemas gráficos já referida, introduziu o mesmo problema a nı́vel da construção de Interfaces Homem-Máquina (IHM). De facto, os problemas de design e ergonomia que surgem, dificilmente se resolvem de imediato, sendo normalmente um processo iterativo, quer na fase de construção de cada protótipo, quer no refinamento dos protótipos como resultado da sua validação pelos utilizadores [Shneiderman 87, Myers 89, Hartson 89]. Em resumo, pode dizer-se que a indefinição dos problemas apresentados à programação em geral e a necessidade de experimentação, bem como o carácter dinâmico das especificações, em resultado das reacções assumidas pelos utilizadores finais, requerem das ferramentas de programação uma grande flexibilidade na definição, validação e restruturação dos programas. Por essa razão, a maior parte dessas ferramentas é desenvolvida sobre linguagens interpretadas ou linguagens que oferecem algum suporte à interpretação. São exemplos disso, o ambiente Smalltalk, os ambientes de LISP já referidos e as ferramentas de construção de interfaces por demonstração, como o Peridot [Myers 88, Myers 87], também baseado no LISP. Por outro lado, surgem ferramentas como o Interface Builder, desenvolvido sobre o Objective-C que, embora compilado, suporta uma forma limitada de execução interpretada de operações sobre os objectos, ou o Cedar [Swinehart 86] que desenvolve um interpretador próprio para a sua linguagem de base. 1.2. CONTEXTO E TRABALHO DE BASE 1.2 3 Contexto e trabalho de base O trabalho que se apresenta nesta tese passou por um processo evolutivo, que decorreu de necessidades encontradas durante o desenvolvimento do Images [Simoes 87, Marques 88, Simoes 88], um Sistema de Construção e Gestão de IHM realizado no âmbito do projecto SOMIW1 [SOMIW 85]. O Images era baseado numa biblioteca de objectos, que podiam ser usados na composição de uma IHM, através da utilização de uma linguagem de especificação dedicada, que formalizava o modelo funcional adoptado, o modelo de Seeheim [Pfaff 85]. A biblioteca era concretizada por um conjunto de classes C++, cujos objectos executavam acções, em consequência da recepção de eventos gerados pelo utilizador da interface. O mecanismo de troca de eventos era realizado por um sistema de suporte de execução, que os traduzia em invocações às operações definidas sobre os objectos. Este suporte, também realizado pelo autor [Carriço 89], constituiu o embrião do trabalho apresentado nesta tese. Das conclusões extraı́das do trabalho realizado no Images, e que levaram à sua evolução, podem enunciar-se as seguintes: • A utilização de uma biblioteca própria de componentes de apresentação de IHM, em detrimento da utilização de bibliotecas normalizadas existentes, como as baseadas no Xt2 [McCormack 89, Young 89], limitava seriamente a aceitação numa comunidade de utilizadores reais. • A adopção de uma linguagem de especificação não correspondia a uma evolução significativa na construção de IHM, do ponto de vista do programador das mesmas. Nesse sentido, optou-se por: • Criar uma biblioteca de componentes de alto nı́vel que permitisse integrar outras bibliotecas existentes, nomeadamente as directamente relacionadas com IHM, reforçando, no entanto, os aspectos de sistematização que pode oferecer 1 SOMIW - Secure Open Multimedia Integrated Workstation - foi um projecto parcialmente financiado pelo programa ESPRIT (Esprit 367). 2 X Toolkit Intrinsics 4 CAPı́TULO 1. INTRODUÇÃO um modelo funcional que englobe, quer o comportamento da interface, quer a sua ligação com a parte computacional da aplicação. • Construir um conjunto de ferramentas integradas que viabilizem a construção interactiva de IHM, minimizando o recurso aos métodos clássicos de programação (edição, compilação, ...). 1.2.1 O 4D O 4D [Antunes 91, Antunes 90b] é uma biblioteca concretizada segundo um modelo que inclui quatro categorias de componentes: apresentação, dados, diálogo, e conversão3 . A categoria de apresentação inclui objectos de interacção com o utilizador, como botões, menús, caixas de texto, editores, etc. Neste aspecto, providen- cia essencialmente um mecanismo de ligação a bibliotecas de componentes de interacção existentes, nomeadamente, na concretização corrente, às bibliotecas Athena [Peterson 89] e Motif [Young 90] baseadas no Xt. A categoria de apresentação engloba também objectos de composição, que permitem encapsular um conjunto de objectos organizados segundo os quatro conceitos do modelo, tornando-o assim um modelo recursivo [Coutaz 89]. Estes modelos, como por exemplo o PAC [Coutaz 87], introduzem grande modularidade na definição da IHM, deixando, no entanto, espaço à flexibilidade necessária para abarcar as mais diversas formas de interacção [Antunes 90a]. A categoria de dados providencia componentes representativos de tipos de dados genéricos (inteiros, ficheiros, listas, etc), bem como de objectos que estabelecem as ligações à parte computacional da aplicação, na IHM. Nesse sentido, é definida claramente, nos objectos de dados, a fronteira entre as duas partes da aplicação, interface e parte computacional, reforçando o que é vulgar chamar independência do diálogo [Hartson 89], e que é, desde há algum tempo, aceite como dado adquirido nestes sistemas. Por sua vez, os objectos de conversão permitem a ligação dos objectos de dados aos de apresentação, providenciando as necessárias transformações dos dados. Finalmente, os objectos de diálogo controlam a sequência de interacção, sendo por 3 O seu nome, 4D, deriva do nome atribuı́do às quatro categorias em inglês: Display, Data, Dialog e Driver. 1.2. CONTEXTO E TRABALHO DE BASE 5 conseguinte, essencialmente, objectos programáveis. No seu funcionamento normal, um objecto de diálogo decidirá a execução de uma sequência de operações sobre objectos de dados, em consequência de um estı́mulo recebido de um objecto de apresentação, resultante, por sua vez, de uma acção do utilizador. Segundo este modelo, os componentes das quatro categorias comunicam entre si, através de “ligações” (links). Estabelecida uma “ligação” de um componente para outro, o primeiro deve ser capaz de desencadear uma acção no segundo, sem que para isso seja necessário conhecer, a priori, tanto o objecto receptor, como a acção que nele vai executar. Desse modo, a noção de “ligação” concretizada segundo estas premissas, permite aumentar significativamente a flexibilidade e capacidade de reutilização dos componentes da biblioteca, oferecendo, no entanto, pelo seu enquadramento no modelo, um comportamento bem definido e autónomo a cada tipo de objectos. 1.2.2 A INGRID A INGRID4 [Carriço 90, Guimaraes 91, aes 91] é uma ferramenta para a construção de IHM, baseada na arquitectura e na biblioteca 4D, quer do ponto de vista da sua construção, quer na forma como se apresenta e guia o utilizador no processo de criação, quer ainda nos componentes que usa e na maneira como os organiza nas interfaces que gera. Na sua concepção foram considerados fundamentais os seguintes objectivos: • Construção interactiva O processo de criação de uma interface inclui a selecção do tipo de objecto que se pretende utilizar, a sua criação, parametrização dos seus atributos e definição das ligações com outros objectos. Nesse processo, o utilizador deve interactuar, em cada passo, com a interface que lhe é apresentada pela ferramenta, sempre que possı́vel de uma forma gráfica. • Programação experimental Na definição da IHM, o utilizador (aqui programador) deve aperceber-se de imediato das alterações que operou, quer do ponto de vista de aspecto exterior 4 INteractive GRaphical Interface Designer. 6 CAPı́TULO 1. INTRODUÇÃO Figura 1.1: INGRID (apresentação), quer do ponto de vista do comportamento (ligações, diálogos, dados e conversores). Nesse sentido, deve ser capaz de verificar a validade da interface a cada passo de evolução, modificando-a, executando-a e experimentando novas soluções, durante o próprio processo de criação. • Flexibilidade e Extensibilidade A vantagem de basear uma ferramenta como INGRID numa biblioteca de componentes, só será realmente significativa, se alterações a essa biblioteca, tanto pela inclusão de novos objectos, como pela modificação dos já existentes, não implicarem grandes alterações na ferramenta. Desse modo, a INGRID deve articular-se com os tipos de objectos da forma mais abstracta possı́vel, não conhecendo por isso a priori, nem a forma de interacção com os objectos, nem a da sua criação. Uma das consequências que decorrem de imediato dos requisitos enumerados para a concretização desta ferramenta, será a necessidade da existência de um suporte de execução, que ofereça capacidades de invocação interpretada de operações sobre os objectos da biblioteca. Por outro lado, uma vez concluı́da uma sessão de edição com a INGRID, irá obter-se uma IHM que, em princı́pio, se pretenderá salvaguardar, quer para executá-la como um aplicação isolada, quer para recuperá-la na sessão seguinte, 1.3. OBJECTIVOS 7 de modo a continuar a edição. Então, em qualquer dos casos, é indispensável a esta ferramenta o acesso a um serviço de salvaguarda e recuperação dos objectos criados ou modificados, durante cada sessão. Mais uma vez, esse mecanismo não deve ser explicitamente definido na INGRID, pelas razões enunciadas, relativas à flexibilidade e extensibilidade da mesma. 1.3 Objectivos A escolha de uma linguagem como o C++ para a concretização da biblioteca 4D e, naturalmente, da própria INGRID, decorreu dos seguintes factores: • ter sido usada no Images, onde existia já algum esforço investido e com o qual se tinham obtido resultados satisfatórios tendo em conta os objectivos propostos; • permitir facilmente a ligação a bibliotecas e sistemas em geral e, em particular, às bibliotecas de objectos de interacção já existentes e normalizadas (Athena, Motif, OpenLook, ...); • adoptar uma aproximação de programação orientada para objectos; • ser uma linguagem compilada e um standard de facto, o que, em princı́pio, a vocaciona melhor para o desenvolvimento de produtos comerciais. • oferecer mecanismos de verificação de tipos em tempo de compilação, que permitem maior segurança no desenvolvimento da ferramenta e biblioteca que se pretende disponibilizar. Em contrapartida, esta linguagem, precisamente por ser compilada, dificilmente oferece qualquer suporte, quer aos requisitos da INGRID, quer à flexibilidade que se prende com a definição da comunicação entre objectos da biblioteca 4D. Nesse sentido, o trabalho apresentado nesta tese, tem como objectivo a introdução no C++ de mecanismos que venham precisamente preencher esses requisitos. Nomeadamente, pretende-se: 8 CAPı́TULO 1. INTRODUÇÃO • criar um serviço que permita definir completamente a invocação de operações sobre objectos em tempo de execução, como forma de assegurar a concretização do conceito de “ligação” no 4D; • oferecer igualmente esse mecanismo, conjuntamente com mecanismos abstractos de criação de objectos e identificação dos mesmos, de forma a suportar as caracterı́sticas de interactividade e programação experimental que se tomaram como premissas na construção da INGRID; • introduzir um modelo uniformizador para as entidades envolvidas na linguagem C++, como forma de garantir a extensibilidade da ferramenta e a adaptabilidade da biblioteca a outras já existentes; • finalmente, disponibilizar um mecanismo genérico de salvaguarda e recuperação de objectos, que permita a sua utilização na biblioteca 4D, e consequentemente na INGRID, de uma forma abstracta, reforçando ainda mais a extensibilidade das mesmas. Embora aplicados a dois sistemas em particular, o 4D e a INGRID, os objectivos estabelecidos podem facilmente generalizar-se, criando um sistema de suporte, que ofereça as condições indispensáveis para a utilização da linguagem de programação C++ no desenvolvimento de ferramentas de programação interactiva, orientada para objectos, ou simplesmente na versatilização e flexibilização do uso dessa linguagem. Assim, podem resumir-se os três objectivos iniciais como a introdução de um mecanismo de suporte à interpretação no C++, e o último como um de suporte à salvaguarda e recuperação de objectos ao nı́vel da linguagem. Deve desde já esclarecer-se, que suporte à interpretação não pressupõe, de modo algum, a concretização de um interpretador da linguagem, embora lhe possa servir de base. De facto, o que se pretende oferecer é um mecanismo que possibilite um acesso interpretado ao código compilado nessa linguagem. Na viabilização destes objectivos foram tomadas as seguintes opções: • Não recorrer à alteração do compilador da linguagem, quer introduzindo extensões à sua sintaxe, quer modificando o código por ele gerado, como forma de garantir a fácil ligação a sistemas já desenvolvidos e a suportar sem grande esforço novas versões do compilador de C++, em constante evolução. 1.4. ESTRUTURA DA TESE 9 • Não sobrecarregar o programador exigindo esforço adicional de programação na disponibilização dos mecanismos propostos, optando, sempre que possı́vel, por oferecer ferramentas de geração automática desse código. 1.4 Estrutura da Tese No capı́tulo 2 começa por introduzir-se um conjunto de conceitos normalmente envolvidos numa aproximação de programação OO. Em face dessas definições, enquadra-se o modelo de tipos do C++, abordando-se algumas questões da sua concretização, que irão influenciar o trabalho aqui apresentado. De seguida, referem-se algumas linguagens e bibliotecas cujas caracterı́sticas se relacionam, de algum modo, com as capacidades de interpretação, e salvaguarda e recuperação de objectos, que se pretendem disponibilizar sobre o C++. A definição dos modelos introduzidos, como forma de oferecer mecanismos de suporte à interpretação e de maneira a uniformizar a visão em tempo de execução de uma linguagem como o C++, é apresentada no capı́tulo 3. No capı́tulo 4 descrevemse as opções tomadas na concepção do serviço de salvaguarda e recuperação de objectos e a sua articulação com o modelo de objectos definido no capı́tulo anterior. No Capı́tulo 5 é feita uma descrição do sistema realizado, tendo em conta o modelo e as opções introduzidas anteriormente, abordando-se os problemas surgidos na sua concretização. Finalmente, no Capı́tulo 6 tiram-se algumas conclusões sobre a concretização e utilização do trabalho realizado e delineiam-se as perspectivas futuras da sua evolução. 10 CAPı́TULO 1. INTRODUÇÃO Capı́tulo 2 Panorâmica Quando se abordam linguagens de programação ou ambientes orientados para objectos (OO), a sua descrição passa pela utilização de um conjunto de termos associados a conceitos, directamente envolvidos nesta aproximação. Na secção seguinte ir-se-ão introduzir as definições de alguns desses termos, apresentando-se uma classificação possı́vel no universo de linguagens e sistemas OO. Com base nessa sistematização classifica-se o modelo de tipos do C++, abordando-se também algumas questões da sua concretização, nomeadamente no que diz respeito à utilização das estruturas, que suportam esse modelo em tempo de execução. A descrição do modelo aqui apresentada foca, sobretudo, os aspectos que estão directamente envolvidos na concretização dos serviços que se pretende facultar, não pretendendo, de modo algum, ser exaustiva. Com o mesmo intuito de oferecer simplesmente uma panorâmica do contexto em que se enquadra o trabalho que se descreve nesta tese, refere-se também um conjunto de linguagens e bibliotecas com ele relacionadas, e em especial aquelas em que se inspiraram algumas das opções tomadas na sua concretização. Sobre essas linguagens discutem-se sobretudo os aspectos ligados às estruturas de suporte existentes em tempo de execução e a forma como são usadas na disponibilização dos mecanismos de interacção com os objectos, salvaguarda e recuperação dos mesmos. 11 12 CAPı́TULO 2. PANORÂMICA 2.1 Conceitos Um dos conceitos fundamentais numa aproximação de programação OO é necessariamente o de objecto. Neste texto ir-se-á adoptar uma definição genérica semelhante à introduzida por Wegner [Wegner 87] e Saunders [Saunders 89]: ∆2.1 Objecto é uma entidade com um estado associado e descrito por um conjunto de dados e sobre a qual é possı́vel executar um conjunto de operações. Porém, uma linguagem de programação não fica automaticamente classificada como OO, simplesmente porque suporta a existência de objectos - Wegner [Wegner 87, Wegner 89] identifica estas como baseadas em objectos. De facto, a designação orientada para objectos só é normalmente aceite, se a linguagem suportar1 objectos, um mecanismo de organização dos mesmos e um de partilha de código. Tomlinson [Tomlinson 89] recorre a estes dois conceitos para sistematizar os sistemas OO em quatro categorias, resultantes das combinações possı́veis entre as concretizações que cada um dos mecanismos pode normalmente assumir: • organização: classes ou protótipos. • partilha: herança ou delegação; Existirão então linguagens baseadas em classes com herança ou delegação e baseadas em protótipos também com cada uma das possibilidades de partilha. Alguns autores [Micallef 88, Stroustrup 88], vão um pouco mais longe, impondo uma visão mais restritiva em relação ao que deverá ser uma linguagem OO. Surgem então termos relativos à protecção de acesso aos objectos - encapsulamento - e à flexibilização dos mecanismos de programação - abstracção e polimorfismo. 2.1.1 Classes e Protótipos ∆2.2 Classe é uma entidade que agrupa objectos com caracterı́sticas semelhantes e um formulário (template) a partir do qual é possı́vel criar esses 1 Uma discussão entre o que deverá ser suportar em contraposicão com permitir pode ser encontrada em [Stroustrup 88]. 2.1. CONCEITOS 13 objectos. Neste sentido, os objectos criados a partir de uma classe, as suas instâncias, terão sempre uma estrutura de dados semelhante. A alteração da estrutura de uma instância, numa linguagem baseada em classes, passa pela alteração da estrutura de todas as instâncias dessa classe e, por conseguinte, pela alteração da classe. Aos componentes da estrutura de uma instância, que descreverão o seu estado, é costume designar variáveis de instância ou, segundo a nomenclatura adoptada no C++, variáveis membro. Os protótipos diferem do par classe/instância, sobretudo porque fundem os dois conceitos numa única entidade, o protótipo [Ungar 87, Chambers 89, Agha 86], permitindo assim maior flexibilidade na definição e alteração da estrutura dos objectos, em detrimento duma maior sistematização na programação. No seguimento deste texto apenas se irão considerar linguagens baseadas em classes, já que nesta classificação fica inserido o C++, sobre o qual incidirá o trabalho realizado. A discussão sobre as vantagens e desvantagens de cada uma das aproximações, assim como uma explicação mais pormenorizada das mesmas, pode ser consultada em [Tomlinson 89, Wegner 87]. 2.1.1.1 Mensagens, Métodos e Funções Membro Tendo em conta que uma classe define o comportamento comum das suas instâncias, é normal que defina também as operações que sobre elas se podem executar. Ao conjunto dessas operações que é possı́vel executar sobre as instâncias de uma classe, dá-se o nome de interface ou protocolo da classe. A cada concretização das operações chama-se, normalmente, um método: ∆2.3 Método é a entidade que define como se executa uma operação. Em C++ os métodos designam-se por funções membro, constituindo em conjunção com as variáveis membro, os membros da classe. Para executar um método é necessário que ocorra uma invocação. Nesta, deve especificar-se o objecto sobre o qual vai ser invocado o método, designado o receptor 14 CAPı́TULO 2. PANORÂMICA da invocação, o nome do método - selector - e os seus argumentos. Em linguagens em que o método associado à sintaxe da invocação é determinado apenas em tempo de execução, o conjunto composto pelo selector e os argumentos é designado por mensagem. Nestes casos, diz-se que uma invocação corresponde ao envio de uma mensagem ao objecto e, porque a associação é feita só na execução, diz-se que há ligação dinâmica2 (dynamic binding). Quando a determinação do método é feita durante a compilação, então diz-se que há ligação estática. 2.1.1.2 Tipos e Tipificação Em alguns trabalhos, nomeadamente em [Wegner 87], é feita uma distinção entre tipo e classe, sendo classe uma forma particular de tipo aplicada a objectos. Este será também o significado aqui utilizado e em especial adaptado a linguagens hı́bridas, como o C++, que enquadram uma aproximação OO, baseada em classes, com uma metodologia de programação clássica. Outras definições de tipo são possı́veis, ligadas ao enquadramento da independência entre interfaces e concretizações, mas a sua discussão sai fora do âmbito desta tese. No seguimento da utilização do termo “tipo”, e adoptando nomenclatura do mesmo autor, dir-se-á que uma linguagem é fortemente tipificada, se a compatibilidade entre todas as expressões que representam valores, puder ser determinada estaticamente. 2.1.2 Herança e Delegação ∆2.4 Herança é o mecanismo que permite executar sobre um objecto, uma operação definida para outro. O significado usual dado a este termo, associa-o às linguagens baseadas em classes e permite, que sobre um objecto, pertencente a uma determinada classe, seja executada uma operação (herdada) definida sobre outra. Diz-se então que a primeira é derivada ou subclasse da segunda, ou que esta é base ou superclasse da 2 O termo ligação dinâmica é adoptado neste trabalho também para referir o mecanismo de edição de ligações durante o carregamento de código num processo já em execução. No entanto, nesses casos, ou é referido como ligação dinâmica de código ou o seu significado pode facilmente ser extraı́do do contexto em que ocorre. 2.1. CONCEITOS 15 primeira. Nos mecanismos de partilha de código por derivação, a subclasse não só herda as operações, como também a estrutura das instâncias. De facto, a estrutura de uma instância é normalmente constituı́da pelas variáveis de instância da classe base, mais as que a subclasse define. Sobre as instâncias da classe derivada, é normalmente possı́vel invocar todas as operações definidas na classe base. Relativamente à herança, como mecanismo de partilha de código em linguagens baseadas em classes, pode ainda distinguir-se herança simples e herança múltipla [Horn 88, Stroustrup 87]. Em herança simples uma classe herda operações e variáveis de apenas uma outra, constituindo assim uma relação hierárquica sob a forma de árvore. Considerando herança múltipla as operações e variáveis podem ser herdadas de várias classes. ∆2.5 Delegação é o mecanismo de partilha de código que permite a um objecto indicar outro, que execute uma dada operação em vez dele. Note-se que neste mecanismo, não se faz qualquer restrição ao objecto sobre o qual é delegada a operação, sendo, por isso, muito flexı́vel e normalmente adoptada em linguagens baseadas em protótipos (e.g. linguagens de Actores). Sobre os dois mecanismos de partilha de código podem encontrar-se diferentes comparações e mesmo definições que sublinham as vantagens e desvantagens de uma e de outra [Stein 87, Agha 86, Wegner 87, Ungar 87, Tomlinson 89]. No C++, embora seja simplesmente usado o mecanismo de herança, múltipla na última versão da linguagem, foi também proposta a introdução de delegação [Stroustrup 87]. No entanto, o mecanismo de delegação a introduzir constitui um subconjunto relativamente restrito e estático do que pode ser este processo de partilha. Nesta proposta, de que não se conhece nenhuma concretização, a delegação pode ser feita sobre uma variável global especı́fica, ou sobre uma variável membro da classe em que se define a delegação. 2.1.3 Encapsulamento Segundo Micallef [Micallef 88] encapsulamento pode ser assim definido: 16 CAPı́TULO 2. PANORÂMICA ∆2.6 Encapsulamento é uma técnica para minimização das interdependências entre módulos escritos separadamente, através da definição de interfaces restritas ao exterior. Para linguagens baseadas em classes, quando existe encapsulamento, um objecto só pode ser acedido através da interface que a sua classe exporta, escondendo os detalhes da concretização presentes no código que constitui os métodos. Assim, é possı́vel a alteração desse código, sem que por isso deva existir alteração (e.g. compilação) dos clientes que invocam os métodos da classe. Neste sentido, Micallef analisa, sobre um conjunto de linguagens baseadas em classes com herança3 , a capacidade que têm de proteger o acesso a variáveis de instância e às operações definidas numa classe, quer do ponto de vista da sua utilização em subclasses, quer do manuseamento genérico das suas instâncias (por clientes). Quando uma linguagem suporta este tipo de mecanismos, e em especial apenas permite o acesso aos dados do objecto através das suas operações, diz-se que a linguagem suporta abstracção de dados [Wegner 87, Stroustrup 88]. 2.1.4 Polimorfismo e Classes abstractas A noção de polimorfismo surge, em primeira aproximação, da necessidade de flexibilizar a programação, em situações em que, objectos pertencentes a classes diferentes, se pretendem usar indiferentemente, respondendo com comportamentos eventualmente distintos a operações identificadas da mesma maneira. Na explicação deste conceito recorrer-se-á a um exemplo, em que se pretende desenhar uma figura composta de formas gráficas diversas (cı́rculos, quadrados, ...). Na realização esperada para uma aproximação OO, cada forma concretizar-se-ia numa classe em particular (Cı́rculo, Quadrado, ...) que desenhasse as suas instâncias, por exemplo, em resposta à invocação desenha. Assim, se os objectos se puderem usar genericamente no mesmo código, o desenho da figura corresponderá a um ciclo que envia a mensagem desenha para cada um dos seus componentes. Em Objective-C seria: 3 Teria sido interessante também incluir nesta comparação uma linguagem como o Eiffel [Meyer 88] que define um mecanismo de exportação selectiva (para classes em particular) diferente das enumeradas no artigo. 2.1. CONCEITOS 17 for (i = 0; i < numero_de_componentes; i++) [componentes[i] desenha]; A mensagem desenha é enviada a cada um dos objectos referenciados em cada elemento de componentes, sem conhecimento da classe a que pertencem. Cada objecto responde, executando o método correspondente que a sua classe define para essa mensagem. Uma discussão pormenorizada sobre polimorfismo pode ser encontrada em [Cardelli 85]. Segundo este: ∆2.7 Uma operação diz-se polimórfica se os seus argumentos podem assumir mais do que um tipo. e um tipo ou classe é polimórfico, se as operações que define, o forem. 2.1.4.1 Formas de polimorfismo Neste sentido pode dizer-se que, se numa linguagem baseada em classes for suportada herança, então a linguagem é polimórfica, já que as operações definidas nas classes base podem ser aplicadas a instâncias de classes derivadas. Cardelli designa esta forma de polimorfismo por polimorfismo de inclusão. Note-se, porém, que o mecanismo de herança, só por si, não soluciona o problema introduzido no exemplo anterior. Este problema é, no entanto, implicitamente resolvido em linguagens, em que o tipo de um objecto é apenas conhecido em tempo de execução e cujo mecanismo de invocação suporta ligação dinâmica. De facto, como no código da invocação não é estaticamente conhecida a classe do objecto, o envio de qualquer mensagem a qualquer objecto é sempre estaticamente válida. Porque há ligação dinâmica, o mecanismo de discriminação seleccionará em tempo de execução o método correcto, correspondente à classe de que o objecto é instância, desde que ele tenha sido definido. Então, para que o exemplo atrás funcionasse correctamente, bastaria que cada classe definisse um método com o mesmo nome (desenha) e que realizasse o desenho correspondente à forma que essa classe representa. Também em linguagens OO fortemente tipificadas é normalmente oferecido um 18 CAPı́TULO 2. PANORÂMICA mecanismo que assegura esta forma de polimorfismo. Segundo esse mecanismo, se sobre uma classe derivada for definido um método com o mesmo nome de outro da classe base, então, sempre que um objecto da classe derivada seja usado em substituição de um da classe base e esse método for invocado, é invocado o método definido na classe derivada. No exemplo anterior, bastaria criar uma classe (e.g. FormaGrafica) que definisse o método desenha, sem qualquer concretização especı́fica. Cada uma das classes gráficas pretendidas derivaria dessa, concretizando o seu método especı́fico desenha. Na nomenclatura usada em sistemas OO, diz-se que uma classe base com estas caracterı́sticas é uma classe abstracta, ou, mais exactamente, uma classe é abstracta se não concretizar completamente a interface que define [Wegner 87]. Aos métodos não concretizados é costume designar-se métodos abstractos. Diz- -se ainda, que os métodos com o mesmo nome, que concretizam as operações nas classes derivadas, são redefinições ou refinamentos dos métodos correspondentes da classe base. Note-se, no entanto, que a redefinição de métodos é possı́vel com ou sem classes/métodos abstractos, podendo ser usada como forma de aumentar a funcionalidade do método herdado da classe base, ou combinar a funcionalidade dos diferentes métodos herdados, para linguagens que ofereçam herança múltipla [Horn 88]. Como as redefinições são identificadas com o mesmo nome do método de origem, é também vulgar designá-las por sobreposições deste. Nesse sentido, esta forma de polimorfismo é classificada por Cardelli como polimorfismo de sobreposição. Micallef [Micallef 88] evita esta classificação e inclui ambos os tipos de polimorfismo descritos sobre a designação de polimorfismo simples, já que só se aplica sobre um dos argumentos da operação, o receptor da invocação, i.e., o receptor de uma mesma operação (identificada pelo mesmo selector) pode assumir diversos tipos e assim, diversos comportamentos. Quando o polimorfismo é aplicado também aos argumentos do método, então diz-se que há polimorfismo múltiplo. O polimorfismo de sobreposição definido por Cardelli engloba também formas de polimorfismo múltiplo. Na forma múltipla, deve ser possı́vel, não só definir diferentes comportamentos para a mesma operação em receptores de tipos diversos, mas também, diferentes comportamentos para o mesmo receptor e a mesma operação para diferentes tipos de argumentos. Ao suportar este tipo de polimorfismo, é da 2.1. CONCEITOS 19 responsabilidade da linguagem executar o método correcto, dependendo do tipo a que pertencem os argumentos com que se invoca. Em [Cardelli 85] é identificada outra forma de polimorfismo: o polimorfismo de coerção. Nesta forma, não se especificam sobreposições de métodos para cada tipo possı́vel de argumentos, mas, ao contrário, os argumentos são implicitamente convertidos para os declarados num determinado método. Finalmente, este autor define ainda um quarto tipo de polimorfismo, de parametrização, que se prende com a existência de tipos e funções genéricas parametrizáveis, em que um dos parâmetros é o tipo sobre o qual se vai concretizar [Meyer 86, Meyer 88]. Note-se que, qualquer das últimas duas formas de polimorfismo, é aplicável, quer ao receptor, quer aos restantes argumentos da invocação, existindo por isso sob as formas simples e múltipla. 2.1.4.2 Tipos Conformes A noção de polimorfismo é ainda interessante se, em vez de observada do ponto de vista da operação polimórfica, for analisada como a capacidade que um objecto de um determinado tipo tem de ser usado em vez de outro, em qualquer expressão da linguagem. De facto, se todas as operações definidas por um tipo A forem polimórficas, podendo ser executadas sobre uma instância de um tipo B, então deve ser possı́vel usar uma instância de B onde quer que se use uma de A. Esta caracterı́stica está normalmente implı́cita quando se usam mecanismos de partilha, em que, por exemplo, sobre um objecto podem ser invocadas todas as operações que se definem na classe base, ou no tipo em que delega. No entanto, não se limitam a eles. De facto, se a linguagem oferecer, por exemplo, polimorfismo de coerção, ortogonal aos mecanismos de partilha, qualquer tipo pode ser usado em vez de outro no qual possa ser convertido (e.g. C++). Uma distinção deve ser feita entre os dois mecanismos acima enumerados. Enquanto que nos mecanismos de partilha, o objecto sobre o qual foi invocado o método, não é alterado para que a invocação ocorra, já na aplicação do polimorfismo de coerção, a utilização só é em geral possı́vel, quer por alteração dos dados do objecto, quer por criação de um novo, do tipo para que ocorre a conversão. Esta distinção 20 CAPı́TULO 2. PANORÂMICA pode ser feita através do conceito de tipo conforme [Meyer 88]. No seguimento deste texto usar-se-á este termo segundo a definição: ∆2.8 Um tipo A diz-se conforme com um tipo B, se uma instância de A puder ser usada onde quer que uma instância de B o seja, sem que para isso ocorra qualquer transformação dos dados da instância de A, ou criação a partir dela, de uma instância de B. 2.2 C++ Nesta secção ir-se-á abordar o modelo de tipos do C++ e os conceitos nele envolvidos, bem como algumas das opções tomadas na sua concretização. Esta descrição basearse-á, em grande parte, nas caracterı́sticas da linguagem, disponı́veis na última versão do compilador (2.0 - [Ellis 90]), não incluindo, no entanto, herança múltipla, já que não foi considerada no trabalho que aqui se apresenta. 2.2.1 Caracterı́sticas gerais O C++ é uma linguagem hı́brida, já que suporta os mecanismos essenciais para a programação OO, como classes, herança, encapsulamento e abstracção, oferecendo também os mecanismos de programação de uma linguagem procedimental como o C [Kernigham 78]. Esta sua caracterı́stica, embora desvantajosa do ponto de vista da homogeneidade do modelo de objectos, concede-lhe grande capacidade de compatibilização com um grande número de bibliotecas e sistemas disponı́veis hoje em dia e mesmo de ligação com outras linguagens. É uma linguagem fortemente tipificada que, por isso mesmo, consegue abarcar um grande número de construções polimórficas, mesmo ao nı́vel do polimorfismo múltiplo. Por outro lado, vem nesse sentido colmatar uma das mais sérias argumentações feitas ao C, a falta de verificação de tipos, cuja resolução se inclui também já nesta linguagem, com o aparecimento da norma ANSI para o C [Kernigham 88]. Como desvantagem apresenta, naturalmente, a falta de flexibilidade que outras linguagens OO não fortemente tipificadas oferecem, mas que, no entanto, o trabalho descrito nesta tese se propõe resolver. 2.2. C++ 2.2.2 21 O modelo de tipos Na classificação de tipos aqui apresentada, segue-se de perto a sistematização feita em [Ellis 90]. Nesse sentido, classificam-se os tipos nas seguintes categorias: • primitivos4 são aqueles cujas operações são definidas pelo próprio compilador e incluem: tipos fundamentais (char, short, ..., float, ... unsigned char, ... enum); apontadores; referências; vectores e funções. • definı́veis pelo utilizador são aqueles cujas operações podem ser definidas pelo utilizador e incluem os tipos identificados pelas palavras chave class, struct e union. Os tipos definidos pela primitiva typedef não constituem uma nova classificação, mas correspondem, em vez disso, à introdução de nomes alternativos para um tipo. 2.2.2.1 Classes e Herança O suporte a classes nesta linguagem é dado pelos tipos definı́veis pelo utilizador. De facto, sobre qualquer deles, define-se a estrutura das suas instâncias bem como as operações (funções membro) que sobre elas se podem invocar. Relativamente aos mecanismos de herança, estes só se podem aplicar aos tipos definidos por class e struct, não podendo uma union ser usada como base, ou derivar de qualquer outra classe5 . Finalmente, numa union a estrutura das instâncias é definida como uma alternativa entre as variáveis membro declaradas. O C++ distingue, para além das funções membro normais aplicáveis às instâncias de uma classe, segundo uma sintaxe igual à do acesso a variáveis membro (objecto.funç~ ao ();), as seguintes: • funções membro estáticas são funções globais que se executam “sobre a classe” (CLASS::funç~ ao ();), mas que podem ser herdadas por classes derivadas, e regem-se pelas mesmas regras de encapsulamento das restantes funções membro. 4 5 built-in no original. As razões para a não aplicação de herança a unions podem ser encontradas em [Ellis 90]. 22 CAPı́TULO 2. PANORÂMICA • construtores são funções membro executadas sempre que é criada uma instância da classe, tendo como função a inicialização dos dados e recursos associados ao objecto, nessa classe. Pode existir mais que um por classe, encarregando-se o compilador de invocar, em cada um deles, os construtores das classes base e das variáveis membro, eventualmente indicados pelo programador, antes de executar o seu código propriamente dito. O seu nome coincide com o da classe em que se definem e a sua invocação insere-se e é indissociável da criação de objectos (e.g. new CLASSE (argumentos);). • destrutores são invocados sempre que uma instância é apagada explicitamente (recorrendo ao operador delete) ou implicitamente libertada (quando termina o seu contexto). Em qualquer dos casos a invocação é sempre implı́cita, i.e., o nome do destrutor nunca é referido. O seu nome é também o mesmo da classe em que se insere, mas precedido por ∼, e existe apenas um por classe. O programador incluirá nesta função membro, as acções de “limpeza” dos recursos associados ao objecto, assegurando o compilador a execução dos destrutores definidos para as variáveis membro e para as classes base, na ordem correcta. • operadores a diferença em relação a outras funções membro é meramente sintáctica, permitindo a redefinição dos sı́mbolos usados em operações primitivas (built-in: +, -, ...) sobre instâncias das classes. A sintaxe da sua utilização é semelhante à de qualquer operador primitivo (e.g. objecto + objecto), o que uniformiza elegantemente a integração com o C. • conversores são operadores que permitem definir a conversão de referências de instâncias da classe, para referências de instâncias de outro tipo (e.g. CLASS::operator char()). O mecanismo inverso pode ser definido por construtores que aceitam, como único argumento, o tipo a partir do qual poderão ser construı́das instâncias da classe (e.g. CLASS (char&);). Na sua funcionalidade enquadram-se com os mecanismos de cast do C. • funções de cópia 2.2. C++ 23 permitem definir alternativas aos mecanismos de cópia de instâncias, definidos pelo compilador (bcopy). Incluem o operador = e o construtor que têm como argumento uma referência para um objecto da própria classe (e.g. CLASS (CLASS&);). O operador é usado pelo compilador, sempre que é feita uma atribuição de um objecto a outro já existente, mesmo que o objecto seja uma variável de instância. O construtor é aplicado na passagem de argumento (por valor), ou, em geral, quando um objecto é criado e inicializado a partir de outro da mesma classe. • operadores de gestão de memória são operadores identificados por new e delete, que permitem definir algoritmos de gestão do espaço usado pelas instâncias das classes. O compilador oferece dois operadores que serão usados por defeito, e que reservam e libertam espaço para as instâncias em memória dinâmica (heap). A utilização destes operadores está directamente ligada à de construtores e destrutores, já que, quando é chamado o operador new é sempre executado um construtor, e quando se invoca o operador delete é executado o destrutor. As variáveis membro podem também ser estáticas ou de instância, sendo as primeiras uma forma de variáveis globais, às quais se aplicam os mecanismos de encapsulamento que a linguagem define. Para as variáveis membro estáticas e os objectos globais, o compilador assegura a sua inicialização, antes da execução de qualquer outro código (main). 2.2.2.2 Encapsulamento O acesso aos membros, variáveis ou funções, de uma classe, para os tipos definidos por class e struct, pode ser especificado segundo três modos: privado, protegido e público. Os membros privados, que se seguem à palavra chave private:, só podem ser acedidos em funções membro da própria classe. Os públicos (public:) estão acessı́veis a qualquer cliente, definindo por conseguinte a interface dos objectos da classe ao exterior. Finalmente, os membros protegidos (protected:) são acessı́veis apenas nas funções membro de classes derivadas. Os mecanismos de encapsulamento são ainda aplicáveis aos membros herdados 24 CAPı́TULO 2. PANORÂMICA por uma classe: uma classe D derivada publicamente de B (class D : public B), tem públicos todos os membros públicos de B, aplicando-se a mesma regra para os protegidos; se não for derivada publicamente (class D : private B), todos os membros de B são privados de D. No entanto, é possı́vel no caso da derivação privada, enumerar os membros públicos de B, que serão públicos em D, o mesmo acontecendo para os membros protegidos. A única diferença entre classes definidas por class e struct é que, nas primeiras os membros são, por defeito, privados, enquanto nas segundas eles serão públicos se nada for dito em contrário. Às union não se podem aplicar os mecanismos de encapsulamento, sendo os membros sempre públicos. A linguagem oferece ainda o mecanismo definido pela palavra chave friend, que permite indicar classes, funções C, ou funções membro de outras classes, que especificamente poderão ter acesso a quaisquer membros da classe (mesmo os privados). 2.2.2.3 Polimorfismo O polimorfismo de inclusão é também aqui implı́cito no mecanismo de herança e articula-se com os modos de encapsulamento, que se aplicam aos membros de uma classe. De facto, sobre um objecto de uma classe podem ser invocadas todas as funções membro públicas da sua classe base, desde que publicamente derivada. Nesse sentido, apenas neste caso, se pode dizer que uma classe é conforme com as suas classes base. Relativamente ao polimorfismo simples de sobreposição, este é suportado pelo que nesta linguagem se denominou métodos virtuais. De facto, se um método (g()) é declarado virtual numa classe base (B) e redefinido numa classe derivada (D), então, se um objecto da segunda (D) for usado numa expressão que invoque o método (ob->g()), sobre um apontador (ou referência) declarado para a classe base (B* ob), o método invocado é na realidade o definido para a classe derivada. Note-se, que esta forma de polimorfismo só funciona para funções membro virtuais e essa “virtualidade” deve ser definida na classe que primeiro define o método. Deve também dizer-se que, como seria de esperar, a classificação de virtual não pode ser utilizada em funções membro estáticas e construtores, já que estes são aplicados explicitamente sobre uma classe. 2.2. C++ 25 Para o polimorfismo de sobreposição múltiplo, o C++ introduz a noção de funções sobrepostas, i.e., funções com o mesmo nome associado, mas com um número e/ou tipo de argumentos diferente. Podem ser funções membro ou funções C quaisquer, sendo da responsabilidade do compilador determinar, a partir do contexto em que é feita a invocação, qual a função a executar. Paralelamente a este mecanismo, a linguagem oferece também a capacidade de especificar argumentos por defeito quando se declara de uma função, que serão usados na invocação, se os argumentos correspondentes forem omitidos. Esta caracterı́stica pode ser vista como uma forma de polimorfismo múltiplo de sobreposição, se se considerar que o seu efeito é o mesmo que declarar funções sobrepostas, para cada uma das combinações possı́veis resultantes da omissão de argumentos. Finalmente, esta linguagem oferece também suporte ao polimorfismo de coerção, quer usando as conversões primitivas existentes no C (e.g. int para float), quer as conversões definidas pelo utilizador, decorrentes da definição de construtores ou operadores de conversão adequados. Na próxima versão da linguagem é esperada também a existência de polimorfismo paramétrico, já anunciada em [Ellis 90] como experimental e concretizada pelo que nessa referência se denomina “templates”. 2.2.3 Aspectos de concretização Na descrição que se segue, considerar-se-á o compilador (pré-compilador) oficial da linguagem (fornecido pela AT&T), que na realidade converte código C++ em código C. No entanto, tudo o que é dito pode também adaptar-se às concretizações que geram directamente assembly (e.g. os compiladores da GNU), tendo em conta que o código por eles gerado é equivalente, salvo algumas optimizações, ao que se obtém compilando o código C do compilador original. Abordar-se-ão em especial os mecanismos de discriminação de métodos e, sobretudo, os que em tempo de execução suportam o modelo de tipos adoptado. 2.2.3.1 Discriminação de métodos A discriminação de métodos no C++ é inteiramente feita em tempo de compilação e tem em consideração, quer os mecanismos de herança, quer os de funções sobrepostas e coerção do tipo dos argumentos. Nesse sentido, a procura de um método baseia- 26 CAPı́TULO 2. PANORÂMICA se primeiramente no nome da classe com que a variável é declarada, de forma a encontrar a classe correspondente. De seguida, procura entre os métodos da classe e das classes base, todos aqueles com o nome especificado na invocação e que admitem6 um número de argumentos igual ao especificado. Finalmente, compara o tipo de cada argumento declarado com o do respectivo argumento especificado, seleccionando, dos métodos encontrados, o que mais semelhanças apresente em relação à sintaxe da invocação. Então, o “melhor” método será sempre aquele cujos tipos dos argumentos são iguais aos da invocação. Se não houver nenhum, o compilador tenta os métodos em que os argumentos especificados sejam de tipos conformes com os declarados. Em último caso recorre a conversões, quer usando as implı́citas na linguagem (entre tipos primitivos), quer as definidas pelo utilizador para cada classe. O resultado final de uma invocação de um método em C++, será a invocação da função que no código C gerado lhe corresponde, e cujo primeiro argumento é um apontador para o objecto invocado. Como no C não existe sobreposição de nomes, para cada função membro declarada, o C++ gera uma função C, cujo nome resulta basicamente da concatenação do nome do método, com o nome da classe e dos tipos dos argumentos. Note-se, no entanto, que tendo em conta, os mecanismos de herança, a utilização de objectos de tipos conformes nos argumentos e possı́veis conversões de tipo, nem sempre é possı́vel, a partir da sintaxe da invocação, determinar imediatamente o nome da função que concretiza o método, devendo para isso recorrer-se à sua prévia discriminação. Finalmente deve dizer-se que todos estes princı́pios são aplicáveis, quer a funções membro de qualquer espécie, quer a funções globais. 2.2.3.2 Tabela de métodos virtuais A concretização do mecanismo de abstracção de classes no C++, passa pela introdução de uma tabela de métodos virtuais que, em herança simples, contém, por ordem, os endereços de todas as funções C que representam as funções virtuais de uma classe7 . Por sua vez, as instâncias dessa classe incluirão um apontador 6 Para métodos declarados com argumentos por defeito o número de argumentos especificados não precisa de coincidir exactamente com os argumentos esperados. 7 Em herança múltipla estas tabelas incluem também um deslocamento. A sua descrição pormenorizada pode ser encontrada em [Ellis 90]. 2.2. C++ 27 para essa tabela, tal como se pode ver na figura 2.1 (instâncias da classe base B). Então, se uma classe (D) for derivada dessa (B), também as suas instâncias terão instância da classe base (B) instância da classe derivada (D) tabela de métodos da classe base f () tabela de métodos da classe derivada parte B de D g () g’ () instância da classe base (B) h () i () class B { virtual f (); virtual g (); virtual h (); }; class D : public B { g (); // g’(); virtual i (); }; Figura 2.1: Tabelas de métodos virtuais uma tabela de métodos própria, acessı́vel na mesma posição que nas instâncias da base. Se a nova classe não redefinir os métodos virtuais da base, as tabelas conterão os mesmos endereços (casos de f () e h () da figura). Se definir novos métodos virtuais, a tabela será aumentada e o endereço das funções C correspondentes será acrescentado a seguir aos da classe base (caso de i ()). Finalmente, se redefinir algum método, o endereço do novo irá substituir o do método herdado na tabela da classe derivada (caso de g’() em relação a g ()). Quando ocorre uma invocação de um destes métodos, o compilador executa o procedimento de procura usual, com base na classe que a sintaxe dessa invocação define estaticamente (e.g. B). Uma vez encontrado o método e detectado que é virtual (pela declaração), obtém o seu ı́ndice na tabela de métodos - igual mesmo para classes derivadas que o redefinam - e gera o código C que executa uma chamada por endereço, para o conteúdo dessa posição, na tabela associada ao objecto invocado: (objecto->tabela [indice]) (argumentos); Deste modo, se o objecto invocado pertencer a uma classe derivada (e.g. D), a variável membro que acede à tabela, indicará a tabela de métodos virtuais da classe derivada e o endereço na posição indicada pelo ı́ndice será o do método redefinido, se tiver havido redefinição. 28 CAPı́TULO 2. PANORÂMICA Pelo que foi dito, pode concluir-se que efectivamente a decisão do método a invocar, no caso dos métodos virtuais, é feita somente em tempo de execução. A este tipo de ligação chamar-se-á ligação tardia, em contraposição com ligação dinâmica, já que na realidade a procura do método a executar é feita em tempo de compilação. 2.3 Suporte aos modelos em tempo de execução Tal como se viu na secção anterior, no C++, todos os mecanismos relacionados com a invocação de funções membro são resolvidos em tempo de compilação, à excepção da decisão entre métodos virtuais sobrepostos, que, mesmo assim, são discriminados na mesma fase. Ao pretender-se estender a flexibilidade de programação nesta linguagem, oferecendo mecanismos interpretados de invocação de funções membro, com o objectivo de suportar, em tempo de execução, a programação interactiva de objectos, ir-se-ão de seguida abordar algumas linguagens OO e bibliotecas igualmente baseadas em classes e herança, e cujas caracterı́sticas serviram de inspiração ao trabalho apresentado nesta dissertação. 2.3.1 O Smalltalk Uma linguagem como o Smalltalk [Goldberg 83b], sendo uma das primeiras linguagens OO, é, ainda hoje, fundamental em qualquer discussão sobre esta aproximação. A simplicidade e homogeneidade do seu modelo tem servido de base a grande número de extensões a linguagens convencionais, e mesmo a linguagens construı́das de raiz, sendo também usada, nesse sentido, no trabalho aqui apresentado, o que justifica a sua descrição em maior detalhe. A flexibilidade que o Smalltalk apresenta, resultante dessa homogeneidade, da escolha de um mecanismo de comunicação por mensagens entre objectos, necessariamente com ligação dinâmica e, finalmente, da aproximação interpretada que adopta, torna-a, sem dúvida, adequada para o desenvolvimento de ferramentas de programação interactiva, de que é prova o próprio ambiente em que se executa [Goldberg 83a, Goldberg 86]. No entanto, a necessidade de ser executada dentro de um ambiente próprio, fechado e que permite aceder e alterar sem restrições qual- 2.3. SUPORTE AOS MODELOS EM TEMPO DE EXECUÇÃO 29 quer dos seus componentes, inclusive os do próprio sistema, embora ideal para o desenvolvimento de protótipos, limita seriamente a sua utilização em aplicações comerciais. Essa limitação põe-se também devido a questões de desempenho, já que a sua execução é obrigatoriamente interpretada. Finalmente, tal como se argumenta em [Wegner 87], a não existência de tipificação, pode complicar de sobremaneira a sua utilização no desenvolvimento de aplicações de grandes dimensões. 2.3.1.1 Modelo de objectos No Smalltalk todas as entidades são objectos, mesmo os normalmente considerados tipos primitivos da linguagem, como inteiros e booleanos. Para além disso, são objectos também as próprias entidades envolvidas no modelo de tipos adoptado. De facto, existem objectos que descrevem classes, os objectos de classe, métodos, etc. Os objectos de classe, por sua vez, são instâncias de classes que, segundo a terminologia adoptada nesta linguagem, se designam por metaclasses. Cada objecto de classe, ou metaclasse, contém: um dicionário cujos elementos associam cada selector ao respectivo objecto de método; um vector com os nomes das variáveis membro de cada classe; e referências para a superclasse e subclasses. instância classe classe super objectos de classe Integer super super Object Class classe classe classe classe objectos de metaclasse Metaclass Integer class classe classe classe Object class super classe super super Metaclass class Class class super Figura 2.2: Relações entre instâncias, classes e metaclasses no Smalltalk Na figura 2.2 podem ver-se as relações entre os objectos de classe e os respectivos 30 CAPı́TULO 2. PANORÂMICA objectos de metaclasse8 . As setas a cheio (classe) indicam a relação instância de e mostram a classe de que o objecto é instância. Note-se que cada objecto de classe é instância de um objecto de metaclasse próprio, existindo, por conseguinte, uma hierarquia paralela de objectos de classe e metaclasse. Esta hierarquia pode ser seguida pelas setas a tracejado, legendadas super, que indicam a superclasse de cada classe. É ainda visı́vel na figura, a classe Object, que constitui a superclasse comum a todas as classes desta linguagem. Desse modo, todas as classes herdam o protocolo por ela definido, o que permite assumir um comportamento comum a todos os objectos. Nomeadamente, todos os objectos são capazes de indicar a classe (objecto de classe) a que pertencem, em resposta à mensagem class. 2.3.1.2 A primitiva de invocação Segundo o modelo adoptado pelo Smalltalk, a primitiva de invocação por mensagem, executa o seguinte algoritmo: • encontrar a classe do objecto receptor da mensagem (objecto class). • descobrir, nesse objecto de classe, o objecto de método que corresponde à mensagem especificada, apenas com base no seu selector. Se não encontrar, tenta no objecto de classe que representa a superclasse até ao topo da hierarquia. • enviar uma mensagem ao objecto encontrado, que descreve o método, para que execute o código a ele associado. Note-se que o segundo ponto concretiza o mecanismo de herança na discriminação do método9 . Por outro lado, como cada instância tem associada uma referência para o seu objecto de classe, a classe em que se inicia a procura é sempre a do objecto receptor e apenas determinada em tempo de execução. Assim, inclui implicitamente o polimorfismo de sobreposição simples no modelo de tipos da linguagem, bem como o de inclusão (por herança). Nesta linguagem não é suportada mais nenhuma forma de polimorfismo. 8 Na realidade no Smalltalk existem ainda classes de que herdam Class e Metaclass e as respectivas metaclasses, que não foram representadas no esquema acima por questões de simplificação. 9 Existem já concretizações que incluem herança múltipla. 2.3. SUPORTE AOS MODELOS EM TEMPO DE EXECUÇÃO 31 Tendo em conta que a primitiva de invocação se pode executar sobre qualquer instância e que os objectos de classe são também instâncias, é igualmente possı́vel o envio de mensagens a estes objectos. Essas mensagens devem corresponder a métodos definidos nas respectivas metaclasses e incluem os métodos de criação de instâncias, normalmente designados por ‘‘new’’, e métodos especı́ficos, que o programador defina para invocar sobre a classe (métodos de classe). Relativamente à execução do código associado ao objecto de método, o suporte de execução constrói a pilha de contexto do método, com os argumentos passados na invocação e as variáveis locais ao mesmo, interpretando, de seguida, cada uma das instruções que constituem o código. Estas, por sua vez, são essencialmente invocações por mensagem, quer a instâncias, quer a classes. Por seu lado, a construção e manuseamento da pilha de contexto é relativamente simples, dada a homogeneidade do seu conteúdo, i.e., referências para objectos. Quanto à invocação interpretada de métodos, o Smalltalk oferece uma interface, que permite o acesso à primitiva de invocação na própria linguagem de programação. De facto, em Object estão definidos os métodos de nome perform:, perform:with:, ..., que aceitam como argumentos um selector, e zero, um, dois, três, ou um vector de argumentos para a mensagem. A sua concretização é simplesmente a execução da primitiva, embebida no suporte de execução, que realiza a invocação da mensagem assim especificada sobre o objecto receptor. Mais uma vez, a homogeneidade do modelo evita dificuldades na criação da pilha de invocação, a partir do vector de referências para objectos, dado como argumento da mensagem para o método perform:withArguments:. 2.3.1.3 Optimizações A primeira optimização feita nesta linguagem é a utilização de uma pré-compilação dos métodos, que transforma a cadeia de caracteres, especificada pelo programador como código do método, numa sequência de instruções (byte-codes), que por sua vez serão interpretadas, pertencentes a um conjunto reduzido e optimizado, que permite aumentar o desempenho da execução. Relativamente aos selectores dos métodos, o resultado dessa compilação transforma-os em referências para objectos, designados sı́mbolos, que identificam 32 CAPı́TULO 2. PANORÂMICA univocamente cada nome. Assim, um selector com o mesmo nome, em diferentes partes do código, corresponde ao mesmo sı́mbolo (o mesmo objecto). Então a comparação de selectores na discriminação de um método é, na realidade, apenas uma comparação de referências. Mesmo com selectores únicos e dicionários de procura rápida (por função de hash) em cada classe, a concretização do algoritmo de discriminação dos métodos, tal como é descrito acima, pode tornar-se demasiado dispendiosa, sobretudo se pensarmos em classes inseridas numa longa hierarquia de derivação. Assim, nas concretizações do suporte de execução do Smalltalk, é normalmente incluı́da uma tabela global de acesso rápido, a que usualmente se dá o nome de cache, e que permite acelerar ainda mais o processo de discriminação para a grande maioria dos casos. O seu funcionamento pode descrever-se como se segue: • A primeira vez que um método é invocado, é feita uma procura segundo o algoritmo atrás descrito. A referência do objecto de método encontrado é então colocada nessa tabela, numa posição que resulta da aplicação de uma função de hash, sobre o selector e sobre a referência da classe do receptor da mensagem; • Nas invocações seguintes, esses dois parâmetros (selector e classe) são usados na mesma função, tendo-se assim acesso imediato ao método procurado; • Se entretanto for invocado outro método, cujo resultado da aplicação da função de hash seja o mesmo (diz-se que houve uma colisão) e que por isso mesmo ocupou a posição do método anterior na cache, há que voltar a recorrer ao algoritmo inicial, repetindo-se o processo. A eficiência deste mecanismo é inversamente proporcional ao número de colisões que ocorram. A diminuição deste número passa por dimensionar a tabela e escolher uma função de hash adequada, conseguindo-se relações de desempenho apreciáveis [Cox 86]. 2.3. SUPORTE AOS MODELOS EM TEMPO DE EXECUÇÃO 2.3.2 33 O Objective-C Abordar-se-á de seguida o Objective-C [Cox 86], que, sendo um linguagem compilada e hı́brida baseada no C, tal como o C++, segue no entanto de perto, o modelo de objectos do Smalltalk. De facto, ao contrário do C++, o Objective-C seguiu fundamentalmente na direcção de flexibilizar a programação, oferecendo mecanismos de invocação por mensagem e um modelo de tipos bastante simplificado. As vantagens e desvantagens apontadas relativamente ao C++ passam, naturalmente, pelas decisões que orientaram a concretização da linguagem. 2.3.2.1 Modelo de objectos O Objective-C aplica somente a um subconjunto de tipos, os mecanismos de suporte à programação OO. Para esse subconjunto de objectos, define, tal como o Smalltalk, uma classe base comum, Object, que permite o acesso, para cada classe, a uma estrutura de dados partilhada por todas as instâncias dessa classe. Essa parte partilhada representa o mesmo papel dos objectos de classe do Smalltalk, incluindo também informação sobre a hierarquia de classes, a estrutura das instâncias e um dicionário de métodos. Por sua vez, cada parte partilhada tem associada outra, tal como os objectos de classe têm os objectos de metaclasse. Em [Cox 86] é dado o nome de objecto fábrica, ao conjunto das duas partes partilhadas de cada classe. Relativamente às estruturas componentes das partes partilhadas de um objecto, deve dizer-se que o dicionário de métodos, tal como é descrito em [Cox 86], é simplesmente uma estrutura, em que cada elemento associa um selector, ao endereço da função C que concretiza o método. Por outro lado, a descrição das instâncias é feita por uma cadeia de caracteres codificada, que identifica os tipos e a sequência com que as variáveis de instância foram declaradas. 2.3.2.2 A primitiva de invocação A primitiva de invocação por mensagem é concretizada por uma função C de nome msg, que aceita como argumentos o receptor (um apontador para a estrutura do objecto), um selector e um número variável de argumentos pertencentes a qualquer tipo C. Uma vez invocada, obtém o endereço da função correspondente ao método, 34 CAPı́TULO 2. PANORÂMICA segundo um algoritmo de discriminação semelhante ao referido para o Smalltalk e que também recorre a uma cache global, para optimização do seu desempenho. Porque se trata de uma linguagem compilada, a transferência do controlo é, no entanto, necessariamente diferente da do Smalltalk. De facto, no Objective-C, a primitiva msg, uma vez encontrado o endereço do método, executa uma ins- trução de salto (JMP), em linguagem máquina, para esse endereço. A utilização dessa instrução, em substituição de uma chamada a procedimento normal, assegura que a pilha de chamada que a função correspondente ao método vai encontrar, é exactamente a mesma que se construiu quando da invocação de modo fica resolvida a passagem de argumentos da primitiva genérica, msg. Desse msg, para qualquer método, ganhando-se também algum tempo na transferência do controlo e consequentemente no desempenho da invocação. No entanto, porque a construção da pilha de invocação é feita em tempo de compilação, não é oferecido suporte definitivo à invocação interpretada de métodos. O suporte a este serviço é simplesmente facultado por métodos de nome perform, tal como no Smalltalk, mas limitados a zero, um ou dois argumentos, que devem obrigatoriamente ser apontadores para objectos pertencentes a classes ou, tendo em conta a uniformidade da passagem de argumentos do C, qualquer argumento que ocupe o mesmo espaço de memória. Relativamente às optimizações, também no Objective-C se simplifica a representação dos selectores dos métodos, embora por um processo diferente do descrito no Smalltalk. Em Objective-C uma invocação a um objecto é transformada, durante a compilação (tradução para C), numa chamada à função msg. Nessa chamada, o selector é substituı́do por uma expressão, que tem como resultado o valor inteiro contido numa tabela, que enumera todos os selectores usados num conjunto de classes compiladas conjuntamente - a categoria. Assim, uma invocação por mensagem será convertida em: __msg (objecto, tab-categoria[numero-do-selector-na-categoria], ... Quando é compilada uma categoria de classes, o compilador cria essa tabela com espaço para todos os selectores distintos que encontrou no código dessa categoria, fazendo corresponder a cada um desses selectores, uma posição distinta na 2.3. SUPORTE AOS MODELOS EM TEMPO DE EXECUÇÃO 35 tabela. Na fase de ligação do código das diversas categorias, atribui aos elementos dessas tabelas, valores que identificam univocamente cada selector na aplicação, i.e., se o mesmo selector (e.g desenha) é usado em categorias diferentes, ocupando eventualmente posições diferentes das respectivas tabelas de cada categoria (e.g. no -de-desenha-em-1 = 1, no -de-desenha-em-2 = 5), a fase de ligação de código encarrega-se de preencher essas posições de cada tabela com o mesmo valor (e.g. tabela-1[1] = 3, tabela-2[5] = 3), diferente do de qualquer outro selector. 2.3.3 O suporte de execução do COMANDOS No contexto do próprio C++, refere-se também aqui, um dos suportes de execução do COMANDOS10 [Marques 89], uma plataforma para o desenvolvimento de aplicações distribuı́das, segundo uma metodologia de programação OO. Em particular abordar-se-á o que foi desenvolvido no INESC [Sousa 91b, Sousa 90], designado IK, disponı́vel através de uma interface C [Sousa 89], mas sobre o qual se adaptou um tradutor de C++, versão 1.2, [Sequeira 91, Sequeira 89] e mais recentemente um de Objective-C [Ferreira 91b], oferecendo assim, para estas linguagens, as caracterı́sticas oferecidas por aquele suporte. Esta plataforma tem como objectivos proporcionar: um tratamento uniforme de objectos do ponto de vista de localização, i.e. quer o objecto se execute no espaço de endereçamento da aplicação, quer remotamente; mecanismos de ligação dinâmico de código (dinamic linking); reciclagem automática de memória (garbage collection), etc. A uniformização do tratamento dos objectos é baseada na existência de identificadores globais dos mesmos, permitindo mesmo estender essa identificação a objectos residentes em disco, de forma a incluı́-los também, na uniformização referida. O IK, desenvolvido simultaneamente com o trabalho que se descreve nesta tese, adopta um modelo de objectos semelhante ao do Smalltalk, ao qual, devido à sua natureza compilada, aplicou, no aspecto da execução dos métodos, uma concretização semelhante à do Objective-C. De facto, oferece também um mecanismo de invocação por mensagem, sobre objectos derivados de uma classe base comum, a classe object, transferindo o controlo para a função C que concretiza o método, recorrendo também 10 O COMANDOS é um projecto financiado pelo programa ESPRIT. 36 CAPı́TULO 2. PANORÂMICA a uma instrução de salto. No que se refere às optimizações do algoritmo de discriminação, no entanto, optou-se pela utilização de caches por classe e, relativamente aos selectores, por variáveis que os representam univocamente. Essas variáveis são usadas na invocação em substituição dos selectores, mas a sua criação e inicialização é apenas feita durante a ligação dinâmica do código correspondente às classes (ficheiros “.o”), com valores distintos para selectores diferentes. Também nesta plataforma não é dado suporte explı́cito à invocação interpretada de métodos. No entanto, a resolução que adopta para a codificação e descodificação dos argumentos de uma invocação, quando esta é feita a um objecto em execução noutro espaço de endereçamento, oferece algumas caracterı́sticas interessantes neste domı́nio. De facto, quando executa a descodificação, na aplicação remota em que se executa o objecto invocado, é construı́da uma pilha de invocação homogénea, que apenas contém apontadores para os argumentos, que resultaram do desempacotamento da mensagem. Então, em vez de ser chamado directamente o método, ou, mais correctamente, a função C que concretiza o método, é invocada uma outra função C, associada também a cada método da classe, e que transforma a pilha genérica assim formada, naquela que o método espera encontrar na sua execução. 2.3.4 Bibliotecas C++ O OOPS [Gorlen 87] e o ET++ [Gamma 88], duas bibliotecas escritas em C++, concretizam algumas das classes disponı́veis no Smalltalk. Nesse sentido, adoptam ainda uma simplificação do modelo de objectos dessa linguagem, mais uma vez limitado a um subconjunto de classes, derivadas de uma classe base comum (Object), e introduzindo objectos de classe para cada uma delas. Como não pretendem oferecer um mecanismo de invocação por mensagem, nem mesmo criação genérica de objectos em tempo de execução, os objectos de classe resumem-se a conter a identificação da classe e a relação de hierarquia com a classe base. O NIHCL [Gorlen 90], uma evolução do OOPS, inclui já herança múltipla. 2.4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS 2.3.5 37 Outras linguagens Cabe também aqui referir as linguagens OO derivadas do LISP, por exemplo o Flavors [Moon 86] e o CommonLoops [Bobrow 86], que apresentam caracterı́sticas de algum modo semelhantes às do Smalltalk, quer no sentido da flexibilidade oferecida, resultante de uma aproximação interpretada e não tipificada, quer na sua execução num ambiente próprio, quer no modelo de objectos em tempo de execução que adoptam. Será, porém, interessante referir o suporte que o CommonLoops oferece ao polimorfismo de sobreposição múltiplo, que não existe no Smalltalk, e o ênfase especial dado no Flavors à combinação de métodos redefinidos em classes derivadas, segundo herança múltipla. Noutro extremo, podem referir-se as linguagens fortemente tipificadas e compiladas, como o Eiffel [Meyer 88] e o Trellis/Owl [Schaffert 86], mas que se baseiam também em classes e herança, e adoptam igualmente mecanismos de invocação por mensagem. O seu suporte de execução existe igualmente, mas é totalmente transparente ao programador. 2.4 Salvaguarda e recuperação de objectos A introdução de mecanismos de salvaguarda e recuperação (SR) de objectos, quer nas próprias linguagens, quer em bibliotecas disponı́veis, é cada vez mais uma realidade. Os objectivos de serviços desta natureza estão geralmente associados à necessidade de manter persistente o estado dos objectos, alterado durante a execução da aplicação em que se inserem, e ao qual se pretende ter acesso, mesmo depois da terminação desta. O mecanismo de recuperação deve ser capaz de reactivar os objectos num novo contexto de execução da aplicação, ou mesmo de outra, a partir de uma representação externa passiva, resultante da operação complementar de salvaguarda. O problema a resolver é normalmente a definição de uma representação externa, cuja semântica descreva o estado dos objectos, independentemente do contexto de execução em que se inserem. Essa dependência é encontrada nas referências entre objectos, já que, por um lado não pode haver referências na representação externa de um objecto, para outro num contexto de execução, por outro lado, para que o 38 CAPı́TULO 2. PANORÂMICA primeiro seja descrito na totalidade, alguma forma de ligação deve existir entre os dois. A solução normalmente adoptada, passa por definir um mecanismo de salvaguarda que, caso um objecto inclua uma referência a outro nas suas variáveis de instância, então o segundo deve ser guardado quando é guardado o primeiro e, na representação externa deste, deve incluir-se uma referência à representação externa do outro (figura 2.3). Naturalmente, as referências entre objectos na representação externa deverão ser independentes do contexto de execução, quer sejam locais a essa representação, quer globais ao sistema em que as aplicações se executam (e.g. identificadores COMANDOS). Este algoritmo deve ser recursivo, tendo porém o cuidado contexto de execução salvaguarda recupera a conjunto de salvaguarda b a b c c representação externa Figura 2.3: Salvaguarda e recuperação de objectos de não guardar duas vezes o mesmo objecto em cada operação de salvaguarda. No caso da figura mostra-se um exemplo em que a salvaguarda do objecto a, implica também a salvaguarda de b e c. Pode então definir-se a seguinte noção: ∆2.9 Um conjunto de salvaguarda é um conjunto de objectos para os quais, qualquer que seja a referência feita nas suas variáveis de instância, o objecto referido pertence também ao conjunto de salvaguarda. Na figura 2.3 pode ver-se um conjunto de salvaguarda formado pelos objectos a, b 2.4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS 39 e c. Note-se que, embora não possa haver referências para fora do conjunto, pode, no entanto, haver referências nos objectos que estão fora dele, para os objectos que lhe pertencem. As linguagens e bibliotecas já atrás referidas oferecem também mecanismos de SR, baseados em conjuntos de salvaguarda, cuja funcionalidade se suporta, em grande medida, na informação de que dispõem em tempo de execução, acerca da estrutura das instâncias e relação entre tipos. De facto, a salvaguarda de um objecto pode decompor-se na salvaguarda de cada uma das suas variáveis de instância, recursivamente. Assim, o conhecimento da estrutura de cada instância, definida pela sua classe, é indispensável. Por outro lado, devido à capacidade de nestas linguagens se poderem usar, em expressões que assumem estaticamente um tipo, objectos de outro tipo (desde que conforme com o primeiro, em linguagens fortemente tipificadas), somente em tempo de execução é possı́vel a identificação exacta do tipo real do objecto e, por conseguinte, do formato da sua estrutura. 2.4.1 O Smalltalk O Smalltalk baseia o seu mecanismo de SR de objectos, na homogeneidade do seu modelo e na natureza interpretada que adopta. Por um lado, a informação implı́cita de tipo disponı́vel para qualquer objecto (objecto class.), permite-lhe sempre identificar a estrutura das instâncias em tempo de execução e, por sua vez, o tipo de cada uma das variáveis de instância. Desse modo, concretiza o mecanismo de salvaguarda genérico (storeOn:), na própria classe base Object, que se articula com as classes de escrita que oferece (Stream), de forma a gerar uma representação externa, segundo a sintaxe da própria linguagem. A representação que resulta de uma operação de salvaguarda, é composta pelo código necessário para enviar a mensagem “new” à classe do objecto, identificada pelo nome, seguida da atribuição directa a cada uma das variáveis de instância (instVarAt:put:), de objectos guardados da mesma forma, recursivamente. Por outro lado, a recuperação corresponde simplesmente à interpretação do código gerado na salvaguarda. A identificação de objectos globais ao sistema, como por exemplo as próprias classes, é resolvido de uma forma simples, usando o nome 40 CAPı́TULO 2. PANORÂMICA dos objectos, já que a unicidade desses nomes é garantida, à partida, no dicionário global do ambiente em que obrigatoriamente se executa. 2.4.2 O Eiffel O Eiffel, sendo uma linguagem construı́da de raiz, define também facilmente um mecanismo de SR de objectos genérico, com base na informação de tipo, facultada pelo suporte de execução que o compilador se encarrega de gerar. A diferença fundamental relativamente ao Smalltalk, é que a concretização das funções genéricas de SR, é feita numa classe especı́fica, Storable, de que outras classes podem derivar, passando a dispor do mecanismo de SR oferecido. Também relativamente à sintaxe da representação externa, o Eiffel difere da aproximação adoptada no Smalltalk, em parte devido à inexistência de interpretador da linguagem. A sintaxe dessa representação passa pela escrita, para cada objecto, de um cabeçalho que inclui o seu endereço, seguida da escrita dos dados, tal como existem no próprio contexto de execução. Na operação de recuperação é lido o cabeçalho, cuja informação permite obter o tipo do objecto, criado o espaço onde será recuperado, com base na informação do tipo, e finalmente lidos os dados directamente sobre esse espaço. Em seguida, convertem-se eventuais referências contidas nos dados do objecto, para outros anteriormente recuperados, e daqueles para o novo objecto. 2.4.3 O Objective-C O Objective-C, dada a heterogeneidade de tipos que abarca, por se tratar de uma linguagem hı́brida, inclui um mecanismo limitado de SR de objectos. De facto, no objecto fábrica de cada classe, é incluı́do um vector de caracteres que descreve a estrutura da instância, identificando o tipo de cada variável de instância: ’i’ para o tipo int, ’s’ para o tipo short, ’@’ para referências de objectos de classes derivadas de Object, etc. Se as variáveis de instância não pertencerem a tipos fundamentais ou referências, o compilador inclui uma codificação da estrutura da variável na cadeia de caracteres, que a descreve desde os tipos fundamentais que a compõem: "i{is@}i" será uma instância composta por um tipo int, uma estrutura composta 2.4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS 41 por um tipo int, um short e uma referência, e finalmente outro tipo int. Tendo em conta essa descrição, são definidos dois métodos sobre a classe Object que executam a SR de objectos. Tal como no Smalltalk, o formato da representação externa é fixo, mas ao contrário daquele, a sua sintaxe descreve sequencialmente os dados de cada objecto, numa representação por cadeias de caracteres legı́veis, que dificilmente se constitui numa linguagem de programação. As referências entre objectos são substituı́das na representação externa pela ordem de salvaguarda do objecto referido, sendo na recuperação transformadas, de novo, nos endereços dos objectos no novo contexto. Porque se trata de uma linguagem baseada no C, o Objective-C não resolve todos os tipos possı́veis que uma variável pode assumir, nomeadamente se for declarada como um apontador. Nalgumas concretizações do suporte à linguagem, nomeadamente a disponibilizada com o sistema NEXT [Thompson 89], o programador deverá redefinir os métodos de SR, indicando o tipo exacto de cada variável de instância dessa classe e invocando, naturalmente, o mesmo método na classe base. Nesta realização, no entanto, essa redefinição é obrigatória, o que desvirtua de algum modo as ideias iniciais propostas para a linguagem [Cox 86]. 2.4.4 O IK No suporte de execução do COMANDOS, já atrás referido, é oferecido também um mecanismo de SR, para as instâncias das classes derivadas da classe object. Esse serviço articula-se também com o mecanismo de identificação a nı́vel de sistema e com um serviço de nomes, que concretiza a noção de objecto persistente. Segundo este conceito, um objecto, uma vez salvaguardado, pode ser partilhado por várias aplicações, constituindo, mesmo em disco, um objecto com um estado, sobre o qual se poderão invocar operações. Uma vez invocado, o objecto é ligado ao contexto da aplicação que invocou e a invocação é feita localmente. Se noutra aplicação ocorrer agora uma invocação ao mesmo objecto, é feita uma invocação remota sobre ele, no primeiro contexto, assegurando-se assim a sua unicidade na globalidade do sistema. Quando os dois contextos de execução terminam, o objecto é de novo salvaguardado. 42 CAPı́TULO 2. PANORÂMICA No trabalho que aqui se apresenta, este não é, no entanto, o objectivo do serviço proposto. De facto, se se considerarem ferramentas de construção de aplicações, ao terminar cada sessão apenas se pretende salvaguardar uma “imagem” da aplicação e, por conseguinte, dos objectos que a constituem. Na recuperação, os objectos recriados deverão constituir novos objectos e, na maior parte dos casos, se outra sessão da ferramenta for invocada sobre a mesma imagem da aplicação, não deverá ocorrer partilha dos objectos. Então, trata-se aqui simplesmente de um serviço de SR de objectos, que permite salvaguardar imagens de um conjunto de salvaguarda e recuperá-las, activando objectos independentes em diferentes contextos. No entanto, porque um mecanismo que ofereça objectos persistentes, deverá ser sempre suportado por um mecanismo de SR do estado dos objectos, igualmente baseado em conjuntos de salvaguarda, ir-se-á aqui abordar essa componente do suporte à persistência dado na plataforma IK. Desse ponto de vista, o funcionamento da SR de objectos do IK é de algum modo semelhante ao usado no Eiffel. De facto, um objecto é guardado como um bloco de dados, residindo em disco, segundo uma representação externa idêntica à que tinha no contexto de execução11 . No entanto, porque as referências entre objectos de classes derivadas de object são globais ao sistema, as referências entre estes objectos são ainda válidas na representação externa, não necessitando de conversão quando o objecto é reactivado noutro contexto. Por outro lado, o controlo dos objectos referenciados a partir de um objecto feito persistente é realizado, não por um formulário do objecto, mas por uma função, gerada para cada classe pelo compilador da linguagem que se adoptar, e que invoca uma função especı́fica, neste caso uma função de escrita, para cada objecto referenciado. A ocorrência de ciclos fechados de referências é também detectada, evitando a escrita de um objecto, mais que uma vez. No mecanismo de detecção de referências para outros objectos, o IK considera simplesmente referências para objectos de classes derivadas de object e, portanto, identificadores globais. Quando um objecto contém um apontador C para outro, é da responsabilidade do programador definir funções especı́ficas de SR, em que irá 11 Este facto, deve-se a que nesta plataforma se assume que possı́veis conversões entre as representações internas e externas de forma a torná-las, por exemplo independentes da máquina, são feitas à posteriori por um serviço dedicado. 2.4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS 43 indicar a sua intenção de guardar/recuperar o objecto apontado, responsabilizandose o suporte de execução por invocar essas funções, nas operações de escrita e leitura. Esta concretização, embora eficiente, torna a representação externa dependente da máquina em que o objecto se executa, quando feito persistente. Nesse sentido, está prevista uma evolução neste sistema, em que, à custa da existência de um formulário semelhante ao que os objectos fábrica do Objective-C contêm, poderá proceder a uma transformação dos dados do objecto, para uma representação externa, possivelmente concretizada segundo a norma XDR12 . Esse formulário existe já nas estruturas de suporte de execução, sendo usado no empacotamento de objectos de classes derivadas de object, quando utilizados como argumentos numa invocação a um objecto em execução num contexto remoto. Uma outra alternativa aponta para incluir a chamada às funções de codificação e descodificação para XDR, em funções geradas para cada classe e que invocam aquelas, para cada variável de instância. 2.4.5 O OOPS e o ET++ O OOPS e o ET++ oferecem, para além da informação de tipo em tempo de execução, um mecanismo de SR de objectos, baseado num conjunto de regras de programação impostas ao programador. De facto, por cada classe que o programador inclua na hierarquia de classes dessas bibliotecas, deve definir dois métodos, um para salvaguarda e outro para recuperação, que invocam os métodos correspondentes da classe base e que sucessivamente salvaguardam ou recuperam cada uma das variáveis de instância da classe. O suporte dado resume-se simplesmente à detecção de ciclos fechados no conjunto de salvaguarda, de modo a evitar a salvaguarda repetida do mesmo objecto, na mesma operação. Na recuperação, o sistema encarrega-se de criar espaço para o objecto, invocando a função membro de recuperação e gerindo a transformação de referências entre objectos, na representação externa, para as respectivas referências (endereços), no novo contexto de execução. Tanto no OOPS como no ET++, o formato de escrita dos objectos é também fixo, e semelhante ao adoptado no Objective-C. No NIHCL, uma evolução recente do OOPS, inclui-se já algum su12 XDR é uma normalização para a representação de dados, proposta pela Sun. 44 CAPı́TULO 2. PANORÂMICA porte à diversificação da sintaxe de salvaguarda dos objectos, definindo classes abstractas de escrita e leitura, que poderão ser redefinidas para cada representação externa que se pretenda oferecer. No entanto, a sua concretização parece não tirar partido deste facto, mantendo conhecimento sintáctico nas classes dos objectos a guardar/recuperar e semântico nas classes de escrita e leitura. 2.5 Sı́ntese Neste capı́tulo, começaram por se definir um conjunto de conceitos, normalmente usados numa aproximação de programação orientada para objectos. Em seguida, descreveu-se, com base nesses conceitos, o modelo de tipos da linguagem C++, adoptados, linguagem e modelo, na concretização do suporte à programação aqui proposto. Abordaram-se também modelos de suporte semelhantes, concretizados em linguagens e bibliotecas já existentes, ou em definição, e em especial os serviços que nesses sistemas são facultados para a invocação interpretada de operações, e para a salvaguarda e recuperação de objectos. Capı́tulo 3 Suporte à interpretação O trabalho aqui apresentado, a que se chamou ICE1 , pretende oferecer um conjunto de primitivas, que sirvam de suporte a ferramentas e ambientes de programação interactiva OO e, genericamente, permitam, em tempo de execução, a definição de interacções sobre os objectos. Nesse contexto, optou-se pela introdução dos três seguintes serviços: • invocação de funções membro por mensagem, como forma de permitir a alteração do estado de um objecto durante a execução, através de uma primitiva genérica. • criação de objectos em tempo de execução, oferecida por uma primitiva que não implique a especificação do tipo dos objectos no código compilado, promovendo desse modo a flexibilidade e extensibilidade dos sistemas que a utilizem. • identificação de objectos por nome, de modo a permitir a utilização de formas de referência, perceptı́veis ao utilizador e independentes do contexto de execução. Pretende-se que os três serviços, e em particular os dois primeiros, sejam, tanto quanto possı́vel, fiéis à semântica introduzida pela linguagem em que serão concretizados, o C++. Esta opção, embora complique a concretização do sistema que se pretende oferecer, tendo em conta as diferentes construções polimórficas da linguagem, permite não só tirar partido das suas vantagens na invocação interpretada, 1 Do inglês: support for Interactive C++ Environments. 45 46 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO como também homogeneizar o modo de programação, quer esta seja feita em tempo de compilação, quer em tempo de execução. A uniformização da interface de acesso a estes serviços deve ser também um objectivo a ter em conta, bem como a homogeneidade do modelo subjacente de objectos, por forma a facilitar a sua utilização. Por outro lado, o modelo deve ser versátil para que possa abarcar a maioria dos casos possı́veis, promovendo, tal como a metodologia em que se baseia, a reutilização de código. Finalmente, a estrutura dos mecanismos de suporte às primitivas deve ser sistematizada, de modo a promover a utilização de ferramentas de geração automática desses mecanismos, não introduzindo mais esforço de programação do que aquele que é exigido na programação das próprias classes. Neste capı́tulo discutem-se algumas questões de realização de cada um dos serviços referidos, focando a sua influência no modelo de objectos, na estrutura das primitivas e na funcionalidade que estas devem oferecer. 3.1 Invocação por mensagens Nas linguagens OO que adoptam um modelo de tipos baseado em classes, como o C++, os métodos (ou funções membro) são definidos para uma classe de objectos, podendo ser invocados sobre qualquer instância dessa classe, ou de classes derivadas. No C++, quando ocorre uma invocação sobre um objecto, o compilador tem como tarefas: • determinar a classe a que esse objecto pertence. • determinar, a partir da declaração dessa classe, o método a ser invocado, considerando os métodos aı́ declarados e os declarados nas classes base. • gerar código para que esse método seja invocado. Ao pretender-se oferecer um mecanismo de invocação por mensagens, as mesmas tarefas passarão a ser realizadas em tempo de execução, degenerando a última na própria transferência de controlo, para a concretização do método invocado. Como consequência, é necessário que a informação respeitante à hierarquia de classes e aos métodos nelas definidas, incluindo o acesso à concretização do 3.1. INVOCAÇÃO POR MENSAGENS 47 método, esteja acessı́vel em tempo de execução. A introdução, formal ou não, de objectos-de-classe como forma de estruturar e permitir o acesso aos métodos considerando os mecanismos de herança, é, como se viu, prática comum em linguagens oferecendo um mecanismo de invocação com estas caracterı́sticas (Smalltalk, Objective-C, CommonLoops, Flavors, . . . ) e será também adoptada no trabalho que aqui se apresenta. É interessante notar, que mesmo no C++, sendo uma linguagem compilada, a tabela de métodos virtuais (2.2.3.2) tem um papel semelhante, como forma de garantir um acesso correcto a esses métodos. No entanto, essa tabela é insuficiente, se se pretender oferecer um mecanismo de invocação por mensagem, porque, por um lado apenas refere métodos virtuais, por outro não tem qualquer informação sobre a identificação do método, permitindo dificilmente a transformação da especificação da mensagem no método correspondente a invocar. O conceito de objectos-de-classe, em C++, para um subconjunto de classes bem definido, derivado de uma classe base comum, foi já introduzido, ainda que com outro objectivo, em trabalhos como o OOPS e o ET++. Também o problema do acesso ao objecto-de-classe a partir das suas instâncias foi resolvido, sendo disponı́vel através de uma função membro de nome isA (ou IsA para o ET++). No sistema aqui apresentado, começar-se-á por estender esse modelo, por forma a incluir nos objecto-de-classe, informação relativa aos métodos declarados na classe que representa (figura 3.1). Pode então definir-se o seguinte: ∆3.1 Um objecto-de-classe é uma instância que representa uma classe C++ (class, struct ou union), identificando-a univocamente e contendo informação sobre a hierarquia e métodos que essa classe define. As classes de que os objecto-de-classe são instâncias designar-se-ão globalmente metaclasses, segundo a mesma nomenclatura introduzida no Smalltalk. No modelo adoptado, continua a considerar-se, tal como nos trabalhos mencionados, uma classe base comum a todos os objectos capazes de receber mensagens. A essa classe chamar-se-à IObject e às classes derivadas, genericamente, classes ICE. Do mesmo modo, as instâncias de qualquer classe ICE serão globalmente referidas por objectos ICE. 48 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO Figura 3.1: Objectos de classe como suporte à invocação por mensagens A existência de uma raiz comum na hierarquia de classes, para além de permitir a obtenção de um mecanismo implı́cito de acesso ao objecto-de-classe para cada instância, o isA, pode introduzir, a esse nı́vel, a primitiva de invocação por mensagem. Designar-se-á esta primitiva por recvMessage, tomando uma invocação por mensagem o seguinte aspecto: objecto_ice.recvMessage (selector-da-mensagem, argumentos); Na figura 3.1 mostram-se os caminhos de execução possı́veis desta primitiva para uma classe qualquer. Note-se, que o papel da primitiva é simplesmente chamar um método (invoke) no objecto-de-classe associado (isA), que, por sua vez, a partir da especificação da mensagem, deve encontrar a concretização do método correspondente, executando-a. Pode assim enunciar-se o seguinte requisito, para o suporte à primitiva de invocação por mensagem: Ψ3.1 Para todos os objectos em que se pretenda fazer uso do mecanismo de invocação de métodos por mensagem, deve existir um objecto-de-classe 3.1. INVOCAÇÃO POR MENSAGENS 49 associado e acessı́vel, contendo a descrição desses métodos e permitindo a execução da sua concretização. 3.1.1 Sobreposição de nomes Num serviço genérico de invocação por mensagem para o C++, o mecanismo de sobreposição de nomes de métodos (method overloading), adoptado no modelo de tipos desta linguagem, deve também ter-se em consideração. Contudo, a possibilidade de existência desta forma de polimorfismo, complica o algoritmo de discriminação de métodos, já que o número e tipo dos argumentos presentes na invocação, determina, como foi visto em 2.2, qual dos métodos será invocado, caso existam métodos com nomes iguais, para uma determinada classe. Considere-se por exemplo a seguinte definição de classe: class X : // ... { X (char*); void foo (int); void foo (X&, char*, int, int); void foo (X&); }; Quadro Υ3.1: Sobreposição de nomes de métodos em C++ a especificação de um selector correspondente ao nome do método (foo), sobre uma instância de X, é insuficiente para que a primitiva de invocação por mensagem, identifique univocamente qual a função membro seleccionada, já que esse nome é comum a três métodos distintos. 3.1.1.1 Selector da mensagem Uma forma de reduzir o problema da identificação de um método, à situação das linguagens onde apenas o nome do método e a identificação da classe são suficientes para a sua discriminação (e.g. Smalltalk, Eiffel, ...), seria a definição de um selector de mensagem, como um identificador que contivesse, não só a informação do nome do método, como também a do tipo de cada um dos seus argumentos, na declaração do método. Para a definição de classe do exemplo anterior, as invocações por mensagem a cada um dos métodos declarados, far-se-ia, por exemplo, da seguinte forma: 50 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO x.recvMessage (selector_de_foo__int, 1); x.recvMessage (selector_de_foo__XR_charP_int_int_, x, "abc", 1, 2); x.recvMessage (selector_de_foo__XR_, x); Naturalmente, esta solução desvirtua claramente o conceito de sobreposição de nomes, já que obriga o programador a discriminar explicitamente o método a invocar, quando na sua definição lhes foram dados nomes iguais. Note-se que também não permite deixar em aberto a possibilidade de concretização do mecanismo de conversão implı́cita do tipo dos argumentos (2.2), já que, nesse caso, é necessário saber também o tipo dos argumentos especificados na invocação, que pode não coincidir com o da declaração. Na invocação que se mostra de seguida: x.recvMessage (selector_de_foo__XR_, "abc"); o argumento, correspondente à cadeia de caracteres "abc" do tipo char*, deve ser convertido para um objecto do tipo X, recorrendo ao construtor X(char*), e só depois, pode ser invocado o método foo(X&). Finalmente, pode argumentar-se sobre a susceptibilidade que esta solução apresenta à introdução de erros, resultantes da não concordância entre o tipo e/ou o número de argumentos da mensagem e o que se especifica no selector. Observe-se a semelhança da especificação, com os argumentos de uma chamada à função printf, scanf ou variantes, onde esse erro é corrente e dificilmente detectável por programadores menos experientes. Pretendendo manter no mecanismo de invocação os mesmos conceitos da linguagem, a solução parece, pois, passar por continuar a usar um selector correspondente apenas ao nome do método, sendo da responsabilidade do serviço oferecido, a extrapolação da informação de tipo a partir dos próprios argumentos, tal como é feita pelo compilador. Note-se que esta opção irá fazer divergir o modelo adoptado, dos modelos usados pela maioria dos suportes de execução atrás referidos (e.g. Smalltalk, Objective-C, IK, ...). De facto, a necessidade de conhecer o tipo com que são declarados os argumentos no método e o tipo dos argumentos que lhe são passados na invocação, irá, por um lado, determinar diferenças na estrutura interna do suporte. Por exemplo, no Smalltalk, Objective-C, não existe qualquer conhecimento sobre o tipo dos 3.1. INVOCAÇÃO POR MENSAGENS 51 argumentos de cada método. Já no IK, essa informação existe, sendo usada no empacotamento das mensagens para invocações remotas, mas apenas pelas dificuldades de determinação do tipo real2 dos argumentos, na invocação. Por outro lado, essa opção tem também repercuções no comportamento do mecanismo de invocação por mensagens. Mais precisamente, porque ao entrar em linha de conta com o tipo dos argumentos na discriminação dos métodos, verifica também na totalidade a sintaxe da mensagem, em tempo de execução, mas antes de invocar o código do método. Neste contexto, difere de linguagens como o Smalltalk ou o Objective-C, em que o erro na especificação de um argumento só será detectado, se, já na execução do método, for enviada uma mensagem ao argumento, que não tenha sido definida pelo seu tipo. Dada a correspondência entre selector e nome do método, passar-se-á a representar o primeiro por uma cadeia de caracteres do tipo char*, que coincida com o nome do método a invocar. Considere-se no entanto, que do ponto de vista da concretização, esta solução pode ser optimizada, de forma a não comprometer desnecessariamente o desempenho do mecanismo de invocação por mensagens. Contudo, mesmo que a solução na prática seja diferente, deve existir um mecanismo que permita a conversão entre a cadeia de caracteres e a representação concreta do selector, como forma de assegurar o suporte à interpretação na invocação dos métodos. x.recvMessage x.recvMessage x.recvMessage x.recvMessage ("foo", ("foo", ("foo", ("foo", 1); x, "abc", 1, 2); x); "abc"); // x.foo (X ("abc")); A utilização da primitiva na invocação dos métodos de nome foo, definidos para a classe X, sobre uma instância dessa classe, deverá, então, poder escrever-se como se mostra no quadro anterior. 3.1.1.2 Identificação de tipo para os argumentos Definida a questão da representação do selector, resta encontrar um mecanismo que permita extrair dos argumentos da mensagem, a informação de tipo necessária à 2 Note-se que esta dificuldade surge apenas para objectos cujo tipo não seja uma classe derivada de object (ver 2.3.3). 52 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO discriminação dos métodos, bem como identificar o tipo dos argumentos presente nas correspondentes declarações. No entanto, o C++ não oferece, em tempo de execução, qualquer informação acerca do tipo de um argumento. O primeiro passo é encontrar uma forma de identificação unı́voca de tipos, em tempo de execução. Nestas condições encontram-se já os objectos ICE. No entanto, pretendendo considerar um mecanismo genérico, o conhecimento do tipo de um argumento não deve limitar-se a um subconjunto de tipos, mas alargar-se a todas as categorias que o C++ introduz, nomeadamente os tipos fundamentais (char, short, ...), apontadores (char*, IObject*), etc. Mais precisamente, pode afirmar-se, como requisito de suporte à primitiva de invocação, o seguinte: Ψ3.2 A informação de tipos deve estender-se a todos os tipos que sejam utilizados na declaração de métodos, sobre os quais possa ser feita uma invocação por mensagem. Note-se que esta condição se alarga a todos os tipos, incluindo o do argumento de retorno, já que também este pode ser decisivo na selecção correcta do método a invocar, ou pelo menos na verificação da sintaxe na invocação3 . A informação de tipo, qualquer que este seja, deve ser uniforme de modo a facilitar os algoritmos de procura de métodos. A inclusão do conhecimento sobre a conformidade entre tipos e a possibilidade de dar resposta a perguntas sobre a sua conversão (ver 2.2.2.3), vai definir a versatilidade do algoritmo de discriminação, concretizado na primitiva de invocação. A solução adoptada, seguindo uma aproximação OO, passa por estender, mais uma vez, a noção de objecto-de-classe, de forma a poder abarcar todos os tipos de uma especificação C++ nas condições de Ψ3.2. Nesse contexto, pode introduzir-se o conceito de objecto-de-tipo: ∆3.2 Um objecto-de-tipo é uma instância que representa um tipo C++, identificando-o univocamente. 3 Nas versões mais recentes do compilador o tipo do argumento de retorno já não é usado para discriminar o método invocado, mas é dada uma notificação de erro caso os tipos de retorno e da variável a atribuir na invocação não coincidam, ou caso não seja possı́vel convertê-los um no outro. 3.1. INVOCAÇÃO POR MENSAGENS 53 Consequentemente, um objecto-de-classe é um objecto-de-tipo, embora o inverso não seja verdadeiro. Às classes de que os objecto-de-tipo são instâncias, continuará a chamar-se metaclasses. Para efeitos de referência, identificar-se-á a classe base de todas as metaclasses, por IType, e aquela cujas instâncias descrevem apenas classes, por IClass. 3.1.1.3 Identificação dos argumentos Se por um lado o mecanismo de identificação de tipos é indispensável, por outro, essa informação deve ser acessı́vel à primitiva de invocação por mensagem, juntamente com o acesso ao valor do argumento, de uma forma homogénea, qualquer que seja o tipo do valor a passar. Considerando apenas, argumentos do tipo das classes ICE, qualquer dos problemas fica resolvido, já que essas classes definem a associação entre objecto-instância e objecto-de-tipo, através do método isA, bastando passar os argumentos, por exemplo, como apontadores para IObject. Neste caso, a primitiva poderia ser declarada sobre qualquer das formas: (1) (2) (3) (4) recvMessage recvMessage recvMessage recvMessage (IObject*, (IObject*, (IObject*, (IObject*, Selector, Selector, Selector, Selector, ...); unsigned int, ...); IObject* []); unsigned int, IObject* []); O primeiro argumento da primitiva corresponde ao argumento de retorno da mensagem, recorrendo-se, nas primeiras duas formas, ao mecanismo de declaração sem definição do número e tipo de argumentos, oferecido pelo C++4 . Note-se que, em qualquer dos casos, a especificação do número de argumentos passado na invocação é necessária, quer explicitamente (casos (2) e (4) do quadro anterior), quer através de um terminador adequado (e.g. ponteiro nulo nos casos (1) e (3)). Contudo, recorrendo ao mecanismo de sobreposição de nomes, poder-se-á declarar variantes à primitiva de invocação, de forma a omitir o número de argumentos para o caso de um número pequeno de valores (e.g. 0 a 3). Por exemplo, 4 Tendo admitido que os argumentos são todos do tipo IObject*, a extracção dos argumentos da pilha de chamada ao método, pode facilmente ser concretizada por chamadas consecutivas ao comando “va arg(IObject*)”. 54 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO se (1) no quadro abaixo estiver definido, a invocação da primitiva para a execução do método foo(X&) pode ser feita como em (2) se mostra. (1) recvMessage (IObject*, Selector, IObject&); (2) x.recvMessage (0, "foo", x); No entanto, este mecanismo não viabiliza a passagem de argumentos que não pertençam a classes ICE, já que, para estes, não existe qualquer forma de associação implı́cita entre o valor e o objecto-de-tipo respectivo. Neste contexto, introduz-se a noção de identificador-de-objecto, como forma de uniformizar o acesso aos argumentos e respectivos objectos-de-tipo, na primitiva de invocação de métodos por mensagem: ∆3.3 Um identificador-de-objecto é uma instância que permite identificar um objecto (ou valor), qualquer que seja o seu tipo, e associá-lo ao objecto-de-tipo respectivo. Na figura 3.2 representa-se um identificador-de-objecto. A indicação da objecto-de-tipo identificador-de-objecto isA (opcional) objecto/valor Figura 3.2: Identificador de objectos ligação opcional através do método isA, pretende assinalar o caso em que o identificador-de-objecto refere um objecto ICE, existindo apenas nessa situação. A definição de construtores adequados para esta classe de objectos, que se designará por IOID (Ice Object IDentifier), em conjunção com o mecanismo automático de conversão de tipos, oferecido pelo C++, continuará a permitir a especificação 3.1. INVOCAÇÃO POR MENSAGENS 55 de argumentos das classes ICE na primitiva de invocação, sem que, mesmo assim, passe a haver referência explı́cita aos objecto-de-tipo (e.g. IOID (IObject&)). Esses mecanismos podem mesmo estender-se a um conjunto relativamente extenso de tipos, por exemplo, tipos fundamentais ou tipos de sistema (FILE*, ostream, ...). Resolve-se assim, para um grande número de casos, o problema da referência explı́cita ao objecto-de-tipo e, consequentemente, o problema da introdução de erros, por discordância entre o tipo real do argumento e o objecto-de-tipo associado. A utilização de identificadores-de-objecto tem, no entanto, uma desvantagem em relação a formas em que, por exemplo, a especificação dos objectos-de-tipo é separada da dos argumentos. De facto, numa solução como esta, poder-se-ia evitar o tempo de construção dos identificadores. Contudo, a sua criação prévia, não recorrendo ao mecanismo de conversão automática, para além da sua eventual reutilização, pode minimizar o problema onde os aspectos de desempenho sejam importantes. 3.1.2 Definição completa de funções membro Definidos os componentes da especificação de uma mensagem, põe-se agora a questão da descrição, em tempo de execução, das funções membro a ser invocadas. Essa descrição deverá conter a identificação da mensagem que lhe corresponde, incluindo o selector, acesso aos objectos-de-tipo dos argumentos e ao objecto-de-classe em que o método foi definido. Deve ainda oferecer mecanismos de transferência de controlo para a sua concretização. Neste contexto, pode introduzirse a noção de objecto-de-método como se segue: ∆3.4 Um objecto-de-método é uma instância que representa uma função membro de uma classe C++, identificando-a univocamente e oferecendo mecanismos de acesso à execução do seu código. e definir-se o seguinte requisito ao suporte da primitiva de invocação: Ψ3.3 Para todas as funções membro, para as quais se pretenda fazer uso do mecanismo de invocação por mensagem, deve existir um objecto-de-método associado, que contenha a sua descrição. 56 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO Note-se que a descrição de uma função membro deve incluir, também, todos os aspectos relevantes da sua definição, envolvidos no algoritmo de procura de métodos (2.2). Assim, os objectos-de-método devem conter informação sobre a protecção com que a função membro foi declarada (private, protected ou public), o modo de acesso (virtual ou simples) e valores dos argumentos omitidos. As questões de protecção devem estender-se também aos objectos de classe, já que uma função membro pública numa classe base privada, é privada na classe derivada. À classe de que os objectos de método são instâncias, chamou-se IMethod. Figura 3.3: Objectos de classe e de método como suporte à invocação por mensagens Na figura 3.3, representa-se o modelo de suporte à concretização da primitiva de invocação de mensagens, incluindo os objectos-de-método. Nessa figura, pode seguir-se o fluxo de execução da primitiva recvMessage, representado pela linha a tracejado e correspondente ao envio de uma mensagem ao objecto ICE de nome "x", referente ao método "método-K" herdado pela classe a que "x" pertence ("X"). É interessante observar, que os métodos invocados neste processo, respectivamente sobre a instância (recvMessage), o objecto-de-tipo (invoke) e o objecto-de-método (execute), correspondem a cada um dos pontos realçados no inı́cio desta secção, nomeadamente a determinação do tipo do objecto sobre o qual é feita a invocação, do método a executar e finalmente, a transferência do controlo da execução para a 3.1. INVOCAÇÃO POR MENSAGENS 57 concretização desse método. É ainda de realçar, que segundo a primitiva de invocação por mensagem, todos os métodos se comportam como virtuais, já que a procura do objecto-de-método é sempre feita a partir do tipo real do objecto, em tempo de execução (isA). Se se pretender seguir a semântica exacta do C++, pode sempre recorrer-se à primitiva invoke definida directamente sobre os objectos-de-classe, que deverá, tendo em conta a informação existente nos objectos-de-método, resolver as diferenças entre métodos virtuais e não virtuais. Por exemplo, considerem-se as classes: class X : public /*...*/ { public: void norm (); virtual void virt (); }; class Y : public X { public: void norm (); void virt (); }; e seja y uma instância de Y. Para as seguintes invocações: X* xp = &y; classe_X->invoke (voidArg, *xp, "norm"); classe_X->invoke (voidArg, *xp, "virt"); // xp->norm (); // xp->virt (); como a procura do objecto-de-método é feita a partir do objecto-de-classe de X, sobre o qual se invocou a mensagem, no primeiro caso, o método chamado será o definido na classe X, tal como seria de esperar, considerando a expressão equivalente em comentário (trata-se de uma função não virtual). No segundo caso, a primitiva invoke deverá detectar que o método é virtual e, eventualmente, reiniciar a procura a partir do tipo real (classe Y) da instância (*xp), que pode obter, por exemplo, se o seu segundo argumento for um identificador-de-objecto. 3.1.3 Funções membro especiais O C++ introduz um conjunto de funções membro com caracterı́sticas especiais, no que diz respeito à sintaxe e semântica da invocação. Três podem ser agrupadas, já que são igualmente invocadas sobre instâncias de classes: • operadores 58 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO • conversores • destrutor Os construtores estão envolvidos na criação e inicialização de objectos, pelo que a sua integração no modelo de suporte à concretização dos serviços oferecidos pelo ICE, é discutida em 3.2. 3.1.3.1 Operadores Nos operadores, a semântica da invocação no C++ é semelhante à das funções membro normais, pelo que o envio de mensagens, como forma de executar as suas concretizações, usando a primitiva recvMessage, é imediata, desde que o selector corresponda ao nome do operador a ser invocado. 3.1.3.2 Conversores No que diz respeito aos conversores, a situação é idêntica se forem consideradas as regras definidas nas últimas versões da linguagem. De facto, para versões anteriores, o algoritmo de procura deveria apenas considerar os conversores definidos para a classe do objecto sobre o qual se fez a invocação, já que não eram considerados os mecanismos de herança para estas funções membro. Em qualquer dos casos, o nome do operador e consequentemente o selector, corresponderá ao tipo para o qual se pretende fazer a conversão. A descrição dos operadores e conversores, pode, por conseguinte, ser feita de forma semelhante às das restantes funções membro: através de um objecto-de-método. Por exemplo, a invocação por mensagem do operador *= e do conversor para um valor do tipo int, sobre um objecto y, pertencente a uma classe Y que defina essas funções membro, será, respectivamente: y.recvMessage (voidArg, "*=", y); y.recvMessage (valor_inteiro, "int"); Se se pretender incluir também, a conversão implı́cita, no algoritmo de discriminação de métodos do próprio mecanismo de invocação por mensagem, deve ainda 3.1. INVOCAÇÃO POR MENSAGENS 59 ser considerado o acesso interno à concretização dos conversores, eficientemente, de forma a não comprometer, em excesso, o desempenho desta primitiva. 3.1.3.3 Destrutor e operador delete No caso dos destrutores, a situação altera-se um pouco, já que a sua invocação na linguagem ocorre de maneira diferente, em contextos diferentes (ver 2.2). No entanto, se se considerar o seu impacto no mecanismo de invocação por mensagem, apenas serão interessantes as situações em que a invocação do destrutor é explı́cita. Essas situações resultam da invocação do operador delete e, na última versão da linguagem [Ellis 90], da invocação do próprio destrutor. Este último caso, porém, deve ser usado apenas em situações muito particulares, não sendo considerado neste trabalho. No caso da invocação do operador delete são executadas duas acções: • a “limpeza” do espaço utilizado, por invocação da função que representa o destrutor; • a libertação do espaço de memória reservado para o objecto, invocando a função que representa o operador delete, propriamente dito; sendo a segunda acção invocada pela primeira. Na realidade, pode então dizerse que a expressão delete yp, em que yp é um apontador para uma instância de uma classe Y, corresponde simplesmente à invocação do destrutor definido para essa classe. A semântica do algoritmo de discriminação do destrutor não segue, no entanto, os passos duma invocação normal, já que para esta função membro, não são utilizados os mecanismos de herança. De facto, sempre que uma classe é declarada sem destrutor, o compilador gera um automaticamente, quer no ponto de cada invocação5 (semelhante às funções membro inline), quer definindo uma nova função e utilizando os mecanismo normais de invocação, se nas classes base o destrutor for declarado virtual. 5 Caso essa classe não declare um operador delete, não seja usada herança múltipla e não declare variáveis membro, é simplesmente feita uma chamada ao destrutor da classe base. 60 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO Note-se que a solução adoptada pelo compilador para o caso dos destrutores virtuais, pode igualmente ser usada para os restantes casos, deixando então de haver necessidade de distinção, entre a forma de resolução das invocações explı́citas ao destrutor e às restantes funções membro. Adoptando esta perspectiva para o mecanismo de invocação por mensagem, a solução passa por obter a descrição do destrutor em tempo de execução, mesmo quando este não foi declarado na classe em questão6 . Pode então propor-se, como requisito à primitiva de invocação, o seguinte: Ψ3.4 Quando para uma classe que não declare um destrutor, se pretende dar a possibilidade de destruição das suas instâncias, através de invocações por mensagem, é necessário incluir um objecto-de-método que descreva o destrutor, bem como gerar a sua concretização. A representação em tempo de execução dos destrutores, embora possa ser feita através de objectos-de-método, deve, no entanto, revestir-se de algum cuidado, já que o envio da mensagem delete deve seleccionar a concretização do destrutor e não do operador delete, propriamente dito. De facto, seguindo a mesma polı́tica do compilador, o acesso, através do mecanismo de invocação por mensagem a qualquer concretização desse operador, não deve ser permitido, a não ser pelo próprio destrutor. Por exemplo, a seguinte invocação por mensagem: yp->recvMessage (voidArg, "delete"); invoca o destrutor sobre o objecto apontado pela variável yp e liberta o espaço de memória por ele reservado. 3.1.4 Generalização da invocação por mensagem Tendo em conta as premissas apresentadas em relação à concretização de um serviço de invocação de métodos por mensagem, e o modelo de suporte obtido segundo as 6 Nas versões mais recentes da linguagem, o operador = (const X&) definido sobre uma classe X tem exactamente as mesmas caracterı́sticas dos destrutores, do ponto de vista da semântica de discriminação. Assim, para estas versões, as considerações feitas para os destrutores podem ser aplicadas para esse operador. 3.1. INVOCAÇÃO POR MENSAGENS 61 mesmas (ver figura 3.3), pode viabilizar-se, com alguma facilidade, a generalização do universo de objectos, a que um serviço dessa natureza está apto a aplicar-se. De facto, do que atrás foi exposto em relação à primitiva recvMessage, apenas não é possı́vel alargar a um conjunto de objectos não pertencentes a classes ICE, o mecanismo de ligação implı́cita, através da função membro isA. Quer os objectos-de-classe, quer os objectos-de-método, podem ser criados a partir da informação extraı́da de uma definição de classe qualquer (class, struct ou union), sem que seja necessário que ela derive de uma classe base comum (IObject). No entanto, a introdução de identificadores-de-objecto, resolvendo o problema da associação entre tipo e valor, na passagem de argumentos para a primitiva de invocação, pode também resolver o mesmo problema, na generalização do universo de objectos que podem receber mensagens. Considerando-se a definição da primitiva recvMessage na classe que introduz os identificadores-de-objecto, esta primitiva pode, tal como a correspondente nos objectos ICE, invocar a função membro, que na realidade concretiza o serviço (invoke) no objecto-de-classe (ver figura 3.3), já que é possı́vel o acesso a esse objecto. Naturalmente, todos os requisitos Ψ3.1 Ψ3.2, Ψ3.3 e Ψ3.4, enumerados nesta secção, se continuam a aplicar às classes e métodos, para os quais se pretende utilizar o recvMessage. A generalização do mecanismo de invocação por mensagens, pode mesmo alargar-se a todos os tipos C++, caso se adopte uma visão homogénea do modelo de tipos desta linguagem. De facto, considerando a definição de objecto dada em ∆2.1 e as noções de operador, tal como são introduzidas pelo C++ no modelo de tipos, pode dizer-se que, por exemplo, para uma variável a do tipo int*, a expressão: (*a) += 1; // a.operator *() .operator +=(1); Quadro Υ3.2: Operadores sobre tipos primitivos. é equivalente à invocação do operador *, sobre o objecto a do tipo apontador para inteiro, seguida da chamada ao operador binário +=, sobre o objecto resultante da operação anterior. Adoptando esta perspectiva no modelo de suporte à primitiva de invocação e 62 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO admitindo que: • Um objecto-de-método pode descrever um operador primitivo da linguagem, oferecendo também um mecanismo para sua execução; • Um objecto-de-tipo, ainda que não seja um objecto-de-classe, pode aceder a informação sobre as operações que podem ser executadas sobre as suas instâncias. O termo operações foi usado para designar, globalmente, o conjunto que compreende, funções membro e operadores primitivos, podendo reintroduzir-se o requisito definido em Ψ3.1 do seguinte modo: Ψ3.5 Para todos os objectos em que se pretenda fazer uso do mecanismo de invocação por mensagem, deve existir um objecto-de-tipo associado e acessı́vel, contendo a descrição das operações que sobre eles podem ser invocadas e permitindo a execução da sua concretização. Generalizando o conceito de função membro, de forma a incluir também os operadores primitivos, a condição Ψ3.3 de existência de objecto-de-método e a sua definição ∆3.4, mantêm-se válidas neste contexto. Pode então representar-se o modelo de suporte à invocação por mensagem, como se mostra na figura 3.4. Note-se que o algoritmo de procura, executado pela primitiva invoke no objecto-de-tipo, deve, no caso de o tipo ser uma classe que deriva de outra e consequentemente este ser um objecto-de-classe, incluir os mecanismos de herança na discriminação do método, tal como foi representado na figura 3.3 para os objectos ICE. Considerando a existência de objectos-de-tipo que descrevam o tipo int e int*, e de objectos-de-método que representem os operadores primitivos para esses tipos, pode escrever-se, da seguinte maneira, o código correspondente ao apresentado no quadro Υ3.2: 3.2. CRIAÇÃO DE OBJECTOS EM TEMPO DE EXECUÇÃO 63 Figura 3.4: Generalização da invocação por mensagem. int tmp; IOID ia(a); IOID itmp(tmp); // ... ia.recvMessage (itmp, "*"); itmp.recvMessage (voidArg, "+=", 1); As vantagens da generalização do serviço de invocação por mensagens a todos os tipos C++, são interessantes, sobretudo no caso do desenvolvimento de interpretadores da linguagem. De facto, neste caso, o acesso às operações primitivas, embebidas no compilador, e normalmente a concretizar no interpretador, pode ser oferecido pelo mecanismo de invocação, directamente, através de uma interface uniforme. Considerando que, na interpretação de um contexto de execução em C++ (entre {}), o interpretador representa as variáveis através de identificadores-de-objecto, qualquer que seja a expressão que encontre, ela pode ser traduzida trivialmente para uma invocação à primitiva recvMessage, independentemente do tipo dos argumentos. 3.2 Criação de objectos em tempo de execução A criação de objectos em C++ pode, como foi visto em 2.2, ocorrer segundo sintaxes diferentes em contextos diferentes. Tomando como exemplo a definição de classe, mostrada no quadro Υ3.1 da secção anterior, o código seguinte faz uso do mecanismo 64 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO de criação de objectos, de três formas distintas. (1) X x ("objecto-1"); (2) X* xp = new X ("objecto-2"); (3) xp->foo ("objecto-3"); // xp->foo (X ("objecto-3")); Quadro Υ3.3: Três comandos que envolvem a criação de objectos Em qualquer dos casos, a operação envolve a classe do objecto e tem como resultado uma instância dessa classe. Se for adoptada uma perspectiva puramente OO, em que qualquer operação deve ser executada sobre um objecto, pode dizer-se que a criação de objectos é uma operação realizada sobre a classe, e não sobre as suas instâncias, visto que estas não existem antes da operação. Este facto, é claramente expresso em linguagens como o Smalltalk e mesmo numa linguagem hı́brida como o Objective-C. Nelas, a criação de instâncias é o resultado da invocação de um método (new) sobre o objecto de classe (ou objecto fábrica, respectivamente). Tendo em conta o modelo de suporte obtido na secção anterior, é natural que também, no trabalho que aqui se apresenta, a criação de um objecto em tempo de execução seja resultado da invocação de uma função membro, sobre o objecto-de-classe respectivo. No C++ há, no entanto, que ter em consideração os dois passos envolvidos na criação de um objecto: • a reserva do espaço de memória para o objecto. • a inicialização dos dados desse espaço. No primeiro caso não existe a instância, pelo que esta acção, a ser executada sobre um objecto, deve ser invocada sobre o objecto-de-classe. No segundo, correspondente à invocação do construtor, isso já não é, em geral, verdadeiro. De facto, o papel do construtor é apenas a inicialização dos dados da instância. No entanto, pode argumentar-se que existem inicializações dependentes da linguagem, como é o caso da inicialização da tabela de métodos virtuais, feitas no construtor e que devem executar-se antes da utilização do espaço reservado para o objecto, como uma instância com todas as caracterı́sticas definidas pela sua classe. Consequentemente, pode dizer-se que também o construtor é invocado sobre a classe. Esta 3.2. CRIAÇÃO DE OBJECTOS EM TEMPO DE EXECUÇÃO 65 situação é tanto mais verdadeira, quanto se considerar que nos mecanismos normais de utilização da linguagem, não é possı́vel a invocação do construtor sem incluir implicitamente a reserva de espaço de memória. Nesse contexto, diz-se que um construtor é um método de classe, em oposição aos métodos de instância em que são englobadas as restantes funções membro já referidas, e a criação de objectos no ICE corresponde à sua invocação sobre o objecto-de-classe. 3.2.1 A primitiva de criação de objectos Defina-se então uma primitiva, que permita o acesso uniforme à criação de objectos, em tempo de execução. Designar-se-á create (já que new é palavra reservada na linguagem). Tendo em conta que a sobreposição de nomes se aplica também aos construtores, as considerações feitas na secção anterior relativamente às restantes funções membro, podem também aplicar-se neste caso. Desse modo, a utilização da primitiva para a criação de um objecto, correspondendo à linha (2) do exemplo apresentado no quadro Υ3.3, será: X* xp = (X*)classe_X.create ("objecto-2"); em que classe X se refere ao objecto-de-classe da classe X. Do mesmo modo que para a sobreposição de nomes, praticamente todas as restantes caracterı́sticas discutidas para as funções membro, tais como os argumentos por omissão e a protecção, se aplicam, também aos construtores. Por outro lado, também o acesso à execução da sua concretização deve ser possı́vel. Assim, a representação de construtores em tempo de execução, é igualmente feita através de objectos-de-método. Note-se, que a própria definição de objecto-de-método em ∆3.4 não exclui os construtores, se, tal como é apresentado no manual da linguagem [Ellis 90], estes forem considerados funções membro. Nesse contexto, a premissa equivalente a Ψ3.3 pode formular-se aplicando-a agora aos construtores: Ψ3.6 Para todos os construtores, para os quais se pretenda fazer uso do mecanismo de criação de objectos em tempo de execução, deve existir um objecto-de-método associado, que contenha a sua descrição. 66 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO Se se aplicarem os mesmos conceitos que para o serviço de invocação de métodos por mensagem (ver figura 3.3), o acesso aos objectos-de-método que representam os construtores, deveria ser efectuado através de objectos que representam a classe da instância, sobre a qual foi feita a invocação. Sendo os construtores métodos de classe, a instância corresponde ao objecto-de-classe. Por conseguinte, os construtores deveriam ser acessı́veis, através de objectos que representassem as metaclasses. Designem-se por objectos-de-metaclasse. Esta solução é adoptada, por exemplo, no Smalltalk e no Objective-C, e pode ver-se, para o primeiro, na figura 2.2. A utilização de objectos-de-metaclasse introduz, no entanto, alguma redundância na representação de classes. De facto, paralela à hierarquia de objectos-de-classe, vai existir uma de objectos-de-metaclasse, que duplica a informação relativa aos mecanismos de derivação. Figura 3.5: Acesso aos construtores A solução que se adopta no ICE, representada na figura 3.5, passa por incluir no objecto-de-classe, a informação que permite o acesso aos objectos-de-método, que representam os construtores. A condição Ψ3.5 pode então enunciar-se, para o caso da primitiva create, do seguinte modo: Ψ3.7 Para todas as classes em que se pretenda fazer uso do mecanismo de criação de objectos em tempo de execução, deve existir 3.2. CRIAÇÃO DE OBJECTOS EM TEMPO DE EXECUÇÃO 67 um objecto-de-classe associado, contendo a descrição dos construtores definidos para a classe e permitindo a execução das suas concretizações. Naturalmente, as estruturas de acesso aos métodos de instância e aos construtores, devem ser diferentes, já que o mecanismo que permite a sua execução, é também diferente. A invocação do método invoke, sobre o objecto-de-classe, não deve, em caso nenhum, executar os construtores definidos para essa classe, nem a função membro create, os métodos definidos para as suas instâncias. Na figura, pode observar-se a existência de um objecto-de-classe, identificado por metaclasse comum. Esse objecto permite fechar o ciclo de representação do modelo de suporte, representando a classe de que todos os objectos-de-classe são instâncias. Por conseguinte, sendo os objectos-de-classe objectos ICE, a função membro isA definida para os objectos-de-classe, quando invocada, indica sempre esse objecto. Ele próprio é uma instância da classe que representa. A figura representa ainda o fluxo de execução das primitivas create e recvMessage, distinguindo as estruturas envolvidas no objecto-de-classe. Embora não representado, o mecanismo de discriminação de métodos correspondente à primitiva recvMessage, deve incluir a procura nas classes base, se for caso disso (ver figura 3.3). No entanto, o mesmo já não pode ser dito relativamente à primitiva de criação de objectos, já que os construtores não são herdados, pelo que a procura se deve limitar ao objecto-de-classe, para o qual foi feita a invocação. Notese que, apesar de não serem herdados, os construtores encarregam-se de invocar os das classes base, mas esse é um mecanismo ortogonal à invocação dos mesmos, já que é incluı́do nos próprios construtores. Na realidade, as considerações tecidas sobre a semântica associada aos destrutores em 3.1.3.3, podem também ser aplicadas aos construtores. De facto, para além de não serem herdados, o compilador de C++ encarrega-se de gerar um construtor sem argumentos, no caso de não ser declarado nenhum para uma dada classe7 . Nesse contexto, a condição Ψ3.4 de suporte à primitiva de invocação por mensagem, deve também ser introduzida para o caso dos construtores e da primitiva create: 7 Na última versão da linguagem é mesmo gerado um segundo construtor utilizado nas operações cópia devidas à inicialização e passagem e retorno de argumentos para funções [Ellis 90]. 68 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO Ψ3.8 Quando, para uma classe que não declare um construtor, se pretende dar a possibilidade de criação das suas instâncias, através da primitiva de criação, é necessário incluir um objecto-de-método, que descreva o construtor sem argumentos para essa classe, bem como gerar a sua concretização. Também o acesso interno aos construtores deve ser considerado, se, na primitiva de invocação por mensagens, se incluir o mecanismo de conversão implı́cita de tipos. 3.2.2 Integração com a invocação por mensagens Se os construtores são métodos de classe e as classes são também objectos, então, para que o mecanismo de invocação de métodos por mensagem seja coerente, deve ser possı́vel o envio de uma mensagem a um objecto-de-classe, com um intuito de criar uma instância. objecto_de_classe.recvMessage (selector-de-construtor, args); Comparando o comportamento dos construtores em conjunção com o operador new, com a dos destrutores com o operador delete, é de esperar que o selector da mensagem que invoca um construtor, seja representado pela cadeia de caracteres "new". Retomando o exemplo da classe X do quadro Υ3.1, a criação de uma instância, recorrendo ao mecanismo de invocação por mensagem, será: IOID ix; classe_X.recvMessage (ix, "new", "abc"); No entanto, algumas considerações devem ser feitas sobre a semântica desta primitiva, para o caso dos objectos-de-classe. De facto, neste caso, há dois factores que a distinguem da primitiva originalmente definida para a generalidade dos objectos: • a inclusão do acesso aos construtores no próprio objecto em que é invocada a primitiva, o objecto-de-classe. 3.2. CRIAÇÃO DE OBJECTOS EM TEMPO DE EXECUÇÃO 69 • a não aplicação dos mecanismos de herança aos construtores. A solução pode simplesmente passar por redefinir a primitiva recvMessage para os objectos-de-classe, de forma a invocar a primitiva create. No entanto, sendo os objectos-de-classe também instâncias de uma classe, a metaclasse, então as funções membro nesta definidas, devem também poder invocar-se sobre os objectos-de-classe. Neste caso, a primitiva deve funcionar como a original e a concretização encontrada terá que ter em conta os dois casos. 3.2.3 Funções membro estáticas No algoritmo de discriminação de mensagens da primitiva recvMessage para os objectos de classe, devem ainda ser consideradas as funções membro estáticas. De facto, estas funções, tal como são introduzidas no C++, têm o comportamento que seria de esperar encontrar em métodos de classe, i.e., a classe sobre a qual são invocadas, deve ser especificada na invocação, e a sua existência, embora ligada à classe que as define, não depende da existência de qualquer instância dessa classe. A estas funções membro é ainda aplicável o mecanismo de herança, não sendo no entanto possı́vel, defini-las virtuais. Por exemplo, a invocação de uma função membro estática foo definida para uma classe A, sobre uma classe B derivada de A, pode ser feita do seguinte modo: B::foo (); // invoca A::foo () em que B deriva de A O acesso a estas funções membro, através da primitiva de invocação por mensagem, deveria, por conseguinte, ser feito segundo as mesmas caracterı́sticas da primitiva original, para a generalidade dos objectos. No entanto, a não utilização de um objecto-de-metaclasse para cada classe, implica, mais uma vez, a sua inclusão no próprio objecto-de-classe, numa estrutura de acesso própria (ver figura 3.6). A sua representação é naturalmente feita através de objectos-de-método, já que as suas caracterı́sticas estruturais em nada diferem das restantes funções membro. Assim, a redefinição da primitiva de invocação por mensagem para os objectos-de-classe, caso a mensagem não corresponda a um construtor, nem a uma 70 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO Figura 3.6: Mensagens sobre objectos-de-classe. função membro da metaclasse, deve incluir a procura de objectos-de-método que correspondam às funções membro estáticas, nas estruturas que se lhe referem, definidas no objecto-de-classe e nos objectos-de-classe base. Na figura 3.6 estão representados os fluxos possı́veis de execução da primitiva recvMessage, para a invocação de mensagens sobre objectos de classe. 3.2.4 Generalização a todos os tipos C++ Tendo em conta o modelo de suporte obtido para a criação de instâncias em tempo de execução (figura 3.5), a sua extensão, de forma a englobar neste serviço os tipos C++ que não sejam classes, não oferece quaisquer dificuldades. O primeiro passo é alargar o conceito de construtor, considerando as expressões 3.2. CRIAÇÃO DE OBJECTOS EM TEMPO DE EXECUÇÃO 71 de criação de valores de tipos primitivos, como invocações de construtores oferecidos pela linguagem e definidos para esses tipos. Por exemplo, a expressão: int* x = new int (7); // cria um inteiro e inicializa com o valor 7 invoca o “construtor” definido para o tipo int e inicializa o objecto criado com o valor 7. Nesse contexto, representando o código correspondente à criação de um objecto de um tipo primitivo por meio de objectos-de-método, a introdução nos objectos-de-tipo de uma estrutura de acesso a esses objectos-de-método, oferece o suporte necessário à primitiva create, para a criação de objectos pertencentes a esses tipos. Do mesmo modo, a criação de objectos por mensagem fica também acessı́vel. x = (int*) tipo_int.create (7); tipo_int.recvMessage (x, "new", 7); O código acima, utiliza as duas primitivas, de forma equivalente, para executar a expressão apresentada no quadro anterior. A condição Ψ3.6 mantém-se válida neste contexto, dado o conceito alargado de construtor. O requisito Ψ3.7 pode reformular-se aplicando-o aos objectos-de-tipo: Ψ3.9 Para todos os tipos, em que se pretenda fazer uso do mecanismo de criação de objectos em tempo de execução, deve existir um objecto-de-tipo associado, contendo a descrição dos construtores definidos para esse tipo e permitindo a execução das suas concretizações. Também a representação do modelo de suporte, pode facilmente ser visualizada na figura 3.5, se em vez da legenda objecto-de-classe, que assinala o objecto sobre o qual é invocada a função membro create, estiver a legenda objecto-de-tipo. 72 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO 3.3 Serviço de nomes Nas secções anteriores abordaram-se os serviços de interacção com objectos, através de primitivas genéricas, com o intuito de suportar a invocação, em tempo de execução, dos mecanismos da linguagem que permitem parametrizar e criar objectos. Contudo, para que esse suporte se complete, é necessário oferecer um serviço que permita especificar qual o objecto sobre o qual se pretende fazer a invocação e quais os que serão tomados como argumentos de uma mensagem. A forma comum de identificação em tempo de execução, tal como é definido nas próprias primitivas de interacção, é feita através do endereço do objecto ou do endereço do identificador-de-objecto. No entanto, essa forma não é em geral perceptı́vel ao utilizador, nem mesmo ao programador de C++, em que na realidade os objectos são referenciados por nome, o nome da variável associada. O trabalho aqui apresentado propõe-se, por isso, oferecer um serviço que permita a tradução de nomes, cadeias de caracteres, nos endereços dos objectos que lhe estão associados. O serviço de nomes é representado, em tempo de execução, por um objecto, globalmente acessı́vel, e tendo como caracterı́sticas essenciais a possibilidade de registo de nomes associados a um objecto, a sua remoção e a capacidade de indicar qual o objecto associado a determinado nome. Naturalmente, um nome deve identificar univocamente um objecto. A esta última primitiva chamou-se findObject, estando o serviço de nomes acessı́vel através da variável iceNameService. A sua integração com os objectos ICE é feita através da classe IObject, que, na sua definição, permite a especificação de um nome a associar a cada objecto, no serviço de nomes. Também os objectos-de-tipo associam, implicitamente, o nome do tipo que representam a si próprios. Desta forma, a criação e parametrização de objectos ICE acessı́veis por nome, pode ser feita sem qualquer referência ao contexto de execução, a menos da variável de acesso ao serviço de nomes. A articulação deste serviço com os restantes, pode ser vista na figura 3.7. Nesta figura, representam-se as primitivas descritas neste capı́tulo e a sua interacção com os serviços de suporte. O serviço pode ser comparado a uma tabela de sı́mbolos [Schreiner 85, Aho 85] de um compilador ou interpretador, tendo em conta que oferece um mecanismo de identificação por nome e permite aceder ao conhecimento semântico dos objectos, 3.3. SERVIÇO DE NOMES findObject 73 recvMessage create IObject / IOID Serviço de Nomes Serviço de invocação por mensagens Serviço de criação de objectos Informação de tipo Figura 3.7: Integração dos serviços de suporte à interpretação que identifica através da ligação isA de cada objecto. Nesse sentido, o ICE propõe ainda a introdução da noção de contexto (no C++ definido por {}) na identificação dos objectos. Esta noção é concretizada associando, a cada objecto registado no serviço de nomes, outro, que se designará dono, para além do nome. O objecto dono pode então identificar um contexto, em tempo de execução. Se, quando um objecto é acrescentado ao serviço, for especificado o seu dono, então, qualquer operação que sobre o primeiro se pretenda realizar, remoção do nome ou acesso ao endereço, deve ser acompanhada da referência ao segundo. Objectos sem dono, corresponderão a objectos globais. Por exemplo, os objectos-de-tipo estão sempre nesta categoria. Com a introdução da noção de dono, a condição de unicidade na identificação pode estender-se, aplicando-a ao conjunto dono-nome para cada objecto. Esta caracterı́stica do serviço de nomes, permitirá a sua utilização como tabela de sı́mbolos, em interpretadores de linguagens que admitam a existência de diferentes espaços de nomes (e.g o C++), ou simplesmente, como uma forma de hierarquização dos objectos, encontrada, por exemplo, em sistemas de janelas ou bibliotecas de objectos de interacção. A integração com os objectos ICE não estaria completa, se o mecanismo de associação entre objecto e nome, fosse estabelecido apenas num sentido. De facto, se um objecto é destruı́do, os seus nomes devem ser retirados do serviço, de modo a não criar incoerências ou acessos a objectos não existentes. Por outro lado, num sistema interactivo, se o objecto pretende notificar alguma ocorrência ao utilizador (e.g. uma mensagem de erro), a mensagem deve conter uma identificação do objecto, 74 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO de modo a que o utilizador a localize. De preferência, essa identificação deve seguir a mesma forma que o utilizador usa para se referir ao objecto. Assim, o serviço de nomes do ICE, deverá oferecer também um mecanismo de conversão de um endereço, no conjunto de nomes que lhe está associado, e remoção de um objecto englobando a libertação de todos os seus nomes. Dada a possı́vel utilização do serviço de nomes como tabela de sı́mbolos, associando informação de tipo aos objectos nela registados, e a necessidade de ter alguma uniformidade no acesso a esses objectos, optou-se por limitar aos objectos ICE, os mecanismos de registo no serviço de nomes. No entanto, a utilização de objectos que encapsulem a ligação implı́cita entre instância e objecto-de-tipo, permitirá resolver a questão, desde que também eles sejam objectos ICE. Mais uma vez, a solução adoptada é a utilização de identificadores-de-objecto para este fim. Sı́ntese Neste capı́tulo, apresentou-se o modelo de objectos em que se baseiam as primitivas de suporte à interpretação, na interacção com objectos C++. As primitivas introduzidas incluem: • um serviço de invocação de métodos por mensagem (recvMessage) • um mecanismo de criação de objectos, em tempo de execução, através de uma interface uniforme (create). • um serviço de nomes (iceNameService). As duas primeiras baseiam-se num modelo, que inclui a descrição de tipos e funções membro C++, em geral, através de objectos que se designaram objectos-de-tipo e objectos-de-método, respectivamente. O serviço de nomes apresenta-se de igual modo através de um objecto, globalmente acessı́vel, que permite uma organização hierárquica de objectos e um mecanismo de identificação por nome (findObject). É também introduzido neste capı́tulo, um factor homogeneizador baseado em duas classes, que integram os serviços descritos, quer para objectos derivados de uma classe base comum (IObject) e genericamente designados por objectos ICE, quer 3.3. SERVIÇO DE NOMES 75 para a generalidade dos tipos C++, através da noção de identificador-de-objecto (IOID). 76 CAPı́TULO 3. SUPORTE À INTERPRETAÇÃO Capı́tulo 4 Salvaguarda e recuperação de objectos Tendo apresentado um conjunto de mecanismos, que permitem a criação e modificação de objectos em tempo de execução, como forma de suporte à programação interactiva, discute-se agora a solução adoptada pelo ICE para resolver o problema de salvaguardar e recuperar o estado desses objectos, assegurando assim a continuidade do esforço de programação. Em todo o texto, assumir-se-á, a menos que algo seja dito em contrário, que as operações de salvaguarda e recuperação (SR) sobre um objecto, envolverão também todos aqueles que fazem parte do seu conjunto-de-salvaguarda, tal como foi definido em ∆2.9. Desta forma, tendo em conta que os objectos assim identificados, formam um conjunto fechado de referências, i.e., não há referências para objectos fora do conjunto, assegurar-se-á mais facilmente a independência entre as representações persistentes e os objectos no contexto de execução. Neste capı́tulo discutir-se-ão as caracterı́sticas que um serviço de SR de objectos desta natureza deve incluir, tendo em conta a diversidade de situações em que se pode aplicar e a influência que a semântica da linguagem terá na sua concretização. Assim, na primeira secção, aborda-se a necessidade de tornar flexı́veis os aspectos sintácticos das representações persistentes do estado dos objectos, introduzindo-se o modelo em que se articulam as entidades, que oferecem este serviço. Na secção seguinte, apresentam-se as opções tomadas na disponibilização de um mecanismo, que permita a descrição sistemática do conteúdo dos objectos, de modo a que se possa 77 78 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS oferecer um serviço automático de SR, como forma de exigir um esforço mı́nimo de programação, tentando, no entanto, abarcar todos os casos possı́veis subjacentes à linguagem adoptada. Na terceira secção, descreve-se o funcionamento das primitivas de SR e a maneira como se enquadram com o restante modelo de suporte, até agora introduzido. De seguida, abordar-se-ão as questões ligadas com o controlo dos objectos envolvidos nas operações de SR e a possibilidade de limitar o seu número a subconjuntos dos conjuntos-de-salvaguarda. Finalmente, refere-se a possibilidade de gerar, como uma das representações externas possı́veis, código C++ que, uma vez compilado, possa reconstruir os objectos que desse modo foram salvaguardados. 4.1 Versatilidade na representação externa Enquanto que a representação interna de um objecto é univocamente definida pelo seu tipo e pela máquina em que se executa, já a sua representação externa pode assumir as mais diversas formas, sendo normalmente condicionada pela utilização que dela se pode fazer. De facto, a flexibilidade admitida para esta representação, pode ser usada como forma de melhorar o desempenho do serviço de SR ou, simplesmente, de modo a compatibilizar a descrição de aplicações, utilizando linguagens de configuração que outras bibliotecas ou aplicações possam executar (e.g. a geração de UIL [Young 90] numa ferramenta de construção de interfaces em que os widgets Motif são representados internamente por objectos C++). Tomando uma ferramenta de programação genérica, considerem-se os seguintes passos na construção de uma aplicação: • um protótipo será desenvolvido, numa primeira fase, durante várias sessões, por um grupo de pessoas que utiliza máquinas do mesmo tipo. • o protótipo será distribuı́do por outros grupos para teste e ajuste, possivelmente em máquinas com diferentes processadores. • atingido um protótipo estável, o resultado pode constituir uma versão da aplicação, a distribuir por eventuais clientes. No primeiro caso, a representação externa não exige grandes cuidados de tradução, a partir da representação interna, sendo apenas necessária a substituição das re- 4.1. VERSATILIDADE NA REPRESENTAÇÃO EXTERNA 79 ferências entre objectos, por referências entre representações externas dos mesmos, não dependentes do contexto de execução. De facto, isso é possı́vel, já que as questões de alinhamento e espaço ocupado pelos objectos, podem ser ignoradas neste caso. Na segunda situação, pelo contrário, já a forma de representação externa deve ser totalmente independente da máquina, sofrendo, porventura, alteração relativamente à representação interna. Por exemplo, valores inteiros podem ser traduzidos para cadeias de caracteres equivalentes, sendo feita na recuperação, a tradução inversa. As formas de representação externa, como uma cadeia de caracteres, são adoptadas, por exemplo, nos mecanismos de SR do Objective-C, ou na versão textual do OOPS. Outras formas, poderiam recorrer, alternativamente, à biblioteca de representação externa independente da máquina XDR. Finalmente, no terceiro caso, em que o desempenho na recuperação é fundamental, a representação externa deve permitir recuperações rápidas, por exemplo, gerando na salvaguarda, código que possa ser compilado e cuja execução recupere os objectos. Um mecanismo semelhante ao proposto na salvaguarda de objectos do Smalltalk, seria uma das hipóteses a considerar (ver 2.4.1). Já a recuperação, neste caso, não assume as caracterı́sticas desejadas, tendo em conta a natureza interpretada da linguagem. Para além da sua utilização como meio de salvaguardar contextos de aplicações alteradas, em tempo de execução, a SR de objectos pode também ser usada com outros objectivos: • transferência de objectos entre aplicações, em que uma aplicação escreve num meio de comunicação qualquer, de que outra poderá ler. A representação externa corresponde à representação intermédia normalmente usada nestes casos, e que permite tornar o mecanismo independente do contexto e da máquina em que o objecto se executa. • duplicação encadeada de objectos na mesma aplicação, usando, quer meios persistentes, quer simplesmente considerando o próprio espaço de endereçamento como o meio de salvaguarda, e fundindo os mecanismos de leitura e escrita num só. Entenda-se por duplicação encadeada1 , o 1 Correspondente ao deepCopy do Smalltalk. 80 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS mecanismo de cópia que não só copia os dados do objecto, mas que, no caso destes corresponderem a uma referência para outro objecto, duplica também o segundo, recursivamente. Em qualquer dos casos, o serviço envolve a capacidade de identificação e SR do conjunto-de-salvaguarda associado a um objecto: no primeiro caso porque se pretende ter independência do contexto de execução; no segundo, pela própria definição dada para duplicação encadeada de objectos. No entanto, ambas utilizam representações externas e meios de salvaguarda diferentes e, possivelmente, diferentes das usadas nos exemplos de utilização dados para a construção interactiva de uma aplicação. As soluções adoptadas nas linguagens de programação e bibliotecas, que oferecem serviços de SR são em geral limitadas. Por exemplo, no Eiffel e no Objective-C, é apenas possı́vel uma forma de representação externa, oferecendo-se simplesmente mecanismos para a utilização de diferentes meios de salvaguarda ou comunicação, que mesmo assim são tornados virtuais pelo sistema operativo (nome de ficheiro, ou descritor como forma de acesso a sockets ou pipes). No OOPS é alargado o mecanismo anterior, oferecendo duas formas de representação externa, uma binária e dependente da máquina, e outra textual e independente. No Smalltalk são utilizadas instâncias da classe Stream como objectos de escrita, podendo, por conseguinte, ser definidas classes derivadas, que redefinam os meios de salvaguarda. Porém, a redefinição da representação externa fica restrita a partes do código gerado, já que parte é incluı́da na codificação dos próprios métodos da classe Object, nas primitivas genéricas de SR. Neste caso, a mudança completa da representação externa passaria pela alteração desses métodos, sempre que representações diferentes fossem desejadas, implicando a impossibilidade da coexistência de formas diversas de SR. Na perspectiva de se encontrar uma solução, para o mecanismo de SR de objectos, que englobe versatilidade na representação externa e no meio sobre a qual se deve concretizar, pode estabelecer-se uma comparação, com as caracterı́sticas que definiram a evolução dos modelos de E/S nos sistemas operativos [Marques 90]. De facto, também sobre os mecanismos de E/S, se podem tecer considerações relativamente à diversidade do comportamento e representação de dados, que apresentam os periféricos em que se pretende executar essas operações. Assim, tal como 4.1. VERSATILIDADE NA REPRESENTAÇÃO EXTERNA 81 nesses modelos se evoluiu para a introdução de periféricos virtuais, uniformização das funções de E/S e definição de gestores de periféricos (device drivers) que executam a interacção real com os respectivos controladores, também no trabalho que aqui se apresenta, se irá oferecer: • uma classe base abstracta que permite o acesso aos mecanismos de SR, constituindo o equivalente ao periférico virtual. • uma interface genérica definida nessa classe, que uniformize as funções deste serviço. • a possibilidade de definir classes especı́ficas, dependentes da representação externa e meio de salvaguarda utilizados, e cujas instâncias executam as operações reais de escrita e leitura. À classe abstracta de E/S, proposta no ICE, deu-se o nome de IIO (ICE Input Output). Aos objectos instância das classes que dela derivam, e que correspondem no modelo comparado aos próprios gestores de periféricos, chamar-se-á globalmente objectos-de-E/S e às suas classes, classes de E/S. As primitivas de SR serão designadas, respectivamente, storeObject e retrieveObject, sendo definidas sobre essas classes. Na figura 4.1, mostra-se o modelo de E/S adoptado, destacando a storeObject retrieveObject es objectos-de-E/S duplicacação em memória representação textual/independente sobre descritores representação em código C++ sobre ficheiros ... Figura 4.1: Modelo de Entradas/Saı́das ou salvaguarda e recuperação de objectos versatilidade de opções que se podem tomar na forma de representação dos objectos, a partir de uma interface comum. O acesso ao serviço, pode ser feito através de um 82 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS apontador para a classe abstracta de E/S, representado na figura sob a legenda es, e ao qual se podem atribuir os objectos de qualquer das classes de E/S disponı́veis. 4.2 Salvaguarda e recuperação automáticas Para que a representação externa de um objecto seja completa, deve incluir toda a informação que o objecto contém, em tempo de execução. Desse modo, a introdução de um mecanismo de SR de objectos, envolve necessariamente o conhecimento semântico da sua representação interna, tal como é definida pelos tipos a que pertencem. De facto, para que um objecto seja guardado na sua totalidade, é necessário saber quais os objectos que nele são referidos, de modo a poder acederlhes, e quais os seus tipos, permitindo que também eles sejam guardados, recursivamente, até incluir todo o conjunto-de-salvaguarda na sua representação externa. Por outro lado, o conhecimento do tipo de todos os componentes envolvidos na estrutura de uma instância, poderá viabilizar a definição de formas diferentes de representação externa, para diferentes tipos da linguagem. A possibilidade de oferecer um serviço de SR de objectos, passa, então, pela capacidade de aceder à informação sobre a sua estrutura. Nas linguagens e bibliotecas que suportam serviços desta natureza, podem identificar-se duas abordagens: • ou essa informação é incluı́da de forma automática (i.e. sem intervenção do programador) no sistema de suporte a primitivas genéricas de SR (e.g. Smalltalk, Objective-C); • ou o programador, com conhecimento dessa estrutura, inclui para cada classe, código que execute a SR do estado dos objectos para essa classe (e.g. OOPS). Como um dos objectivos principais é a minimização, tanto quanto possı́vel, do esforço exigido ao programador, para que os serviços a oferecer no ICE estejam disponı́veis, deverá, sem dúvida, adoptar-se a primeira solução. No entanto, não oferecendo o C++, em tempo de execução, qualquer informação sobre a estrutura das instâncias de cada classe, a viabilidade deste mecanismo passa por encontrar uma forma sistemática de descrição dos objectos, que possa ser gerada automaticamente por ferramentas adequadas. Por exemplo, a utilização de um analisador 4.2. SALVAGUARDA E RECUPERAÇÃO AUTOMÁTICAS 83 da declaração de classes C++, como aquele que será proposto neste trabalho, pode nesse caso gerar, com alguma simplicidade, o código necessário para que essa informação esteja acessı́vel a primitivas genéricas de SR. Contudo, tal como é referido em [Cox 86] (para o C) e [Gorlen 87], o C++ não oferece um mecanismo de identificação de tipos, livre de ambiguidades. Por exemplo, a uma variável membro declarada do tipo int*, tanto pode ser atribuı́do um apontador para um valor inteiro, um vector de inteiros, ou mesmo algo menos imediato, recorrendo ao mecanismo de casts oferecido na linguagem. Ou seja, o tipo com que uma variável membro é declarada na sua classe, pode não coincidir com o tipo do objecto que na realidade lhe é atribuı́do, nomeadamente no caso de apontadores. Por outro lado, podem ocorrer situações em que, apesar dos tipos serem concordantes, a semântica associada ao objecto requer a execução de operações dependentes do contexto. Por exemplo, se um objecto tem como variável membro um descritor de ficheiro, é natural que na operação de recuperação se pretenda reabrir o ficheiro e não simplesmente usar o número inteiro correspondente a esse descritor, que não terá qualquer significado no novo contexto. No OOPS e no ET++ (2.4.5) qualquer dos problemas é evitado, responsabilizando o programador pela concretização de funções especı́ficas de SR para cada classe, tendo por conseguinte liberdade de introduzir inicializações de contexto e identificar, inequivocamente, o tipo dos objectos referenciados. No Objective-C (2.4.3), embora a SR seja automática, é estabelecido que, caso o tipo das variáveis membro não pertença a um conjunto de tipos limitado (que não inclui, por exemplo, a situação atrás referida - int*), o mecanismo não pode ser usado. No trabalho aqui apresentado, optou-se, por isso, por uma solução hı́brida. De facto, no ICE oferece-se um mecanismo de SR genérico, que assume tipos por defeito para todos os tipos possı́veis da linguagem, baseando-se em informação sobre a estrutura das instâncias, acessı́vel em tempo de execução, e a ser extraı́da da declaração das próprias classes. Quando ocorrem situações em que o tipo do objecto atribuı́do a uma variável, não coincida com o que é adoptado por defeito (que deverá ser o mais usual), ou que devam executar-se parametrizações de contexto, então é dada a possibilidade ao programador de definir alternativas, recorrendo a um mecanismo de redefinição de funções especı́ficas, semelhante ao adoptado nas 84 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS bibliotecas de C++ referidas. Por exemplo, se uma variável membro é do tipo char*, o mecanismo genérico de SR deverá assumir, que o objecto atribuı́do em tempo de execução, é sempre uma cadeia de caracteres terminada por um nulo (o caso mais usual), resolvendo a situação transparentemente. Caso isso não aconteça, o programador deve indicar, redefinindo as funções de SR para essa classe, qual é o tipo real dessa variável (e.g. um apontador para um caracter). Na concretização corrente do modelo de objectos persistentes do IK (ver 2.4.4), a solução adoptada, semelhante à do ICE, do ponto de vista da coexistência de uma solução hı́brida, difere desta, sobretudo porque no IK não é necessária a informação de tipo de cada variável de instância, já que usa uma representação externa semelhante à interna. Por outro lado, a identificação dos objectos referenciados, limitando-se aos objectos de classes derivadas da classe base comum, é feita recorrendo a uma função gerada pelo compilador da linguagem, e não a um formulário descritivo da estrutura da instância. 4.2.1 Descrição das instâncias no modelo de suporte A integração da informação sobre a estrutura das instâncias, no modelo de suporte às primitivas oferecidas pelo ICE, é naturalmente feita através dos objectos-de-classe. Assim, adoptando a solução proposta pela generalidade das linguagens que definem um serviço de SR automática de objectos, poderá associar- -se a cada objecto-de-classe, informação sobre as variáveis membro definidas na classe que ele representa. A estrutura completa de uma instância é descrita pela informação que o seu objecto-de-classe possui, em conjunção com a dos objectos-de-classe das suas classes base. Neste contexto, pode introduzir-se a seguinte condição ao modelo de suporte dos serviços oferecidos neste trabalho, de modo a incluir as primitivas genéricas de SR de objectos: Ψ4.1 Para todas as classes em que se pretenda fazer uso do mecanismo automático de SR das suas instâncias, deve existir um objecto-de-classe associado, que contenha a descrição das suas variáveis membro. 4.2. SALVAGUARDA E RECUPERAÇÃO AUTOMÁTICAS 85 Essa informação deve suportar mecanismos de acesso a cada variável membro de qualquer instância da classe, e permitir identificar o tipo com que foi declarada. 4.2.1.1 Localização das variáveis membro no objecto Em linguagens como o Smalltalk, em que se adopta um modelo de objectos uniforme, a localização das variáveis de instância a partir do tamanho do objecto é imediata, já que todas elas são referências a outros objectos, ocupando, por conseguinte, o mesmo espaço em memória. Nestes casos, o endereço de cada variável pode ser encontrado, considerando o endereço do inı́cio do objecto e a ordem em que a variável ocorre na definição da classe. No C++, no entanto, não é possı́vel fazer deduções sobre o tamanho das variáveis membro de uma classe, somente a partir do tamanho das suas instâncias. Essas variáveis podem ser apontadores, as próprias instâncias de outra classe, tipos fundamentais, etc, ocupando, eventualmente, espaços de memória diferentes. Por outro lado, nem mesmo sabendo o tamanho definido pelo tipo de cada variável membro, se pode determinar as suas localizações na estrutura da classe, sem ter em conta as questões de alinhamento, impostas pelo ambiente de execução. De facto, por razões de eficiência, as variáveis membro podem ser dispostas de maneira diferente, em estruturas de instância diferentes, ainda que para a mesma máquina. Por exemplo, para as seguintes definições de classe: class A { char a; char b; // ...}; class B { char a; B b; // ...}; não é possı́vel dizer que a variável b pode ser sempre acedida, somando a mesma constante ao endereço de inı́cio do objecto, quer este seja instância de A ou B. Por exemplo, numa Sun 3/50 a diferença entre o endereço de um objecto e o da sua variável membro b, é -1 ou -2 respectivamente, se o objecto pertencer a A ou B. Felizmente, para uma classe em particular e sobre uma determinada máquina, é assegurado que, qualquer que seja a sua instância, a diferença entre o endereço de inı́cio desta e o de qualquer das suas variáveis de membro, é sempre o mesmo. 86 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS Para o caso do exemplo anterior, o deslocamento da variável membro b em relação ao inı́cio de qualquer instância de A, será sempre -1, e para qualquer instância de B, -2. Esta informação será então usada no modelo de suporte do ICE, traduzindo-se na seguinte condição: Ψ4.2 Para cada variável membro de uma classe, sobre a qual se pretenda fazer uso do mecanismo de automático de SR de objectos, deve ser incluı́do, no respectivo objecto-de-classe, o deslocamento (offset) da variável, relativamente ao inı́cio das instâncias dessa classe. 4.2.1.2 Identificação do tipo das variáveis membro Do mesmo modo que para a localização, também a informação explı́cita de tipo para as variáveis de instâncias no Smalltalk, é desnecessária. De facto, o conhecimento da localização dessas variáveis, que são referências a objectos, implica o conhecimento do seu tipo, representado pelo objecto-de-classe respectivo2 . No ICE essa informação só é implı́cita, tal como foi introduzida no capı́tulo anterior, para os objectos ICE. Assim, se todas as variáveis membro de qualquer classe, fossem, por exemplo, apontadores para objectos ICE, então, sabendo a sua localização, o acesso ao objecto-de-classe poderia ser feito, recorrendo à função membro isA. Não sendo este o caso geral das classes C++, e porque não se pretende oferecer um mecanismo limitado, são utilizados, também neste contexto, os objectos-de-tipo segundo a mesma perspectiva com que foram introduzidos no capı́tulo anterior. No seguimento do requisito relativo à existência de objectos-de-tipo, para os argumentos de uma função membro Ψ3.2, pode agora introduzir-se a seguinte condição: Ψ4.3 A informação de tipos deve estender-se a todos os tipos que sejam utilizados na declaração da estrutura das instâncias de uma classe, sobre a qual se pretenda fazer uso do mecanismo automático de SR de objectos. Note-se que, se para um objecto é conhecido o tipo de cada um dos seus componentes, então para cada um destes a mesma condição deve ser aplicada recursiva2 Aliás, nesta linguagem, o próprio acesso ao objecto-de-classe no mecanismo de salvaguarda é encapsulado através de métodos definidos sobre a classe Object. Esses, por sua vez usam primitivas da máquina virtual que finalmente acedem à informação presente nos objectos-de-classe. 4.2. SALVAGUARDA E RECUPERAÇÃO AUTOMÁTICAS 87 mente, de forma a incluir todos os tipos envolvidos no conjunto-de-salvaguarda em que se inserem. 4.2.2 Primitivas que definem a SR automática Incluı́da a informação sobre a estrutura das instâncias nos objectos-de-classe, é então normal que sejam definidas primitivas para SR de instâncias nas metaclasses, de forma a poderem ser invocadas sobre esses objectos. Essas primitivas devem estar livres de conteúdo sintáctico sobre a representação externa dos objectos, articulandose com as primitivas que definem a interface aos objectos-de-E/S, de modo a permitir a versatilidade proposta nas formas de representação. Às primitivas responsáveis pela SR do conteúdo semântico associado às instâncias de uma classe, deu-se o nome de storeInstance e retrieveInstance, respectivamente, devendo incluir como argumentos o objecto a ser salvaguardado ou recuperado, e o objecto-de-E/S com que se pretende fazer a operação. No seu funcionamento normal, incluirão uma chamada à primitiva correspondente da classe base, seguida, para cada uma das variáveis membro que a sua classe define, da invocação, agora sobre o objecto-de-E/S, da primitiva que executará a escrita, ou leitura, do objecto correspondente à variável membro em questão. objecto-de-classe (classe base) objectos-de-tipo tipo-de-a SRInstância (retorno) objecto-de-classe variáveis membro SRInstância tipo-de-b objecto-de-E/S a b ESMembro<tipo-de-a, a> ESMembro<tipo-de-b, b> Figura 4.2: Salvaguarda e recuperação automática de objectos. 88 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS Na figura 4.2 pode ver-se, a tracejado, o fluxo de execução das primitivas descritas, referidas por SRInstância, até à invocação das primitivas de escrita/leitura de variáveis membro (ESMembro), sobre o objecto-de-E/S. Note-se que, deste modo, a execução de SRInstância corresponde à salvaguarda/recuperação da totalidade da informação, directamente incluı́da por uma instância. 4.2.3 Mecanismo de redefinição da SR automática A definição de alternativas ao mecanismo automático de SR de objectos no ICE, para uma classe qualquer, é, como se disse, feita através de funções especı́ficas, de forma semelhante à adoptada por outras bibliotecas em C++, que oferecem este serviço. No entanto, enquanto que nestas a SR de objectos é totalmente concretizada por essas funções, no ICE há que integrá-las com o mecanismo genérico oferecido, de tal forma que, apenas nos casos em que essa redefinição seja pretendida, se evita o uso do serviço automático. objecto-de-classe classe base objecto-de-tipo objecto-de-tipo SRInstância tipo-de-a tipo-de-c objecto-de-classe variáveis membro função específica de SR SRInstância SRMembro<tipo-de-a, a> verifica ESMembro<tipo-de-a, a> ESAlternativo<tipo-de-c, c> objecto-de-E/S Figura 4.3: Redefinição da salvaguarda e recuperação automática de objectos 4.2. SALVAGUARDA E RECUPERAÇÃO AUTOMÁTICAS 4.2.3.1 89 As primitivas envolvidas na redefinição Na figura 4.3 representa-se, nas linhas a tracejado, o fluxo de execução do conjunto de primitivas identificadas por SRInstância, para uma classe em que se definiu uma alternativa ao mecanismo automático de SR. A invocação de SRInstância, sobre o objecto-de-classe que representa a classe base, é incluı́da na função especı́fica, embora pudesse ser feita implicitamente antes da chamada a essa função, de forma a dar mais flexibilidade ao mecanismo de redefinição, que assim pode executar operações dependentes do contexto, mesmo antes da recuperação dos dados do objecto, referentes às classes base. Por outro lado, pode também observar-se na figura, a utilização de primitivas de SR de variáveis membro sobre os objectos-de-classe, identificadas por SRMembro3 , com o intuito de permitirem a verificação do tipo especificado para essa variável na função de redefinição, antes de invocarem as operações correspondentes sobre o objecto-de-E/S. Às primitivas sobre o objecto-de-classe deu-se o nome storeMember e retrieveMember, respectivamente para salvaguarda e recuperação. Finalmente, refere-se a utilização de outro conjunto de primitivas sobre os objectos-de-E/S, cujo objectivo é executar a operação de SR do objecto, usando variáveis alternativas às variáveis membro da instância. A função dessas primitivas, identificada por ESAlternativo, será semelhante à das identificadas por ESMembro, com excepção, eventualmente, das caracterı́sticas sintácticas da representação externa. É interessante notar que, no caso de existir uma função alternativa à execução do algoritmo de SR automáticas para uma classe, a informação que no seu objecto-de-classe deveria constar para o suporte a esse serviço, pode assim ser omitida. Este facto, ressalvado na figura pela representação, a tracejado, da estrutura que contém essa informação, tem como consequência, para os casos em que essa informação não exista, a impossibilidade de verificar o tipo da redefinição da SR das variáveis membro, bem como a sua utilização com outros objectivos (e.g. inspecção de objectos). Por outro lado, permite evitar a introdução de mais objectos-de-tipo, 3 Estas primitivas são definidas sobre os objectos-de-classe, ao contrário das referidas por ESMembro que se usam nos objectos-de-E/S. 90 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS se na redefinição da SR se puderem usar soluções alternativas de descrição do estado da instância. 4.2.3.2 As funções especı́ficas Relativamente às funções especı́ficas, deve dizer-se que a introdução de funções virtuais, implicaria aqui, tal como acontece no OOPS e no ET++, a limitação da utilização deste serviço, a classes derivadas de uma classe base comum. Não sendo esse um objectivo deste trabalho, e tendo em conta que se deve, nos objectos-de-classe, ter acesso à execução dessas funções, então deve ser incluı́do nestes objectos, por exemplo, o endereço em que essas funções são definidas, resumindo-se a sua invocação a uma chamada por endereço. 4.3 As primitivas de salvaguarda e recuperação Na secção anterior, abordaram-se os mecanismos gerais de SR associados à informação contida nos objectos, e cuja estrutura é definida pelos respectivos tipos. De seguida, ir-se-á descrever a forma como as primitivas que oferecem o serviço de SR, definidas sobre o objecto-de-E/S virtual, recorrem a esses mecanismos nas operações de escrita e leitura de objectos, e, em geral, as questões associadas à execução do algoritmo que concretizam. 4.3.1 Utilização da informação do tipo do objecto Considere-se as funções membro storeObject e retrieveObject definidas sobre a classe IIO, a que se chamará em conjunto ESObjecto. O comportamento genérico dessas primitivas, independentemente da sintaxe da representação externa, concretizada pelo objecto-de-E/S em particular e relativo ao tipo do objecto a guardar ou recuperar, poderá resumir-se ao seguinte: • caso o objecto pertença a um tipo fundamental (char, int, ...), então a primitiva deve ser capaz de executar a operação de escrita/leitura directamente, já que, neste caso, são apenas colocadas questões sintácticas sobre a sua representação externa. 4.3. AS PRIMITIVAS DE SALVAGUARDA E RECUPERAÇÃO 91 • se o objecto for um apontador ou vector em geral, cada um dos objectos referenciados deve igualmente ser escrito/lido, recursivamente. • sendo o objecto a escrever/ler uma instância de classe, a primitiva deve invocar SRInstância sobre o objecto-de-classe correspondente (passado como argumento no identificador-de-objecto). Assim, em conjunção com o comportamento atrás definido para as primitivas SRInstância, fica assegurada a SR de objectos (ou valores) de qualquer tipo, podendo ser feita, respectivamente, por: objecto_de_ES->storeObject (objecto); objecto_de_ES->retrieveObject (objecto); em que objecto de ES corresponde a uma instância de uma classe derivada de IIO. A necessidade de ter acesso à informação de tipo do objecto que se pretende guardar/recuperar, leva a que o argumento destas primitivas seja, mais uma vez, um identificador-de-objecto. Note-se que o último ponto indicado acima, define também recursividade no algoritmo, se se tiver em atenção que as primitivas SRInstância invocarão, sobre o objecto-de-E/S, uma das primitivas identificadas atrás por ESMembro ou ESAlternativo. Estas por sua vez, de nome storeMember, retrieveMember4 e storeAlternate, retrieveAlternate, respectivamente, já que também executam a SR de objectos correspondentes a variáveis membro, ou variáveis alternativas definidas no contexto das funções especı́ficas de SR, deverão ter um comportamento semelhante a ESObjecto, à parte, novamente, de questões sintácticas. Nesse contexto, poderá definir-se uma interface a essas primitivas, que inclua, pelo menos, um identificador-de-objecto, que indique o tipo da variável a escrever ou ler e o seu endereço. 4 Tal como as SRMembro definidas sobre os objectos-de-classe. 92 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS 4.3.1.1 Resolução de tipos na salvaguarda Do que foi dito sobre as possı́veis diferenças entre o tipo com que é declarada a variável membro e o tipo real do objecto que lhe é atribuı́do, que deve coincidir com o da representação externa, devem ser feitas algumas considerações, de modo a evitar, sempre que possı́vel, a necessidade de redefinição do mecanismo de SR. Viu-se que, no caso das variáveis membro serem apontadores para objectos ICE e podendo na declaração da classe ser usada uma forma genérica de referência a estes objectos (e.g. IObject*), é possı́vel aceder ao tipo real do objecto referenciado, recorrendo à função membro isA. Então, é simples, neste caso, antes de executar a salvaguarda do objecto apontado, substituir-se o tipo declarado pelo tipo real do objecto. No caso das variáveis membro serem outro tipo de apontadores, o mecanismo anterior não é, em geral, aplicável. No entanto, pode ainda considerar-se a situação em que as variáveis são declaradas como void* mas têm atribuı́dos objectos ICE. Neste caso, é sufi- ciente encontrar um mecanismo, que permita a distinção entre os objectos ICE e os restantes, a partir do seu endereço. A solução adoptada passa pela utilização do serviço de nomes, no qual apenas estão registados objectos ICE. Bastará então interrogar o serviço sobre a existência do objecto, usando os mecanismos de acesso directo que este deve oferecer (por exemplo, para interrogar os nomes associados a um endereço). 4.3.1.2 Resolução de tipos na recuperação A resolução de tipos na recuperação não impõe, em princı́pio, grandes cuidados. De facto, desde que na representação externa, seja incluı́da a informação necessária para a identificação inequı́voca do tipo de cada objecto descrito, o algoritmo terá apenas que ter em consideração eventuais degradações da informação, verificando, para isso, a compatibilidade dos tipos lidos, com aqueles que são esperados. Note-se que, na maioria dos casos, os tipos devem coincidir exactamente, i.e., se uma variável membro é do tipo A, em que A é uma classe, então o tipo do objecto descrito deve ser A. Já no caso da variável membro ser um apontador, o objecto por ele apontado e descrito na representação externa, deve apenas ser conforme com a informação de 4.3. AS PRIMITIVAS DE SALVAGUARDA E RECUPERAÇÃO 93 tipo de que se dispõe, em tempo de execução. Repare-se, que esta solução vem de encontro à resolução de tipos apontadores, na salvaguarda. 4.3.2 Criação de objectos na recuperação Enquanto a salvaguarda de objectos envolve apenas a descrição do seu estado, já a sua recuperação implica também a criação do espaço de memória, em que o objecto se irá reconstituir, seguida então, do seu preenchimento a partir dos dados encontrados na sua representação externa. No entanto, é importante referir, que o espaço onde se irá recuperar o objecto, nem sempre deve ser criado. De facto, no funcionamento normal das primitivas de recuperação de objectos, devem considerar-se as seguintes situações, em que não deve ocorrer reserva de espaço para o objecto: • para a primitiva retrieveObject, caso se especifique o espaço de memória para o objecto, no identificador-de-objecto que é passado por argumento. • para as primitivas retrieveMember, já que, como o objecto corresponde à própria variável membro, o espaço existe obrigatoriamente. • para retrieveAlternate, já que sendo normalmente objectos locais à função de redefinição, pode mais facilmente, ser aı́ controlada a sua longevidade. Então, a criação de objectos resume-se, em geral, ao caso do “objecto”, passado como argumento, ser um apontador, em que se deve criar o objecto apontado. 4.3.2.1 Integração com o serviço de nomes Na recuperação de objectos, deve ainda ter-se em consideração, as possı́veis colisões que os nomes dos objectos a recuperar, irão introduzir no serviço de nomes da aplicação, que os está a ler. De facto, quando se introduziu o serviço de nomes global do ICE, impôs-se que um nome, eventualmente composto pela referência ao seu dono, identifica univocamente um objecto. Então, se a um objecto a recuperar, foi dado um nome igual ao de um que exista em execução, o primeiro não poderá ser registado naquele serviço. Neste sentido, os objectos-de-E/S devem oferecer capacidade de serem parametrizados com alternativas definidas pelo utilizador, de forma a que, situações 94 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS como esta, possam ser resolvidas de diferentes maneiras. Por outro lado, comportamentos por defeito poderão ser adoptados, de modo a que essas situações ocorram transparentemente, na maioria dos casos. A classe virtual de objectos-de-E/S considera por defeito que, no caso de nenhuma alternativa ser especificada e o tipo do objecto, com o qual se deu a colisão, ser igual ao do objecto a reconstruir, então o espaço do objecto em execução é utilizado, sobrepondo-se a informação extraı́da da representação externa, à que o objecto apresenta nesse instante. Esta situação ocorre normalmente quando um objecto criado no próprio código compilado da aplicação, foi alterado e guardado, sendo recuperado na mesma aplicação. 4.3.2.2 Reserva do espaço de memória O mecanismo mais simples de criação do espaço de memória para a reconstrução de um objecto, passaria pela invocação da função malloc, ou semelhante, desde que conhecido o tamanho do objecto a recuperar. No entanto, na criação desse espaço, estão implicadas inicializações referentes às tabelas de métodos virtuais, as quais dificilmente se poderiam concretizar, de maneira independente do compilador usado. Assim, o recurso ao mecanismo normal de reserva desse espaço no C++, para uma dada classe, parece ser a solução indicada. Esse mecanismo passa, como se viu, pela invocação do construtor associado à execução do operador new correspondente. No entanto, neste caso irá ocorrer uma inicialização dos dados e, eventualmente, do contexto em que o objecto se irá inserir, definida pelo próprio construtor, que não corresponderá, em certas situações, aos requisitos de um mecanismo de recuperação de objectos. Por exemplo, considere-se uma biblioteca de classes para a construção de interfaces com o utilizador. Numa situação normal, a criação de objectos de uma dessas classes, corresponderá à apresentação no visor de uma forma gráfica associada (e.g. um botão, icon, menú, etc). Utilizando o esquema acima, o utilizador veria, provavelmente, um objecto a aparecer, desaparecer e finalmente aparecer noutro local, com outra forma. Como alternativa, a solução a adoptar parece passar pela utilização de um construtor especı́fico e obrigatório, cuja única função deve ser, simplesmente, executar as inicializações intrı́nsecas à linguagem. Esta solução é, por exemplo, adoptada no 4.3. AS PRIMITIVAS DE SALVAGUARDA E RECUPERAÇÃO 95 OOPS e no ET++. De forma a encontrar uma solução de compromisso, entre a obrigação de declarar esse construtor, para todas as classes em que o mecanismo é usado, e a utilização de um construtor qualquer, que existe, implı́cita ou explicitamente, para todas as classes (ver 3.2.1), optou-se por recorrer à utilização de uma função sem argumentos, que deverá invocar o construtor adequado. Essa função poderá ser definida automaticamente pela ferramenta de geração dos objectos-de-tipo associada ao ICE, invocando um construtor também sem argumentos, cuja existência é assegurada pelo compilador. Em alternativa, o programador poderá indicar uma função de criação dos objectos da classe, concretizando-a de forma a invocar o construtor que pretenda. Em qualquer dos casos, os objecto-de-classe deverão oferecer uma interface (createRetrieveSpace) à execução dessa função. Os objectos-de-E/S invocarão então, essa interface, sempre que for necessária a criação de espaço para a recuperação de um objecto, preenchendo de seguida esse espaço, segundo o algoritmo atrás descrito, com os dados lidos (e possivelmente convertidos) do meio de salvaguarda que utilizem. 4.3.3 Sintaxe das representações e meios de salvaguarda Relativamente às primitivas de SR, definidas na classe IIO, falou-se, até agora, na forma como se articulam com a informação do tipo dos objectos e com as restantes primitivas definidas sobre os objectos-de-classe. No entanto, será também nos objectos-de-E/S, que irão ser estabelecidas: • as formas de representação externa; • os meios de salvaguarda a utilizar. Naturalmente, tendo em conta a versatilidade pretendida, a polı́tica de utilização e gestão de qualquer dos pontos acima, deve ser da exclusiva responsabilidade das classes derivadas de IIO. Estas, ao contrário, deverão, tanto quanto possı́vel, ignorar as formas de manuseamento de tipos e interacção com os objectos-de-classe. Nesse sentido, as primitivas ESObject, ESMember e ESAlternate, irão invocar métodos 96 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS abstractos (e.g. writeClassHeader, readClassHeader, ...), cuja concretização em classes derivadas irá definir as formas de escrita e leitura. 4.4 Operações sobre o conjunto-de-salvaguarda Nas secções anteriores ficou definido o mecanismo que, a partir de um objecto sobre o qual se pretende executar uma operação de salvaguarda ou recuperação, permite percorrer todos os objectos envolvidos no seu conjunto-de-salvaguarda, executando recursivamente o algoritmo concretizado no serviço. No entanto, pela própria definição de conjunto-de-salvaguarda, existirão, porventura, ciclos fechados de referências entre objectos, que é necessário detectar, de forma a assegurar a terminação da execução do algoritmo. Por outro lado, podem ocorrer situações, em que a salvaguarda ou recuperação de todos os objectos incluı́dos num dado conjunto-de-salvaguarda é desnecessária, ou mesmo indesejável. Então, a introdução de mecanismos que reduzam o número de objectos envolvidos nas operações de SR, trará, sem dúvida, vantagens para a versatilidade deste serviço. 4.4.1 Detecção de objectos já guardados ou recuperados A detecção dos objectos sobre os quais já se aplicou o algoritmo, é feita recorrendo a uma tabela, pertencente ao objecto-de-E/S, em que são registados - na salvaguarda antes de ser feita a operação de escrita, e na recuperação depois de criado o espaço para o objecto. Assim, sempre que sobre um objecto se execute uma operação de salvaguarda, é verificado se, nessa tabela, o seu endereço já foi registado: se não constar, regista-se e o algoritmo continua, salvaguardando a descrição do objecto; se já estiver registado, o objecto-de-E/S compõe uma referência independente do contexto de execução, e inclui-a na representação externa onde o objecto foi referenciado, em vez da sua descrição. Na recuperação, a tabela tem um papel inverso, já que deve permitir a conversão das referências encontradas na representação externa, para os endereços dos objectos no contexto de execução. No OOPS, no ET++ e no Objective-C é usado o mesmo processo, para o controlo dos objectos envolvidos nos conjuntos-de-salvaguarda. Nestes, tal como foi feito no 4.4. OPERAÇÕES SOBRE O CONJUNTO-DE-SALVAGUARDA 97 ICE para algumas concretizações de objectos-de-E/S, é usada uma forma simples de referência entre objectos na representação externa, que inclui o número de ordem do objecto na sequência de salvaguarda ou recuperação. 4.4.1.1 Referências para variáveis membro O mecanismo atrás descrito funciona bem, enquanto não se considerarem possı́veis referências aos endereços das variáveis membro de um objecto. De facto, neste caso, algum cuidado deve ser tomado, tendo em conta as possibilidades oferecidas por uma linguagem como o C++. class X : public /*...*/ { X* xp; Y* yp; public: Y y; void set (X* ob) {xp = ob; yp = & ob->y;} }; X a, b; a.set (& b); Suponha-se, por exemplo, o caso da classe acima em que se usou a função membro set na parametrização de duas instâncias, como se mostra do lado direito do quadro. Nesta situação, o objecto a refere b e a variável membro y de b. Se apenas forem registados, na tabela de salvaguarda, os objectos na sua globalidade, tal como é feito nas bibliotecas e linguagem acima referidas, então, na representação externa resultante da invocação de uma operação de salvaguarda sobre a, o objecto correspondente à variável membro y de b estaria descrita duas vezes, como se mostra na figura 4.4. Esta situação não seria grave, se, na recuperação dessa representação externa, não se acabasse por gerar também um objecto do tipo Y, referenciado em a, mas autónomo, embora com o mesmo conteúdo da variável membro y de b (ver figura 4.4). Uma solução possı́vel, parece passar por incluir na tabela que regista os objectos guardados, também os endereços das respectivas variáveis membro. Por outro lado, tendo em conta que as referências a essas variáveis podem ocorrer antes da sua recuperação (veja-se o caso de ter feito também b.set(& a);), então o seu registo na tabela do objecto-de-E/S em uso, deve ser feito assim que o seu endereço possa 98 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS a(1) b(2) a xp y yp xp y yp a xp y yp b xp y yp xp y yp xp y (3) (3) yp salvaguarda b recuperação Figura 4.4: Referência a variáveis membro não resolvida ser acedido, i.e., quando o objecto de que fazem parte é guardado. Na recuperação, de igual modo, quando um objecto é criado, tanto o seu endereço como os endereços de todas as suas variáveis membro, devem ser colocados nessas tabelas. Em qualquer dos casos, o acesso a essa informação deve ser sempre feito, recorrendo à informação contida nos objectos-de-classe de cada instância. Mesmo assim, esta solução, não resolve todos os casos possı́veis de acesso aos endereços das variáveis membro. De facto, considere-se o caso em que a função membro set é definida como se segue: void X::set (X* ob) { yp = & ob->y; } Então, para o mesmo código, a referência do objecto a para o objecto b deixou de existir, ficando, apesar disso, a referência de a para o membro y de b. Nesta situação, e em situações idênticas, não é possı́vel, somente pela informação que é dada na declaração da classe, deduzir que o objecto y é na realidade uma variável membro de b. Para estes casos, embora desaconselhados em boa programação OO, e já que serão resultantes de opções tomadas na definição da classe, é proposto no ICE, que todas as variáveis membro a cujo endereço se possa aceder em objectos distintos, sejam registadas pelo programador no serviço de nomes global oferecido, indicando como dono a instância em que estão incluı́das. Neste caso, o serviço de SR deve assegurar que, para todos os objectos para os quais é declarado um dono no serviço de nomes globais, primeiro é guardado o objecto dono e, só depois, o dependente. Então, esta solução, em conjunção com o registo dos endereços tal como foi acima 4.4. OPERAÇÕES SOBRE O CONJUNTO-DE-SALVAGUARDA 99 apresentado, durante a operação de salvaguarda, resolve a grande maioria dos casos, mesmo para concretizações menos correctas. 4.4.2 Limitação do número objectos envolvidos Considere-se, por exemplo, um editor com as caracterı́sticas da INGRID, em que objectos de apresentação (botões, menús, ...) são criados e parametrizados, de modo a constituı́rem uma interface para uma aplicação. Possivelmente, esses objectos são ligados a outros, por exemplo uma tela onde se irão definir as suas caracterı́sticas espaciais, que, no entanto, serão parte constituinte do editor e não da interface a desenvolver. No fim de cada sessão de edição, a interface até então definida, deve editor tela Ficheiro aplicação caixa aplicação objecto-de-E/S caixa-texto caixa escreve storeObject lê retrieveObject ... caixa-texto ... Figura 4.5: Utilização do mecanismo de SR num editor de interfaces. ser guardada num meio persistente, de modo a continuar o seu desenvolvimento em sessões seguintes. A utilização de um mecanismo ideal de SR de objectos para realizar estas funções, pode ser visto na figura 4.5. Utilizando a primitiva oferecida e admitindo a existência de uma classe derivada IIO, a que se chamará ITextualIO, que concretiza, como sintaxe de representação, uma descrição textual dos objectos (e.g semelhante à do OOPS), o seguinte código poderia ser incluı́do no editor e executado em consequência de um pedido de salvaguarda ou recuperação: 100 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS ITextualIO io (nome-do-ficheiro); if (salvaguarda) io.storeObject (aplicacao); else { IOID tmp; io.retrieveObject (tmp); } A criação do objecto-de-E/S implicará também a abertura de um ficheiro, cujo nome é, por exemplo, especificado pela pessoa que está a utilizar o editor. No entanto, usando um serviço de SR como até agora foi proposto, a operação de salvaguarda implicaria, tanto a salvaguarda dos objectos componentes da interface, como daqueles que, pertencendo ao editor, fazem contudo parte do seu conjunto-de-salvaguarda. Na operação de recuperação da sessão de edição seguinte, os objectos pertencentes ao editor seriam também recuperados, sobre os já existentes, resultando em simples perda de tempo e, porventura, na destruição de parametrizações feitas no editor, antes da nova edição. O problema assim introduzido pode resolver-se, por um lado reduzindo o número de objectos descritos numa representação externa, como consequência de uma operação de escrita, e por outro, introduzindo no serviço a capacidade de os readaptar ao contexto de execução em que irão ser reconstruı́dos, estabelecendo as ligações que foram omitidas na representação externa. Mais uma vez, a utilização de objectos especı́ficos de escrita e leitura, os objectos-de-E/S, e do serviço de nomes global, permitindo a identificação de objectos no contexto de execução, mas independentemente deste, oferece a possibilidade de resolver esta questão. Assim, o serviço de base oferecido pelos objectos-de-E/S, inclui a possibilidade do programador especificar um conjunto de objectos, que, embora pertencentes ao conjunto-de-salvaguarda a ser guardado, não serão descritos na representação externa resultante dessa operação. Então, onde quer que nessa representação devesse estar a descrição desses objectos, é colocada, em vez disso, uma referência independente, quer do contexto de execução, quer da representação externa. Na operação de recuperação, quando é detectada a presença de uma dessas referências, deve ser possı́vel transformá-la no endereço de um objecto equivalente, em execução no novo contexto. A utilização de referências alternativas para esses objectos, constituı́das 4.4. OPERAÇÕES SOBRE O CONJUNTO-DE-SALVAGUARDA 101 pelo nome com que estão registadas no serviço de nomes global, é uma solução que pode ser usada em grande número de casos e que, por essa razão, é oferecida como o mecanismo por defeito na generalidade das classes de objectos-de-E/S. Considere-se então, o exemplo atrás mencionado. Admita-se, por exemplo, que a ligação aos objectos do editor é feita no objecto raiz da interface da aplicação a que se chamará "aplicacao", para o objecto "tela" do editor (ver figura 4.5). Então, se na salvaguarda se definir, sobre o objecto-de-E/S, que a descrição do objecto "tela" deve ser substituı́da pelo seu nome, na recuperação, o próprio comportamento por defeito do objecto-de-E/S, quando ocorre uma colisão de nomes, pode ser usado para restabelecer as ligações com o editor. No entanto, esta situação só é tão simples, para o caso em que o contexto em que se recupera o objecto, é semelhante àquele em que se efectuou a operação de salvaguarda. Considere-se que no mesmo editor se pretende usar o mecanismo de SR a fim de concretizar um serviço, denominado na literatura por clipboard, realizado sobre outra aplicação e que permita a visualização de objectos retirados ou copiados de uma interface em construção, que posteriormente poderão ser incluı́dos noutra, ou na mesma interface (ver figura 4.6). Tal como se mostra na figura, a execução de objecto-de-E/S editor tela aplicação storeObject objecto-de-E/S caixa caixa-texto retrieveObject escreve canal lê clipboard vista caixa-texto Figura 4.6: Utilização do mecanismo de SR na concretização de um clipboard. uma operação de cópia de um objecto em edição (caixa-texto) para a aplicação clipboard, seria idealmente realizada pela invocação da primitiva de salvaguarda sobre um objecto-de-E/S, no editor, que enviaria a descrição do objecto seleccionado, 102 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS para um canal de comunicação (e.g. pipe ou socket), previamente estabelecido entre editor e clipboard. Por sua vez, esta aplicação executava uma operação de leitura sobre o mesmo canal, reconstruindo assim o objecto seleccionado no editor, no seu espaço de endereçamento. A inclusão do objecto noutra interface e possivelmente noutra execução da aplicação editor, seria simplesmente feita, recorrendo a uma operação inversa sobre outro canal de comunicação. Recorrendo à solução proposta para a situação anteriormente exemplificada, com o intuito de resolver a limitação dos objectos envolvidos nas operações de SR, poder-se-ia indicar, por exemplo, que o objecto a não guardar seria, neste caso, a caixa onde se insere o objecto identificado por caixa-de-texto, que se pretende copiar. No entanto, na aplicação clipboard não existe, eventualmente, nenhum objecto com o mesmo nome do objecto caixa. Por outro lado, pretende-se que o objecto copiado venha a aparecer sobre um objecto especı́fico desta aplicação. Os objectos-de-E/S, tal como definidos na própria classe abstracta de E/S, oferecem, neste sentido, um mecanismo de parametrização, que permite especificar acções a executar, quando na recuperação de um conjunto-de-salvaguarda é detectada a falta de um objecto. 4.5 A geração de código A generalização da possibilidade de definir diferentes sintaxes na representação externa dos objectos, leva a considerar a hipótese de concretizar, sobre o mecanismo oferecido, objectos-de-E/S especı́ficos para a geração de código. Contudo, à primeira vista, a concretização de um serviço SR automático, que recorra à geração de código, parece essencialmente diferente de outra, que passa simplesmente pela descrição do estado de um objecto. De facto, no primeiro caso, será de esperar encontrar código que invoque métodos definidos pela classe e que dificilmente, a partir da sua declaração ou da declaração das variáveis membro da sua classe, se poderão associar à parametrização desta ou daquela variável membro. Já no segundo, como se trata de escrever informação sobre as próprias variáveis membro, a sua declaração é, como se viu, normalmente suficiente para concretizar um serviço com essas caracterı́sticas. Porém, não pretendendo incluir neste serviço, todo o conhecimento que o progra- 4.5. A GERAÇÃO DE CÓDIGO 103 mador da classe colocou no seu código, pode optar-se por uma solução de compromisso, semelhante à utilizada no mecanismo de salvaguarda de objectos do Smalltalk (ver 2.4.1). Nesse sentido, pode pensar-se no código gerado, simplesmente como uma sequência de invocações a primitivas, que parametrizam cada uma das variáveis membro de uma instância. No Smalltalk essas primitivas corresponderiam às mensagens instVarAt:put: que, sendo definidas na classe Object, estão disponı́veis para todos os objectos. Por exemplo: (A basicNew instVarAt: 1 put: (B basicNew ...); instVarAt: 2 put: (B basicNew ...); ... seria parte do código gerado em Smalltalk, na salvaguarda de uma instância da classe A, cujas primeiras duas variáveis referissem duas instâncias de uma classe B. No ICE, no entanto, surgem alguns problemas, já que, por um lado não existe uma classe base comum, por outro é possı́vel a existência de variáveis membro, que não sejam simplesmente referências para objectos. O primeiro caso pode facilmente resolver-se introduzindo a primitiva sobre os objectos-de-classe, visto que também é neles possı́vel o acesso às variáveis membro da instância. O segundo soluciona-se oferecendo uma outra primitiva, que permita simplesmente a obtenção da referência para cada variável membro, utilizando essa referência como um objecto normal a recuperar. Considere-se agora em C++, a classe A, em que a primeira variável membro é do tipo B* e a segunda do tipo B. A* o1 = classe_A.createRetrieveSpace (); B* o2 = classe_B.createRetrieveSpace (); // parametriza o2 classe_A.setMember (o1, 1, o2); B* o3 = classe_A.getMember (o1, 2); // parametriza o3 ... Note-se que, na segunda variável membro, se utiliza o seu espaço no objecto o1. 104 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS Com esta finalidade de salvaguardar descrições desta natureza, as funções membro correspondentes à salvaguarda de variáveis membro sobre os objectos-de-E/S, deverão incluir, como argumento, uma indicação que lhes permita saber, por exemplo, o ı́ndice ou o nome da variável. Nesse caso, poderão gerar código, que invoque funções como getMember e setMember do exemplo acima (apenas se usou o ı́ndice). Relativamente à geração de código que preencha o estado de um objecto, existe, no entanto, uma complicação imposta pela forma como se permite a redefinição do mecanismo automático de SR. De facto, quando são usadas variáveis locais às funções especı́ficas de redefinição, não é obviamente possı́vel o recurso à solução proposta atrás, para as variáveis membro. No entanto, sendo as funções especı́ficas definidas na programação da classe, podem ser facilmente incluı́das na mesma classe outras funções, que executem a parametrização correspondente à que se obtém pela utilização do valor da variável alternativa, e que serão chamadas no código gerado. Nesse caso, na chamada à primitiva storeAlternate, deve ser possı́vel especificar o nome das funções a chamar, para a recuperação do objecto. Sı́ntese Nas secções anteriores abordaram-se as questões sintácticas e semânticas relativas à representação externa dos objectos, discutindo-se a viabilidade de introdução de um serviço de SR de objectos, versátil na forma de representação, mas que exigisse um esforço insignificante ao programador das classes, sobre as quais esse mecanismo seria utilizado. Introduziu-se, nesse sentido de flexibilização, a noção de objectos especı́ficos por onde se concentraram todas as actividades de escrita e leitura - os objectos-de-E/S. Definiram-se, sobre esses objectos, as primitivas que concretizam o serviço de SR (storeObject, retrieveObject), e descreveu-se a forma como se articulam com as primitivas (storeInstance e retrieveInstance) que, nos os objectos-de-classe, manipulam a informação acerca da estrutura das instâncias, de forma a incluı́rem automaticamente, as descrições completas dos objectos a salvaguardar ou recuperar. Abordaram-se também, mecanismos de limitação do número de objectos guardados em cada operação de salvaguarda, e formas de reinserção de representações exter- 4.5. A GERAÇÃO DE CÓDIGO 105 nas incompletas assim geradas, no mesmo ou em contextos de execução diferentes. Finalmente, discutiu-se a possibilidade de incluir a geração de código compilável, como uma das representações externas a oferecer neste serviço. 106 CAPı́TULO 4. SALVAGUARDA E RECUPERAÇÃO DE OBJECTOS Capı́tulo 5 O ICE Neste capı́tulo ir-se-á descrever a concretização feita, para o sistema que se propôs nos capı́tulos anteriores. Começar-se-á por abordar as classes que uniformizam o acesso aos serviços oferecidos no ICE, e que constituem, por isso mesmo, a base de desenvolvimento do sistema. De seguida, introduzem-se as classes que descrevem o comportamento dos objectos-de-tipo e que, conjuntamente com os objectos-de-método, apresentados logo depois, concretizam a estrutura de suporte que existirá em tempo de execução, e sobre a qual, é então possı́vel a realização dos mecanismos de invocação interpretada e salvaguarda e recuperação de objectos. Nessa sequência, aborda-se na secção 5.4, a ferramenta de geração automática do código que permitirá a criação dessas estruturas, focando-se em especial os aspectos relativos ao código a gerar. Finalmente, nas duas últimas secções, descreve-se a concretização do serviço de nomes e dos objectos-de-E/S disponı́veis. 5.1 Interface comum aos objectos O ICE define o protocolo comum a todos os objectos, introduzindo as classes IObject e IOID. A primeira é uma classe abstracta, base de todas as classes ICE, que oferece uma integração completa com todos os serviços disponı́veis nesta biblioteca. A classe IOID corresponde aos identificadores-de-objecto e permite uniformizar o tratamento aos objectos em geral, mesmo os não derivados de IObject. 107 108 CAPı́TULO 5. O ICE 5.1.1 IObject Nesta classe, sem variáveis membro, define-se a maioria das primitivas que concretizam os serviços do ICE. 5.1.1.1 Interface ao serviço de nomes IObject (char* name, IObject* owner=0) Construtor que regista o objecto no serviço de nomes, sob o nome e dono especificados. O destrutor, virtual, dos objectos ICE retira-os do mesmo serviço. static IObject* getObject (char* name, IObject* owner=0) Função membro de classe que permite o acesso a um objecto por nome. virtual char* name () virtual IObject* owner () Retorna o nome e dono, respectivamente, de um objecto ICE. virtual Bool setName (char* name, IObject* owner=0) virtual Bool xgeName (char* name, IObject* owner=0) virtual Bool unName () Permitem a definição, alteração e remoção dos nomes associados ao objecto. Sendo o serviço de nomes também um objecto ICE, o primeiro objecto ICE ao qual seja atribuı́do um nome, e que não deve ser o próprio serviço de nomes, cria o serviço de nomes global, inicialmente sem nome associado. Esta opção é tomada tendo em conta que nos objectos globais do C++, não é possı́vel, em princı́pio, determinar a ordem da sua inicialização. Note-se também, que para grande parte das aplicações, a interface definida em IObject será suficiente para o acesso pretendido ao serviço de nomes, sendo a classe que concretiza este serviço, completamente transparente ao utilizador. 5.1.1.2 Interface de acesso à informação de tipo IType* typeOfIObject () Função global que permite o acesso ao objecto-de-tipo da classe. Uma função semelhante (typeOf<nome-do-tipo>) deve existir para todos os objectos-de-tipo, aos quais se pretende aceder. virtual IClass* isA () Função virtual que deve ser redefinida para todas as classes ICE e que permite aceder ao objecto-de-classe a partir da instância. 5.1. INTERFACE COMUM AOS OBJECTOS 109 Bool isMemberOf (IClass*) Bool isKindOf (IClass*) Responde afirmativamente, se a classe da instância em que for invocado (isA), for equivalente ou conforme, respectivamente, com a especificada no argumento. virtual IType* myType () Bool isInstanceOf (IType*) Bool isTypeConformTo (IType*) Idênticas às anteriores, mas para redefinir em IOID de forma a aceder ou testar o tipo do objecto identificado, em vez do IOID. A primeira função é definida como global, de forma a poder ter um mecanismo genérico de acesso a qualquer objecto-de-tipo, sem necessidade de alteração da declaração desse tipo. A segunda, restrita aos objectos ICE, deve ser incluı́da na declaração de todas as classes que derivam de IObject. A definição das duas é, como se verá, automaticamente feita pela ferramenta de geração da descrição do objecto-de-tipo e objectos-de-método de cada classe. 5.1.1.3 Interface ao serviço de invocação por mensagem virtual Bool vRecvMessage (IOID&, ISymbol&, Uint, IOID []) Executa o método correspondente à mensagem especificada nos argumentos. O primeiro argumento identifica o objecto ao qual será atribuı́do o retorno desse método. Se for especificado um identificador-de-objecto associado ao tipo void (voidArg), a atribuição é omitida. O segundo é o selector do método e pode ser especificado sob a forma de uma cadeia de caracteres1 . Os dois últimos dizem respeito ao número e vector de identificadores-de-objecto, dos argumentos da mensagem. Bool Bool Bool Bool Bool Bool recvMessage recvMessage recvMessage recvMessage recvMessage recvMessage (IOID&, (IOID&, (IOID&, (IOID&, (IOID&, (IOID&, ISymbol&) ISymbol&, IOID&) ISymbol&, IOID&, IOID&) ISymbol&, IOID&, IOID&, IOID&) ISymbol&, IOID&, IOID&, IOID&, IOID&, ...) ISymbol&, IOID []) Definem uma interface alargada à função membro anterior, para mensagens com 0, 1, 2 ou 3 argumentos, uma lista de 4 ou mais terminados por voidArg, ou um vector. Estas funções são declaradas inline, à excepção da declarada com ... No restante texto, sempre que uma função receber um número e um vector de argumentos, sejam eles identificadores-de-objecto, endereços ou objectos-de-tipo, e para a qual sejam declaradas funções que definem uma interface alternativa (semelhante 1 A classe ISymbol será descrita na secção 5.3 em que se discutirá a sua relevância no desempenho do algoritmo de discriminação de métodos. 110 CAPı́TULO 5. O ICE à das funções anteriores), estas são omitidas em toda a sua extensão, sendo simplesmente incluı́da na declaração da primeira (e.g. vRecvMessage) mais uma função com o nome das segundas (e.g. recvMessage) e a indicação das sobreposições possı́veis. Por exemplo: virtual Bool vRecvMessage (IOID&, ISymbol&, Uint, IOID []) Bool recvMessage (IOID&, ISymbol&, <0..4+,[]>IOID) Note-se que, os terminadores das listas de 4 ou mais argumentos, terão o valor 0 em vez de voidArg, para as listas de endereços (void*) e objectos-de-tipo (IType*). A função membro vRecvMessage, correspondente à primitiva de invocação por mensagem, executa o algoritmo de discriminação tal como foi descrito no capı́tulo 3. Na discriminação da mensagem são considerados os mecanismos de herança, sobreposição de nomes de métodos e argumentos por defeito. A concretização de facto deste serviço é feita na função membro invoke (ou vInvoke), executada sobre o objecto-de-tipo (isA) e definida nas metaclasses (ver 5.2). Considere-se, por exemplo, uma classe Contador derivada de IObject, que define uma função membro “add(int=1)” e uma instância dessa classe de nome contador. Considerem-se ainda as invocações por mensagem feitas sobre essa instância, mostradas no quadro abaixo. Note-se que, por um lado, não sendo add um método de IObject, a função membro correcta é chamada, já que isA, redefinida para Contador, retorna o seu objecto-de-tipo (typeOfContador()). Por outro lado, na segunda invocação pode observar-se o recurso a argumentos por defeito e na terceira, a execução de um método herdado de IObject. contador.recvMessage (voidArg, "add", 1); contador.recvMessage (voidArg, "add"); contador.recvMessage (voidArg, "setName", "contador"); Sendo vRecvMessage também uma função virtual, as classes derivadas podem redefini-la, incluindo, por exemplo, mecanismos de delegação no serviço que oferece. A própria classe IOID redefine-a nesse sentido, delegando a execução das mensagens sobre o objecto que identifica (ver 5.1.2). A classe IObject, ainda no contexto do acesso ao serviço de invocação de métodos por mensagem, define um conjunto de funções membro, que permitem 5.1. INTERFACE COMUM AOS OBJECTOS 111 interrogar um objecto sobre a sua capacidade de perceber uma mensagem ou um selector (understandsMessage, understandsSelector), ou qual o tipo do objecto retornado por uma mensagem (whichTypeReturns). Define ainda funções membro para acesso directo aos objectos-de-método definidos na classe a que o objecto pertence, e que poderão ser usadas, como se verá, para a optimização do mecanismo de invocação. virtual IMethod* vGetMessageMethod (IType*, ISymbol&, Uint, IType* []) IMethod* getMessageMethod (IType*, ISymbol&, <0..4+,[]>IType*) Retorna o objecto-de-método correspondente à mensagem especificada nos argumentos e definida pela classe (ou classe base) a que pertence o objecto. A concretização destas funções segue os mesmos princı́pios enunciados para vRecvMessage, recorrendo a métodos do objecto-de-classe associado por isA. 5.1.1.4 Interface ao serviço de salvaguarda e recuperação de objectos virtual Bool storeOn (IIO& io) Guarda o objecto no meio de salvaguarda e segundo a representação externa definida pelo objecto-de-E/S especificado como argumento. virtual Bool retrieveFrom (IIO& io) Recupera os dados do objecto sobre o seu espaço de memória corrente, a partir de uma representação externa contida num meio de salvaguarda definido no objecto-de-E/S especificado como argumento. Estas funções membro fazem uso directo das primitivas de salvaguarda e recuperação de objectos, definidas no objecto-de-E/S respectivo, tendo, por conseguinte, a mesma semântica associada (ver capı́tulo 4): uma operação de salvaguarda, guarda os dados do objecto e os dos objectos do conjunto-de-salvaguarda em que está inserido; uma operação de recuperação reconstrói também todos os objectos referidos no primeiro, recursivamente. 5.1.2 IOID A classe IOID, derivada da classe IObject, irá definir o comportamento dos identificadores-de-objecto, incluindo para cada instância: tipo o apontador para o objecto-de-tipo que corresponde ao objecto identificado. 112 CAPı́TULO 5. O ICE valor o endereço do objecto identificado. Duas funções membro, type() e value(), permitem o acesso a estas variáveis. 5.1.2.1 Criação de identificadores-de-objecto O protocolo definido para os identificadores-de-objecto inclui, tal como foi proposto, um conjunto de construtores que permite, que na especificação de uma mensagem ocorram conversões implı́citas, que tornem transparente a utilização dos objectos-de-tipo associados. Nesse sentido, na classe IOID incluem-se con- strutores para todos os tipos fundamentais (IOID(char&), IOID(short&), ... IOID(unsigned char&), ...), apontadores para esses tipos (IOID(char*&), ...) e objectos ou apontadores para objectos ICE. Os últimos são ape- nas definidos para IObject (IOID(IObject&), IOID(IObject*&)), já que o próprio compilador resolve as conversões de tipos derivados2 . Também são declarados construtores para void* e tipos normalizados de entrada/saı́da (FILE*, ostream e istream). Finalmente, inclui-se ainda um construtor genérico, em cujos argumentos são explicitamente referidos o tipo e o endereço do objecto a identificar (IOID(IType*, void*)). Um construtor sem argumentos cria um identificador-de-objecto associado ao tipo void, do qual voidArg é um exemplo. Para todos os tipos acima enumerados são ainda declarados construtores que aceitam um nome e um dono, para além do argumento correspondente ao valor a referir pelo identificador-de-objecto (e.g. IOID(char*, IObject*, char&)). Estes construtores registam o identificador-de-objecto no serviço de nomes global do ICE, em nome do objecto que identificam, permitindo deste modo o acesso por nome a objectos não ICE. 5.1.2.2 Utilização dos identificadores-de-objecto Na declaração de todos os construtores que oferecem a capacidade de conversão implı́cita de um objecto num identificador-de-objecto, utiliza-se o mecanismo de 2 Em algumas distribuições do compilador de C++ da AT&T na versão 2.0 isto não acontece, supostamente devido a um erro, já que no manual da linguagem [Ellis 90] se afirma que esta conversão é implı́cita. Também para o compilador de C++, versão 1.37, da GNU equivalente à versão 2.0, esta situação é resolvida de acordo com o manual e com o que aqui é dito. 5.1. INTERFACE COMUM AOS OBJECTOS 113 passagem de argumentos por referência (e.g. IOID(int&)), permitindo que essa conversão possa ocorrer e evitando cópias desnecessárias. De facto, ao pretender incluir o próprio mecanismo de passagem por referência na invocação por mensagens, o endereço do objecto a passar como argumento deve estar acessı́vel no identificador-de-objecto respectivo, quando da transferência da execução. Por exemplo, considere-se de novo a classe Contador e defina-se: void Contador::addTo (int& arg) {arg += _contagem_corrente;} A invocação por mensagem seguinte: contador.recvMessage (voidArg, "addTo", arg_int); executa sobre a instância contador, a função membro addTo, com um argumento inteiro arg int a passar por referência. Nesta situação o compilador cria automaticamente um identificador-de-objecto, usando o construtor “IOID(int& arg)”, que atribui o endereço do seu argumento (& arg) à variável membro valor. Como o argumento do construtor é declarado como uma referência, o endereço atribuı́do a valor é o mesmo do argumento da mensagem3 (& arg int). Se o construtor fosse declarado “IOID(int arg)”, o endereço atribuı́do a valor seria o endereço de arg, que se tornaria inválido no fim da execução do construtor e antes da invocação. Se, por outro lado, fosse declarado “IOID(int* arg)” a conversão implı́cita nunca ocorreria. Note-se que, mesmo a especificação de constantes como argumentos de uma mensagem é ainda possı́vel com estes construtores. Nessa situação, o compilador encarrega-se de criar uma variável temporária do tipo da constante, à qual esta é atribuı́da, e cujo endereço é usado como argumento para o construtor adequado. No quadro seguinte mostra-se uma invocação que recorre a este mecanismo e a respectiva expansão feita pelo compilador para o argumento da mensagem. 3 A utilização de casts não é recomendada já que o compilador, nas versões correntes, se encontrar um cast cria sempre uma variável auxiliar para fazer a possı́vel conversão. Assim, o endereço atribuı́do à variável membro valor será o da variável auxiliar e não o do argumento da mensagem. 114 CAPı́TULO 5. O ICE contador.recvMessage (voidArg, "add", 1); int temporario = 1; IOID argumento (temporario) // IOID (int&) contador.recvMessage (voidArg, "add", argumento); Neste ponto, deve definir-se claramente o papel dos identificador-de-objecto: Um identificador-de-objecto deve ser usado da mesma maneira que uma referência ou apontador (genéricos), tendo em conta o comportamento para eles definido no C++. Se for adoptada esta premissa, situações como as que se seguem podem ser evitadas. Considere-se então uma classe que concretiza um mecanismo de invocação tardio e programável: class Invocacao : // ... IObject* receptor; ISymbol simbolo; IOID argumento; // ... em que se definiram as seguintes funções membro: void Invocacao::programa (IObject* ob, ISymbol& s, int arg) { receptor = ob; simbolo = s; argumento = arg;} // IOID (int&) seguido de IOID = IOID void Invocacao::chama () { receptor->recvMessage (voidArg, simbolo, argumento);} Quando no método chama é feita a invocação, o erro ocorre porque o objecto inteiro arg, que se refere no identificador-de-objecto argumento, já deixou de existir. Note-se que o mesmo erro ocorreria se, em vez de IOID, se declarasse argumento como uma referência para um valor inteiro (int&). De facto, em ambos os casos, 5.2. OS OBJECTOS-DE-TIPO 115 usam-se as referências como contentores do objecto. Obviamente, a solução é, tal como se fez para o receptor e para o sı́mbolo, guardar não a referência mas o próprio valor. Na declaração da classe, bastaria substituir IOID por int. 5.1.2.3 Redefinição do protocolo herdado de IObject Tal como já foi referido, a classe IOID redefine a interface de acesso aos serviços oferecidos pelo ICE, delegando a sua execução sobre o objecto que cada identificador-de-objecto refere na sua variável membro valor. De certo modo, esta concretização vem de encontro ao mecanismo de delegação proposto em [Stroustrup 87] para o próprio C++, em que a execução da função membro é feita sobre o objecto apontado numa variável membro da instância em que foi invocada. As funções redefinidas para delegação incluem as referentes a informação de tipo, invocação por mensagem e salvaguarda e recuperação de objectos, segundo a classificação introduzida na descrição de IObject. Este mecanismo é concretizado, substituindo nessas funções membro a chamada à função membro isA, que devolveria o objecto-de-tipo de IOID, pelo objecto-de-tipo referido na variável membro “tipo” do identificador. Por exemplo, a invocação: int x = 2; IOID (x).recvMessage (voidArg, "+=", 1); incrementa o valor inteiro x de 1. Note-se que, o método correspondente ao operador + está definido no tipo int e é acessı́vel no objecto-de-tipo que o descreve, embora recvMessage seja invocado num identificador-de-objecto. 5.2 Os objectos-de-tipo Um dos componentes fundamentais do modelo de suporte aos serviços que o ICE oferece, é, como ficou definido nos capı́tulos anteriores, o objecto-de-tipo. Nesta secção, ir-se-ão descrever as soluções encontradas para a concretização das classes que definem o comportamento destes objectos, as metaclasses do ICE. Tendo em conta a definição de objecto-de-tipo dada em ∆3.2 e as considerações 116 CAPı́TULO 5. O ICE feitas sobre a generalização dos mecanismos oferecidos a qualquer tipo da linguagem, é natural a definição de uma hierarquia de metaclasses, que reflicta, de algum modo, a classificação do modelo de tipos que se assume e que coincide com o da linguagem de concretização (ver figura 5.1). IType IBasicType IFunctionType IPointerType IVectorType IClass IReferenceType Figura 5.1: A hierarquia de metaclasses do ICE 5.2.1 Interface comum aos objectos-de-tipo IType é uma classe ICE, abstracta, que define o protocolo comum a todos os objectos-de-tipo. 5.2.1.1 Acesso por nome A criação de uma instância de IType ou derivada, implica normalmente a especificação de um nome, o nome do tipo, que permite registar o objecto no serviço de nomes como global, i.e., sem dono. Deste modo, qualquer objecto-de-tipo pode ser acedido por nome através da função definida em IObject para esse fim (e.g. IObject::getObject ("Contador")). Por outro lado, IType redefine a interface de alteração e remoção de nomes, de modo a assegurar que um tipo tem sempre um nome global associado. 5.2.1.2 Teste da relação entre tipos virtual Bool isEqualTo (IType*) virtual Bool isConformTo (IType*) Responde se o tipo sobre o qual se invocou a função membro é, respectivamente, equivalente ou conforme com o tipo especificado como argumento. 5.2. OS OBJECTOS-DE-TIPO 117 Estas funções são usadas, por exemplo, na verificação dos tipos dos argumentos especificados numa invocação por mensagem (ver 5.3.2) e na definição do protocolo de acesso à informação de tipo para a generalidade dos objectos (ver 5.1.1.2). Na classe IType estas funções são abstractas, já que a sua concretização será necessariamente diferente se o tipo for uma classe, em que, por exemplo, os mecanismos de herança terão que ser considerados, ou outro tipo qualquer. Relativamente ao acesso à informação de tipo para instâncias de IType note-se que, sendo IType um IObject, também para os objectos-de-tipo estão definidas, quer as funções de acesso ao objecto-de-metaclasse, objecto-de-tipo de IType (typeOfIType, isA, myType), quer as de teste desse objecto (isKindOf, isInstanceOf, ...). Por exemplo, considere-se ainda a classe Contador. contador.isKindOf (typeOfIObject ()); contador.isKindOf (typeOfIType ()); contador.isA ()->isConformTo (typeOfIObject ()); contador.isA ()->isConformTo (typeOfIType ()); contador.isA ()->isKindOf (typeOfIObject ()); contador.isA ()->isKindOf (typeOfIType ()); // FALSE // FALSE Os dois primeiros casos são idênticos aos segundos, já que estes correspondem à concretização que IObject faz daqueles - Contador é conforme com IObject mas não com IType. Os dois últimos correspondem à invocação de isKindOf sobre o objecto-de-tipo de Contador. Testa-se neste caso, se o tipo do objecto-de-tipo de Contador (uma metaclasse) é conforme com IType e IObject, o que é verdadeiro. IType oferece ainda uma interface que permitiria considerar a conversão de argumentos na invocação de funções membro, respectivamente através de construtores (canBeCreatedFrom) e operadores de conversão (isConvertibleTo). 5.2.1.3 Criação de objectos virtual IMethod* vGetConstructor (Uint, IType* []) IMethod* getConstructor (<0..4+,[]>IType*) Retorna o objecto-de-método correspondente ao construtor cujos argumentos são compatı́veis com os especificados. O primeiro argumento é o número de argumentos a especificar. O segundo é um vector com os seus tipos. virtual void* vCreate (IOID&, Uint, IOID []) void* create (<0..4+,[]>IOID) 118 CAPı́TULO 5. O ICE Retorna um apontador para uma instância do tipo, criada usando um construtor cuja sintaxe se especifica nos argumentos. O primeiro argumento de vCreate permite a verificação do tipo do objecto ao qual será atribuı́do o apontador para a instância. Se voidArg for especificado, a verificação é omitida. A função vGetConstructor é abstracta por questões de optimização do acesso e quantidade de objectos-de-método correspondentes, já que para os tipos primitivos é possı́vel definir objectos comuns a todas as variantes desse tipo. Em qualquer dos casos, a concretização desta função membro deverá ter sempre em conta, a possibilidade de especificação de argumentos de tipo conforme com os da declaração do construtor e da omissão de argumentos que existam por defeito. A concretização de vCreate é feita à custa de vGetConstructor, seguida da invocação da função execute definida sobre os objectos-de-método para a execução do código a eles associado (ver 5.3). Se o objecto-de-método correspondente aos argumentos especificados não existir, é notificada uma mensagem de erro. Esta função pode ser redefinida em classes derivadas, com o intuito de aumentar o desempenho na sua execução, nomeadamente para os casos referidos acima. IType oferece ainda a possibilidade de criação de vectores de instâncias recorrendo à função membro createVector ou ao objecto-de-método respectivo, usando getVectorConstrutor. A utilização do protocolo de criação de instâncias segue as premissas propostas para este serviço, em 3.2.1. Por exemplo, a criação de duas instâncias de IObject poderia ser feita como se mostra de seguida: typeOfIObject ()->create ("objecto1"); typeOfIObject ()->create ("objecto2", typeOfIObject ()); No primeiro caso, usou-se a capacidade de omissão de argumentos na invocação do construtor (IObject (char*, IObject*=0)). No segundo, o argumento especificado é de um tipo conforme (IType*) com o tipo da declaração (IObject*). 5.2.1.4 Invocação de métodos por mensagem virtual IMethod* vGetMethod (IType*, ISymbol&, Uint, IType* []) IMethod* getMethod (IType*, ISymbol&, <0..4+,[]>IType*) Retorna o objecto-de-método correspondente ao método associado à mensagem especificada nos argumentos. O primeiro argumento corresponde ao argumento de retorno 5.2. OS OBJECTOS-DE-TIPO 119 e os três últimos, respectivamente, ao selector da mensagem e ao número e vector de tipos, dos argumentos da mensagem. virtual Bool vInvoke (IOID&, IOID&, ISymbol&, Uint, IOID []) Bool invoke (IOID&, IOID&, ISymbol&, <0..4+,[]>IOID) Executa o método que corresponde à sintaxe expressa nos argumentos e definido no tipo representado pelo objecto-de-tipo em que for invocada. O segundo argumento corresponde à instância do tipo, sobre a qual se vai invocar o método. A função membro vGetMethod é também definida nas classes que derivam de IType, pelas mesmas razões apontadas para vGetConstrutor e deve concretizar o algoritmo de discriminação de métodos, segundo as premissas definidas no capı́tulo 3. vInvoke recorre vGetMethod e executa o código associado ao objecto-de-método assim obtido, invocando execute sobre este. Tal como se disse, a função membro invoke é usada na concretização da primitiva recvMessage definida na classe IObject. Então as duas invocações seguintes têm o mesmo mesmo efeito - executar o método add sobre o objecto contador. contador.recvMessage (voidArg, "add", 1); contador.isA ()->invoke (voidArg, contador, "add", 1); Por outro lado, a utilização da primitiva recvMessage sobre os objectos-de-tipo é também possı́vel, já que IType deriva de IObject. Segundo a concretização de Iobject, os métodos que se podem invocar através de recvMessage sobre os objectos-de-tipo são, por exemplo, os que IType define. No entanto, e tal como proposto em 3.2.2, a primitiva é redefinida nesta classe, de forma a incluir também criação de objectos, identificando a mensagem com selector new. typeOfContador ()->recvMessage (&contador, "new", "contador1"); A detecção do selector new é feita por simples comparação do sı́mbolo (instância de ISymbol) argumento, com um sı́mbolo declarado estático para a classe IType e que representa também o selector new4 . A utilização de um selector “new []” possibilita a criação de vectores de objectos por mensagem5 . 4 Note-se que a comparação de sı́mbolos é um mecanismo rápido, tal como se define em 5.3.1. A concretização da invocação do operador new redefinido com mais que um argumento, não está, por enquanto, contemplada neste trabalho, 5 120 CAPı́TULO 5. O ICE A redefinição feita a recvMessage é alargada à restante interface de IObject, que lida, quer com o acesso aos objectos-de-método (vGetMessageMethod), quer com interrogações com eles relacionadas (understandsMethod, ...). Por outro lado, IType define ainda uma interface própria ao teste do protocolo definido para cada tipo e um mecanismo de acesso sequencial aos seus objectos-de-método. 5.2.2 Os tipos primitivos Os tipos primitivos incluem tipos fundamentais, apontadores e os que deles derivam, e os tipos representativos de funções (figura 5.1), abordando assim a generalidade dos tipos C++, cujas operações não são definı́veis pelo utilizador. 5.2.2.1 Tipos fundamentais A classe IBasicType concretiza a interface definida em IType para os tipos fundamentais de C++. No respeitante à relação entre tipos utilizou-se, como regra, que cada tipo só é conforme com ele próprio. De facto, as regras que se estabelecem em [Ellis 90] para promoções e conversões de tipos fundamentais, embora implicitamente feitas pelo compilador, envolvem operações sobre o valor do argumento, que depende do seu tipo (e.g. alinhamentos, trucamento, ...). Nesse contexto, essas operações (e.g. int para char, int para double, ...), são aqui definidas como objectos-de-método estando acessı́veis como operadores de conversão. Para a criação de instâncias sem inicialização é definido um objecto-de-método, único e partilhado, que cria simplesmente o espaço baseado no tamanho do tipo. Esse tamanho é facilmente obtido, através do objecto-de-tipo que representa o tipo fundamental respectivo (receptor da mensagem do ponto de vista do objectos-de-método ver 5.3.3). É também definido outro objecto-de-método para a criação de instâncias com inicialização, que recorre novamente ao objecto-de-tipo em que foi invocado, para atribuir o valor à instância criada, consoante o tipo em questão. Tendo isto em conta, a concretização de vGetConstructor fica bastante simplificada, sobretudo se pensarmos que a sintaxe especificada, ou tem um argumento conforme com o próprio tipo, ou não tem nenhum. Por outro lado, também a realização de vInvoke 5.2. OS OBJECTOS-DE-TIPO 121 é melhorada, evitando a invocação dos objectos-de-método e incluindo o código por eles executado igualmente nesta função membro. Finalmente a classe IBasicType concretiza os mecanismos de invocação de mensagens (vGetMethod, vInvoke, ...), definindo as operações básicas entre tipos fundamentais (+, -, ...), quer directamente (vInvoke), quer através de objectos-de-método adequados (vGetMethod). Os argumentos esperados nos objectos-de-método definidos, são sempre do tipo long ou double não se definindo os objectos-de-método para as restantes combinações6 . Na invocação destes objectos-de-método, a primitiva vInvoke executa as conversões necessárias, quando são especificados argumentos de tipos fundamentais distintos. A biblioteca em que se apresenta o ICE, cria, desde logo, os objectos-de-tipo que correspondem aos tipos fundamentais, segundo o mecanismo de criação de objectos globais, disponı́vel no C++: IBasicType type_char ("char", IBT_char, sizeof(char)); IBasicType type_short ("short", IBT_short, sizeof(short)); // ... Estes estão acessı́veis, quer através do respectivo nome das variáveis globais, quer por meio de funções exportados na declaração de IBasicType: typeOfchar (), ... 5.2.2.2 Apontadores A classe IPointerType define o comportamento genérico dos apontadores. Nomeadamente, estabelece as relações de equivalência e conformidade entre tipos, tendo em conta o tipo que é usado na definição daquele que representa, i.e., um tipo A* é conforme com (ou equivalente a) um tipo B*, se A for conforme com (ou equivalente a) B. Introduz apenas conversões para o tipo int. A sua concretização, no que diz respeito à criação de ponteiros, é semelhante à de IBasicType, com a excepção de que o tamanho do espaço reservado é constante (sizeof(void*)) e a inicialização é uma atribuição simples. Define também os objectos-de-método, correspondentes à operação que permite aceder ao valor referenciado (* unário) e aos 6 Note-se que o compilador de C tem exactamente o mesmo comportamento quando se trata de funções definidas com argumentos de tipos primitivos distintos destes. 122 CAPı́TULO 5. O ICE operadores de soma (+, += e ++) e subtracção, que entram em linha de conta com o tamanho do tipo que constitui o apontador. As noções de vector de objectos (IVectorType) e referência (IReferenceType) não são concretizados, já que o seu papel é dispensável, na maioria dos casos. De facto, nas verificações de tipo feitas pelo C++ é sempre possı́vel usar um vector, onde se espera um apontador, e vice-versa. Por outro lado, a referência não introduz realmente uma nova categoria de tipos, mas simplesmente um mecanismo sintáctico para facilitar o modo de programação. Quando usada na declaração de uma variável, o seu papel é apenas introduzir um novo mecanismo de acesso para o mesmo objecto. No ICE equivale a um novo nome no serviço de nomes, ou simplesmente um novo identificador-de-objecto. Como argumento de uma função, do ponto de vista de verificação de tipos, um tipo pode ser sempre usado onde uma referência para ele é esperada, e vice-versa. A diferença reside apenas na forma como o argumento é realmente passado, o que deverá ser resolvido na transferência de controlo para a execução do código associado aos objectos-de-método. 5.2.2.3 Funções A metaclasse IFunctionType é usada na concretização actual do ICE simplesmente para verificação de tipos. A operação essencial a definir sobre funções e por conseguinte a definir nesta classe, seria o operador que permite a sua invocação. A sua concretização envolveria um mecanismo semelhante ao adoptado para a classe que define objectos-de-método que permitem o acesso à execução de métodos compilados (ver 5.3.3), ou uma solução inerentemente dependente da máquina que construiria a pilha de chamada à função, a partir de um vector de apontadores (interface genérica). 5.2.3 Os tipos definidos pelo utilizador IClass define o comportamento genérico dos tipos definı́veis pelo utilizador (segundo a nomenclatura introduzida em [Ellis 90]), que englobam os tipos definidos pelas palavras chave: class, struct e union. Esta classe, porque representa uma classe C++ qualquer, inclui nas suas variáveis membro um conjunto de listas (vectores), que irão ser parametrizadas, dependendo do tipo que cada instância representa. Assim, incluirá as seguintes variáveis membro: 5.2. OS OBJECTOS-DE-TIPO 123 base e protecção um apontador para o objecto-de-classe da sua classe base e o modificador de protecção (public ou private) para o acesso aos membros dessa classe. métodos, métodos-de-classe, construtores, conversores e destrutor Apontadores para as tabelas dos objectos-de-método definidos em cada uma das categorias correspondentes ao nome da variável. variáveis Apontador para o vector das estruturas descritivas das variáveis membro de cada classe. função-de-criação-na-recuperação Apontador a função que permite a criação do espaço para as instâncias da classe nas operações de recuperação. função-de-salvaguarda e função-de-recuperação Apontadores para as funções que definem alternativas aos mecanismos de salvaguarda e recuperação (SR) automáticas. 5.2.3.1 Relações entre classes IClass introduz o conceito de herança (simples), nas funções membro que IType define para teste de tipos conformes, e disponibiliza o acesso à informação sobre a hierarquia, em que cada classe se insere (baseClasse). Também o acesso ao objecto-de-tipo de uma instância de IClass é possı́vel, através das funções membro isA e typeOfIClass (). Por outro lado, porque é uma classe, o seu objecto-de-tipo é um objecto-de-classe e, por conseguinte, uma instância de si própria. Na figura 5.2 estam representadas as ligações entre um objecto ICE qualquer, cuja classe deriva de IObject, e o respectivo objecto-de-classe e, por sua vez, deste para os objectos-de-classe a que está ligado, quer por baseClass, quer por isA. Note-se que, tal como foi proposto em 3.2, existe um objecto-de-classe, o que representa IClass, que fecha a árvore de metaclasses pela relação de isA. Por outro lado, porque as instâncias de IClass possibilitam, por parametrização, a representação completa, incluindo métodos de classe, de qualquer classe C++, os objectos-de-metaclasse existentes em qualquer execução do ICE, resumem-se aos que representam cada uma das classes apresentadas nesta secção. 124 CAPı́TULO 5. O ICE objectos-de-classe objectos-de-metaclasse IObject IType baseClass isA isA baseClass baseClass instância Contador contador isA IClass isA isA Figura 5.2: Ligações entre objectos, objectos-de-classe e objectos-de-metaclasse 5.2.3.2 Criação de objectos e invocação por mensagem Na criação de objectos e invocação por mensagem, está envolvido o mecanismo genérico de procura do método, ou objecto-de-método, correspondente a uma determinada mensagem. A concretização deste algoritmo na versão corrente do ICE, definido nas funções membro vGetConstrutor e vGetMethod, recorre simplesmente ao teste sucessivo da sintaxe que uma mensagem apresenta, em relação a todos os construtores (no caso da criação) ou métodos que estão enumerados nas tabelas respectivas do objecto-de-classe, até que encontre o objecto-de-método correcto. No caso da vGetMethod, se essa procura falhar nas tabelas do objecto-de-classe invocado, irá ser continuada sucessivamente nos objecto-de-classe base, de forma a incluir os mecanismos de herança na invocação por mensagem. Este mecanismo corresponde exactamente à concretização do algoritmo proposto em 3. O teste da validade da sintaxe especificada para uma mensagem sobre cada objecto-de-método, corresponde à comparação do selector da mensagem com o desse objecto - este passo é omitido na primitiva vGetConstrutor. Verificado o selector, valida-se o tipo do argumento de retorno e, finalmente, o número e tipo dos restantes argumentos. Qualquer dos testes é feito à custa de funções membro definidas na classe que representa os objectos-de-método (ver 5.3). É também verificada a protecção dos métodos, permitindo somente o acesso aos métodos públicos da classe, e aos da classe base, se esta for declarada pública. O acesso aos membros protegidos é também viável, mas através de funções mem- 5.2. OS OBJECTOS-DE-TIPO 125 bro especı́ficas (getProtectedMethod, ...). Por outro lado, IClass define uma interface de acesso e execução dos objectos-de-método associados a funções membro estáticas, vGetClassMethod e vInvokeClassMethod, cujo funcionamento é semelhante ao de vGetMethod, e vInvoke à excepção da tabela de métodos em que estes são procurados (métodos-de-classe em vez de métodos). Considere-se novamente a classe Contador, e um apontador pc para uma instância dessa classe: typeOfContador ()->invoke (voidArg, *pc, "setName", "pc"); typeOfContador ()->invokeClassMethod (pc, "getObject", "pc"); Na primeira linha é atribuı́do um nome ao objecto, recorrendo a uma invocação por mensagem à função membro setName, definida na classe base IObject. Na segunda, chama-se uma função membro estática, também herdada da classe base, que permite o acesso por nome ao objecto. Relativamente às primitivas envolvidas na invocação por mensagem herdadas de IObject (vRecvMessage, vGetMessageMethod, ...), IClass redefine-as, de forma a incluir também a invocação das funções membro estáticas. Deste modo, e em conjunção com a criação de objectos por mensagem concretizada por IType, fica completamente definido o comportamento do serviço de invocação por mensagem sobre os objectos-de-tipo, tal como se propôs em 3.2.2. 5.2.3.3 Interface à salvaguarda e recuperação A inclusão da informação sobre o formato das instâncias no objecto-de-classe, é feita por um vector de estruturas descritivas de cada variável membro, contendo: modificador indica a protecção da variável (public, ...) e se é simples ou um vector. tipo, nome e deslocamento apontador para o tipo, nome e deslocamento em relação ao inı́cio da instância. A utilização do modificador para distinção entre os tipos simples e os vectores, permite colmatar a inexistência de uma metaclasse para a representação de vectores. Assim, se a variável membro for um vector, os (14) bits de maior peso do modificador (16 bits) contêm o tamanho do vector, senão terão o valor zero. 126 CAPı́TULO 5. O ICE As primitivas definidas em IClass incluem: Bool storeInstance (void*, IIO*) Bool retrieveInstance (void*, IIO*) Salvaguarda/recupera um objecto da classe em que foi invocado, cujo endereço é especificado no primeiro argumento, segundo uma representação externa definida pelo objecto-de-E/S usado. Bool storeBaseInstance (void*, IIO*) Bool retrieveBaseInstance (void*, IIO*) Idênticas à anterior para as variáveis membro definidas nas classes base. Bool storeMember (IOID&, IIO*) Bool retrieveMember (IOID&, IIO*) Salvaguarda/recupera uma variável membro, especificada no primeiro argumento, pertencente a uma instância da classe em que foi invocada. Bool storeArrayMember (IOID&, Uint, IIO*) Bool retrieveArrayMember (IOID&, Uint, IIO*) Salvaguarda/recupera uma variável membro, pertencente a uma instância da classe em que foi invocada, e que é um vector de objectos do tipo especificado no primeiro argumento, com o tamanho especificado no segundo. O endereço da variável membro incluı́do no identificador-de-objecto, deve corresponder ao inı́cio do vector. inline void* createRetrieveSpace () Responde um apontador para o espaço de memória onde será recuperado o objecto. As primeiras funções concretizam o mecanismo de SR, tal como se descreveu no capı́tulo 4, verificando inicialmente se alguma função especı́fica que redefina a sequência de SR, foi definida para a classe. Nesse caso, storeInstance e retrieveInstance executam simplesmente a função correspondente, acessı́vel nas variáveis membro de IClass atrás referidas. Caso contrário, invocam storeBaseInstance ou retrieveBaseInstance, seguida de storeMember ou retrieveMember sobre os objectos-de-E/S (ver 5.6), para cada uma das variáveis membro. Se a variável membro em questão for um vector, o seu tamanho é extraı́do do modificador e storeArrayMember ou retrieveArrayMember são chamados alternativamente, também sobre os objectos-de-E/S. As funções storeMember e retrieveMember e as equivalentes para vectores, definidas sobre IClass, verificam previamente se o tipo especificado é conforme com o da variável membro correspondente que inclui na estrutura de descrição da instância, executando depois a função com o mesmo nome, mas sobre os objectos-de-E/S. Juntamente com storeBaseInstance e retrieveBaseInstance, 5.2. OS OBJECTOS-DE-TIPO 127 estas funções tem como finalidade a sua utilização nas funções especı́ficas de redefinição da sequência de SR automáticas. Por exemplo: Bool VectorDeInt::storer (IIO* io) { typeOfVectorDeInt ()->storeBaseInstance (this, io); typeOfVectorDeInt ()->storeMember (_tamanho, io); typeOfVectorDeInt ()->storeArrayMember (_vector, _tamanho, io);} redefine o mecanismo de salvaguarda de uma classe, que contenha uma variável ( vector) do tipo int*, que na realidade seja um vector de inteiros, cujo tamanho é conhecido noutra variável ( tamanho). O tipo das funções especı́ficas de redefinição da SR é, tal como se pode ver no exemplo, uma função membro com um único argumento, um apontador para um objecto-de-E/S. No entanto, essa definição de tipo, implicaria o conhecimento de uma classe de que a função seria membro (e.g. IObject), o que limitaria a utilização do mecanismo às classes derivadas dessa. Nesse sentido, a solução adoptada passa por definir esses tipos, com a estrutura das funções C equivalentes: typedef Bool (*Storer) (void*, IIO* io); typedef Bool (*Retriever) (void*, IIO* io); Tipo das funções especı́ficas de redefinição da SR de objectos. O primeiro argumento corresponde à instância e o segundo ao objecto-de-E/S. A utilização de funções membro é então possı́vel fazendo o cast necessário para estes tipos, já que se trata apenas de uma questão sintáctica7 . Esta solução permite também alargar o uso do mecanismo de SR a classes já existentes, e sobre as quais não é possı́vel alterar a declaração, acrescentando funções membro. De facto, nesse caso é normalmente possı́vel definir funções C, que recorram simplesmente às primitivas storeAlternate e retrieveAlternate sobre os objectos-de-E/S, circundando assim as questões de protecção eventualmente definidas na classe. A função createRetrieveSpace, executa simplesmente a função cujo endereço está contido na variável membro correspondente no objecto-de-classe, sobre a qual foi invocada. Essa função deverá invocar o construtor adequado, segundo as premissas discutidas na secção 4.3.2.2. 7 Desde que as funções não sejam virtuais. 128 CAPı́TULO 5. O ICE IClass oferece ainda um mecanismo de acesso sequencial à informação sobre a estrutura das instâncias, através de objectos de leitura (IVarsReader), semelhantes aos que se propõe nos exemplos de acesso a elementos de uma lista, quer em [Stroustrup 86], quer em [Meyer 88] para o Eiffel. Também o acesso directo às variáveis membro pode ser feito através das funções membro setMember e getMember, que serão usadas no código gerado pelos objectos-de-E/S especı́ficos, definidos para salvaguardar objectos sob a forma de código C++. 5.2.3.4 Parametrização de objectos-de-classe A diversidade de formas que uma instância de IClass pode assumir, é directamente proporcional à liberdade que um programador tem para definir uma classe em C++. Nesse contexto, as instâncias desta classe podem ser parametrizadas, na criação, recorrendo a um construtor que aceita como argumentos, os correspondentes a todas as variáveis membro definidas para a classe. Os objectos-de-classe podem ainda ser parametrizados após a sua criação e consequentemente em tempo de execução, definindo-se apenas, na versão corrente, funções membro para acrescentar métodos, sob a forma de objectos-de-método, às tabelas respectivas (addMethod, addConstructor, ...). 5.3 Os objectos-de-método Os objectos-de-método oferecem a descrição completa de funções membro e operações em geral, e cooperam activamente na concretização do algoritmo de procura do serviço de invocação por mensagem e criação de objectos em tempo de execução. Por outro lado, tal como ficou expresso na sua definição em ∆3.4, um objecto-de-método deve também permitir o acesso ao seu código. Neste contexto, admitir-se-á a possibilidade de coexistência de código compilado e interpretado, não perdendo no entanto de vista, que o objectivo principal do suporte à interpretação aqui oferecido, deve sobretudo reflectir-se num serviço de invocação interpretada de métodos compilados. As classes envolvidas na concretização de objectos-de-método são: IMethod ISymbol 5.3. OS OBJECTOS-DE-MÉTODO 129 ICompiledMethod IInterpretedMethod correspondendo, respectivamente, à classe abstracta de objectos-de-método, aos objectos-de-método que acedem a código compilado e aos que interpretam código. A classe da direita diz respeito à concretização do conceito de selector e desempenha um papel fundamental em todo o algoritmo de discriminação. 5.3.1 Os selectores dos métodos A classe ISymbol representa cadeias de caracteres únicas, permitindo acelerar a comparação de nomes. A sua concretização inclui, para cada instância (sı́mbolo), a seguinte variável membro: string o endereço de uma cadeia de caracteres que o sı́mbolo representa. e partilha, entre todas as instâncias, uma tabela (tabela de sı́mbolos) que inclui todas as cadeias de caracteres cujo endereço pode ser referido nessa variável. O mecanismo que garante a unicidade das cadeias de caracteres referidas nas instâncias de ISymbol, é normalmente definido na sua criação. De facto, quando é criado um sı́mbolo usando um construtor que aceita, como argumento, uma cadeia de caracteres, é verificado se uma cadeia idêntica se encontra já registada na tabela de sı́mbolos partilhada. Caso não esteja, é criada uma cópia da cadeia especificada que é inserida na tabela. Em qualquer dos casos, à variável membro do novo sı́mbolo, é sempre atribuı́do o endereço da cadeia de caracteres registada na tabela de sı́mbolos. Deste modo, se duas instâncias de ISymbol representam a mesma cadeia de caracteres, então o endereço que referem é o mesmo. - Criaç~ ao de sı́mbolos ISymbol (const char*) ISymbol (const char*, Bool) Cria uma instância de ISymbol tal como foi descrito acima. A primeira versão é implicitamente usada quando uma cadeia de caracteres é especificada como selector de uma mensagem. Na segunda versão, permite que a cadeia de caracteres passada como argumento seja usada na tabela de sı́mbolos, 130 CAPı́TULO 5. O ICE no caso de lá não existir uma idêntica. Esta versão pode ser usada, por exemplo, quando no argumento são especificadas cadeias de caracteres constantes (e.g. ISymbol ("abc", FALSE)), evitando assim, a criação do espaço para a nova cadeia. - Comparaç~ ao de sı́mbolos inline Bool operator == (ISymbol&) inline Bool operator != (ISymbol&) Verifica se dois sı́mbolos representam a mesma, ou cadeias diferentes de caracteres. As funções membro são definidas inline evitando assim a invocação da função. Ambas se resumem à comparação dos endereços das variáveis membro string. Assegurada a unicidade das cadeias de caracteres e, por conseguinte, a rapidez na comparação de sı́mbolos, tornou-se, no entanto, a sua criação um processo mais demorado. Contudo, tendo concretizado a tabela de sı́mbolos como uma tabela de hash, mesmo o processo de procura, na criação de um sı́mbolo, é relativamente rápido. Essa tabela de hash é concretizada sob a forma de uma tabela aberta, composta por um vector relativamente extenso de apontadores para árvores balanceadas de elementos, que conterão as cadeias de caracteres que constituem os sı́mbolos. A própria classe ISymbol oferece funções estáticas para a pesquisa e inserção de sı́mbolos na tabela. 5.3.2 Interface comum aos objectos-de-método A classe IMethod define a interface comum aos objectos-de-método e concretiza ainda os mecanismos envolvidos na pesquisa de métodos, que permitem associar uma mensagem com o objecto-de-método respectivo. Esta classe disponibiliza também toda a informação sobre a estrutura de um método, no sentido alargado do termo introduzido no capı́tulo 3. As instâncias desta classe incluem: selector identificador único do nome do método, concretizado através de um sı́mbolo que represente esse nome. Dois métodos com o mesmo nome devem ter o mesmo selector, independentemente do número e tipo dos seus argumentos. modificadores Informação sobre a protecção com que o método é declarado e se o método é virtual, inline, ou normal. tipo-que-o-define e tipo-de-retorno objectos-de-tipo do tipo em que o método ou operação foi definido e do seu tipo de retorno. 5.3. OS OBJECTOS-DE-MÉTODO 131 numero-e-tipo-de-argumentos número de argumentos com que o método se declarou, e vector dos objecto-de-tipo, que identificam os seus tipos. numero-e-endereço-dos-argumentos-por-defeito número de argumentos por defeito com que o método se declarou, e vector dos endereços dos objectos (ou valores), que serão usados por defeito na invocação do método. Para a criação de objectos-de-método de classes derivadas desta, é definido um construtor que aceita, como argumentos, os valores a atribuir às variáveis acima enumeradas. A classe IMethod define ainda um conjunto de funções membro que facultam o acesso à informação neles contida (returnType (), selector (), ...), permitindo também o uso de um objecto para leitura sequencial do tipo e valor por defeito, de cada um dos argumentos (IArgsReader). 5.3.2.1 Verificação da validade de uma mensagem inline Bool canSelectorBe (ISymbol&) Verifica se o selector especificado é o nome do método. inline Bool canReturnTypeBe (IType*) Verifica se o valor retornado pelo método, pode ser atribuı́do a uma instância do tipo especificado. Bool vCanMessageArgsBe (Uint, IType* [], IMWrongArgs&) Bool canMessageArgsBe (<0..4+,[]>IType*) Verifica se o tipo dos argumentos a passar para a execução do método, pode ser o especificado no vector de tipos. O primeiro argumento indica o número de tipos do vector e o segundo, esse vector. O último permite identificar o erro ocorrido e o seu valor pode ser usado como argumento na função respectiva de notificação de erro, definida nesta classe (reportWrongArguments). Segundo as regras estabelecidas para a atribuição de valores no C++ 2.0, o retorno de um método só pode ser atribuı́do a um objecto, se o tipo de retorno for conforme com o do objecto. Por exemplo, se B for conforme8 com A e se um método retornar uma instância de B, o retorno do método pode ser atribuı́do a uma instância de A. No entanto, a menos que os tipos envolvidos sejam apontadores, a atribuição envolve operações de conversão mais ou menos complexas (atribuição parcial, alinhamento, etc). Por exemplo, se A e B forem classes, a atribuição de B a A não é uma operação de cópia simples (bcopy), mas uma cópia selectiva, em que 8 Note-se que a noção de conforme se alarga a tipos que não sejam classes. 132 CAPı́TULO 5. O ICE apenas as variáveis membro de B que existem em A, são copiadas9 . Por essa razão, a concretização feita para canReturnTypeBe, apenas considera os casos em que as atribuições são feitas sem necessidade de conversões extra, o que permite ignorar o tipo do objecto, na execução do código do método. Ou seja, a regra é: se o tipo for apontador pode ser conforme; senão deverá ser idêntico. A função membro vCanMessageArgsBe retornará afirmativamente, se o tipo dos argumentos especificados, for conforme com o tipo dos argumentos da declaração do método, cuja informação está contida no objecto-de-método. Esta função tem ainda em consideração a possı́vel utilização de argumentos por defeito, já que verifica, caso o número de argumentos especificado seja menor que aquele que o método espera, se o número de argumentos por defeito é suficiente para colmatar as omissões. 5.3.2.2 Execução do código associado ao objecto-de-método virtual void vExecuteNoCheck (void*, void*, void* []) void executeNoCheck (void*, void*, <0..4+,[]>void*) Esta função, a concretizar pelas classes derivadas, deve executar o código associado ao objecto-de-método. Os argumentos correspondem, respectivamente, ao endereço do objecto ao qual será atribuı́do o valor de retorno, ao do objecto sobre o qual se vai invocar o código e a um vector de endereços para os argumentos a passar para essa execução. A especificação de 0 no primeiro argumento deve evitar a atribuição do argumento de retorno. Nesta função membro não é feita qualquer verificação, nem tão pouco são preenchidos os argumentos por defeito declarados para o método. void getPlacedDefaultsOn (void* []) Preenche o vector especificado, com os endereços dos objectos declarados como valores por defeito para o método. Os argumentos são colocados no vector nas posições correctas para a chamada à função membro anterior. virtual Bool vExecute (IOID&, IOID&, Uint, IOID []) Bool execute (<0..4+,[]>IOID) Executa o código associado ao objecto-de-método, desde que os argumentos especificados sejam válidos. Na última função membro, recorre-se às funções definidas atrás para a verificação da sintaxe especificada, invocando-se de seguida a função membro vExecuteNoCheck, se a sintaxe estiver correcta. notificação de erro adequada. Caso contrário, é feita uma Se não forem especificados todos os argumen- tos que o método a executar aceita e existirem argumentos por defeito, estes 9 Esta capacidade de atribuição de instâncias de classes a instâncias de classes base é apenas possı́vel nesta versão da linguagem. 5.3. OS OBJECTOS-DE-MÉTODO 133 são usados, preenchendo um vector de endereços, através da função membro getPlacedDefaultsOn. Note-se que as funções membro de invocação por mensagem definidas sobre as metaclasses e em especial sobre IClass, executam também um procedimento semelhante, quando pretendem executar o código associado ao objecto-de-método, que seleccionaram. 5.3.3 Os objectos-de-método para código compilado A interface aos objectos-de-método para acesso à execução do código de métodos compilados, definida na classe ICompiledMethod, concretiza simplesmente vExecuteNoCheck abstracta em IMethod. Nesse sentido, é introduzido o conceito de gestores de métodos (method-handlers) concretizados sob a forma de funções C, que convertem uma chamada a uma função, segundo um protótipo pré-definido, na invocação da função membro, construtor ou operador em geral, cujo código se pretende executar. O tipo associado aos gestores de métodos é: typedef void (*IMethodHandler) (void*, void*, void* []) Tipo dos gestores de métodos. Os argumentos correspondem exactamente àqueles com que a função membro vExecuteNoCheck se declara. Para cada operação compilada ou incluı́da no próprio compilador (builtin), à qual se pretenda associar um objecto-de-método, deve ser criado um gestor de método especı́fico que execute o código dessa operação. O seu endereço deve ser posteriormente usado na parametrização do objecto-de-método “compilado” correspondente, que desse modo o executa, quando vExecuteNoCheck é invocado. 5.3.3.1 Definição dos gestores de métodos A definição de gestores de métodos é um mecanismo sistemático, facilmente adaptável à geração por uma ferramenta adequada, como a que se propõe neste trabalho. Considere-se, por exemplo, a seguinte classe, que define as várias formas de funções membro que é possı́vel encontrar na declaração de uma classe C++: 134 CAPı́TULO 5. O ICE class Contador : // ... static int contagemTotal Contador Contador operator int virtual Contador& operator += int contagemLocal }; (); (int valor); (); (); (Contador&); (); Tome-se primeiramente, os gestores de métodos correspondentes às funções membro, acima declaradas e referidas em 3.2 como métodos de classe: void mh_contagemTotal (void* ret, void*, void*[]) { if (ret) *(int*)ret = Contador::contagemTotal (); else Contador::contagemTotal ();} void mh_ctor_int (void* ret, void*, void* args[]) { *(Contador**)ret = new Contador (*(int*)args[0]);} É de notar, que estes gestores não fazem uso do segundo argumento que, do ponto de vista dos objectos-de-método, corresponde à instância sobre a qual se pretende fazer a invocação e por conseguinte ao objecto-de-classe de Contador10 . Por outro lado, a utilização da expressão condicional no código dos gestores, permite que os métodos possam ser invocados sem argumento de retorno, correspondendo, deste modo, à especificação do comportamento esperado para a primitiva vExecuteNoCheck. No exemplo do construtor e nos seguintes, esse código será omitido, estando no entanto presente na concretização, sempre que o método retorne um valor. 10 Note-se que na concretização dos gestores do código que permite a criação de tipos fundamentais, este argumento é usado para determinar o tamanho do objecto a criar (ver 5.2.2.1). 5.3. OS OBJECTOS-DE-MÉTODO 135 void mh_op_int (void* ret, void* ob, void* []) { *(int*)ret = ((Contador*)ob)->operator int ();} void mh_op_plus_equal (void* ret, void* ob, void* args[]) { *(Contador*)ret = *(Contador*)ob += (*(Contador*)args[0]));} void mh_contagemLocal (void* ret, void* ob, void* []) { *(int*)ret = ((Contador*)ob)->contagemLocal ();} void mh_dtor (void*, void* ob, void*[]) { delete ((Contador*)ob);} 5.3.3.2 Resolução das invocações virtuais para métodos compilados É interessante notar que a utilização de gestores de métodos para o acesso à execução do código de uma função membro, resolve, por si só, a invocação de funções membro virtuais no serviço de invocação por mensagem. De facto, o código do gestor de métodos corresponde exactamente ao código que seria normal usar, para chamar uma função membro. Por conseguinte, se na invocação do gestor, for passado como receptor, um objecto de uma classe derivada que redefina a função membro a chamar, caso essa função seja virtual, então a redefinição será chamada. Nesse caso, se apenas existirem objectos-de-método para acesso a funções membro compiladas, pode ser reduzida a quantidade de código a gerar, se a ferramenta que procede a essa geração, detectar os métodos virtuais redefinidos e omitir a criação dos objectos-de-método correspondentes. Uma consequência importante é que, já que a resolução dos métodos virtuais pode ser feita a nı́vel do objecto-de-método, e dadas as caracterı́sticas da linguagem, a própria utilização do mecanismo de invocação por mensagem pode ser subdividida, permitindo a optimização do seu desempenho. De facto, se uma vez conhecida a sintaxe da mensagem, se obtiver o objecto-de-método correspondente, recorrendo à função membro getMessageMethod ou equivalente sobre o objecto-de-tipo, a invocação propriamente dita, pode ser feita recorrendo directamente a vExecuteNoCheck, ou ainda, não existindo métodos interpretados, à função inline definida em ICompiledMethod de nome executeCompiled. Dessa forma, o 136 CAPı́TULO 5. O ICE tempo perdido em cada invocação por mensagem, relativamente à invocação directa do método, é apenas o tempo da invocação por endereço do gestor do método, mais o da conversão dos argumentos por acesso ao objecto referenciado. Para uma classe com 20 funções membro e para cada uma das primitivas, nomeadamente, executeCompiled e vExecuteNoCheck no objecto-de-método, vInvoke no objecto-de-classe e vRecvMessage no próprio objecto, a tabela abaixo apresenta a razão entre os tempos de invocação de uma das suas funções membro virtuais, através dessas primitivas, e a mesma invocação, directamente em C++. A primitiva executeCompiled vExecuteNoCheck vInvoke vRecvMessage 0 args. 1 arg. 2 args. 3.94 4.06 4.06 5.94 6.40 6.58 106.94 149.76 151.91 157.87 182.50 188.27 3 args. 4 args. 4.95 5.07 6.74 6.90 163.29 164.9 207.97 215.97 Tabela 5.1: Tempos de invocação para cada serviço. tabela apresenta os valores para funções membro com 0, 1, 2, 3 e 4 argumentos. Note-se que, para as primitivas sobre os objectos-de-método, não haverá qualquer degradação, se a classe definir um número maior de funções membro ou, se a função membro invocada for definida numa classe base, da classe do objecto em que se invoca. 5.3.4 Os objectos-de-método para código interpretado Tal como os objectos-de-método acima descritos, também os que permitem o acesso à execução de código interpretado, definidos na classe IInterpretedMethod, redefinirão a função membro de execução vExecuteNoCheck introduzida em IMethod. No entanto, a classe IInterpretedMethod define também as funções membro que permitirão a programação dos objectos-de-método, e cujo código, a interpretar, será guardado em variáveis membro de cada instância desta classe. Tendo em conta a maior importância que mereceu a realização da classe de acesso aos métodos compilados, a concretização actual de IInterpretedMethod constitui apenas um protótipo do que poderá ser uma classe representativa de métodos interpretados, que inclua toda a sintaxe e semântica permitida no C++. De facto, algumas restrições foram impostas a estes objectos, que simplificam bastante a sua 5.4. A GERAÇÃO AUTOMÁTICA DE OBJECTOS-DE-TIPO 137 concretização e a forma como integram no serviço de invocação. Quanto à concretização, pode dizer-se que a classe IInterpretedMethod define objectos-de-método que executam uma sequência de invocações pré-programadas. Assim, cada objecto-de-método “interpretado” inclui, nas suas variáveis membro, uma lista, em que cada elemento é composto por: método-a-invocar apontador para um objecto-de-método a ser invocado. retorno, receptor endereços dos objectos de retorno e receptor do método a invocar. vector-de-argumentos vector de endereços dos argumentos a passar para o método. A redefinição feita sobre a função vExecuteNoCheck chama sucessivamente para cada elemento da lista, a mesma função (vExecuteNoCheck), com os argumentos e sobre o objecto-de-método, com que foi programada. Quanto à parametrização, apenas se definiram funções membro para acrescentar e remover métodos a invocar e os respectivos argumentos. Note-se que, a introdução de métodos interpretados, compromete o mecanismo de resolução de métodos virtuais, tal como foi definido para os métodos compilados. De facto, um método interpretado nunca irá constar da tabela de métodos virtuais das classes derivadas, a menos que algum mecanismo, não concretizado, o possa lá registar. Desse modo, por exemplo, um método virtual interpretado nunca poderá ser chamado em código compilado, a menos que se recorra à invocação por mensagem (recvMessage). Por essa razão, a definição de métodos interpretados limitou-se a funções não virtuais, não se permitindo também, em tempo de execução, a substituição de métodos (apenas do seu código). 5.4 A geração automática de objectos-de-tipo A geração do código correspondente à criação dos objectos-de-tipo, e em especial dos objectos-de-classe, estruturas descritivas das instâncias, objectos-de-método e correspondentes gestores para acesso a código compilado, está a cargo de uma ferramenta, a que se designou ICE-TOP (ICE Type Object Parser). O ICE-TOP, 138 CAPı́TULO 5. O ICE fazendo parte integrante da distribuição do ICE, é um analisador de declarações de tipos C++ que gera, para cada tipo declarado: • um objecto-de-tipo correspondente, acessı́vel globalmente por uma variável de nome type seguida do nome do tipo; • um objecto-de-tipo correspondente ao apontador para o tipo, acessı́vel por uma variável de nome type seguida do nome do tipo e do sufixo “P”; • duas funções, para o acesso aos endereços de cada um dos objectos acima, de nome typeOf<nome-do-tipo>, seguido de “P” para o segundo objecto. Se o tipo for uma classe: • se for declarada uma função membro isA, gera a sua concretização: CLASS::isA () {return &type CLASS;}; • se não for declarada uma função membro estática ou função C de nome createRetrieveSpace, sem argumentos e retornando um apontador para um objecto da classe (ou void*), o analisador gera também a sua concretização: void* createRetrieveSpace () {return new CLASS;}; • gera ainda um objecto-de-método estático para cada função membro (no sentido geral) e o correspondente gestor do método; • e cria uma estrutura que descreve cada variável membro da classe. Quer os objectos-de-método, quer as estruturas, são organizadas em vectores que são passados, juntamente com o endereço de createRetrieveSpace, para o construtor do objecto-de-classe. Finalmente, se duas funções membro ou funções C, forem declaradas com o nome storer e retriver e a sintaxe definida para as funções de redefinição da SR, também o seu endereço é passado como argumento para o construtor do objecto-de-classe. Se a declaração do tipo for um typedef que introduza apenas um novo nome (e.g. typedef A B), a criação do novo objecto-de-tipo é omitida e são apenas geradas duas referências (do tipo e do apontador), com o nome esperado (IType& type B 5.4. A GERAÇÃO AUTOMÁTICA DE OBJECTOS-DE-TIPO 139 e IType& type BP), e às quais é atribuı́do o objecto-de-tipo original (type A e type AP). São geradas também as correspondentes funções de acesso aos endereços (inline). 5.4.1 Regras da utilização de nomes A existência de um gerador automático de descrições de tipos, precisamente por ser automático, requer a definição de um conjunto de regras que, de algum modo, resolvam as possı́veis declarações omitidas num ficheiro de declarações (.h) em C++. Por exemplo, na declaração de uma classe, não é necessário que todos os tipos referidos sejam definidos, nem mesmo nos ficheiros incluı́dos, desde que esses tipos sejam usados na definição de apontadores: class A; class B; class C { // ... A* membro (B*, void*); // ... Do mesmo modo que o compilador de C++ faz para os tipos, também o ICETOP assume a existência dos objectos-de-tipo relativos aos tipos não declarados no ficheiro, e que, no entanto, são usados na composição de outros. Para além disso, de forma a evitar a construção de tipos usados frequentemente, tais como são os apontadores para um tipo, e na sequência do que é gerado quando uma declaração de tipo é encontrada, também para os objectos-de-tipo apontador, o ICE-TOP admite a sua existência. Finalmente, para esses objectos, assume uma forma de acesso idêntica à que se teria, se a sua declaração fosse encontrada. Para o exemplo acima, é admitida a existência de type A, type AP, type B, e type BP. Como solução de compromisso, quando um tipo não está dentro dos acima mencionados (e.g. T**), é criado um objecto-de-tipo local ao código gerado. É importante referir, que o ICE-TOP não altera as declarações ou definições da classe (ou tipo em geral) analisada, limitando-se simplesmente a criar código noutro ficheiro (de extensão .C.c), cuja ligação (ld) com o restante pode ser omitida, caso os serviços que o ICE oferece, não sejam requeridos. Mesmo para classes ICE que incluam a declaração da função membro isA, a geração do código pode também 140 CAPı́TULO 5. O ICE ser evitada, sendo suficiente a definição desse método e da função que retorna o apontador para o tipo (typeOf<nome-do-tipo>) - por exemplo retornando o tipo da classe base. Porém, se esse tipo for usado noutra classe em que se pretende passar o ICETOP, e em que se assume a existência do objecto-de-tipo correspondente, segundo as regras definidas acima, então ocorrerá um erro na ligação do programa. Nesse caso são oferecidas duas soluções: • cria-se o objecto-de-tipo sob o nome que se espera, ainda que sob a sua forma mais simples, por exemplo, como instância de IType. • colocam-se todas as referências ao tipo entre comandos do pré-processador de C, nomeadamente, #ifndef ICE TOP e #endif, que evitam a geração do objecto-de-método ou descrição de variável membro correspondentes. 5.4.2 Concretização O ICE-TOP foi concretizado usando as ferramentas lex e yacc [Sun 86a, Sun 86b], segundo algumas das recomendações feitas em [Schreiner 85]. Note-se que, a gramática reconhecida por este analisador, limita-se a instruções de declaração de C++, não incluindo, por isso mesmo, toda a complexidade da linguagem. Por outro lado, como a sua função é paralela à do compilador de C++, ignora os problemas decorrentes do reconhecimento de erros, o que também simplificou, de sobremaneira, a sua concretização. Estas simplificações vêm de encontro à facilidade com que se realizaram as actualizações feitas, decorrentes das sucessivas versões da linguagem. Quanto aos detalhes da sua concretização, eles não serão aqui descritos, até porque ela não foi realizada, mas simplesmente orientada, pelo autor do trabalho que aqui se apresenta. 5.5 O serviço de nomes O serviço de nomes global do ICE, acessı́vel como se disse através da variável global iceNameService, é uma instância da classe INameService. Esta classe, derivada de 5.5. O SERVIÇO DE NOMES 141 IObject, é concretizada por duas tabelas de hash que permitem acelerar o acesso aos objectos, quer por nome, quer por endereço. Essas tabelas correspondem a duas classes não ICE, se bem que concretizadas pelo autor, e que, de algum modo, oferecem um bom exemplo de integração entre classes derivadas e não derivadas de IObject. A sua concretização é feita de um modo semelhante ao descrito para a tabela de sı́mbolos partilhada pelas instâncias de ISymbol, sendo, no entanto, as colisões resolvidas por uma lista ligada. A função de hash correspondente à tabela de nomes, leva em linha de conta o nome, de uma forma idêntica à tabela de sı́mbolos. No entanto, porque os objectos registados podem ter nomes iguais, desde que tenham donos diferentes, quando ocorre uma colisão, a discriminação é inicialmente feita pelo endereço e, em último caso, pelo nome, limitando normalmente assim, a utilização de strcmp a uma vez por procura. A utilização de uma função de hash apenas sobre o nome, em vez de discriminar de imediato o par nome/dono, está relacionado com a possibilidade de usar este serviço na detecção de nomes num contexto. De facto, quando um objecto é registado com um nome que já foi atribuı́do a outro, embora para outro dono, o novo objecto é colocado na lista de colisão antes dos seus homónimos. Assim, se for feita uma procura para o último objecto registado com um dado nome, o objecto retornado é o primeiro a ocorrer na lista de colisões respectiva da tabela (correspondente ao último contexto). Para o caso da tabela que permite o acesso por endereço, a função de hash incide simplesmente sobre o endereço, tal como a resolução das colisões. 5.5.1 Interface ao serviço de nomes IObject* findObject (char*, IObject* =0) Retorna um objecto registado no serviço de nomes, com o nome e o dono especificados. IObject* findLastObject (char*) Retorna o último objecto registado com o nome especificado. Bool getObjectFullName (IObject*, char*&, IObject*&) Retorna, nos dois últimos argumentos, o nome e o dono do objecto especificado. Bool addObject (IObject*, char*, IObject* =0) Regista um novo objecto no serviço de nomes, com o nome e o dono especificados. Bool removeObject (IObject*) Retira o objecto especificado do serviço. 142 CAPı́TULO 5. O ICE A semântica associada a estas funções membro está de acordo com o comportamento definido acima e em 3.3. A classe INameService permite ainda o acesso sequencial aos objectos nela registados, por recurso a um leitor INSReader semelhante aos já referidos para outras classes. 5.6 Os objectos-de-E/S Os objectos-de-E/S são responsáveis pela especificação sintáctica das representações externas, resultantes de uma operação de salvaguarda, e pela sua reconversão em representações internas equivalentes, na recuperação. Devem ainda resolver a ocorrência de ciclos fechados de referências, evitando a descrição repetida do mesmo objecto em cada operação de salvaguarda, e possibilitar a especificação de subconjuntos de um conjunto-de-salvaguarda, de modo a limitar o número de objectos envolvidos em cada representação externa. Na recuperação, devem detectar a ocorrência de objectos não guardados e adaptar as referências, para eles expressas na representação externa, ao novo contexto em que se irão executar. 5.6.1 Interface comum aos objectos-de-E/S IIO é uma classe abstracta, também derivada de IObject, que define a interface comum aos objectos-de-E/S. Inclui os mecanismos básicos para o controlo dos objectos envolvidos em cada operação de escrita e leitura, deixando às classes derivadas, a especificação de sintaxes diversas e utilização de diferentes meios de salvaguarda. Como variáveis membro introduz: tabela-de-detecção-de-ciclos onde são registados os endereços dos objectos guardados e recuperados. Na salvaguarda é usada para a detecção de ciclos fechados de referências e transformação de referências internas (endereços) em referências externas. Na recuperação permite a transformação inversa. tabela-de-objectos-a-não-guardar onde devem ser registados os objectos cuja descrição não se pretende guardar, mesmo que sejam referenciados por outros envolvidos nas operações de salvaguarda. na-colisão-de-nomes, na-falta-de-objecto, na-referência-objectos-não-guardados Apontadores para as funções que serão chamadas na recuperação, respectivamente, 5.6. OS OBJECTOS-DE-E/S 143 quando: ocorre uma colisão de nomes; é detectada a primeira referência de um objecto não guardado; é encontrada qualquer referência a esses objectos. A primeira variável membro é concretizada como uma tabela de acesso rápido (hash) e associativa, no sentido em que faz corresponder: na salvaguarda, o endereço do objecto guardado, a uma possı́vel identificação na representação externa (nesta concretização o número de ordem pela qual é guardado); na recuperação, faz a associação inversa. Naturalmente, o acesso é optimizado relativamente ao endereço, no primeiro caso, e em relação à identificação externa, no segundo. A segunda é uma tabela de hash simples, não associativa, em que o utilizador da classe regista, recorrendo a addToNoStoreList), os objectos cuja descrição não pretende guardar. Por fim, as restantes variáveis membro são apontadores para funções, cujo tipo se definiu do seguinte modo: typedef IObject* (*IIONameCollisionHandler) (IObject*, IType*); Estas funções devem retornar um objecto pertencente a um tipo conforme com aquele que é passado no segundo argumento. O primeiro argumento corresponde ao objecto existente no contexto de execução, com o qual iria ocorrer a colisão de nomes, se a recuperação prosseguisse normalmente. typedef IObject* (*IIOObjectFaultHandler) (IType*, char*); Devem retornar também um objecto conforme com o tipo passado (no primeiro argumento), sendo o segundo argumento o nome que o objecto, cuja descrição não foi guardada, possuı́a no contexto que o salvaguardou. typedef void (*IIONotifyNotStoredHandler) (IObject*, IObject*); Devem enquadrar o objecto especificado no primeiro argumento (cuja descrição não foi guardada), no novo contexto de execução. No segundo argumento é passado o objecto que, na representação externa em recuperação, o referencia. Os objectos-de-E/S podem ser parametrizados com funções dos tipos acima, através das funções membro onNameCollision, onNotStoredFault e onNotStored, respectivamente. Se não for especificada nenhuma função para resolver as possı́veis colisões de nomes, o objecto-de-E/S utiliza o objecto com o qual se daria a colisão, caso este seja de um tipo conforme com o esperado. Caso contrário, gera uma mensagem de erro. Na situação em que não se especificou uma função do tipo IIOObjectFaultHandler, e se não tiver havido colisão de nomes, o objecto-de-E/S gera igualmente uma mensagem de erro. Finalmente, na detecção de referências para objectos não guardados, não é executada nenhuma acção por defeito, sendo 144 CAPı́TULO 5. O ICE usado o objecto obtido por um dos processos anteriores, tal como se tivesse sido guardado. 5.6.1.1 Primitivas de salvaguarda e recuperação virtual Bool storeObject (IOID&) virtual Bool retrieveObject (IOID&) Salvaguarda/recupera um objecto identificado pelo argumento. virtual Bool storeMember (IOID&, IClass*, char*, int) virtual Bool retrieveMember (IOID&) Salvaguarda/recupera uma variável membro. No primeiro argumento é especificado o tipo e endereço da variável. Na primeira função são ainda especificados: a classe em que a variável membro se definiu, o seu nome e a ordem em que aparece na definição. virtual Bool storeArrayMember (IOID&, int, IClass*, char*, int) virtual Bool retrieveArrayMember (IOID&, int) Salvaguarda/recupera uma variável membro do tipo vector. O segundo argumento corresponde ao tamanho do vector. Os restantes são equivalentes aos das funções membro anteriores. virtual virtual virtual virtual Bool Bool Bool Bool storeAlternate (IOID&, ISymbol&) retrieveAlternate (IOID&) storeArrayAlternate (IOID&, int, ISymbol&) retrieveArrayAlternate (IOID&, int) Salvaguarda/recupera uma variável não membro, cujo tipo e endereço se especifica no primeiro argumento. O argumento correspondente ao sı́mbolo nas funções de salvaguarda, corresponde ao nome do método a invocar na recuperação, se a sintaxe da salvaguarda for código C++. Estas funções concretizam o algoritmo descrito na secção 4.3 e utilizam as tabelas, acima referidas, para a detecção de objectos guardados e recuperados (4.4.1) e para a reintegração dos objectos em novos contextos de execução (4.4.2). Para além disso, invocam funções especı́ficas de escrita e leitura, não concretizadas nesta classe, que definirão a sintaxe de cada uma das operações (e.g. writeBegin, writeBeginMember, ..., readBegin, readBeginMember, etc), ou a forma como é escrito/lido o conteúdo do objectos, propriamente dito (e.g. writeInt, writePointer, ..., readInt, readPointer, etc). 5.6.2 Objectos especı́ficos Derivadas da classe abstracta representativa dos objectos-de-E/S, estão disponı́veis, no ICE, as seguintes classes: 5.6. OS OBJECTOS-DE-E/S 145 • ITextualIO gera uma representação externa independente da máquina, sob a forma de uma cadeia de caracteres, semelhante às sintaxes adoptadas nos serviços de SR do Objective-C, do ET++ e a textual do OOPS. • IBinIO gera uma representação externa sem transformação da representação interna, com excepção das referências entre objectos. Nestas usa, tal como a classe anterior, o número de ordem da salvaguarda como forma de identificação externa dos objectos. • ICodeIO gera uma representação externa sob a forma de código C++ que poderá ser compilado, e cuja execução recupera os objectos salvaguardados. O código gerado recorre à primitiva getMember sobre os objectos-de-classe, para obter o endereço das variáveis membro, que de seguida parametriza, ou à função especificada em storeAlternate para as variáveis não membro. Todas as classes usam como meio de salvaguarda descritores de ficheiros, oferecendo também uma interface à abertura de ficheiros por nome. Relativamente à classe IIO, estas apenas redefinem, consoante a sintaxe que adoptam, as funções membro de escrita e leitura. Considere-se, por exemplo, a seguinte classe: class X : public IObject { X* _xp; int _i; public: X () :() {_xp = this; _1 = 10;} // ... e as classes de E/S ITextualIO e ICodeIO. As tabelas seguintes mostram o código que permite guardar uma instância x de X e a representação externa gerada, segundo cada uma das classes de E/S. A instância foi inicializada tal como é definido pelo construtor. 146 CAPı́TULO 5. O ICE ITextualIO io; io.openWriteFile ("nome.dat"); io.storeObject (x); ICodeIO io; io.openWriteFile ("nome.c"); io.funcNameIs ("exemplo"); io.storeObject (x); :X! @0 {@1 10} X* exemplo () { X* o1 = typeOfX ()->createRetrieveSpace (); typeOfX ()->setMember (o1, 1, o1); typeOfX ()->setMember (o1, 2, 10); return o1;} Capı́tulo 6 Conclusão O ICE, apresentado nesta tese, é um sistema constituı́do por uma biblioteca de classes C++ e uma ferramenta de análise de declarações da mesma linguagem. A ferramenta gera a definição de um conjunto de estruturas, que permitem suportar, em tempo de execução, os serviços oferecidos pela biblioteca. Esses serviços incluem um mecanismo de invocação interpretada de operações, quer de criação, quer de parametrização de objectos, um serviço de nomes, que permite a identificação hierárquica dos mesmos, e um mecanismo genérico de salvaguarda e recuperação (SR). Esta funcionalidade integra-se num modelo de objectos uniforme, que permite ter uma visão homogénea da generalidade das entidades (tipos, objectos e valores) envolvidas na linguagem. A utilização do ICE como suporte, quer à ferramenta INGRID de construção interactiva de interfaces Homem-Máquina, quer à biblioteca 4D de componentes dessas interfaces, validou a concretização deste trabalho, justificando largamente o esforço nele despendido. De facto, a introdução de capacidades interpretativas numa linguagem como o C++, ofereceu a estes sistemas a flexibilidade que requeriam, sem perder o elevado grau de abertura e compatibilidade que esta linguagem oferece. Por outro lado, a uniformidade do modelo adoptado no ICE, providenciou a extensibilidade desejada para ambos os sistemas, que durante a sua evolução integraram novas classes de objectos (nomeadamente os resultantes da introdução da biblioteca Motif), sem necessidade de alterações significativas. Finalmente, a disponibilização de um mecanismo de SR ao nı́vel da linguagem, permitiu incluı́-lo na biblioteca 4D sem grande esforço e, consequentemente, também na INGRID, em que se usou como forma de salvaguardar e recuperar as interfaces construı́das por esta ferramenta. 147 148 CAPı́TULO 6. CONCLUSÃO Relativamente à utilização de uma ferramenta simples de análise de declarações C++, deve dizer-se que, por um lado, resolveu a maior parte do trabalho, que de outro modo seria exigido aos programadores que pretendessem ter acesso aos serviços do ICE. Por outro lado, o facto de não ser um compilador extensivo da linguagem, facilitou em grande medida a sua concretização e adaptação a todas as evoluções da mesma. Finalmente, porque não altera o código das classes, limitandose a acrescentar código que servirá de suporte aos serviços, proporcionou uma ligação simples a bibliotecas já existentes, nomeadamente às utilizadas pelo 4D (Athena e Motif) e mesmo àquelas a que se recorreu na concretização do ICE (listas e tabelas de hash). A crı́tica fundamental feita à utilização do ICE, prende-se com a quantidade de código que é gerado pela ferramenta, e que, por vezes, é ligado com aplicações que usam, só parcialmente, os serviços oferecidos. De facto, embora esse código possa ser omitido quando não se pretendem usar esses serviços, é por vezes difı́cil ao programador das classes decidir, a priori, se um conjunto de métodos irá ou não ser invocado usando as primitivas do ICE, ou se instâncias da classe irão ser guardadas e recuperadas, qualquer que seja a aplicação em que se usem, ou mesmo para uma aplicação em particular. A solução parece passar por encontrar um mecanismo automático de ligação de código em tempo de execução, que só o carregaria caso fosse necessário. 6.1 Trabalho Futuro 6.1.1 Evolução Funcional Como perspectivas futuras, prevê-se, desde já, a introdução de herança múltipla nos mecanismos oferecidos pelo ICE. A sua concretização, embora simples do ponto de vista da ferramenta de análise, tem algumas consequências na forma de realização do serviço de invocação. De facto, a utilização de herança múltipla no C++, implica eventualmente conversões no objecto invocado (mudança de endereço), feitas implicitamente pelo compilador da linguagem, e que deverão igualmente ser incluı́das no ICE. A solução para este problema parece ter algumas semelhanças com a que deve ser encontrada para os mecanismos de conversão de argumentos, existentes na invocação de métodos do C++, cuja concretização no ICE está também a ser 6.1. TRABALHO FUTURO 149 estudada. O problema essencial reside no facto de a cada mensagem passar a corresponder um conjunto de invocações, que inclui a invocação ao método pretendido, precedida das conversões necessárias. Nesse caso, o mecanismo de “pré-compilação”, que devolve um objecto-de-método, passaria a retornar uma sequência desses objectos, que incluı́sse também os correspondentes às conversões. Do ponto de vista das ferramentas, poderá também ser interessante a introdução, nos objectos ICE, de um mecanismo de reciclagem automática de memória, presente na maioria dos ambientes de programação interactivos. A integração de um mecanismo desta natureza nas classes do ICE, não poria, em princı́pio, grandes problemas, já que estando essas classes acessı́veis, o seu código pode ser alterado, de modo a incluir as directivas normalmente requeridas pelas concretizações disponı́veis, para reciclagem de objectos em C++ [Bartlett 89]. No entanto, essas concretizações não resolvem problemas de acesso a endereços dentro do objecto e chamada a destrutores, quando o objecto é libertado. Para além disso, tendo em conta as preocupações de compatibilização do ICE com bibliotecas e sistemas já existentes, essa integração deverá oferecer igualmente algum suporte à gestão de memória para bibliotecas em geral. Uma solução para este problema será, por exemplo, a adopção de um mecanismo de reciclagem genérico para o C [Bartlett 88, Capingler 88], que, em contrapartida, poderá trazer problemas graves de desempenho. Quanto ao melhoramento do ICE, pensa-se que o esforço deve incidir sobretudo na ferramenta de análise, de forma a diminuir substancialmente o código por ela gerado, por exemplo, omitindo a geração de objectos-de-método, correspondentes a redefinições de funções membro virtuais. Relativamente à biblioteca, existem sempre as questões de robustez cuja resolução depende, naturalmente, das reacções à sua utilização, e a possibilidade de aumentar a eficiência dos serviços, nomeadamente no que diz respeito ao mecanismo de invocação de operações e SR de objectos. Note-se que, em relação ao primeiro, a melhoria do serviço pode ser concretizada com base num algoritmo de discriminação de métodos, semelhante aos adoptados no Smalltalk, Objective-C, ou IK, mas que tenha em conta a sobreposição de métodos e a possı́vel conversão de argumentos. Contudo, este mecanismo só irá realmente introduzir alguma melhoria, se se admitir que, na utilização do serviço, nem sempre é possı́vel, ou se pretende, recorrer à solução de pré-compilação que é proposta nesta tese. 150 CAPı́TULO 6. CONCLUSÃO Finalmente, algum trabalho deverá ser realizado, já em fase de estudo, com o intuito de solucionar o problema exposto no fim da secção anterior. A solução parece ser a introdução de um mecanismo de ligação dinâmica, para o código gerado pela ferramenta de análise do ICE. No entanto, esse código denota um problema comum à adopção de ligação dinâmica de código em C++ [Sousa 91a]: a utilização de objectos globais, os objectos-de-tipo, cuja construção deveria ocorrer, segundo a semântica do C++, antes da execução da função main(), não é possı́vel, se esse código for ligado dinamicamente. Porém, não havendo nos objectos-de-tipo qualquer inicialização de contexto, e pretendendo precisamente, que esses objectos sejam incluı́dos no código, apenas quando são referidos, o problema que se coloca é simplesmente a detecção do acesso a esses objectos. Essa detecção pode facilmente ser concretizada nas funções membro isA ou nas funções typeOf<tipo>. Na concretização actual do ICE, haveria que proibir o acesso directo aos objectos-de-tipo, e gerar as funções acima, por exemplo, no próprio ficheiro de cada classe, de maneira a ligarem o restante código gerado, na primeira vez que fossem invocadas. Para a ligação dinâmica, propriamente dita, o próprio editor de ligações (ld), presente nos sistemas Unix BSD 4.1 e seguintes, oferece as caracterı́sticas necessárias. 6.1.2 Integração com o IK Tendo em conta os pontos atrás referidos, sobre a inclusão no ICE, de serviços de reciclagem automática de memória e ligação dinâmica de código, oferecidos no IK (2.3.3 e 2.4.4), e ainda os pontos comuns existentes no modelo e estruturas de suporte de ambos, ir-se-á agora analisar as possibilidades de integração destes dois sistemas. Discutir-se-á este problema segundo duas perspectivas: • integração sobre estruturas de suporte comuns; • integração dos serviços; 6.1.2.1 Suporte comum à execução dos serviços Do ponto de vista das estruturas de dados associadas aos mecanismos de invocação por mensagem, as diferenças fundamentais residem na informação de tipo associada aos argumentos dos métodos, quer relativos à declaração do método, quer ao 6.1. TRABALHO FUTURO 151 tipo dos argumentos reais passados na invocação. De facto, no IK, a informação respeitante aos tipos dos argumentos na declaração dos métodos, é codificada numa cadeia de caracteres, que decompõe cada tipo numa sequência de tipos fundamentais, de forma semelhante á descrição de instâncias do Objective-C (ver 2.4.3). Por outro lado, aparte os objectos derivados de object, a informação de tipo não está acessı́vel para nenhum outro objecto ou valor, que se use quando o método é invocado. Tendo isto em consideração, dificilmente se poderia concretizar um mecanismo de invocação interpretada, segundo a semântica que se adoptou no ICE. Naturalmente, este problema não se põe no IK, já que não é seu objectivo oferecer mecanismos de interpretação, deixando os problemas de verificação de tipo de argumentos aos compiladores que o usem como suporte (e.g. o EC++). No que se refere ao serviço de SR, as estruturas associadas à descrição das instâncias não são usadas na concretização actual do IK, tal como se disse em 2.4.4. Por outro lado, a utilização de funções de varrimento de referências para as instâncias de cada classe, gera uma descrição incompleta destas, que não permite alcançar a versatilidade que no ICE se propôs para este serviço. Mais uma vez, a divergência nos objectivos, determinaram diferenças nas estruturas de suporte que, embora pudessem ser colmatadas, resultariam na perda de eficiência, essencial no IK, ou inversamente, na diminuição da flexibilidade de utilização possı́vel do serviço, no ICE. Também a gestão das identificações dos objectos a nı́vel do sistema, necessária no IK para a concretização do conceito de objectos persistentes, é supérflua para a maior parte das aplicações que se têm em vista, para o serviço de SR do ICE. Naturalmente, em qualquer dos sistemas, este problema poderia ser resolvido: no IK, dando acesso ao mecanismo de SR subjacente; no ICE, definindo uma classe de E/S que gerisse as identificações. Finalmente, é importante apontar a diferença de base que apresentam as estruturas de ambos os sistemas. No ICE pretendeu-se seguir de raiz, na própria concepção do sistema, uma aproximação de programação orientada para objectos, em C++, de forma a que o utilizador possa explicitamente manusear as entidades do modelo, tal como usa as suas próprias classes. Já no IK, não se põe, à partida, esse problema visto que se trata de uma concretização em C, para um suporte à execução de código gerado por compiladores. 152 CAPı́TULO 6. CONCLUSÃO 6.1.2.2 Integração de serviços O modo normal de integração das caracterı́sticas oferecidas pelo IK, nos objectos ICE é, sem dúvida, a utilização do compilador de C++ para essa plataforma (o EC++), sobre as classes que que se descreveram neste trabalho. Derivar-se-ia também IObject da classe base do IK, incluindo assim todas as capacidades deste sistema nos objectos ICE, mantendo no entanto as estruturas que o ICE usa para concretizar os seus serviços. Porém, surgem algumas dificuldades, na versão corrente dos dois sistemas, que dificultam esta aproximação. Por um lado, o problema já referido da existência objectos globais no código gerado pelo ICE, que não se compatibilizam com o mecanismo de ligação dinâmica de código, disponı́vel no IK. Por outro lado, as alterações ao código decorrentes de utilização do EC++, dificilmente permitiriam manter o serviço de SR do ICE. Finalmente, a incompatibilidade desse compilador, correspondente à versão C++ 1.1, com algumas construções usadas no ICE, já na versão 2.0 do compilador dessa linguagem, bem como algumas restrições relativas à utilização de variáveis membro que sejam instâncias de classes, dificultam a sua integração, exigindo algum esforço na conversão do ICE e sobretudo um retrocesso na sua evolução, ou, inversamente, na concretização do compilador EC++ sobre a versão 2.0. Por outro lado, na evolução do IK, está já prevista a definição de duas bibliotecas, autónomas, que oferecem precisamente, serviços de reciclagem automática de memória para o C++ [Ferreira 91a] e ligação dinâmica de código [Sousa 91a]. O primeiro, pode integrar-se facilmente, resolvendo já as questões de invocação dos destrutores na libertação dos objectos e possı́vel existência de apontadores para variáveis de instância. Por outro lado, propõe ainda um mecanismo genérico de gestão de memória em bibliotecas que, tal como foi dito, é importante para manter os objectivos que o ICE pretende atingir. A biblioteca de ligação dinâmica de código pode facilmente integrar-se do modo descrito anteriormente, com vantagens, sobretudo de desempenho, relativamente a outros mecanismos que oferecem o mesmo serviço. 6.1. TRABALHO FUTURO 6.1.3 153 Perspectivas de exploração Tal como se disse na definição dos objectivos propostos para o ICE, embora o sistema sirva de suporte à biblioteca 4D e à ferramenta INGRID, não se limita, de modo algum, a esse universo de aplicação. De facto, os serviços que oferece, podem facilmente ser usadas na definição de interpretadores de linguagens destinadas à configuração de aplicações, em tempo de execução. Nesse sentido, a utilização do serviço de nomes permite identificar os objectos dessas aplicações. O serviço de invocação por mensagem oferece a capacidade de execução das operações de parametrização sobre esses objectos. Por outro lado, esses mesmos serviços poderão permitir a definição de formas de comunicação entre aplicações, ou componentes da aplicação em execução em diferentes processos. O serviço de nomes poderá identificar os objectos remotos, sendo relativamente simples transformar uma mensagem, recebida num socket, numa invocação a um objecto. Uma aplicação imediata, aplicada à gestão de interfaces Homem-Máquina (IHM), será a separação de interface e componente computacional da aplicação, em contextos de execução diferentes, cada uma adoptando as capacidades interpretativas do ICE, para transformar pedidos de invocação remotos, em invocações a objectos locais. Um protótipo desta aproximação, é descrito em [Antunes 90c]. Finalmente, tal como o seu nome faz prever, a utilização do ICE como suporte a ambientes de programação interactiva, sobre bibliotecas de classes C++, pode também ser realizada. De facto, a informação que oferece sobre as classes, em tempo de execução, poderá ser usada como forma de navegação, podendo os restantes serviços permitir a definição de novas classes, segundo uma perspectiva de programação experimental, de um modo semelhante à que a INGRID faz para os componentes de IHM. 154 CAPı́TULO 6. CONCLUSÃO Bibliografia [aes 91] N. Guimar aes, L. Carriço, e P. Antunes. INGRID : An Object Oriented Interface Builder. Em Proceedings of the TOOLS’91 Conference, Santa Barbara, California, Santa Barbara, California, Julho 1991. [Agha 86] Gul Agha. An Overview of Actor Languages. SIGPLAN Notices, 21(10):58–67, Outubro 1986. [Aho 85] Alfred V. Aho, Ravi Sethi, e Jeffrey D. Ullman. Compilers, Principles, Techniques, Tools. Addison-Wesley, Reading, Massachusetts, 1985. [Antunes 90a] P. Antunes. Sistemas de gestão de interfaces homem-máquina: Uma taxonomia. Relatório Técnico INESC-0061, INESC, 1990. [Antunes 90b] P. Antunes. A toolkit for interactive construction of user interfaces. Relatório Técnico INESC-0059, INESC, 1990. [Antunes 90c] P. Antunes e L. Carriço. Medidas e considerações sobre a separação entre interface e componente computacional de uma aplicação. Relatório técnico, INESC, 1990. [Antunes 91] P. Antunes. Uma biblioteca para a construção interactiva de interfaces homem-máquina. Tese de Mestrado, Instituto Superior Técnico, Lisboa, Portugal, 1991. [Bartlett 88] Joel Bartlett. Compacting Garbage Collection with Ambiguous Roots. Relatório técnico, DEC Western Research Laboratory, Fevereiro 1988. Technical Report 88/2. [Bartlett 89] Joel Bartlett. Mostly-Copying Garbage Collection Picks Up Generation and C++. Relatório técnico, DEC Western Research Laboratory, Fevereiro 1989. Technical Note TN-12. [Bobrow 86] Daniel G. Bobrow et al. CommonLoops Merging Lisp and ObjectOriented Programming. Em OOPSLA ’86 Proceedings, páginas 17–29, Portland, Oregon, Setembro 1986. [Capingler 88] Michael Capingler. A Memory Allocator with Garbage Collection for C. Em Proceedings of the Winter 1988 Usenix Conference, páginas 325–330, Dallas, 1988. 155 [Cardelli 85] Luca Cardelli e Peter Wegner. On Understanding Types, Data Abstraction, and Polymorphism. Computing Surveys, 17(4), Dezembro 1985. [Carriço 89] L. Carriço et al. Run-time support for the images toolkit. Relatório técnico, INESC, 1989. [Carriço 90] L. Carriço, N. Guimar aes, e P. Antunes. INGRID: A graphical tool for user interface construction. Em Proceedings of the EUUG ’90 Conference, páginas 177–185. EUUG, 1990. [Chambers 89] Craig Chambers, David Ungar, e Elgin Lee. An Efficient Implementation of Self a Dynamically-Typed Object-Oriented Language Based on Prototypes. Em OOPSLA ’89 Proceedings, páginas 49–70, New Orleans, Louisiana, Outubro 1989. [Coutaz 87] J. Coutaz. The Construction of User Interfaces and the Object Paradigm. Em ECOOP ’87, European Conference on ObjectOriented Progr., páginas 121–130, Paris, Junho 1987. [Coutaz 89] J. Coutaz. Architecture Models for Interactive Software. Em Proceedings of the ECOOP ’89 Conference, Nottingham, Julho 1989. [Cox 86] B. Cox. Object-Oriented Programming An Evolutionary Appoach. Addison-Wesley, 1986. [Ellis 90] Margaret A. Ellis e Bjarne Stroustrup. The Annotated C++ Reference Manual. Addison-Wesley, Reading, Massachusetts, 1990. [Ferreira 91a] Paulo Ferreira. Garbage collection in c++. Relatório técnico, INESC, 1991. [Ferreira 91b] Paulo Ferreira e Pedro Antunes. Um tradutor de objective-c para o ambiente comandos. Relatório técnico, INESC, Março 1991. [Gamma 88] Erich Gamma, André Weinand, e Rudolf Marty. ET++ - An Object-Oriented Application Framework in C++. Em EUUG Autumn 1988, páginas 159–174, Cascais, Outubro 1988. [Goldberg 83a] A. Goldberg. Smalltalk-80: The Interactive Programming Environment. Addison-Wesley, 1983. [Goldberg 83b] A. Goldberg e D. Robson. Smalltalk-80: The Language and Its Implementation. Addison-Wesley, 1983. [Goldberg 86] Adele Goldberg. The Influence of an Object-Oriented Language on the Programming Environment. Em David R. Barstow, Howard E. Shrobe, e Erik Sandewall, editores, Interactive Programming Environments, capı́tulo 8, páginas 141–174. McGrawHill, 1986. 156 [Gorlen 87] Keith E. Gorlen. An Object Oriented Class Library for C++ Programs. Software Practice and Experience, 17(12):899–922, Dezembro 1987. [Gorlen 90] K. Gorlen, S. Orlow, e P. Plexico. Data Abstraction and ObjectOriented Programming in C++. John Wiley & Sons, 1990. [Guimaraes 91] N. Guimaraes. INGRID: Interactive Graphical Interface Designer. Tutorial presented at the 5th Annual X Technical Conference, Boston, Janeiro 1991. [Hartson 89] H. Hartson e D. Hix. Human-computer interface development: Concepts and systems for its management. ACM Computing Surveys, 21(1), Março 1989. [Horn 88] Bruce L. Horn. An Introduction To Object Oriented Programming, Inheritance and Method Combination. Relatório Técnico CMU-CS-87-127, CMU-CS, Janeiro 1988. [Kernigham 78] B.W. Kernigham e D.M. Ritchie. The C Programming Language. Prentice-Hall, 1978. [Kernigham 88] B.W. Kernigham e D.M. Ritchie. The C Programming Language. Prentice-Hall, 1988. [Marques 88] J. Marques, L. Simoes, e N. Guimaraes. A Uims and integrated environment for the somi workstation. Em Proceedings of the ESPRIT ’88 Conference, Brussels, Novembro 1988. [Marques 89] José Alves Marques e Paulo Guedes. Extending the Operating System to Support an Object-Oriented Environment. Em OOPSLA ’89 Proceedings, páginas 113–122, New Orleans, Louisiana, Outubro 1989. [Marques 90] José Alves Marques e Paulo Guedes. Fundamentos de Sistemas Operativos. Editorial Presença, Lisboa, 1990. [McCormack 89] J. McCormack, P. Asente, e R. Swick. Xtoolkit Intrinsics - C Language Interface, X Window System X11R4, Dezembro 1989. [Meyer 86] B. Meyer. Genericity versus Inheritance. Em OOPSLA ’86 Proceedings, páginas 391–405, Portland, Oregon, Setembro 1986. [Meyer 88] B. Meyer. Object Oriented Software Construction. Prentice-Hall, 1988. [Micallef 88] Josephine Micallef. Encapsulation, Reusability and Extensibility in Object-Oriented Programming Languages. JOOP, páginas 12–35, Abril 1988. 157 [Moon 86] David A. Moon. Object-Oriented Programming with Flavors. Em OOPSLA ’86 Proceedings, páginas 1–8, Portland, Oregon, Setembro 1986. [Myers 87] B. A. Myers. Creating highly-interactive and graphical user interfaces by demonstration. Em Ronald M. Baecker e William A. S. Buxton, editores, Readings in Human-Computer Interaction. Morgan Kaufmann Publishers Inc, 1987. [Myers 88] B. Myers. Creating User Interfaces by Demonstration. Academic Press, Inc., 1988. [Myers 89] B. A. Myers. User-interface tools: Introduction and survey. IEEE Software, páginas 15–23, Janeiro 1989. [Peterson 89] C. Peterson. Athena Widget Set - C Language Reference, X Window System X11R4, 1989. [Pfaff 85] Gunter E. Pfaff, editor. User Interface Management Systems. Springer-Verlag, 1985. [Saunders 89] J. H. Saunders. A Survey of Object-Oriented Programming Languages. JOOP, páginas 5–11, Março 1989. [Schaffert 86] Craig Schaffert e all. An Introduction to Trellis/Owl. Em OOPSLA ’86 Proceedings, páginas 9–16, Portland, Oregon, Setembro 1986. [Schreiner 85] Axel T. Schreiner e Jr. H.George Friedman. Introduction to Compiler Construction with Unix. Prentice-Hall, 1985. [Sequeira 89] Manuel Sequeira. EC++ - Implementation Report. Relatório técnico, INESC, Dezembro 1989. Ref. Inesc-0007. [Sequeira 91] Manuel Sequeira. EC++: Uma Linguagem para a Programação num Sistema Distribuı́do Orientado a Objectos. Tese de Mestrado, IST, Lisboa, Junho 1991. [Sheil 86] B. A. Sheil. Power Tools for Programmers. Em David R. Barstow, Howard E. Shrobe, e Erik Sandewall, editores, Interactive Programming Environments, capı́tulo 2, páginas 19–30. McGrawHill, 1986. [Shneiderman 87] B. Shneiderman. Designing the User Interface: Strategies for Effective Human-Computer Interaction. Addison-Wesley, 1987. [Shu 88] Nan C. Shu. Visual Programming. Van Nostrand Reinhold, 1988. [Simoes 87] L. Simoes e J. Marques. Images - an object oriented UIMS. Em Human-Computer Interaction - INTERACT ’87, Portugal, Outubro 1987. IFIP. 158 [Simoes 88] L. Simoes et al. IMAGES - an approach to an object oriented UIMS. Em Proceedings of the Autumn 1988 EUUG Conference, Portugal, Outubro 1988. EUUG. [SOMIW 85] SOMIW. Secure Open Multimedia Integrated Workstation. Relatório técnico, Esprit, 1985. [Sousa 89] Pedro Sousa e Paulo Guedes. Ik Run Time Support - Interface Definition. Relatório técnico, INESC, Dezembro 1989. Ref. Inesc0008. [Sousa 90] Pedro Sousa et al. IK Implementation Report. Relatório Técnico INESC-0014, ESPRIT COMANDOS Project, Outubro 1990. [Sousa 91a] Pedro Sousa. Dynamic linking. Presented at the Extensions to C++ Workshop, Lisbon, Julho 1991. [Sousa 91b] Pedro Manuel Sousa. Concepção e Realização de um Sistema de Suporte à Execução de Objectos. Tese de Mestrado, IST, Lisboa, Junho 1991. [Stein 87] Lynn Andrea Stein. Delegation is Inheritance. Em OOPSLA ’87 Proceedings, páginas 138–146, Orlando, Florida, Outubro 1987. [Stroustrup 86] B. Stroustrup. Wesley, 1986. [Stroustrup 87] Bjarne Stroustrup. Multiple Inheritance for C++. Em Proceedings of the Spring’87 EUUG Conference, páginas 189–207, Helsinki, Maio 1987. [Stroustrup 88] B. Stroustrup. What Is Object Oriented Programming. IEEE Software, Maio 1988. [Sun 86a] Sun. Lex - A Lexical Analyzer Generator, Fevereiro 1986. [Sun 86b] Sun. Yacc - Yet Another Compiler-Compiler, Fevereiro 1986. [Swinehart 86] D. Swinehart, P.Zellweger, R.Beach, e R.Hagmann. A Structural View of the Cedar Programming Environment. ACM Transactions on Programming Languages and Systems, 8(4), Outubro 1986. [Teitelman 86] W. Teitelman e L. Masinter. The Interlisp Programming Environment. Em David R. Barstow, Howard E. Shrobe, e Erik Sandewall, editores, Interactive Programming Environments, capı́tulo 4, páginas 83–96. McGraw-Hill, 1986. [Thompson 89] T. Thompson. The Next Step. BYTE, páginas 265–269, Março 1989. The C++ Programming Language. 159 Addison- [Tomlinson 89] Chris Tomlinson, Mark Scheevel, e Won Kim. Sharing and Organization Protocols in Object-Oriented Systems. JOOP, páginas 25–36, Novembro 1989. [Ungar 87] David Ungar e Randall B. Smith. Self: The Power of Simplicity. Em OOPSLA ’87 Proceedings, páginas 227–242, Orlando, Florida, Outubro 1987. [Wegner 87] Peter Wegner. Dimensions of Object-Based Language Design. Em OOPSLA ’87 Proceedings, páginas 168–182, Orlando, Florida, Outubro 1987. [Wegner 89] P. Wegner. Learning the Language. BYTE, páginas 245–253, Março 1989. [Young 89] D. Young. X Window Systems, Programming and Applications With Xt. Prentice-Hall, 1989. [Young 90] D. Young. OSF/Motif Reference Guide. Prentice-Hall, 1990. 160