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

Documentos relacionados