martes, 29 de noviembre de 2011
TITULO
UNIVERSIDAD NACIONAL AUTÓNOMA
DE MÉXICO
INSTITUTO DE GEOFÍSICA
“Aplicaciones del Cómputo en Paralelo a la
Modelación de Sistemas Terrestres”
T E S I S
QUE PARA OBTENER EL GRADO DE
MAESTRO EN CIENCIAS DE LA TIERRA
(MODELACIÓN DE SISTEMAS TERRESTRES)
P R E S E N T A:
MAT. ANTONIO CARRILLO LEDESMA
DIRECTOR DE TESIS:
DR. ISMAEL HERRERA REVILLA
2006
OBJETIVO
Como objetivos particulares de este trabajo tenemos:
Mostrar cómo aplicar la metodología para manejar problemas de gran
tamaño (descomposición de malla fina).
Mostrar cómo descomponer un dominio en un conjunto de subdominios
que den una partición en la que el tiempo de cálculo sea mínimo para una
configuración de hardware dada.
Mostrar cuales son las posibles optimizaciones aplicables a una configuración
de hardware dada.
Mostrar que es posible trabajar problemas con una malla muy fina en un
equipo paralelo pequeño.
DESARROLLO
La necesidad de entender su entorno y anticiparse a los acontecimientos tiene
raíces muy profundas en el ser humano. Desde la prehistoria, el hombre trató de
predecir a la naturaleza, pues de ella dependía su supervivencia, para lo cual inicialmente
nuestros antepasados utilizaron a la brujería, así como el pensamiento
mágico y el religioso. Sin embargo, el medio más efectivo para predecir el comportamiento
de la naturaleza es el método científico (o su antecesor el método
empírico) y es por eso que este anhelo humano ancestral, a través de la historia,
ha sido motor de la ciencia.
La maduración y el progreso de la predicción científica es, sin duda, el resultado
del avance general de la ciencia, pero además ha habido elementos catalizadores
esenciales sin los cuales esto no hubiera sido posible. La predicción científica,
además de ser científica, es matemática y computacional. En la actualidad,
cuando deseamos predecir el comportamiento de un sistema, los conocimientos
científicos y tecnológicos se integran en modelos matemáticos los cuales se convierten
en programas de cómputo que son ejecutados por las computadoras tanto
secuenciales como paralelas (entenderemos por una arquitectura paralela a un
conjunto de procesadores interconectados capaces de cooperar en la solución de
un problema).
MARCO TEÓRICO
los modelos matemáticos de los sistemas continuos, independientemente de su
naturaleza y propiedades intrínsecas, pueden formularse por medio de balances,
cuya idea básica no difiere mucho de los balances de la contabilidad financiera,
fue el resultado de un largo proceso de perfeccionamiento en el que concurrieron
una multitud de mentes brillantes.
Los modelos matemáticos de los sistemas continuos son ecuaciones diferenciales,
las cuales son parciales (con valores iniciales y condiciones de frontera)
para casi todos los sistemas de mayor interés en la ciencia y la ingeniería, o sistemas
de tales ecuaciones. Salvo para los problemas más sencillos, no es posible
obtener por métodos analíticos las soluciones de tales ecuaciones, que son las
que permiten predecir el comportamiento de los sistemas.
La capacidad para formular los modelos matemáticos de sistemas complicados
y de gran diversidad, es sin duda una contribución fundamental para el
avance de la ciencia y sus aplicaciones, tal contribución quedaría incompleta y,
debido a ello, sería poco fecunda, si no se hubiera desarrollado simultáneamente
su complemento esencial: los métodos matemáticos y la computación electrónica.
En cambio, la diversidad y complejidad de problemas que pueden ser tratados
con métodos numéricos y computacionales es impresionante.
Los modelos de los sistemas continuos -es decir, sistemas físicos macroscópicos
tales como los yacimientos petroleros, la atmósfera, los campos electromagnéticos,
los océanos, los metalúrgicos, el aparato circulatorio de los seres
humanos, la corteza terrestre, lo suelos y las cimentaciones, muchos sistemas
ambientales, y muchos otros cuya enumeración ocuparía un espacio enormecontienen
un gran número de grados libertad.
Por ello, la solución numérica por los esquemas tradicionales tipo diferencias
finitas y elemento finito generan una discretización del problema, la cual es usada
para generar sistemas de ecuaciones algebraicos [8]. Estos sistemas algebraicos
en general son de gran tamaño para problemas reales, al ser estos algoritmos
secuenciales su implantación suele hacerse en equipos secuenciales y por ello no
es posible resolver muchos problemas que involucren el uso de una gran cantidad
de memoria.
Actualmente para tratar de subsanar la limitante de procesar sólo en equipos
secuenciales, se usan equipos paralelos para soportar algoritmos secuenciales mediante
directivas de compilación, haciendo ineficiente su implantación en dichos
equipos.
CONCLUSIONES
ESTA TESIS ES UN NA DE LAS MEJORES APLICACIONES QUE PUEDE TENER UN PROGRAMA DE SISTEMAS EL SABER Y CONOCER COMO SON LOS CAMBIOS DE LA TIERRA POR QUE ESTA ESTA EN CONSTANTES MOVIMIENTOS SI SE TUVIERA MAS TIEMPO PARA DETECTAR SISMOS Y ASI PODER SALVAR MAS VIDAS DADO QUE TODAS LAS PERSONAS NO PUEDEN MOVERSE TAN RÁPIDO Y SABER EN QUE AFECTA AL PLANETA
TITULO
UNIVERSIDAD DE OVIEDO
Departamento de Informática
TESIS DOCTORAL
SISTEMA COMPUTACIONAL DE PROGRAMACIÓN
FLEXIBLE DISEÑADO SOBRE UNA MÁQUINA
ABSTRACTA REFLECTIVA NO RESTRICTIVA
Presentada por
Francisco Ortín Soler
para obtención del título de Doctor por la Universidad de Oviedo
Dirigida por el
Profesor Doctor D. Juan Manuel Cueva Lovelle
Oviedo, Diciembre de 2001
OBJETIVO
Esta tesis describe el modo en el que distintas técnicas de reflectividad pueden ser
empleadas para el desarrollo de un sistema computacional de programación extensible y
adaptable dinámica mente, sin sufrir dependencia alguna de un lenguaje de programación
específico, y empleando para ello una plataforma virtual heterogénea.
MARCO TEÓRICO
Se diseña una máquina abstracta, de tamaño y complejidad semántica reducida, como
la raíz computacional del sistema, que otorga primitivas básicas de reflectividad. Tanto
su tamaño restringido como su capacidad introspectiva, la hacen susceptible de ser implantada
en entornos computacionales heterogéneos, constituyendo adicionalmente un entorno
computacional independiente de la plataforma.
DESARROLLO
Haciendo uso de las facultades reflectivas ofrecidas por la máquina abstracta su nivel
de abstracción computacional podrá ser extendido dinámicamente, utilizando para ello
su propio lenguaje de programación sin necesidad de modificar la implementación reducida
de la máquina virtual, y, por tanto, sin perder portabilidad de su código. El empleo de su
capacidad extensible se utilizará, a modo de ejemplo, en el diseño de abstracciones propias
de persistencia, distribución, planificación controlada de hilos y recolección de basura. Todas
las abstracciones ofrecidas mediante la extensión de la plataforma, utilizando su propio
lenguaje, son adaptables para cualquier aplicación, en tiempo de ejecución.
Se diseña un sistema de procesamiento genérico de lenguajes disponiendo de las características
reflectivas de la plataforma, dando lugar a una independencia global del lenguaje
de programación seleccionado por el programador. Cualquier aplicación podrá interactuar
con otra bajo el modelo computacional de objetos ofrecido por la máquina abstracta,
independientemente del lenguaje en el que hayan sido creadas.
La flexibilidad dinámica del sistema creado es superior, tanto en expresividad como
en el espectro de facetas computacionales adaptables, a los actualmente existentes. La estructura
de las aplicaciones en ejecución, y la especificación léxica, sintáctica y semántica del
lenguaje de programación, son parámetros configurables dinámicamente, tanto por la propia
aplicación –en cualquiera que sea su lenguaje de programación–, como por cualquier
otro programa. El sistema reflectivo ofrecido no posee restricción alguna respecto a las
características computacionales a configurar, ni respecto al modo de expresar su adaptación.
CONCLUSIONES
MIS CONCLUSIONES ES ESTA TESIS ES QUE EL IMPACTO LABORAL QUE ESTA PUEDA TENER ES ALTO POR QUE EN ESTOS TIEMPOS SE NECESITA UN LENGUAJE PARA COMUNICARSE CON LAS MAQUINAS Y ESTAS AUTOMATIZARLAS POR ESTA RAZÓN LOS CODIGOS QUE SE EMPLEAN SON MUY FÁCILES PARA LA COMUNICACIÓN HOMBRE-MAQUINA.
domingo, 27 de noviembre de 2011
TESIS DOCTORAL
UNIVERSIDAD DE OVIEDO
Departamento de Informática
TESIS DOCTORAL
SISTEMA COMPUTACIONAL DE PROGRAMACIÓN
FLEXIBLE DISEÑADO SOBRE UNA MÁQUINA
ABSTRACTA REFLECTIVA NO RESTRICTIVA
Presentada por
Francisco Ortín Soler
para obtención del título de Doctor por la Universidad de Oviedo
Dirigida por el
Profesor Doctor D. Juan Manuel Cueva Lovelle
Oviedo, Diciembre de 2001
RESUMEN
Esta tesis describe el modo en el que distintas técnicas de reflectividad pueden ser
empleadas para el desarrollo de un sistema computacional de programación extensible y
adaptable dinámicamente, sin sufrir dependencia alguna de un lenguaje de programación
específico, y empleando para ello una plataforma virtual heterogénea.
Se diseña una máquina abstracta, de tamaño y complejidad semántica reducida, como
la raíz computacional del sistema, que otorga primitivas básicas de reflectividad. Tanto
su tamaño restringido como su capacidad introspectiva, la hacen susceptible de ser implantada
en entornos computacionales heterogéneos, constituyendo adicionalmente un entorno
computacional independiente de la plataforma.
Haciendo uso de las facultades reflectivas ofrecidas por la máquina abstracta su nivel
de abstracción computacional podrá ser extendido dinámicamente, utilizando para ello
su propio lenguaje de programación sin necesidad de modificar la implementación reducida
de la máquina virtual, y, por tanto, sin perder portabilidad de su código. El empleo de su
capacidad extensible se utilizará, a modo de ejemplo, en el diseño de abstracciones propias
de persistencia, distribución, planificación controlada de hilos y recolección de basura. Todas
las abstracciones ofrecidas mediante la extensión de la plataforma, utilizando su propio
lenguaje, son adaptables para cualquier aplicación, en tiempo de ejecución.
Se diseña un sistema de procesamiento genérico de lenguajes disponiendo de las características
reflectivas de la plataforma, dando lugar a una independencia global del lenguaje
de programación seleccionado por el programador. Cualquier aplicación podrá interactuar
con otra bajo el modelo computacional de objetos ofrecido por la máquina abstracta,
independientemente del lenguaje en el que hayan sido creadas.
La flexibilidad dinámica del sistema creado es superior, tanto en expresividad como
en el espectro de facetas computacionales adaptables, a los actualmente existentes. La estructura
de las aplicaciones en ejecución, y la especificación léxica, sintáctica y semántica del
lenguaje de programación, son parámetros configurables dinámicamente, tanto por la propia
aplicación –en cualquiera que sea su lenguaje de programación–, como por cualquier
otro programa. El sistema reflectivo ofrecido no posee restricción alguna respecto a las
características computacionales a configurar, ni respecto al modo de expresar su adaptación.
Toda la flexibilidad ofrecida es dinámica, no siendo necesario finalizar la ejecución de
una aplicación para su configuración, pudiéndose adaptar ésta a requisitos surgidos dinámicamente,
imprevisibles en tiempo de desarrollo.
PALABRAS CLAVE
Máquina abstracta, extensibilidad, adaptabilidad, introspección, reflectividad estructural,
computacional y de lenguaje, modelo de objetos basado en prototipos, separación de
aspectos e incumbencias, entorno de programación, intérprete genérico.
ABSTRACT
This thesis describes the way in which different reflective technologies can be used
to develop a dynamically adaptable and extensible computational system, without any dependency
of a concrete programming language, built over a heterogeneous computing platform.
An abstract machine with a reduced instruction set is built as the root computation
system’s engine; it offers the programmer basic reflection computation primitives. Its reduced
size and its introspective capabilities, make it easy to be deployed in heterogeneous
computational systems, becoming a platform-independent computational system.
By using the reflective features offered by the abstract machine, the programming
abstraction level can be extended. This would be programmed on its own language, without
needing to modify the virtual machine’s source code and, therefore, without loosing
code portability. As an example of its extensiveness, the programming environment is programmed
(using the abstract machine programming language) achieving facilities such as
persistence, distribution, thread scheduling or garbage collection. All this new abstractions
are adaptable at runtime to any system application.
A generic processing language system is designed, making the whole system independent
of the language the programmer may select. Any application can interact with each
other, by using the abstract machine’s computational model, whatever the programming
language the application has been codified in.
The dynamic flexibility the system achieves is greater –both in expressiveness and
in the adaptable computational-features set– than any other existing one. At runtime, the
application structure and its programming language specification (lexical, syntactic and semantic
features) are completely customisable; it can be adapted by the own application as
well as by any other program –no matter the language they have been programmed in. The
reflective system developed has no restriction at all: it can adapt any computational trait
without any expressiveness restriction –it may use the whole abstract machine programming
language. This flexibility offered is dynamic: there is no need to stop the application
execution in order to adapt it to any runtime requirement, unexpected at design time.
KEYWORDS
Abstract machine, extensibility, adaptability, introspection, structural, computational
and linguistic reflection, prototype-based object-oriented object model, separation of
concerns, aspect oriented programming, programming environment, generic interpreter.
I
TABLA DE CONTENIDOS
CCCAAAPPPÍÍÍTTTUUULLLO 111 : INTRODUCCIÓN........................................................................................................... 1
1.1 INTRODUCCIÓN ............................................................................................................................. 1
1.2 OBJETIVOS ................................................................................................................................... 2
1.2.1 Independencia del Lenguaje de Programación y Plataforma.............................................. 2
1.2.2 Interoperabilidad Directa .................................................................................................... 2
1.2.3 Extensibilidad...................................................................................................................... 3
1.2.4 Adaptabilidad no Restrictiva................................................................................................ 3
1.3 ORGANIZACIÓN DE LA TESIS ......................................................................................................... 4
1.3.1 Introducción y Requisitos del Sistema.................................................................................. 4
1.3.2 Sistemas Existentes Estudiados ............................................................................................ 4
1.3.3 Diseño del Sistema ............................................................................................................... 5
1.3.4 Aplicaciones, Evaluación, Conclusiones y Trabajo Futuro ................................................. 5
1.3.5 Apéndices ............................................................................................................................. 5
CCCAAAPPPÍÍÍTTTUUULLLO 222 : REQUISITOS DEL SISTEMA...................................................................................... 7
2.1 REQUISITOS DE LA PLATAFORMA DE COMPUTACIÓN .................................................................... 7
2.1.1 Sistema Computacional Multiplataforma............................................................................. 7
2.1.2 Independencia del Lenguaje de Programación.................................................................... 8
2.1.3 Independencia del Problema................................................................................................ 8
2.1.4 Tamaño y Semántica Computacional Reducida................................................................... 8
2.1.5 Flexibilidad Computacional................................................................................................. 9
2.1.5.1 Semántica Operacional Abierta........................................................................................9
2.1.5.2 Introspección y Acceso al Entorno................................................................................... 9
2.1.5.3 Semántica Computacional Abierta ................................................................................... 9
2.1.6 Interfaz de Acceso y Localización del Código Nativo .......................................................... 9
2.1.7 Nivel de Abstracción del Modelo de Computación .............................................................. 9
2.2 REQUISITOS DEL ENTORNO DE PROGRAMACIÓN ......................................................................... 10
2.2.1 Desarrollo de Características de Computación (Extensibilidad) ...................................... 10
2.2.2 Selección de Características de Computación (Adaptabilidad)......................................... 10
2.2.3 Identificación del Entorno de Programación..................................................................... 10
2.2.4 Autodocumentación Real.................................................................................................... 11
2.3 REQUISITOS CONCERNIENTES AL GRADO DE FLEXIBILIDAD ....................................................... 11
2.3.1 Conocimiento Dinámico del Estado del Sistema................................................................ 11
2.3.2 Modificación Estructural Dinámica................................................................................... 11
2.3.3 Modificación Dinámica del Comportamiento .................................................................... 11
2.3.4 Modificación Computacional sin Restricciones ................................................................. 11
2.3.5 Modificación y Selección Dinámica del Lenguaje ............................................................. 12
2.3.6 Interoperabilidad Directa entre Aplicaciones.................................................................... 12
2.4 REQUISITOS GENERALES DE SISTEMA......................................................................................... 12
2.4.1 Independencia del Hardware ............................................................................................. 12
2.4.2 Independencia del Sistema Operativo ................................................................................ 13
2.4.3 Interacción con Sistemas Externos..................................................................................... 13
II
2.4.4 Interoperabilidad Uniforme................................................................................................13
2.4.5 Heterogeneidad ..................................................................................................................13
2.4.6 Sistema de Programación Único ........................................................................................13
2.4.7 Independencia del Lenguaje...............................................................................................14
2.4.8 Flexibilidad Dinámica No Restrictiva ................................................................................14
2.4.9 Interoperabilidad Directa...................................................................................................14
CCCAAAPPPÍÍÍTTTUUULLLO 333 : MÁQUINAS ABSTRACTAS .......................................................................................15
3.1 PROCESADORES COMPUTACIONALES ..........................................................................................15
3.1.1 Procesadores Implementados Físicamente.........................................................................15
3.1.2 Procesadores Implementados Lógicamente .......................................................................16
3.2 PROCESADORES LÓGICOS Y MÁQUINAS ABSTRACTAS ................................................................17
3.2.1 Máquina Abstracta de Estados...........................................................................................17
3.3 UTILIZACIÓN DEL CONCEPTO DE MÁQUINA ABSTRACTA............................................................18
3.3.1 Procesadores de Lenguajes ................................................................................................18
3.3.1.1 Entornos de Programación Multilenguaje ......................................................................20
3.3.2 Portabilidad del Código .....................................................................................................20
3.3.3 Sistemas Interactivos con Abstracciones Orientadas a Objetos.........................................21
3.3.4 Distribución e Interoperabilidad de Aplicaciones..............................................................23
3.3.4.1 Distribución de Aplicaciones..........................................................................................24
3.3.4.2 Interoperabilidad de Aplicaciones ..................................................................................24
3.3.5 Diseño y Coexistencia de Sistemas Operativos ..................................................................26
3.3.5.1 Diseño de Sistemas Operativos Distribuidos y Multiplataforma....................................26
3.3.5.2 Coexistencia de Sistemas Operativos .............................................................................27
3.4 APORTACIÓN DE LA UTILIZACIÓN DE MÁQUINAS ABSTRACTAS..................................................28
CCCAAAPPPÍÍÍTTTUUULLLO 444 : PANORÁMICA DE UTILIZACIÓN DE MÁQUINAS ABSTRACTAS.................29
4.1 PORTABILIDAD DE CÓDIGO .........................................................................................................29
4.1.1 Estudio de Sistemas Existentes ...........................................................................................29
Máquina-p .....................................................................................................................................29
OCODE .........................................................................................................................................30
Portable Scheme Interpreter ...........................................................................................................30
Forth ..............................................................................................................................................31
Sequential Parlog Machine.............................................................................................................31
Code War.......................................................................................................................................31
4.1.2 Aportaciones y Carencias de los Sistemas Estudiados.......................................................32
4.2 INTEROPERABILIDAD DE APLICACIONES......................................................................................33
4.2.1 Estudio de Sistemas Existentes ...........................................................................................33
Parallel Virtual Machine.................................................................................................................33
Coherent Virtual Machine ..............................................................................................................34
PerDiS ...........................................................................................................................................35
4.2.2 Aportaciones y Carencias de los Sistemas Estudiados.......................................................36
4.3 PLATAFORMAS INDEPENDIENTES ................................................................................................36
4.3.1 Estudio de Sistemas Existentes ...........................................................................................36
Smalltalk-80 ..................................................................................................................................36
Self ................................................................................................................................................38
Java................................................................................................................................................39
The IBM J9 Virtual Machine..........................................................................................................43
.NET ..............................................................................................................................................44
4.3.2 Aportaciones y Carencias de los Sistemas Estudiados.......................................................45
4.4 DESARROLLO DE SISTEMAS OPERATIVOS DISTRIBUIDOS Y MULTIPLATAFORMA........................47
4.4.1 Estudio de Sistemas Existentes ...........................................................................................47
Merlin ............................................................................................................................................47
Inferno ...........................................................................................................................................47
Oviedo3 .........................................................................................................................................48
4.4.2 Aportaciones y Carencias de los Sistemas Estudiados.......................................................50
4.5 MÁQUINAS ABSTRACTAS NO MONOLÍTICAS...............................................................................51
4.5.1 Estudio de Sistemas Existentes ...........................................................................................51
Virtual Virtual Machine..................................................................................................................51
Adaptive Virtual Machine ..............................................................................................................53
III
Extensible Virtual Machine............................................................................................................ 54
Distributed Virtual Machine........................................................................................................... 54
4.5.2 Aportaciones y Carencias de los Sistemas Estudiados....................................................... 55
4.6 CONCLUSIONES ........................................................................................................................... 56
CCCAAAPPPÍÍÍTTTUUULLLO 555 : SISTEMAS FLEXIBLES NO REFLECTIVOS......................................................... 59
5.1 IMPLEMENTACIONES ABIERTAS .................................................................................................. 59
5.1.1 Aportaciones y Carencias .................................................................................................. 61
5.2 FILTROS DE COMPOSICIÓN .......................................................................................................... 62
5.2.1 Aportaciones y Carencias .................................................................................................. 62
5.3 PROGRAMACIÓN ORIENTADA A ASPECTOS ................................................................................. 63
5.3.1 Aportaciones y Carencias .................................................................................................. 65
5.4 PROGRAMACIÓN ADAPTABLE ..................................................................................................... 65
5.4.1 Aportaciones y Carencias .................................................................................................. 66
5.5 SEPARACIÓN MULTIDIMENSIONAL DE INCUMBENCIAS ............................................................... 66
5.5.1 Aportaciones y Carencias .................................................................................................. 68
5.6 SISTEMAS OPERATIVOS BASADOS EN MICRONÚCLEO................................................................. 68
Amoeba ......................................................................................................................................... 69
Mach.............................................................................................................................................. 70
5.6.1 Aportaciones y Carencias .................................................................................................. 71
5.7 CONCLUSIONES ........................................................................................................................... 72
CCCAAAPPPÍÍÍTTTUUULLLO 666 : REFLECTIVIDAD COMPUTACIONAL.................................................................. 73
6.1 CONCEPTOS DE REFLECTIVIDAD ................................................................................................. 73
6.1.1 Reflectividad...................................................................................................................... 73
6.1.2 Sistema Base....................................................................................................................... 74
6.1.3 Metasistema....................................................................................................................... 74
6.1.4 Cosificación....................................................................................................................... 74
6.1.5 Conexión Causal ................................................................................................................ 75
6.1.6 Metaobjeto......................................................................................................................... 75
6.1.7 Reflectividad Completa ...................................................................................................... 76
6.2 REFLECTIVIDAD COMO UNA TORRE DE INTÉRPRETES.................................................................. 76
6.3 CLASIFICACIONES DE REFLECTIVIDAD........................................................................................ 78
6.3.1 Qué se refleja ..................................................................................................................... 78
6.3.1.1 Introspección .................................................................................................................. 78
6.3.1.2 Reflectividad Estructural ................................................................................................ 79
6.3.1.3 Reflectividad Computacional .........................................................................................79
6.3.1.4 Reflectividad Lingüística................................................................................................ 79
6.3.2 Cuándo se produce el reflejo.............................................................................................. 80
6.3.2.1 Reflectividad en Tiempo de Compilación ...................................................................... 80
6.3.2.2 Reflectividad en Tiempo de Ejecución........................................................................... 80
6.3.3 Cómo se expresa el acceso al metasistema ........................................................................ 81
6.3.3.1 Reflectividad Procedural ................................................................................................ 81
6.3.3.2 Reflectividad Declarativa ............................................................................................... 81
6.3.4 Desde Dónde se puede modificar el sistema ...................................................................... 81
6.3.4.1 Reflectividad con Acceso Interno................................................................................... 81
6.3.4.2 Reflectividad con Acceso Externo ................................................................................. 81
6.3.5 Cómo se ejecuta el sistema................................................................................................. 82
6.3.5.1 Ejecución Interpretada.................................................................................................... 82
6.3.5.2 Ejecución Nativa ............................................................................................................ 82
6.4 HAMILTONIANS VERSUS JEFFERSONIANS .................................................................................... 82
6.5 FORMALIZACIÓN........................................................................................................................ 83
CCCAAAPPPÍÍÍTTTUUULLLO 777 : PANORÁMICA DE UTILIZACIÓN DE REFLECTIVIDAD ................................. 85
7.1 SISTEMAS DOTADOS DE INTROSPECCIÓN .................................................................................... 85
ANSI/ISO C++ RunTime Type Information (RTTI) ..................................................................... 85
Plataforma Java .............................................................................................................................. 86
CORBA......................................................................................................................................... 88
COM.............................................................................................................................................. 90
7.1.1 Aportaciones y Carencias de los Sistemas Estudiados....................................................... 91
IV
7.2 SISTEMAS DOTADOS DE REFLECTIVIDAD ESTRUCTURAL ............................................................92
Smalltalk-80 ..................................................................................................................................92
Self, Proyecto Merlin......................................................................................................................94
ObjVlisp ........................................................................................................................................96
Linguistic Reflection in Java ..........................................................................................................98
Python............................................................................................................................................99
7.2.1 Aportaciones y Carencias de los Sistemas Estudiados.....................................................100
7.3 REFLECTIVIDAD EN TIEMPO DE COMPILACIÓN..........................................................................100
OpenC++ .....................................................................................................................................100
OpenJava .....................................................................................................................................101
Java Dynamic Proxy Classes........................................................................................................102
7.3.1 Aportaciones y Carencias de los Sistemas Estudiados.....................................................103
7.4 REFLECTIVIDAD COMPUTACIONAL BASADA EN META-OBJECT PROTOCOLS.............................104
Closette........................................................................................................................................104
MetaXa ........................................................................................................................................105
Iguana ..........................................................................................................................................107
Cognac.........................................................................................................................................108
Guanará .......................................................................................................................................110
Dalang .........................................................................................................................................111
NeoClasstalk................................................................................................................................112
Moostrap......................................................................................................................................113
7.4.1 Aportaciones y Carencias de los Sistemas Estudiados.....................................................114
7.5 INTÉRPRETES METACIRCULARES...............................................................................................116
3-Lisp ..........................................................................................................................................116
ABCL/R2.....................................................................................................................................117
MetaJ ...........................................................................................................................................118
7.5.1 Aportaciones y Carencias de los Sistemas Estudiados.....................................................119
7.6 CONCLUSIONES .........................................................................................................................120
7.6.1 Momento en el que se Produce el Reflejo.........................................................................120
7.6.2 Información Reflejada ......................................................................................................120
7.6.3 Niveles Computacionales Reflectivos ...............................................................................121
CCCAAAPPPÍÍÍTTTUUULLLO 888 : LENGUAJES ORIENTADOS A OBJETOS BASADOS EN PROTOTIPOS.......123
8.1 CLASES Y PROTOTIPOS ..............................................................................................................123
8.2 UTILIZACIÓN DE LENGUAJES ORIENTADOS A OBJETOS BASADOS EN PROTOTIPOS ...................125
8.2.1 Reducción Semántica........................................................................................................125
8.2.2 Inexistencia de Pérdida de Expresividad..........................................................................125
8.2.3 Traducción Intuitiva de Modelos......................................................................................126
8.2.4 Coherencia en Entornos Reflectivos.................................................................................126
8.3 CONCLUSIONES .........................................................................................................................128
CCCAAAPPPÍÍÍTTTUUULLLO 999 : ARQUITECTURA DEL SISTEMA ..........................................................................129
9.1 CAPAS DEL SISTEMA .................................................................................................................129
9.1.1 Máquina Abstracta ...........................................................................................................130
9.1.2 Entorno de Programación ................................................................................................130
9.1.3 Sistema Reflectivo No Restrictivo .....................................................................................130
9.2 ÚNICO MODELO COMPUTACIONAL DE OBJETOS .......................................................................131
9.3 MÁQUINA ABSTRACTA..............................................................................................................132
9.3.1 Conjunto Reducido de Primitivas.....................................................................................133
9.3.2 Mecanismo de Extensibilidad ...........................................................................................133
9.3.3 Selección del Modelo Computacional ..............................................................................134
9.3.4 Interacción Directa entre Aplicaciones............................................................................134
9.3.5 Evaluación Dinámica de Datos como Código..................................................................135
9.4 ENTORNO DE PROGRAMACIÓN ..................................................................................................135
9.4.1 Portabilidad e Independencia del Lenguaje.....................................................................135
9.4.2 Adaptabilidad ...................................................................................................................135
9.4.3 Introspección ....................................................................................................................136
9.5 SISTEMA REFLECTIVO NO RESTRICTIVO ...................................................................................136
9.5.1 Características Adaptables...............................................................................................137
9.5.2 Independencia del Lenguaje.............................................................................................137
V
9.5.3 Grado de Flexibilidad de la Semántica Computacional .................................................. 137
9.5.4 Adaptabilidad No Restrictiva ........................................................................................... 138
CCCAAAPPPÍÍÍTTTUUULLLO 111000 : ARQUITECTURA DE LA MÁQUINA ABSTRACTA ........................................ 141
10.1 CARACTERÍSTICAS PRINCIPALES DE LA MÁQUINA ABSTRACTA................................................ 141
10.1.1 Reducción de Primitivas Computacionales...................................................................... 141
10.1.2 Extensibilidad.................................................................................................................. 143
10.1.3 Definición del Modelo Computacional............................................................................. 143
10.1.4 Adaptabilidad ................................................................................................................... 144
10.1.5 Interacción Directa entre Aplicaciones............................................................................ 145
10.1.6 Manipulación Dinámica del Comportamiento ................................................................. 145
10.2 TÉCNICAS DE DISEÑO SELECCIONADAS .................................................................................... 146
CCCAAAPPPÍÍÍTTTUUULLLO 111111 : ARQUITECTURA DEL SISTEMA REFLECTIVO NO RESTRICTIVO......... 147
11.1 ANÁLISIS DE TÉCNICAS DE OBTENCIÓN DE SISTEMAS FLEXIBLES ............................................ 147
11.1.1 Sistemas Flexibles No Reflectivos .................................................................................... 147
11.1.2 Sistemas Reflectivos ......................................................................................................... 148
11.1.2.1 Reflectividad Dinámica ............................................................................................148
11.2 SISTEMA COMPUTACIONAL REFLECTIVO SIN RESTRICCIONES.................................................. 150
11.2.1 Salto Computacional ........................................................................................................ 152
11.2.2 Representación Estructural de Lenguajes y Aplicaciones................................................ 154
11.3 BENEFICIOS DEL SISTEMA PRESENTADO ................................................................................... 157
11.4 REQUISITOS IMPUESTOS A LA MÁQUINA ABSTRACTA............................................................... 158
CCCAAAPPPÍÍÍTTTUUULLLO 111222 : DISEÑO DE LA MÁQUINA ABSTRACTA.......................................................... 159
12.1 ESPECIFICACIÓN DE LA MÁQUINA ABSTRACTA ........................................................................ 159
12.1.1 Noción de Objeto.............................................................................................................. 159
12.1.2 Entorno de Programación................................................................................................ 160
12.1.3 Objetos Primitivos............................................................................................................ 160
Referencias.................................................................................................................................. 161
12.1.3.1 Objeto nil.................................................................................................................. 161
12.1.3.2 Objetos Cadenas de Caracteres................................................................................. 161
12.1.3.3 Objeto Extern .......................................................................................................... 163
12.1.3.4 Objeto System.......................................................................................................... 164
12.1.4 Posibles Evaluaciones...................................................................................................... 165
12.1.4.1 Creación de Referencias ........................................................................................... 166
12.1.4.2 Asignación de Referencias ....................................................................................... 166
12.1.4.3 Acceso a los Miembros ............................................................................................ 166
12.1.4.4 Evaluación o Descosificación de Objetos Miembro................................................. 167
12.1.4.5 Comentarios ............................................................................................................. 168
12.1.4.6 Evaluación de Objetos Cadena de Caracteres .......................................................... 168
12.1.5 Delegación o Herencia Dinámica .................................................................................... 168
12.1.6 Reflectividad Estructural y Computacional ..................................................................... 169
12.1.7 Objetos Miembro Primitivos ............................................................................................ 169
12.2 EXTENSIÓN DE LA MÁQUINA ABSTRACTA ................................................................................ 171
12.2.1 Creación de Nuevas Abstracciones .................................................................................. 171
12.2.2 Evaluación de Objetos...................................................................................................... 172
12.2.2.1 Evaluación de Objetos Miembro .............................................................................. 173
12.2.2.2 Evaluación de Objetos Cadena de Caracteres .......................................................... 174
12.2.3 Creación de Objetos Evaluables ...................................................................................... 174
12.2.3.1 Creación de Objetos con Identificador ..................................................................... 174
12.2.3.2 Igualdad de Objetos.................................................................................................. 174
12.2.3.3 Operaciones Lógicas ................................................................................................ 175
12.2.3.4 Iteraciones ................................................................................................................ 177
12.2.3.5 Recorrido de Miembros............................................................................................ 179
12.2.3.6 Bucles Infinitos......................................................................................................... 180
12.2.4 Aritmética de la Plataforma............................................................................................. 181
12.2.4.1 Miembros de Semántica Aritmética ......................................................................... 182
12.2.5 Miembros Polimórficos de Comparación......................................................................... 183
12.2.6 Miembros de Objetos Cadena de Caracteres................................................................... 185
VI
12.2.7 Creación de Abstracciones e Instancias...........................................................................186
12.2.7.1 Creación de Abstracciones .......................................................................................186
12.2.7.2 Copia de Prototipos ..................................................................................................188
12.2.8 Implementaciones Externas Requeridas...........................................................................190
12.3 IMPLANTACIÓN Y ACCESO A LA PLATAFORMA..........................................................................191
CCCAAAPPPÍÍÍTTTUUULLLO 111333 : DISEÑO DEL ENTORNO DE PROGRAMACIÓN..............................................195
13.1 RECOLECCIÓN DE BASURA........................................................................................................195
13.1.1 Diseño General del Sistema..............................................................................................196
13.1.2 Recolección Genérica.......................................................................................................196
13.1.3 Algoritmos de Recolección ...............................................................................................197
13.1.4 Políticas de Activación .....................................................................................................197
13.2 PLANIFICACIÓN DE HILOS .........................................................................................................198
13.2.1 Creación de Métodos........................................................................................................198
13.2.2 Introducción de Código Intruso de Sincronización ..........................................................199
13.2.3 MOP para la Evaluación de Código ................................................................................200
13.2.4 Creación de Hilos.............................................................................................................201
13.2.5 Planificación Genérica.....................................................................................................202
13.3 SISTEMA DE PERSISTENCIA........................................................................................................203
13.3.1 Diseño General del Sistema..............................................................................................203
13.3.2 Implantación de un Flujo de Persistencia ........................................................................204
13.3.3 MOP para Acceso al Miembro .........................................................................................205
13.3.4 Objetos Delegados............................................................................................................206
13.3.5 Recuperación de Objetos Persistentes..............................................................................208
13.4 SISTEMA DE DISTRIBUCIÓN .......................................................................................................209
13.4.1 Diseño General del Sistema..............................................................................................209
13.4.2 Primitivas del Sistema de Distribución ............................................................................211
13.4.3 Objetos Delegados............................................................................................................212
13.4.4 Recolector de Basura Distribuido ....................................................................................213
13.5 CONCLUSIONES .........................................................................................................................215
CCCAAAPPPÍÍÍTTTUUULLLO 111444 : DISEÑO DE UN PROTOTIPO DE COMPUTACIÓN REFLECTIVA SIN
RESTRICCIONES................................................................................................................................217
14.1 SELECCIÓN DEL LENGUAJE DE PROGRAMACIÓN........................................................................217
Introspección ...............................................................................................................................218
Reflectividad Estructural ..............................................................................................................218
Creación, Manipulación y Evaluación Dinámica de Código........................................................219
Interacción Directa entre Aplicaciones.........................................................................................219
14.2 DIAGRAMA DE SUBSISTEMAS ....................................................................................................220
14.2.1 Subsistema nitrO...............................................................................................................221
14.2.2 Subsistema objectBrowser ................................................................................................221
14.2.3 Subsistema langSpec.........................................................................................................221
14.2.4 Subsistema metaLang .......................................................................................................221
14.2.5 Subsistema appSpec..........................................................................................................222
14.2.6 Subsistema GI...................................................................................................................222
14.3 SUBSISTEMA METALANG...........................................................................................................222
14.3.1 Diagrama de Clases .........................................................................................................222
14.4 SUBSISTEMA LANGSPEC ............................................................................................................223
14.4.1 Diagrama de Clases .........................................................................................................224
14.4.2 Especificación de Lenguajes mediante Objetos................................................................225
14.5 SUBSISTEMA APPSPEC ...............................................................................................................227
14.5.1 Diagrama de Clases .........................................................................................................228
14.5.2 Creación del Árbol Sintáctico ..........................................................................................229
14.6 SUBSISTEMA GI.........................................................................................................................230
14.6.1 Diagrama de Clases .........................................................................................................231
14.6.2 Evaluación del Árbol Sintáctico .......................................................................................232
14.7 SUBSISTEMA NITRO...................................................................................................................233
14.7.1 Diagrama de Clases .........................................................................................................233
CCCAAAPPPÍÍÍTTTUUULLLO 111555 : ÁMBITOS DE APLICACIÓN DEL SISTEMA.....................................................235
VII
15.1 SISTEMAS DE PERSISTENCIA Y DISTRIBUCIÓN........................................................................... 235
15.1.1 Adaptabilidad a los Cambios ........................................................................................... 236
15.1.2 Replicación y Movilidad de Objetos Flexible................................................................... 237
15.1.3 Independencia del Aspecto y Selección Dinámica ........................................................... 237
15.1.4 Agentes Móviles Inteligentes y Autodescriptivos.............................................................. 238
15.1.5 Distribución de Aplicaciones Independientemente de su Lenguaje ................................. 239
15.2 SISTEMAS DE COMPONENTES .................................................................................................... 239
15.3 PARADIGMA DE PROGRAMACIÓN DINÁMICAMENTE ADAPTABLE ............................................. 240
15.3.1 Polimorfismo Dinámico ................................................................................................... 240
15.3.2 Gestión de Información Desconocida en Fase de Desarrollo.......................................... 241
15.3.3 Patrones de Diseño Adaptables........................................................................................ 242
15.4 APLICACIONES TOLERANTES A FALLOS .................................................................................... 243
15.5 PROCESAMIENTO DE LENGUAJES .............................................................................................. 244
15.5.1 Depuración y Programación Continua ............................................................................ 244
15.5.2 Generación de Código...................................................................................................... 245
15.6 ADAPTACIÓN DE LENGUAJES DE PROGRAMACIÓN .................................................................... 246
15.6.1 Adaptación al Problema................................................................................................... 246
15.6.2 Agregación de Características al Lenguaje ..................................................................... 247
15.7 SISTEMAS OPERATIVOS ADAPTABLES....................................................................................... 247
15.8 ADAPTACIÓN Y CONFIGURACIÓN DE SISTEMAS ........................................................................ 248
15.8.1 Gestión y Tratamiento del Conocimiento......................................................................... 248
15.8.2 Personalización de Aplicaciones...................................................................................... 248
CCCAAAPPPÍÍÍTTTUUULLLO 111666 : EVALUACIÓN DEL SISTEMA ............................................................................. 251
16.1 COMPARATIVA DE SISTEMAS .................................................................................................... 251
16.2 CRITERIOS DE EVALUACIÓN...................................................................................................... 252
16.3 EVALUACIÓN ............................................................................................................................ 253
16.4 JUSTIFICACIÓN DE LAS EVALUACIONES .................................................................................... 258
16.5 CONCLUSIONES ......................................................................................................................... 261
16.5.1 Evaluación de las Plataformas......................................................................................... 261
16.5.2 Evaluación de los Criterios para Construir un Entorno de Programación ..................... 261
16.5.3 Evaluación del Grado de Flexibilidad ............................................................................. 261
16.5.4 Evaluación Global del Sistema ........................................................................................ 262
16.5.5 Evaluación Absoluta......................................................................................................... 262
CCCAAAPPPÍÍÍTTTUUULLLO 111777 : CONCLUSIONES..................................................................................................... 265
17.1 SISTEMA DISEÑADO.................................................................................................................. 266
17.1.1 Máquina Abstracta........................................................................................................... 266
17.1.2 Entorno de Programación................................................................................................ 266
17.1.3 Sistema Reflectivo No Restrictivo..................................................................................... 267
17.2 PRINCIPALES VENTAJAS APORTADAS ....................................................................................... 267
17.2.1 Heterogeneidad de la Plataforma .................................................................................... 267
17.2.2 Extensibilidad.................................................................................................................. 268
17.2.3 Adaptabilidad ................................................................................................................... 268
17.2.4 Modelo Computacional Único e Independiente del Lenguaje.......................................... 269
17.2.5 Flexibilidad ...................................................................................................................... 269
17.3 FUTURAS LÍNEAS DE INVESTIGACIÓN Y TRABAJO..................................................................... 270
17.3.1 Aplicación a Sistemas Computacionales.......................................................................... 270
17.3.1.1 Sistema de Persistencia Implícita ............................................................................. 270
17.3.1.2 Entorno de Distribución de Agentes......................................................................... 271
17.3.1.3 Semántica del Lenguaje Natural............................................................................... 271
17.3.2 Ampliación y Modificación de la Implementación ........................................................... 272
17.3.2.1 Única Aplicación......................................................................................................272
17.3.2.2 Especificación de Lenguajes .................................................................................... 272
17.3.2.3 Implantación en Distintos Sistemas.......................................................................... 272
17.3.2.4 Ayuda al Desarrollo de Aplicaciones ....................................................................... 272
17.3.2.5 Eficiencia.................................................................................................................. 273
AAAPPPÉÉÉNNNDDDIIICCCEEE AAA : DISEÑO DE UN PROTOTIPO DE MÁQUINA VIRTUAL.................................. 275
A.1 ACCESO A LA MÁQUINA VIRTUAL ............................................................................................ 275
VIII
A.2 DIAGRAMA DE SUBSISTEMAS ....................................................................................................276
A.2.1 Subsistema LanguageProcessor .......................................................................................277
A.2.2 Subsistema Interpreter......................................................................................................277
A.2.3 Subsistema ObjectMemory ...............................................................................................277
A.3 SUBSISTEMA LANGUAGEPROCESSOR ........................................................................................277
A.4 SUBSISTEMA OBJECTMEMORY..................................................................................................278
A.5 SUBSISTEMA INTERPRETER ........................................................................................................280
AAAPPPÉÉÉNNNDDDIIICCCEEE BBB : MANUAL DE USUARIO DEL PROTOTIPO DE COMPUTACIÓN
REFLECTIVA SIN RESTRICCIONES ..............................................................................................283
B.1 INSTALACIÓN Y CONFIGURACIÓN..............................................................................................283
B.2 METALENGUAJE DEL SISTEMA ..................................................................................................283
B.2.1 Gramática del Metalenguaje ............................................................................................284
B.2.2 Descripción Léxica ...........................................................................................................284
B.2.3 Descripción Sintáctica......................................................................................................285
B.2.4 Tokens de Escape y Reconocimiento Automático.............................................................285
B.2.5 Especificación Semántica .................................................................................................286
B.2.6 Instrucción Reify...............................................................................................................286
B.3 APLICACIONES DEL SISTEMA.....................................................................................................286
B.3.1 Gramática de Aplicaciones...............................................................................................286
B.3.2 Aplicaciones Autosuficientes ............................................................................................287
B.3.3 Reflectividad No Restrictiva .............................................................................................287
B.3.4 Reflectividad de Lenguaje.................................................................................................288
B.4 INTERFAZ GRÁFICO...................................................................................................................288
B.4.1 Intérprete de Comandos ...................................................................................................288
B.4.2 Archivos Empleados .........................................................................................................289
B.4.3 Introspección del Sistema.................................................................................................290
B.4.4 Ejecución de Aplicaciones................................................................................................292
AAAPPPÉÉÉNNNDDDIIICCCEEE CCC : REFERENCIAS BIBLIOGRÁFICAS......................................................................295
1
CCAAPPÍÍTTUULLO 11:
INTRODUCCIÓN
A lo largo de este capítulo se describen los principales objetivos buscados en el desarrollo
de esta tesis doctoral, estableciendo un marco de requisitos generales a cumplir en
la tesis enunciada en esta memoria y posteriormente demostrada mediante la creación y
evaluación de una serie de prototipos. Posteriormente se presenta la organización de la
memoria, estructurada tanto en secciones como en capítulos.
1.1 Introducción
El modo en el que un ordenador es programado queda determinado, principalmente,
por los requisitos que los futuros usuarios demandarán de dicha computadora. En el
caso del entorno de computación de una oficina de ingeniería civil, se demandará la utilización
de aplicaciones relacionadas con el diseño asistido por computador. Sin embargo, en
el caso de entidades bancarias, el tratamiento de elevadas cantidades de información es el
proceso más común. La elección del lenguaje de programación utilizado a la hora de implementar
una aplicación, dentro de un amplio espectro de posibilidades, queda condicionada
por el tipo de aplicación a desarrollar.
La semántica de un lenguaje de programación, así como el propósito para el que éste
haya sido diseñado, restringe de algún modo la sencillez con la que pueda emplearse para
resolver un determinado tipo de problema. Los lenguajes de propósito específico [Cueva98]
están enfocados a desarrollar una clase concreta de aplicaciones del modo más sencillo
posible, estando su semántica restringida al propósito para el que fueron creados. Por
otro lado, los denominados lenguajes de propósito general tienen como objetivo el poder
representar la solución de cualquier tipo de problema computacional. Sin embargo, dentro
de esta clasificación existen diferencias significativas. Lenguajes como Java [Gosling96] y
C++ [Stroustrup98], ambos de propósito general, orientados a objetos y de sintaxis análoga,
pueden ser elegidos, por ejemplo, para desarrollar aplicaciones portables y distribuidas –
en el primer caso–, o bien para crear aquéllas en las que prime su eficiencia en tiempo de
ejecución –el caso del lenguaje C++.
La semántica computacional de un lenguaje de programación es algo fijo que se
mantiene constante en las aplicaciones codificadas sobre él. Una aplicación carece de la
posibilidad de modificar la semántica del lenguaje con el que fue creada, para poder amoldarse
a nuevos requisitos que puedan surgir a lo largo de su existencia, sin necesidad de
modificar su código existente –su funcionalidad principal.
CAPÍTULO 1
2
Además de limitar las posibilidades computacionales de una aplicación a la hora de
elegir el lenguaje de programación en el que ésta vaya a ser desarrollado, la interacción entre
aplicaciones desarrolladas sobre distintos lenguajes se lleva a cabo comúnmente mediante la
utilización de mecanismos adicionales. El uso de modelos binarios de componentes (como
por ejemplo COM [Microsoft95]) o de arquitecturas de objetos distribuidos y middlewares
(CORBA [OMG95]), son necesarios para permitir la interacción entre aplicaciones desarrolladas
en distintos lenguajes de programación y, en ocasiones, sobre distintas plataformas.
Justificamos así la necesidad de estudiar las alternativas en la creación de un entorno
computacional de programación, independiente del lenguaje y plataforma seleccionados,
que permita desarrollar aplicaciones en cualquier lenguaje de programación, y que éstas
puedan interactuar entre sí sin necesidad de utilizar ningún mecanismo intermedio. Del
mismo modo, la semántica de un lenguaje de programación no deberá ser algo fijo e invariable
a lo largo del ciclo de vida de las aplicaciones codificadas sobre él; su significado (semántica)
podrá adaptarse a futuros contextos imprevisibles en tiempo de desarrollo.
A lo largo de esta tesis estudiaremos las distintas alternativas, y enunciaremos otras,
para crear un entorno computacional de programación flexible, en el que se satisfagan todos
los objetivos enunciados someramente en el párrafo anterior.
1.2 Objetivos
Enunciaremos los distintos objetivos generales propuestos en la creación del entorno
de programación previamente mencionado, posponiendo hasta el siguiente capítulo la
especificación formal del conjunto integral de requisitos impuestos a nuestro sistema.
1.2.1 Independencia del Lenguaje de Programación y Plataforma
El sistema computacional podrá ser programado mediante cualquier lenguaje y sus
aplicaciones podrán ser ejecutadas sobre cualquier plataforma. La dependencia actualmente
existente en determinados sistemas computacionales, en el que el lenguaje de programación
de aplicaciones y la plataforma de desarrollo imponen una determinada plataforma en su
implantación, deberá ser suprimida. La elección del lenguaje de programación a utilizar
estará únicamente determinada por el modo en el que su expresividad modela el problema,
y por las preferencias del programador.
En nuestro sistema, una aplicación podrá desarrollarse utilizando múltiples lenguajes
de programación, sin necesidad de utilizar una capa de intercomunicación entre sus distintos
módulos. La programación del sistema no tiene por qué llevarse a cabo mediante un
lenguaje conocido; en su código fuente podrá especificarse el lenguaje de codificación utilizado,
capacitando al sistema para la ejecución de la aplicación sin conocimiento previo del
lenguaje a emplear.
1.2.2 Interoperabilidad Directa
Actualmente, la utilización de una capa software intermedia es la solución más extendida
para conseguir interoperabilidad entre aplicaciones desarrolladas en distintos lenguajes
de programación o distintas plataformas físicas. La traducción entre modelos de
componentes, middlewares de distribución o arquitecturas de objetos distribuidos, otorga la
posibilidad de intercomunicar aplicaciones desarrolladas en distintos lenguajes de programación,
sistemas operativos y plataformas. Sin embargo, la utilización de estas capas adicionales
conlleva determinados inconvenientes: una mayor complejidad a la hora de desIntroducción
3
arrollar aplicaciones, múltiples traducciones entre distintos modelos computacionales, dependencia
de los mecanismos utilizados (acoplamiento), y reducción de la mantenibilidad
ante futuros cambios.
Para nuestro sistema, la interoperabilidad entre las distintas capas de una aplicación,
así como entre distintas aplicaciones dentro del sistema, ha de llevarse a cabo sin necesidad
de utilizar de un software intermedio de intercomunicación, de forma independiente al
lenguaje de programación y plataforma utilizados.
El sistema ha de ofrecer un conocimiento dinámico del estado del mismo, ofreciendo
los servicios de cualquier aplicación que esté ejecutándose.
1.2.3 Extensibilidad
Los sistemas computacionales comunes amplían su nivel de abstracción de diversas
maneras; ejemplos son la ampliación del lenguaje de programación, la implementación de
librerías o APIs, o el desarrollo de componentes software.
La modificación del lenguaje de programación supone nuevas versiones de los procesadores
del lenguaje asociado, generando una pérdida de la portabilidad del código desarrollado
para versiones anteriores. La utilización de componentes, APIs o librerías, no
ofrece un conocimiento dinámico de los servicios ofrecidos y carece de un sistema genérico
de ampliación de su funcionalidad para el resto del sistema –en ocasiones, existe una dependencia
adicional de una plataforma o lenguaje de programación.
Nuestro sistema deberá soportar un mecanismo para poder extenderse: conocer dinámicamente
los servicios existentes para poder ampliar éstos, si así es requerido, obteniendo
un mayor nivel de abstracción para el conjunto del entorno de programación, de un
modo independiente del lenguaje y plataforma.
Una aplicación podrá hacer uso de los servicios desarrollados que extiendan el nivel
de abstracción global del sistema –cualquiera que sea su lenguaje de programación y plataforma.
1.2.4 Adaptabilidad no Restrictiva
La adaptabilidad de un sistema computacional es la capacidad para poder amoldarse
a requisitos surgidos dinámicamente, desconocidos en fase de desarrollo. La mayoría de los
sistemas existentes ofrecen mecanismos muy limitados para poder desarrollar aplicaciones
dinámicamente adaptables.
Las aplicaciones desarrolladas en nuestro entorno de programación deberán poder
adaptarse a contextos surgidos dinámicamente, sin haber predicho éstos en fase de desarrollo.
En tiempo de ejecución, una aplicación podrá detectar nuevos requisitos y adaptarse a
éstos sin necesidad de detener su ejecución, modificar su código fuente, y reanudarla.
Tanto la estructura de la aplicación en ejecución, como la semántica del lenguaje de
programación sobre el que haya sido codificada, deberán poder adaptarse sin restricción
alguna. Como estudiaremos en el capítulo 5 y en el capítulo 7, determinados sistemas sólo
permiten adaptar dinámicamente una parte de la estructura de sus aplicaciones, o de su
lenguaje de programación.
Bajo nuestro sistema computacional, una aplicación, cualquiera que sea su lenguaje
de programación, podrá ser desarrollada separando su código funcional de determinados
aspectos independientes de su funcionalidad y, por tanto, reutilizables (depuración, persisCAPÍTULO
1
4
tencia, sincronización, distribución o monitorización). Dinámicamente, los distintos aspectos
del programa podrán ser examinados y modificados, sin necesidad de finalizar su ejecución.
A lo largo de esta memoria, utilizaremos el concepto de flexibilidad como la unión
de las características de extensibilidad y adaptabilidad, propias de un sistema computacional.
Cabe mencionar, como profundizaremos en el capítulo 6, que el objetivo principal
del sistema buscado es la flexibilidad del mismo, siendo ésta comúnmente contraria a su
eficiencia; cuanto más flexible es un entorno computacional, menor es su eficiencia. Queda
por tanto fuera de nuestros principales objetivos el buscar un sistema de computación más
eficiente que los estudiados –pero sí más flexible. Diversas técnicas para obtener una mejora
de rendimientos de ejecución, podrán ser aplicadas al sistema presentado.
1.3 Organización de la Tesis
A continuación estructuramos la tesis, agrupando los capítulos en secciones con un
contenido acorde.
1.3.1 Introducción y Requisitos del Sistema
En este capítulo narraremos la introducción, objetivos y organización de esta tesis.
En el capítulo siguiente estableceremos el conjunto de requisitos impuestos a nuestro sistema,
que serán utilizados principalmente:
Para evaluar las aportaciones y carencias de los sistemas estudiados en la próxima
sección.
Para fijar la arquitectura global de nuestro sistema, así como la arquitectura de
cada una de las capas que éste posee.
Para evaluar los resultados del sistema propuesto, comparándolos con otros sistemas
existentes estudiados –capítulo 16.
1.3.2 Sistemas Existentes Estudiados
En esta sección se lleva a cabo un estudio del estado del arte de los sistemas similares
al buscado. En el capítulo 3 se introduce el concepto de máquina abstracta, así como las
ventajas generales de su utilización. Un estudio y evaluación de distintos tipos de máquinas
abstractas existentes es llevado a cabo en el capítulo 4.
En el capítulo 5, se detalla el análisis de cómo determinados sistemas ofrecen flexibilidad
computacional sin utilizar técnicas de reflectividad. Esta técnica, sus conceptos
principales y distintas clasificaciones, son introducidos en el capítulo 6. La evaluación de
múltiples sistemas reflectivos, sus aportaciones y limitaciones frente a los requisitos impuestos,
son presentados en el capítulo 7.
Finalmente, en el capítulo 8, se presentan y estudian los modelos computacionales
orientados a objetos que, careciendo del concepto de clase, están basados en prototipos.
Introducción
5
1.3.3 Diseño del Sistema
En esta sección se introduce el sistema propuesto basándose en los requisitos impuestos
y los sistemas estudiados. La arquitectura global del sistema y su descomposición
en capas es presentada en el capítulo 9; la arquitectura de la primera capa, máquina abstracta,
en el capítulo 10; el sistema reflectivo sin restricciones e independiente del lenguaje es
presentado en el capítulo 11.
Detallamos los capítulos anteriores presentando el diseño de la máquina abstracta
en el capítulo 12. Sobre ella, desarrollamos un entorno de programación (capítulo 13) que
amplia el nivel de abstracción del sistema sin necesidad de modificar la implementación de
la máquina. En el capítulo 14 se diseña de la tercera capa del sistema que otorga la flexibilidad
máxima, de un modo independiente del lenguaje de programación seleccionado.
1.3.4 Aplicaciones, Evaluación, Conclusiones y Trabajo Futuro
Un conjunto de posibles aplicaciones prácticas de nuestro sistema se presenta en el
capítulo 15, encontrándonos actualmente en fase de desarrollo de parte de ellas –haciendo
uso de los prototipos existentes.
La evaluación del sistema presentado en esta tesis, comparándolo con otros existentes,
es llevada a cabo en el capítulo 16 bajo los requisitos establecidos al comienzo de ésta.
Finalmente, en el capítulo 17, se muestran las conclusiones globales de la tesis, las principales
aportaciones realizadas frente a los sistemas estudiados, y las futuras líneas de investigación
y trabajo a realizar.
1.3.5 Apéndices
Como apéndices de esta memoria se presentan:
En el apéndice A se presenta un diseño de un prototipo, intérprete de la máquina
abstracta diseñada en el capítulo 12.
Manual de usuario del sistema reflectivo no restrictivo (apéndice B), cuyo diseño
fue detallado en el capítulo 14.
El apéndice C constituye el conjunto de referencias bibliográficas utilizadas en
este documento.
7
CCAAPPÍÍTTUULLO 22:
REQUISITOS DEL SISTEMA
En este capítulo especificaremos los requisitos del sistema buscado en esta tesis. Se
identificarán los distintos requisitos y se describirán brevemente, englobando éstos dentro
de distintas categorías funcionales. Los grupos de requisitos serán los propios de la plataforma
básica de computación buscada (§ 2.1), los necesarios para diseñar un entorno de
programación con las características perseguidas (§ 2.2), y los distintos niveles de flexibilidad
requeridos para obtener los objetivos fijados (§ 2.3). Finalmente, identificaremos el
conjunto de requisitos generales de nuestro sistema desde un punto de vista global (§ 2.4).
La especificación de los requisitos del sistema llevada a cabo en este capítulo, tiene
por objetivo la validación del sistema diseñado así como el estudio de los distintos sistemas
existentes similares al buscado. Los requisitos del sistema nos permiten reconocer los puntos
positivos de sistemas reales –para su futura adopción o estudio– así como cuantificar
sus carencias y justificar determinadas modificaciones y/o ampliaciones.
La especificación de los requisitos nos ayudará a establecer los objetivos buscados
en el diseño de nuestro sistema, así como a validar la consecución de dichos objetivos una
vez que éste haya sido desarrollado.
2.1 Requisitos de la Plataforma de Computación
Para desarrollar un sistema computacional flexible y adaptable, que se pueda ejecutar
en cualquier plataforma de forma independiente al lenguaje y que sus aplicaciones sean
distribuibles e interoperables, la capa base de ejecución –la plataforma virtual– ha de satisfacer
un elevado número de requisitos. La selección e implantación de éstos será una tarea
crítica, puesto que todo el sistema será desarrollado sobre esta plataforma; si ésta no ofrece
la suficiente flexibilidad, el entorno de programación no podrá ser completamente adaptable.
Identificaremos brevemente los requisitos que deberá cumplir la plataforma virtual
de nuestro sistema para satisfacer los objetivos descritos en § 1.2.
2.1.1 Sistema Computacional Multiplataforma
La plataforma base de computación para nuestro sistema deberá poder ser implantada
en cualquier sistema existente. Deberá ser totalmente independiente de características
CAPÍTULO 2
8
propias de un microprocesador, sistema operativo o cualquier particularidad propia de un
determinado sistema.
La implantación de nuestra plataforma base de computación deberá poderse integrar
en cualquier sistema computacional con unos requisitos mínimos de procesamiento1.
2.1.2 Independencia del Lenguaje de Programación
La utilización de una plataforma virtual mediante la descripción de una máquina
abstracta –capítulo 3– es común en entornos distribuidos y portables. Sin embargo, la definición
de estas plataformas suele ir ligada a un determinado lenguaje de programación.
La base computacional de nuestro sistema deberá ser independiente de los lenguajes
de programación utilizados para acceder a ella. Mediante un proceso de compilación
deberá ser factible una traducción desde diversos lenguajes de programación al código nativo
de la plataforma.
2.1.3 Independencia del Problema
La descripción de la plataforma no deberá estar enfocada a la resolución de un tipo
de problema. Existen implementaciones que describen una plataforma para abordar problemas
como la persistencia de sistemas orientados a objetos o la intercomunicación de
aplicaciones distribuidas. Este tipo de plataforma es descrito de una forma específica para
la resolución del problema planteado.
Nuestra plataforma computacional deberá ser descrita con flexibilidad frente a los
problemas tratados. No se trata de que se implemente una solución para la mayoría de los
problemas existentes, sino que permita adaptar de una forma genérica su semántica y modelo
computacional para la resolución de cualquier problema que surgiere.
2.1.4 Tamaño y Semántica Computacional Reducida
El primer requisito identificado, “Sistema Computacional Multiplataforma”, obligaba
a la plataforma a ser independiente del sistema en el que se implante. Identificábamos
además la necesidad de que ésta se pudiese ejecutar sobre sistemas con capacidad computacional
reducida. Para que este requisito se pueda llevar a cabo, la plataforma base deberá
implementarse buscando un tamaño reducido.
La semántica computacional básica de la plataforma –primitivas de computación–
deberá ser lo más sencilla posible. Esta semántica es la descripción del funcionamiento
propio de la máquina abstracta. Sin embargo, las primitivas operacionales, o funcionalidades
propias del lenguaje de la máquina, deberán ser externas, accesibles y ampliables, de
forma concreta a cada implementación2.
Dividiendo la semántica operacional y computacional de la plataforma, conseguimos
reducir al máximo la plataforma raíz para que pueda ser implantada en cualquier entorno.
Si el sistema posee capacidad computacional suficiente, podrá ampliar su número de
operaciones computacionales. Esta división impulsa los dos siguientes requisitos.
1 Veremos cómo estos mínimos serán restringidos en el requisito § 2.1.4.
2 Esta diferenciación en la semántica de la plataforma queda patente en el capítulo 12, donde se especifica
la plataforma computacional básica de nuestro sistema.
Requisitos del Sistema
9
2.1.5 Flexibilidad Computacional
Como definíamos en el requisito “Independencia del Problema”, la plataforma deberá
ser lo suficientemente flexible para adaptarse a la resolución de cualquier problema.
No ser trata de prever todos las necesidades de un sistema y resolverlas, sino de diseñar una
plataforma flexible que permita adaptarse a cualquier problema.
Para ello hemos dividido este requisito de adaptabilidad en tres partes:
2.1.5.1 Semántica Operacional Abierta
Las operaciones primitivas del lenguaje de la plataforma deberán ser modificables
para cada implantación. En cada máquina abstracta podremos aumentar el número de primitivas
sobre las existentes para añadir un mayor nivel de abstracción al entorno. Lo conseguido
es una flexibilidad en la semántica operacional del lenguaje.
2.1.5.2 Introspección y Acceso al Entorno
Una aplicación portable deberá ser capaz de conocer las funcionalidades existentes
en el entorno en el que se está ejecutando. Mediante introspección, una aplicación podrá
saber si las operaciones que demanda han sido desarrolladas y obrar en consecuencia. De
esta forma, con una única plataforma, ofrecemos diversos niveles de abstracción y la capacidad
de conocer en cuál nos encontramos.
2.1.5.3 Semántica Computacional Abierta
El mayor grado de flexibilidad de la plataforma se obtendrá gracias un mecanismo
de modificación de su semántica computacional, mediante el propio lenguaje de la máquina
abstracta. Permitir modificar el funcionamiento propio de la máquina, facilitará al programador
modificar la interpretación de un programa sin modificar una línea de código. Además,
la implementación de la máquina nunca es alterada: se modifica su funcionamiento
mediante su propio lenguaje.
2.1.6 Interfaz de Acceso y Localización del Código Nativo
Cumpliendo la especificación de requisitos descritos, la base de nuestro sistema deberá
ser multiplataforma y autoprogramado –aumenta su nivel de abstracción desde un
modelo básico, programándose sobre su propio lenguaje. Todo el código desarrollado sobre
la propia plataforma es portable, sin embargo, la implementación del intérprete de la
máquina abstracta tendrá código nativo dependiente de la plataforma física en la que se
ejecuta –las operaciones primitivas.
La reducida3 parte del sistema que sea dependiente de la plataforma deberá identificarse
de forma separada al resto del código y tendrá una interfaz de acceso bien definida.
La migración del sistema a una plataforma física distinta, se conseguirá únicamente mediante
la implementación de esta parte en la nueva plataforma y la recompilación de la máquina
virtual.
2.1.7 Nivel de Abstracción del Modelo de Computación
El nivel de abstracción proporcionado por la plataforma virtual será el modelo utilizado
para interconectar las distintas aplicaciones del sistema. La selección del correcto nivel
3 Cuanto más reducido sea el tamaño de código implementado dependiente de la plataforma, más sencilla
será la implementación del sistema en una plataforma distinta.
CAPÍTULO 2
10
de abstracción que ofrezca el modelo de computación es una tarea difícil: Un bajo nivel de
abstracción hace más compleja la interoperabilidad de aplicaciones, mientras que un elevado
nivel de abstracción puede generar una plataforma dependiente del lenguaje –
incumpliendo así el requisito § 2.1.2.
Una mala elección del modelo de computación de la plataforma puede dificultar la
obtención de muchos de los requisitos expuestos en este capítulo.
2.2 Requisitos del Entorno de Programación
El diseño de la plataforma de computación se centra en el desarrollo de una máquina
abstracta (capítulo 12) flexible (§ 2.1.5), que posea un tamaño y semántica operacional
reducidos (§ 2.1.4).
Para aumentar su semántica computacional y ofrecer un mayor nivel de abstracción
al programador de aplicaciones, desarrollaremos un entorno de programación con servicios
propios de un sistema operativo distribuido (sin que cobre carácter de sistema operativo4).
Para validar su condición de flexibilidad, el entorno de programación implementará
las distintas funcionalidades de un modo adaptable, de forma que puedan ser seleccionadas
y modificadas dinámicamente.
2.2.1 Desarrollo de Características de Computación (Extensibilidad)
La plataforma de computación deberá ser lo suficientemente extensible como para
poder desarrollar sobre ella otras características computacionales. Dadas las primitivas de la
plataforma (por ejemplo, paso de mensajes, creación de objetos o evaluación de métodos)
el entorno de programación deberá ser capaz de construir sobre estas funcionalidades otras
de mayor nivel de abstracción o substitutivas de las existentes.
2.2.2 Selección de Características de Computación (Adaptabilidad)
Las nuevas características de computación desarrolladas en el entorno de programación
deberán ser reemplazables de forma dinámica, pudiendo modificar o elegir éstas en
función de las necesidades del programador. Si, por ejemplo, el paso de mensajes ha sido
modificado para ser distribuido, deberá poder seleccionarse dinámicamente el protocolo de
comunicaciones utilizado.
2.2.3 Identificación del Entorno de Programación
La única condición para formar parte de la plataforma distribuida es estar ejecutando
una máquina virtual. Como hemos mencionado, el tamaño de ésta y el conjunto de sus
primitivas es reducido, y por ello ampliamos estos con el entorno de programación.
Si un sistema físico posee poca memoria o capacidad de procesamiento, tendrá una
parte mínima del entorno de programación. Por esta razón, en un sistema heterogéneo de
computación, es necesario tener un mecanismo para conocer o identificar el conjunto de
servicios que en el entorno de programación hayan sido instalados.
4 El entorno de programación desarrollado busca facilitar al programador la interacción con la plataforma
base. En ningún momento se trata de desarrollar un sistema operativo sobre la plataforma creada [Álvarez96].
Requisitos del Sistema
11
2.2.4 Autodocumentación Real
En el requisito anterior señalábamos la posibilidad de que existiesen infinitas versiones
del entorno de programación –cada una en función de los intereses y limitaciones de
la unidad de procesamiento. Para conocer exactamente el entorno existente es necesario un
sistema de autodocumentación.
La plataforma deberá facilitar la información de todos los objetos, métodos y atributos
reales existentes de forma real y directa. Si se modifica uno de éstos, el sistema deberá
ser capaz de modificar dicha documentación automáticamente; ésta nunca podrá quedar
desfasada respecto a lo que en el sistema realmente exista.
2.3 Requisitos Concernientes al Grado de Flexibilidad
El principal objetivo del sistema buscado en esta tesis, es obtener sistema con un
elevado grado de flexibilidad que permita constituirse como una plataforma de ejecución
universal, sin necesidad de modificar la implementación de la máquina abstracta.
Desde el punto de vista de la seguridad del sistema computacional, el acceso y modificación
de las distintas aplicaciones representa una técnica a restringir mediante un mecanismo
de seguridad. En el desarrollo de esta tesis nos centraremos en diseñar un sistema
con el grado de flexibilidad fijado en este capítulo, dejando los aspectos de seguridad como
una capa adicional que restrinja el acceso a estas funcionalidades.
2.3.1 Conocimiento Dinámico del Estado del Sistema
Cualquier aplicación deberá ser capaz de conocer el estado del sistema o de otra
aplicación en tiempo de ejecución. Gracias a esta característica podremos desarrollar aplicaciones
dinámicamente adaptables a contextos, puesto que podrán analizarse éstos en tiempo
de ejecución.
2.3.2 Modificación Estructural Dinámica
La estructura de un objeto, aplicación o de todo el entorno de programación, podrá
ser modificada dinámicamente por cualquier aplicación ejecutada sobre la plataforma. El
hecho de poder modificar la estructura de cualquier entidad en tiempo de ejecución, permite
construir sistemas extensibles dinámicamente.
2.3.3 Modificación Dinámica del Comportamiento
La modificación dinámica del comportamiento o semántica de una aplicación, implica
la variación de su comportamiento sin necesidad de modificar su estructura ni su código
fuente. Un programa se ejecutará sobre un entorno variable o adaptable de forma dinámica,
sin modificar la aplicación.
En este punto, el grado de adaptabilidad del sistema es máximo: toda su semántica
puede variar sin necesidad de modificar la aplicación adaptada.
2.3.4 Modificación Computacional sin Restricciones
El requisito anterior justifica la necesidad de tener un mecanismo de modificación
del comportamiento del sistema. Muchos sistemas poseen esta capacidad pero con un conCAPÍTULO
2
12
junto de restricciones. En ellos, la modificación de funciones del comportamiento se lleva a
cabo mediante un protocolo que restringe el conjunto de características que podrán ser
modificadas en un futuro: si un protocolo no acoge la posibilidad de modificar una función,
ésta no podrá ser substituida en tiempo de ejecución.
Nuestro sistema deberá implantar un sistema de flexibilidad computacional en el
que cualquier funcionalidad pueda ser modificada dinámicamente, sin necesidad de especificarlo
a priori. La modificación de la máquina abstracta no deberá ser contemplada para
modificar el entorno en el que se ejecute una aplicación.
2.3.5 Modificación y Selección Dinámica del Lenguaje
Nuestra plataforma computacional deberá ser independiente del lenguaje de programación
seleccionado, de forma que no sólo se deberá poder seleccionar cualquier lenguaje,
sino que se permitirá modificar la semántica de éste dinámicamente. Cualquier aspecto
del lenguaje de programación podrá ser modificado, y nuevas características podrán ser
añadidas.
Distintas aplicaciones en distintos lenguajes podrán interactuar entre sí, además de
permitir desarrollar una aplicación en distintos lenguajes. De forma adicional, una aplicación
podrá especificar dentro de su código el lenguaje de programación utilizado antes de
su ejecución, permitiéndose así su computación sin necesidad de que la especificación del
lenguaje tenga que existir previamente en el sistema.
2.3.6 Interoperabilidad Directa entre Aplicaciones
La flexibilidad ofrecida por nuestro sistema debe ser totalmente dinámica, de forma
que la adaptabilidad y extensibilidad pueda producirse en tiempo de ejecución. Si una aplicación
está ejecutándose, y queremos personalizarla sin detenerla, la interoperabilidad entre
aplicaciones cobra una especial importancia.
Desde una aplicación que se ejecute sobre la misma máquina abstracta, se deberá
tener un acceso directo –sin ningún artificio o middleware– a todas las aplicaciones que estén
corriendo sobre dicha plataforma. De esta forma, el acceso y modificación dinámicos desde
una aplicación a otra es directo, y no será necesario construir una aplicación de un modo
especial para que pueda ser accesible desde otros procesos.
2.4 Requisitos Generales de Sistema
En este punto definiremos brevemente todos los requisitos que debe cumplir nuestro
sistema desde un punto de vista global, y que no hayan sido contemplados en los puntos
anteriores (§ 2.1, § 2.2 y § 2.3).
2.4.1 Independencia del Hardware
La interfaz de acceso a la plataforma no deberá ser dependiente del hardware en el
que haya sino implantado. La plataforma, y en conjunto todo el sistema, deberá poder instalarse
sobre distintos sistemas hardware.
Requisitos del Sistema
13
2.4.2 Independencia del Sistema Operativo
El sistema deberá poder implantarse en cualquier sistema operativo, y por lo tanto
no deberá ser diseñado con características particulares de algún sistema concreto.
2.4.3 Interacción con Sistemas Externos
Las aplicaciones desarrolladas sobre el sistema deberán poder interactuar con aplicaciones
nativas existentes en el sistema operativo utilizado. El acceso a la plataforma deberá
tener una interfaz similar para cada una de los operativos utilizados, siguiendo en cada
caso un método estándar documentado de interacción entre aplicaciones.
Mediante el mecanismo estándar seleccionado, cualquier aplicación existente en un
sistema podrá interactuar con la plataforma diseñada, y el código portable de nuestra plataforma
podrá acceder a los recursos propios del sistema real en ejecución.
2.4.4 Interoperabilidad Uniforme
Los distintos elementos de computación distribuidos que formen el sistema interactuarán
entre sí de un modo uniforme, indiferentemente del hardware, sistema operativo o
lenguaje utilizado. Esta uniformidad implica utilizar un único modelo de objetos y representación
de los datos en entornos heterogéneos.
2.4.5 Heterogeneidad
El sistema deberá poder implantarse en entornos heterogéneos. Para cumplir esta
faceta, la base de la plataforma deberá ser de un tamaño reducido –para poder implantarla
en sistemas computacionales poco avanzados–, el sistema deberá ser extensible –para poder
elevar el nivel de abstracción– e introspectivo –para conocer el entorno del sistema
computacional.
Cualquier elemento de computación, indiferentemente de cuál sea su poder computacional,
deberá poder formar parte de la plataforma diseñada, aportando en cada caso un
distinto número de recursos.
2.4.6 Sistema de Programación Único
En la mayoría de sistemas distribuidos existentes, el arquitecto o ingeniero software
debe dividir una aplicación en elementos ubicados en distintos nodos computacionales. De
alguna forma se debe identificar dónde se va a ejecutar cada parte de la aplicación antes de
que ésta sea implantada.
En el sistema computacional buscado, el programador tendrá la posibilidad de acceder
a cualquier elemento de computación de forma dinámica, y la aplicación –o una parte
de ella– podrá desplazarse en función de una serie de condiciones. Para el programador, el
sistema computacional es una única entidad con distintos nodos computacionales, todos
ellos accesibles en cualquier momento de la ejecución de la aplicación.
CAPÍTULO 2
14
2.4.7 Independencia del Lenguaje
La programación de aplicaciones en el sistema deberá poder realizarse mediante
cualquier lenguaje de programación; por lo tanto, su diseño no se debe enfocar a determinadas
peculiaridades propias de un lenguaje de programación.
La elección del nivel de abstracción del modelo computacional es una tarea difícil:
debe ser lo suficientemente genérica (bajo nivel de abstracción) para la mayoría de lenguajes,
pero ofreciendo una semántica comprensible (mayor nivel de abstracción) para facilitar
la interacción entre aplicaciones.
2.4.8 Flexibilidad Dinámica No Restrictiva
El sistema debe aportar una flexibilidad dinámica en tiempo de ejecución total: se
deberá poder acceder y modificar una aplicación en todas sus características, así como su
entorno de ejecución (el sistema). El mecanismo utilizado para obtener esta flexibilidad no
debe imponer ningún tipo de restricción previa.
Sobre el sistema deberán poder desarrollarse aplicaciones mediante separación de
incumbencias; se define el aspecto funcional de ésta y, una vez finalizada (e incluso en
tiempo de ejecución), se añadirán otros aspectos adicionales referentes a su distribución
física, persistencia o planificación de procesos. Esta separación de incumbencias o aspectos
deberá poder efectuarse sin necesidad de modificar la aplicación en su nivel funcional.
2.4.9 Interoperabilidad Directa
Cada aplicación deberá poder acceder directamente a cualquier otro programa u objeto,
sin necesidad de establecer un mecanismo específico de interconexión. El sistema
deberá verse como un conjunto único computacional (§ 2.4.6) con distintos procesos, todos
ellos interactuando entre sí. El acceso, modificación y reutilización de aplicaciones serán
tareas indiferentes del proceso en el que fueron creadas.
15
CCAAPPÍÍTTUULLO 33:
MÁQUINAS ABSTRACTAS
En este capítulo se define el concepto de máquina abstracta y máquina virtual que
estarán presentes a lo largo de toda la memoria. Analizaremos la evolución histórica que
han tenido así como el objetivo principal buscado en cada una de las etapas.
Aquellos sistemas que utilicen este concepto para obtener objetivos distintos a los
expuestos en § 1.2 y § 2.1, serán descritos de forma superficial. En caso contrario, se explicará
la funcionalidad general y el beneficio obtenido mediante la utilización de máquinas
abstractas, sin detallar los casos reales existentes.
Al finalizar el capítulo se comentará, a modo de conclusión, qué ventajas aporta el
concepto de máquina abstracta a esta tesis y se establecerá una clasificación funcional para
el estudio de los sistemas existentes –que se llevará a cabo en el capítulo 4.
3.1 Procesadores Computacionales
Un programa es un conjunto ordenado de instrucciones que se dan al ordenador
indicándole las operaciones o tareas que se desea que realice [Cueva94]. Estos programas
van a ser ejecutados, animados o interpretados por un procesador computacional.
Un procesador computacional ejecuta las instrucciones propias de un programa que
accederán, examinando o modificando, a los datos pertenecientes a dicho programa. La
implementación de un procesador computacional puede ser física (hardware) o lógica (software)
mediante el desarrollo de otro programa.
3.1.1 Procesadores Implementados Físicamente
Un procesador físico es un intérprete de programas que ha sido desarrollado de
forma física (comúnmente como un circuito integrado). Los procesadores más extendidos
son los procesadores digitales síncronos. Están formados por una unidad de control, una
memoria y una unidad aritmético lógica, todas ellas interconectadas entre sí [Mandado73].
Un computador es un procesador digital síncrono cuya unidad de control es un sistema
secuencial síncrono que recibe desde el exterior (el programador) una secuencia de
instrucciones que le indican las microoperaciones que debe realizar [Mandado73]. La secuencia
de ejecución de estas operaciones es definida en la memoria describiendo un proCAPÍTULO
3
16
grama, pero la semántica de cada instrucción y el conjunto global existente para el procesador
es invariable.
La ventaja de este tipo de procesadores frente a los lógicos es su velocidad al haber
sido desarrollados físicamente. Su principal inconveniente, como decíamos en el párrafo
anterior, es su inflexibilidad.
3.1.2 Procesadores Implementados Lógicamente
Un procesador software es un programa que interpreta a su vez programas de otro
procesador [Cueva98]. Es aquel programa capaz de interpretar o emular el funcionamiento
de un determinado procesador.
La modificación de un programa que emule a un procesador es mucho más sencilla
que la modificación física de un procesador hardware. Esto hace que los emuladores software
se utilicen, entre otras cosas, como herramientas de simulación encaminadas a la implementación
física del procesador que emulan.
La principal desventaja de este tipo de procesadores frente a los procesadores hardware
o físicos es la velocidad de ejecución. Puesto que los procesadores lógicos establecen
un nivel más de computación (son ejecutados o interpretados por otro procesador), es inevitable
que requieran un mayor número de computaciones para interpretar un programa
que su versión hardware. Esta sobrecarga computacional se aprecia gráficamente en la
Figura 3.1:
Programa en A
Instrucciones
programa en A
Datos
programa en A
Instrucción en Ejecución
Lectura/Escritura
de Datos
Procesador Hw A
Procesador Software
de A en B
Procesador Hw B
Instrucciones
Programa en A
Datos
Programa en A Programa en A
y Datos Programa en B
Memoria de A
Memoria de B
Instrucción en
Ejecución de B
Lectura/Escritura
de Datos
Instrucción en Ejecución
de A ejecutada por el
Procesador de A en B
Figura 3.1: Ejecución de un procesador lógico frente a un procesador físico.
Máquinas Abstractas
17
En la parte superior de la figura se muestra cómo el procesador físico A va interpretando
las distintas instrucciones máquina. La interpretación de las instrucciones implica la
lectura y/o escritura de los datos.
En el caso de interpretar a nivel software el programa, el procesador es a su vez un
programa en otro procesador físico (procesador B en la Figura 3.1). Vemos cómo existe
una nueva capa de computación frente al ejemplo anterior. Esto hace que se requieran más
computaciones5 o cálculos que en el primer caso.
En el segundo caso el procesador A es más flexible que en el primero. La modificación,
eliminación o adición de una instrucción de dicho procesador, se consigue con la modificación
del programa emulador. Sin embargo, el mismo proceso en el primer ejemplo,
requiere la modificación del procesador a nivel físico.
3.2 Procesadores Lógicos y Máquinas Abstractas
Una máquina abstracta es el diseño de un procesador computacional sin intención
de que éste sea desarrollado de forma física [Howe99]. Apoyándose en dicho diseño del
procesador computacional, se especifica formalmente la semántica del juego de instrucciones
de dicha máquina en función de la modificación del estado de la máquina abstracta.
Un procesador computacional desarrollado físicamente es también una máquina
abstracta con una determinada implementación [Álvarez98]. Existen multitud de ejemplos
de emuladores de microprocesadores físicos desarrollados como programas sobre otro
procesador. Sin embargo, el nombre de máquina abstracta es empleado mayoritariamente
para aquellos procesadores que no tienen una implementación física.
Un intérprete es un programa que ejecuta las instrucciones de un lenguaje que encuentra
en una archivo fuente [Cueva98]. Su objetivo principal es animar la secuencia de
operaciones que un programador ha especificado, en función de la descripción semántica
de las instrucciones del lenguaje utilizado. Un procesador computacional –implementado
física o lógicamente– es un intérprete del lenguaje que procese.
Se define máquina virtual como un intérprete de una máquina abstracta6 [Howe99].
Una máquina virtual es por tanto un intérprete definido sobre una máquina abstracta. De
esta forma, la máquina abstracta es utilizada en la descripción semántica de las instrucciones
de dicho intérprete.
3.2.1 Máquina Abstracta de Estados
En teoría de lenguajes formales y autómatas, una máquina abstracta es un procedimiento
para ejecutar un conjunto de instrucciones de un lenguaje formal [Howe99]. Una
máquina abstracta define una estructura sobre la cuál es posible representar la semántica
computacional del lenguaje formal asociado. Esta acepción es similar a la anterior, pero está
más enfocada a estudios de computabilidad que a aplicaciones prácticas.
Ejemplos de máquinas abstractas son la máquina de Turing, autómatas lineales acotados,
autómatas de pila y autómatas finitos, que soportan la computación de lenguajes de
tipo 0, 1, 2 y 3 (lenguajes regulares) respectivamente [Cueva91].
5 Mayor número de computaciones no implica siempre mayor tiempo de ejecución. Esta propiedad se
cumpliría si la velocidad de procesamiento del computador B fuese igual a la del computador A.
6 Aunque el concepto de máquina abstracta y máquina virtual no son exactamente idénticos, son comúnmente
intercambiados.
CAPÍTULO 3
18
Las máquinas abstractas de estados (abstract state machines) proporcionan el nexo entre
los métodos formales de especificación y los modelos de computación [Huggins96].
Éstas amplían la tesis de Turing [Turing36] especificando máquinas más versátiles, capaces
de simular cualquier algoritmo en su nivel de abstracción natural (no a bajo nivel) y de forma
independiente al lenguaje de codificación utilizado. A partir de estos conceptos, los
miembros de la comunidad ASM (Abstract State Machine Community) han desarrollado una
metodología para describir máquinas abstractas de estados que modelen cualquier tipo de
algoritmo [Huggins99].
Esta línea de investigación, que utiliza el concepto de máquina abstracta para especificar
formalmente algoritmos computacionales en su nivel de abstracción natural, está
fuera del estudio realizado en esta memoria. Utilizaremos el concepto de máquina abstracta
para diseñar un sistema portable y flexible y no para profundizar en el campo de lenguajes
formales.
3.3 Utilización del Concepto de Máquina Abstracta
En este punto realizaremos un estudio de las distintas aplicaciones prácticas encontradas
al concepto de máquina abstracta –excluyendo la descrita en § 3.2.1. Especificaremos
una clasificación por funcionalidades y una descripción colectiva de lo conseguido con cada
utilización. En el siguiente capítulo (“Panorámica de Utilización de Máquinas Abstractas”)
analizaremos cada caso particular, destacando sus aportaciones y carencias en función de
los requisitos establecidos en el capítulo 2.
3.3.1 Procesadores de Lenguajes
En la implementación de compiladores ha utilizado el concepto de máquina abstracta
para simplificar su diseño [Cueva94]. El proceso de compilación toma un lenguaje de
alto nivel y genera un código intermedio. Este código intermedio es propio de una máquina
abstracta.
Se diseña una máquina abstracta lo más general posible, de forma que se pueda traducir
de ésta a cualquier máquina real existente. Para generar el código binario de una máquina
real, tan sólo hay que traducir el código de la máquina abstracta a la máquina física
elegida, independientemente del lenguaje de alto nivel que haya sido compilado previamente.
n*m Traducciones
Fortran Pascal ... Cobol
i80x86 Motorola
68000
Sun
sparc
...
n Lenguajes
m Plataformas
Compilación Directa a
las Plataformas Existentes
Código Fuente Origen
Código Binario de una Plataforma Específica
Figura 3.2: Compilación directa de n lenguajes a m plataformas.
Máquinas Abstractas
19
Fortran Pascal ... Cobol
i80x86 Motorola
68000
Sun
sparc
...
Código
Intermedio
Compilación a una Máquina Abstracta
Traducción de Máquina Abstracta a Máquina Real
n Lenguajes
m Plataformas
n+m Traducciones
Código Fuente Origen
Código Binario de una Plataforma Específica
Figura 3.3: Compilación de lenguajes pasando por la generación de código intermedio.
En la Figura 3.2 y en la Figura 3.3 se observa cómo el número de traducciones y
compilaciones se reduce cuando tenemos varios lenguajes fuente y varias máquinas destino
existentes7.
El proyecto UNCOL (Universal Computer Oriented Language) proponía un lenguaje intermedio
universal para el diseño de compiladores [Steel60]. El objetivo de este proyecto
era especificar una máquina abstracta universal para que los compiladores generasen código
intermedio a una plataforma abierta.
ANDF (Architecture Neutral Distribution Format) [Macrakis93] tuvo como objetivo un
híbrido entre la simplificación de compiladores y la portabilidad del código (punto siguiente).
Un compilador podría generar código para la especificación de la máquina ANDF siendo
este código portable a distintas plataformas.
Este código ANDF no era interpretado por un procesador software sino que era
traducido (o instalado) a código binario de una plataforma específica. De esta forma se
conseguía lo propuesto con UNCOL: la distribución de un código de una plataforma independiente.
Esta práctica también ha sido adoptada por varias compañías que desarrollan diversos
tipos de compiladores. Un ejemplo la utilización de esta práctica son los productos de
Borland/Inprise [Borland91]. Inicialmente esta compañía seleccionó un mismo “back end”
para todos sus compiladores. Se especifica un formato binario para una máquina compartida
por todas sus herramientas (archivos de extensión obj). Módulos de aplicaciones desarrolladas
en distintos lenguajes como C++ y Delphi pueden enlazarse para generar una
aplicación, siempre que hayan sido compiladas a una misma plataforma [Trados96].
El siguiente paso fue la especificación de una plataforma o máquina abstracta independiente
del sistema operativo que permita desarrollar aplicaciones en distintos lenguajes y
sistemas operativos. Este proyecto bautizado como “Proyecto Kylix” especifica un modelo
de componentes CLX (Component Library Cross-Platform) que permite desarrollar aplicaciones
en C++ Builder y Delphi para los sistemas operativos Win32 y Linux [Kozak2000].
7 En concreto, para n lenguajes y m plataformas, se reduce el número de traducciones para n y m mayores
que dos.
CAPÍTULO 3
20
3.3.1.1 Entornos de Programación Multilenguaje
Apoyándose de forma directa en los conceptos de máquinas abstractas y máquinas
virtuales, se han desarrollado entornos integrados de desarrollo de aplicaciones
multilenguaje.
POPLOG es un entorno de programación multilenguaje enfocado al desarrollo de
aplicaciones de inteligencia artificial [Smith92]. Utiliza compiladores incrementales de
Common Lisp, Pop-11, Prolog y Standard ML. La capacidad de interacción entre los lenguajes
reside en la traducción a una máquina abstracta de alto nivel (PVM, Poplog Virtual
Machine) y la independencia de la plataforma física utilizada se obtiene gracias a la compilación
a una máquina de bajo nivel (PIM, Poplog Implementation Machine) y su posterior conversión
a una plataforma física –como veíamos en la Figura 3.3.
3.3.2 Portabilidad del Código
Aunque todos los procesadores hardware son realmente implementaciones físicas de
una máquina abstracta, es común utilizar el concepto de máquina abstracta para designar la
especificación de una plataforma cuyo objetivo final no es su implementación en silicio.
Probablemente la característica más explotada en la utilización de máquinas abstractas
proviene de su ausencia de implementación física. El hecho de que sea un procesador
software el que interpreta las instrucciones de la máquina, da lugar a una independencia de la
plataforma física real utilizada. Una vez codificado un programa para una máquina abstracta,
su ejecución podrá realizarse en cualquier plataforma8 que posea un procesador lógico
capaz de interpretar sus instrucciones, como se muestra en la siguiente figura:
Programa codificado
en Máquina Abstracta
Procesador
Máquina Abstracta
en Windows NT
Procesador
Máquina Abstracta
en Solaris
Procesador
Máquina Abstracta
en System8
Figura 3.4: Ejecución de un programa portable sobre varias plataformas.
Al igual que un compilador de un lenguaje de alto nivel genera código para una máquina
específica, la compilación a una máquina abstracta hace que ese programa generado
sea independiente de la plataforma que lo ejecute. Dicho programa podrá ser ejecutado por
cualquier procesador –ya sea hardware o software– que implemente la especificación computacional
de un procesador de la máquina abstracta.
8 Entendiendo por plataforma la combinación de microprocesador y sistema operativo.
Máquinas Abstractas
21
Cada procesador computacional de la máquina abstracta podrá estar implementado
acorde a las necesidades y características del sistema real. En la Figura 3.5 se identifican
distintos grados de implementación del procesador de la máquina. En el primer ejemplo se
implementa la máquina físicamente obteniendo velocidad de ejecución del programa al
tener un único nivel de interpretación.
Si se implementa un procesador lógico sobre una plataforma distinta, obtenemos la
portabilidad mencionada en este punto perdiendo velocidad de ejecución frente al caso
anterior. Cada plataforma física que emule la máquina abstracta, implementará de forma
distinta este procesador en función de sus recursos. En el ejemplo mostrado en la Figura
3.4, el procesador implementado sobre el PC con Windows NT podrá haber sido desarrollado
de forma distinta a la implementación sobre el Macintosh con System8.
En la Figura 3.5 se muestra otro ejemplo en el que el emulador de la máquina abstracta
es desarrollado sobre otro procesador lógico. Seguimos teniendo portabilidad del
código y obtenemos una flexibilidad en el propio emulador de la máquina abstracta. Se
puede ver este caso como un procesador (de A sobre B) de un procesador de la máquina
abstracta (sobre A). Volvemos a ganar en flexibilidad a costa de aumentar en número de
computaciones necesarias en la ejecución de un programa de la máquina abstracta9.
Procesador
Software
A de la
Máquina
Abstracta
Procesador A
Programa
en la
Máquina
Abstracta
Procesador
Software B
del Procesador A
Procesador Hardware B
Programa
en la Máquina
Abstracta
Procesador
Software A
de la
Máquina
Abstracta
Procesador Hardware
de la Máquina Abstracta
Programa
en la
Máquina
Abstracta
Memoria
Procesador Hw
Memoria
Procesador A
Memoria
Procesador B
Ejecución
Ejecución
Ejecución
Figura 3.5: Distintos niveles de implementación de un procesador.
Existen multitud de casos prácticos que utilizan el concepto de máquina abstracta
para conseguir programas portables a distintas plataformas. En el capítulo 4 estudiaremos
un conjunto de éstos.
3.3.3 Sistemas Interactivos con Abstracciones Orientadas a Objetos
Con la utilización de los lenguajes orientados a objetos en la década de los 80, el nivel
de abstracción en la programación de aplicaciones se elevó considerablemente
[Booch94]. Sin embargo, los microprocesadores existentes se seguían basando en la ejecución
de código no estructurado y los intentos de desarrollarlos con este nuevo paradigma
finalizaban en fracaso por la falta de eficiencia obtenida causada por la complejidad de la
implementación [Colwel88]. Posteriormente se llevaron a cabo estudios que concluyen que,
haciendo uso de optimizaciones de compilación e interpretación se obtienen buenos ren-
9 Esta estructura de procesamiento de procesamiento se utilizará posteriormente en el capítulo 11.
CAPÍTULO 3
22
dimientos y, por tanto, no es rentable el desarrollo de plataformas físicas orientadas a objetos
[Hölzle95].
Para desarrollar entornos de programación y sistemas que aportasen directamente la
abstracción de orientación a objetos, y que fuesen totalmente interactivos, se utilizó el concepto
de máquina abstracta –orientada a objetos. Los sistemas, al igual los lenguajes de
programación, ofrecían la posibilidad de crear y manipular una serie de objetos. Estos objetos
residían en la memoria de una máquina abstracta. Las diferencias de trabajar con una
máquina abstracta en lugar de compilar a la plataforma nativa son las propias de la dualidad
compilador/intérprete:
Compilador Intérprete
Tiempo de Ejecución - +
Tiempo de Compilación + -
Portabilidad de Código - +
Interacción entre Aplicaciones - +
Flexibilidad - +
Como vimos en los dos puntos anteriores, el utilizar un procesador lógico genera
un mayor número de computaciones en su interpretación perdiendo tiempo de ejecución y
ganando en portabilidad código. Si la plataforma que interpretamos es de un nivel más cercano
al lenguaje (es orientada a objetos), los tiempos de compilación se reducirán al no
producirse cambio de paradigma.
Como se muestra en Figura 3.6, el procesamiento lógico de los programas hace que
todos ellos compartan una zona de memoria asociada a un proceso –el intérprete. La interacción
entre aplicaciones es homogénea por tanto más sencilla de implementar que en el
caso de que se cree un proceso distinto para la ejecución de cada aplicación.
Sistema Operativo
Programa 1 Programa 2 Programa 3
Memoria
Proceso
Ejecución 1
Proceso
Ejecución 2
Proceso
Ejecución 3
Ejecuta
Sistema Operativo
Programa 1 Programa 2 Programa 3
Memoria
Proceso
Ejecución
Procesador
Máquina
Programa 2
Programa 3
Programa 1
Procesador
Máquina
Ejecuta
Interpreta
Representación
en memoria
Representaciones
en memoria
Ejecución Programas Nativos Ejecución Programas Interpretados
Figura 3.6: Diferencia entre la ejecución de programas nativos frente interpretados.
Máquinas Abstractas
23
El hecho de que las aplicaciones puedan interactuar fácilmente y que se pueda acceder
y modificar los distintos objetos existentes, aumenta la flexibilidad global del sistema
pudiendo representar sus funcionalidades mediante objetos y métodos modificables.
Los sistemas desarrollados sobre una máquina abstracta orientada a objetos facilitan
al usuario:
Utilización del sistema con una abstracción más natural a la forma de pensar del
ser humano [Booch94].
Autodocumentación del sistema y de las aplicaciones. El acceso a cualquier objeto
permite conocer la estructura y comportamiento de éste en cualquier momento.
Programación interactiva y continua. Una vez que el usuario entra en el sistema,
accede a un mundo interactivo de objetos [Smith95]. Puede hacer uso de cualquier
objeto existente. Si necesita desarrollar una nueva funcionalidad va creando
nuevos objetos, definiendo su estructura y comportamiento, comprobando
su correcta funcionalidad y utilizando cualquier otro objeto existente de una
forma totalmente interactiva. A partir de ese momento el sistema posee una
nueva funcionalidad y un mayor número de objetos.
Dentro de este tipo de sistemas se pueden nombrar a los clásicos Smalltalk [Mevel87]
y Self [Ungar87] que serán tratados con mayor profundidad en el punto § 4.3.1.
3.3.4 Distribución e Interoperabilidad de Aplicaciones
Esta ventaja en la utilización de una plataforma abstracta es una ampliación de la
característica de portabilidad de código comentada previamente en § 3.3.2. El hecho de
tener una aplicación codificada sobre una máquina abstracta implica que ésta podrá ejecutarse
en cualquier plataforma que implemente esta máquina virtual. Además de esta portabilidad
de código, la independencia de la plataforma física puede ofrecer dos ventajas adicionales:
Una aplicación (su código) podrá ser distribuida a lo largo de una red de computadores.
Los distintos módulos codificados para la máquina abstracta pueden
descargarse y ejecutarse en cualquier plataforma física que implemente la máquina
virtual.
Las aplicaciones pueden interoperar entre sí (con envío y recepción de datos) de
forma independiente a la plataforma física sobre la que estén ejecutándose. La
representación de la información nativa de la máquina abstracta será interpretada
por la máquina virtual de cada plataforma.
Aunque estos beneficios en la utilización de una máquina abstracta ya estaban presentes
en Smalltalk [Goldberg83] y Self [Smith95], su mayor auge tuvo lugar con la aparición
de la plataforma virtual de Java [Kramer96] –que estudiaremos en mayor profundidad
en el § 4.3.1. Esta plataforma impulsó el desarrollo de aplicaciones distribuidas, especialmente
a través de Internet.
CAPÍTULO 3
24
3.3.4.1 Distribución de Aplicaciones
Implementación
en A de la
Máquina
Abstracta
Plataforma A
Implementación física de la
Máquina Abstracta
Ejecución
Aplicación
Ejecución
Aplicación
Implementación
en B de la
Máquina
Abstracta
Plataforma B
Ejecución
Aplicación
Servidor de la Aplicación
Red de Ordenadores
Aplicación
sobre
Máquina
Abstracta
Distribución de una
aplicación portable
Figura 3.7: Distribución de aplicaciones portables.
En la Figura 3.7 se muestra un escenario de distribución de una aplicación desarrollada
sobre una máquina abstracta. Un ordenador servidor de la aplicación, conectado mediante
una red de ordenadores a un conjunto de clientes, posee el código del programa a
distribuir. Mediante un determinado protocolo de comunicaciones, cada cliente demanda la
aplicación del servidor, la obtiene en su ubicación y la ejecuta gracias su implementación de
la máquina virtual. Esta ejecución será computacionalmente similar en todos los clientes,
con el aspecto propio de la plataforma física en la que es interpretada –como mostrábamos
en la Figura 3.4 de la página 20.
Un caso de uso típico del escenario mostrado es una descarga applets en Internet. El
servidor de aplicaciones es un servidor Web. El código de la aplicación es código Java restringido
denominado applet [Kramer96]. El cliente, mediante su navegador Web, se conecta
al servidor utilizando el protocolo HTTP [Beners96] o HTTPS [Freier96] y descarga el
código Java en su navegador. La aplicación es interpretada por la máquina virtual de Java
[Sun95] implementada en el navegador de forma independiente a la plataforma y navegador
que utilizados por el cliente.
3.3.4.2 Interoperabilidad de Aplicaciones
Uno de los mayores problemas en la intercomunicación de aplicaciones distribuidas
físicamente es la representación de la información enviada. Si desarrollamos
dos aplicaciones nativas sobre dos plataformas distintas y hacemos que intercambien
información, deberemos establecer previamente la representación de la información
utilizada. Por ejemplo, una variable entera en el lenguaje de programación C [Ritchie78]
puede tener una longitud y representación binaria distinta en cada una de las
plataformas.
Máquinas Abstractas
25
A la complejidad de definir la representación de la información y traducir ésta
a su representación nativa, se le une la tarea de definir el protocolo de comunicación:
el modo en el que las aplicaciones deben dialogar para intercambiar dicha información.
Existen especificaciones estándar definidas para interconectar aplicaciones
nativas sobre distintas plataformas. Un ejemplo es el complejo protocolo GIOP (General
Inter-ORB Protocol) [OMG95] definido en CORBA [Baker97]. Establece el protocolo
y representación de información necesarios para interconectar cualquier aplicación
nativa a través de la arquitectura de objetos distribuidos CORBA. Este tipo de
middleware proporciona un elevado nivel de abstracción, facilitando el desarrollo de
aplicaciones distribuidas, pero conlleva una serie de inconvenientes:
Requiere una elevada cantidad de código adicional para implementar el protocolo
y la traducción de la información enviada por la red. Este código recibe el
nombre de ORB (Object Request Broker).
Aumenta el volumen de información enviada a través de la red de ordenadores
al implementar un protocolo de propósito general que proporcione un mayor
nivel de abstracción.
Si las aplicaciones se desarrollan sobre una misma máquina abstracta, el envío de la
información se puede realizar directamente en el formato nativo de ésta puesto que existe
una máquina virtual en toda plataforma. La traducción de la información de la máquina a la
plataforma física la lleva a cabo en el intérprete de la máquina virtual. El resultado es poder
interconectar aplicaciones codificadas en una máquina abstracta y ejecutadas en plataforma,
y por lo tanto dispositivos, totalmente dispares.
Máquina
Abstracta
Aplicación A
Máquina
Abstracta
Aplicación A
Máquina
Abstracta
Aplicación A
Máquina
Abstracta
Aplicación A
Lectura y escritura
de información nativa
de la máquina abstracta
Figura 3.8: Interoperabilidad nativa de aplicaciones sobre distintas plataformas.
En el lenguaje de programación Java [Gosling96], podemos enviar los datos en su
representación nativa. No estamos restringidos al envío y recepción de tipos simples de
datos sino que es posible también enviar objetos, convirtiéndolos a una secuencia de bytes
(serialization) [Campione99].
CAPÍTULO 3
26
Mediante el paquete de clases ofrecidas en java.net, es posible implementar un
protocolo propio de un tipo de aplicaciones sobre TCP/IP o UDP/IP [Raman98]. Si deseamos
obtener un mayor nivel de abstracción para el desarrollo de aplicaciones distribuidas,
al igual que teníamos con CORBA, podemos utilizar RMI (Remote Method Invocation) sin
sobrecargar tanto la red de comunicaciones [Sun97e]. RMI desarrolla un protocolo de interconexión
de aplicaciones Java –JRMP, Java Remote Method Protocol– que permite, entre
otras cosas, invocar a métodos de objetos ubicados en otras máquinas virtuales.
La potencia de las aplicaciones distribuidas desarrolladas sobre la plataforma Java
cobra significado cuando unimos las dos características comentadas en este punto: aplicaciones
cuyo código es distribuido a través de la red, capaces de interoperar entre sí sin llevar
a cabo una conversión de datos, de forma independiente a la plataforma física en la que
se estén ejecutando.
3.3.5 Diseño y Coexistencia de Sistemas Operativos
La utilización del concepto de máquina abstracta ha estado presente también en el
desarrollo de sistemas operativos. Podemos clasificar su utilización en función del objetivo
buscado, de la siguiente forma:
Desarrollo de sistemas operativos distribuidos y multiplataforma.
Ejecución de aplicaciones desarrolladas sobre cualquier sistema operativo.
Existen sistemas operativos, como el VM/ESA de IBM [IBM2000], que utilizan la
conjunción de ambas funcionalidades en la utilización de una máquina abstracta.
3.3.5.1 Diseño de Sistemas Operativos Distribuidos y Multiplataforma
Estos sistemas operativos aprovechan todas las ventajas de la utilización de máquinas
abstractas comentadas en los puntos anteriores, para desarrollar un sistema operativo
distribuido y multiplataforma. Sobre la descripción de una máquina abstracta, se implementa
un intérprete de la máquina virtual en toda aquella plataforma en la que vaya a desarrollarse
el sistema operativo. En el código de la máquina, se desarrollan servicios propios del
sistema operativo que permitan interactuar con el sistema y elevar el nivel de abstracción; lo
conseguido finalmente es:
Una aplicación para este sistema operativo es portable a cualquier plataforma.
Las aplicaciones no se limitan a utilizar los servicios de la máquina, sino que
podrán codificarse en un mayor nivel de abstracción: el ofrecido por los servicios
sistema operativo.
El propio sistema operativo es portable, puesto que ha sido desarrollado sobre
la máquina abstracta. No es necesario pues, codificar cada servicio para cada
plataforma.
La interoperabilidad de las aplicaciones es uniforme, al estar utilizando únicamente
el modelo de computación de la máquina abstracta. En otros sistemas
operativos es necesario especificar la interfaz exacta de acceso a sus servicios.
Las aplicaciones desarrolladas sobre este sistema operativo pueden ser distribuidas
físicamente por el sistema operativo, ya que todas serán ejecutadas por
un intérprete de la misma máquina abstracta.
Máquinas Abstractas
27
En la comunicación de aplicaciones ejecutándose en computadores distribuidos
físicamente, no es necesario establecer traducciones de datos. La interacción es
directa, al ejecutarse todas las aplicaciones sobre la misma máquina abstracta –
en distintas máquinas virtuales o intérpretes.
Existen distintos sistemas operativos desarrollados sobre una máquina abstracta ya
sean comerciales, de investigación o didácticos. En el capítulo 4 analizaremos los sistemas
existentes y lo que pueden aportar a esta tesis.
3.3.5.2 Coexistencia de Sistemas Operativos
En este apartado veremos la utilización del concepto de máquina virtual desde un
punto de vista distinto. Este concepto puede definirse como un acceso uniforme a los recursos
de una plataforma física (de una máquina real). Es una interfaz de interacción con
una plataforma física que puede ser dividida en un conjunto de máquinas virtuales.
La partición de los recursos físicos de una plataforma, mediante un acceso independiente,
permite la ejecución de distintos sistemas operativos inmersos en el operativo
que se encuentre en ejecución. Dentro de un sistema operativo, se desarrollan tantas máquinas
virtuales como sistemas inmersos deseemos tener. La ejecución de una aplicación
desarrollada para un sistema operativo distinto al activo, se producirá sobre la máquina
virtual implementada para el sistema operativo “huésped”. Esta ejecución utilizará los recursos
asignados a su máquina virtual.
La ventaja obtenida se resume en la posibilidad de ejecutar aplicaciones desarrolladas
sobre cualquier sistema operativo sin tener que reiniciar el sistema, es decir, sin necesidad
de cambiar el sistema operativo existente en memoria.
El principal inconveniente frente a los sistemas descritos en el punto anterior,
“Diseño de Sistemas Operativos Distribuidos y Multiplataforma”, es la carencia de interacción
entre las aplicaciones ejecutadas sobre distintos sistemas operativos: no es posible
comunicar las aplicaciones “huésped” entre sí, ni con las aplicaciones del propio operativo.
Por este motivo, desecharemos la utilización de máquinas virtuales en este sentido.
El primer sistema operativo que utilizó de esta forma una máquina virtual fue el
producto OS/2 de IBM. Se declaró como el sucesor de IBM del sistema operativo DOS,
desarrollado para microprocesadores intel 80286. Este sistema era capaz de ejecutar en un
microprocesador intel 80386 varias aplicaciones MS-DOS, Windows y propia aplicaciones
gráficas nativas, haciendo uso de la implementación de distintas máquinas virtuales.
IBM abandonó el proyecto iniciado con su sistema operativo OS/2. Sin embargo,
parte de sus características fueron adoptadas en el desarrollo de su operativo VM/ESA
desarrollado para servidores IBM S/390 [IBM2000]. Este sistema permite ejecutar aplicaciones
desarrolladas para otros sistemas operativos, utilizando una máquina virtual como
modo de acceso a los recursos físicos. El empleo de una máquina virtual es aprovechado
además para la portabilidad e interoperabilidad del código: cualquier grupo de aplicaciones
desarrolladas sobre esta máquina abstracta puede interoperar entre sí, de forma independiente
al tipo de servidor sobre el que se estén ejecutando.
Otro producto que utiliza el concepto de máquina virtual para multiplexar el acceso
a una plataforma física es VMware Virtual Platform [Jones99]. Ejecutándose en Windows
NT o en Linux permite lanzar aplicaciones codificadas para Windows 3.1, MS-DOS, Windows
NT, Linux, FreeBDS y Solaris 7 para intel.
CAPÍTULO 3
28
3.4 Aportación de la Utilización de Máquinas Abstractas
A lo largo de este capítulo hemos estudiado concepto de máquina abstracta y las
ventajas que aporta la utilización de las mismas. De todas ellas podemos indicar que las
aportaciones a nuestro sistema buscado, descrito en el capítulo 1, son:
1. Independencia del lenguaje de programación (§ 3.1.1).
2. Portabilidad de su código (§ 3.3.2).
3. Independencia de la plataforma (§ 3.3.2).
4. Elección del nivel de abstracción base para el sistema global (§ 3.3.3).
5. Interacción única entre aplicaciones, utilizando un único modelo de computación
(§ 3.3.3).
6. Programación interactiva y continua (§ 3.3.3).
7. Distribución de aplicaciones (§ 3.3.4.1).
8. Interoperabilidad nativa de aplicaciones distribuidas (§ 3.3.4.2).
9. Desarrollo de servicios operativos mediante código único, portable y distribuible
(§ 3.3.5.1).
Los sistemas existentes, que haciendo uso de máquinas abstractas consigan estos
beneficios, serán estudiados en el próximo capítulo.
29
CCAAPPÍÍTTUULLO 44:
PANORÁMICA DE UTILIZACIÓN DE MÁQUINAS
ABSTRACTAS
En el capítulo anterior introdujimos los conceptos de máquina abstracta y máquina
virtual así como las distintas posibilidades prácticas que podían aportar a un sistema informático.
Del abanico global de ventajas que ofrece su utilización, subrayamos un conjunto
de ellas como adoptables al sistema propuesto en el capítulo 1.
En función de una clasificación práctica de sistemas existentes, estudiaremos las
distintas implementaciones realizadas, siguiendo los requisitos establecidos en el capítulo 2,
destacando los requisitos logrados y las carencias existentes.
Una vez descritos y analizados los distintos sistemas, identificaremos qué requisitos
han sido cumplidos por los casos prácticos existentes y cómo adaptarlos a nuestros objetivos,
así como sus insuficiencias y las necesidades surgidas para superarlas.
4.1 Portabilidad de Código
Como comentábamos en § 3.3.2, una de las ventajas más explotada en la utilización
de máquinas abstractas es la portabilidad de su código. La ausencia de necesidad de una
implementación física de su procesador computacional implica que el código desarrollado
para esta plataforma puede ser ejecutado en cualquier sistema que implemente dicho procesador.
A continuación estudiaremos un conjunto de casos prácticos en los que se utilizaron
máquinas abstractas para obtener, principalmente, portabilidad de un código generado.
4.1.1 Estudio de Sistemas Existentes
Máquina-p
El código-p era el lenguaje intermedio propio de la máquina abstracta máquina-p
[Nori76] utilizada inicialmente en el desarrollo de un compilador del lenguaje Pascal [Jensen91].
La Universidad de California en San Diego (UCSD) desarrolló un procesador que
ejecutaba código binario de la máquina-p (código-p). Adoptó la especificación de la máquiCAPÍTULO
4
30
na abstracta para desarrollar así un proyecto de Pascal portable. Se llegó a disponer de soporte
para multitarea y se desarrolló el p-System: un sistema operativo portable, codificado
en Pascal y traducido a código-p [Campbell83]. La única parte del sistema que había que
implementar en una plataforma concreta era el emulador de la máquina-p. El sistema se
llegó a implantar en diversos microprocesadores como DEC LSI-11, Zilog Z80, Motorola
68000 e intel 8088 [Irvine99].
Al igual que los lenguajes C y Algol, el Pascal es un lenguaje orientado a bloques
(orientado a marcos de pila10, desde el punto de vista de implementación) [Jensen91] y esto
hizo que el criterio fundamental en la especificación de la máquina-p fuese orientarla a una
estructura de pila.
El p-System implementado tenia la siguiente distribución de memoria, desde las direcciones
superiores a las inferiores:
El código (p-código) propio del sistema operativo (p-System).
La pila del sistema (creciendo en sentido descendente).
La memoria heap (creciendo en sentido ascendente).
El conjunto de las pilas propias de hilos según se iban demandando en tiempo
de ejecución.
Los segmentos globales de datos (de constantes y variables).
El intérprete o simulador de la máquina abstracta.
La máquina abstracta llegó a tener un procesador hardware. Western Digital implementó
en 1980 la máquina-p en el WD9000 Pascal Microengine. Éste estaba basado en el
microprocesador programable WD MCP-1600.
OCODE
OCODE [Richards71] fue el nombre asignado al lenguaje ensamblador de una máquina
abstracta diseñada como una máquina de pila. Se utilizaba como código intermedio
de un compilador de BCPL [Richards79], obteniendo portabilidad del código generado
entre distintos sistemas.
BCPL (Basic CPL) es un lenguaje de sistemas desarrollado en 1969. Es un descendiente
del lenguaje CPL (Combined Programming Language); se programa en un bajo nivel de
abstracción, carece de tipos, está orientado a bloques y proporciona vectores de una dimensión
y punteros. Es un lenguaje estructurado y posee procedimientos con paso por
valor.
La ejecución de aplicaciones permite comunicarse mediante memoria compartida,
donde se almacenan variables del sistema y de usuario. BCPL fue utilizado para desarrollar
el sistema operativo TRIPOS, posteriormente renombrado a AmigaDOS.
Portable Scheme Interpreter
Es una implementación de un compilador del lenguaje Scheme [Abelson2000] una
máquina virtual PSI (Portable Scheme Interpreter). La compilación del código Scheme a la máquina
virtual permite ejecutar la aplicación en cualquier sistema que posea este intérprete.
10 Stack Frame Oriented: Por cada bloque se apila un marco o contexto propio de la ejecución de ese
bloque.
Panorámica de Utilización de Máquinas Abstractas
31
El sistema permite añadir primitivas, depurar una aplicación sobre la máquina virtual
(mediante el Portable Scheme Debugger) y la interpretación del código se considera “aceptable”
en términos de eficiencia.
Forth
Otro ejemplo de especificación de una máquina abstracta para conseguir portabilidad
de código es la máquina virtual de Forth [Brodie87]. Este lenguaje fue desarrollado en
la década de los 70 por Charles Moore para el control de telescopios. Es un lenguaje sencillo,
rápido y ampliable que es interpretado en una máquina virtual, consiguiendo ser portable
a distintas plataformas y útil para empotrarlo en sistemas.
El simulador de la máquina virtual de Forth posee dos pilas. La primera es la pila de
datos: los parámetros de una operación son tomados del tope de la pila y el resultado es
posteriormente apilado. La segunda pila es la pila de valores de retorno: se apilan los valores
del contador de programa antes de una invocación a una subrutina, para poder retornar
al punto de ejecución original una vez finalizada ésta.
Sequential Parlog Machine
En este caso, además de la portabilidad del código, el concepto de máquina abstracta
se utilizó para desarrollar un lenguaje multitarea. SPM (Sequential Parlog Machine) [Gregory87]
es una máquina virtual del lenguaje de programación lógica Parlog [Clark83].
En § 3.3.3, estudiábamos cómo la interpretación del lenguaje para una plataforma
virtual, facilita la intercomunicación de las aplicaciones en ejecución. En este caso, la máquina
abstracta SPM permite implementar el lenguaje Parlog definido como “Parallel-Prolog”
[Clark83].
Code War
La utilización de una máquina abstracta para que su código sea portable a distintas
plataformas, cobra, en este caso, un carácter de originalidad respecto a los sistemas anteriores:
La portabilidad del código es utilizada para crear programas que “luchen” entre sí, tratándose
de eliminarse los unos a los otros.
Code War es un juego entre dos o más programas –no usuarios o jugadores– desarrollados
en un lenguaje ensamblador denominado Redcode: código nativo de una máquina
abstracta denominada MARS (Memory Array Redcode Simulator) [Dewdney88]. El objetivo
del juego es desarrollar un programa que sea capaz de eliminar todos los procesos de los
programas contrarios que estuvieren ejecutándose en la máquina virtual, quedando tan solo
él en la memoria de la máquina.
Gracias a la utilización de una máquina virtual es posible desarrollar programas y
jugar en Code War en multitud de plataformas: UNIX, IBM PC compatible, Macintosh y
Amiga. Para tener una plataforma estándar de ejecución, se creo ICWS (International Core
War Society), responsable de la creación y mantenimiento del estándar de la plataforma de
Code War, así de cómo la organización de campeonatos –siendo “King of the Hill” uno de
los más conocidos, accesible mediante Internet.
CAPÍTULO 4
32
MARS
sobre A
Programa 1
Plataforma A
MARS
sobre C
Programa 3
Plataforma C
MARS
sobre B
Programa 2
Plataforma B
MARS
Programa 1 Programa 2 Programa 3
Batalla disputada entre los distintos programas
Codificación y prueba
de ejecución de distintos
programas en distintas
plataformas
Plataforma Ejecución Juego
Figura 4.1: Desarrollo y ejecución de programas para Code War.
El sistema en el que los programas son ejecutados es realmente simple. El núcleo
del sistema es su memoria: un vector de instrucciones inicialmente vació. El código de cada
programa es interpretado de forma circular, de manera que cuando finaliza su última instrucción,
se vuelve a ejecutar la primera.
La máquina virtual de MARS ejecuta una instrucción de cada programa en cada
turno. Una vez evaluada la instrucción de un programa, toma otro código y ejecuta una
instrucción de éste. La interpretación de cada instrucción lleva siempre el mismo tiempo,
un ciclo de la máquina virtual, sea cual fuere su semántica. De esta forma, el tiempo de
procesamiento es distribuido equitativamente a lo largo de todos los programas que estuvieren
en memoria [Dewdney90].
Cada programa podrá tener un conjunto de procesos en ejecución. Estos procesos
son almacenados por la máquina virtual en una pila de tareas. Cuando se produce el turno
de ejecución de un programa, un proceso de éste será desapilado, y su siguiente instrucción
será ejecutada. Los procesos que no sean destruidos durante la evaluación de su instrucción
serán introducidos nuevamente en la pila de tareas.
4.1.2 Aportaciones y Carencias de los Sistemas Estudiados
Todos los sistemas estudiados en § 4.1.1 están basados principalmente en la portabilidad
del código escrito para una plataforma virtual: requisito § 2.1.1. En la mayoría de los
casos, los distintos lenguajes de programación son compilados a una plataforma intermedia,
y ésta es interpretada en distintas plataformas, dando lugar a la portabilidad del código generado.
En el caso de Code War, esta característica supone la posibilidad de jugar con un
programa desarrollado y probado en cualquier entorno.
En la totalidad de los sistemas estudiados, se produce una dependencia del lenguaje
de programación que se desea que sea portable. La máquina abstracta se diseña en función
de un lenguaje de programación de alto nivel, restringiendo así la traducción a ella desde
otros lenguajes de programación (requisito § 2.1.2).
Panorámica de Utilización de Máquinas Abstractas
33
Las plataformas son diseñadas para resolver únicamente el problema de hacer que
el código de un lenguaje sea portable. En el caso de Code War, la plataforma es desarrollada
para permitir establecer batallas entre programas. Esto rompe con el requisito § 2.1.3,
que impone el diseño de una plataforma de forma independiente a la resolución de un único
problema.
El resto de requisitos propios de la plataforma buscada, identificados en § 2.1, no
han sido tenidos en cuenta en los sistemas estudiados.
4.2 Interoperabilidad de Aplicaciones
En este punto estudiaremos distintos casos prácticos en los que se establecen especificaciones
computacionales, para permitir que distintas aplicaciones puedan interoperar
entre sí. Si bien el concepto de máquina abstracta no es utilizado como una plataforma
completa de computación, veremos cómo puede describirse como un pequeño middleware
para interconectar distintas aplicaciones.
4.2.1 Estudio de Sistemas Existentes
Parallel Virtual Machine
Parallel Virtual Machine (PVM) es un sistema software, que permite la interacción
de un conjunto heterogéneo de computadores UNIX, interconectados entre sí, pudiéndose
abstraer el conjunto como un programa en una máquina multiprocesador [Geist94].
PVM fue diseñado para interconectar los recursos de distintos computadores, proporcionando
al usuario una plataforma multiprocesador, capaz de ejecutar sus aplicaciones
de forma independiente al número y ubicación de ordenadores utilizados.
El proyecto PVM comenzó en el verano de 1989, en el “Oak Ridge National Laboratory”,
donde se construyó el primer prototipo; éste fue interno al laboratorio y no se distribuyó
públicamente. La segunda versión se implementó en la Universidad de Tennessee,
finalizándose en marzo de 1991. A partir de esta versión, PVM comenzó a utilizarse en
multitud de aplicaciones científicas. La versión 3 se desarrolló en febrero de 1993 y fue
distribuida pública y gratuitamente por Internet.
PVM es un conjunto integrado de herramientas y librerías software, que emulan un
entorno de programación de propósito general, flexible, homogéneo y concurrente, desarrollado
sobre un conjunto heterogéneo de computadores interconectados. Los principios
sobre los que se ha desarrollado, son:
Acceso a múltiples computadores: Las tareas computacionales de una aplicación
son ejecutadas en un conjunto de ordenadores. Este grupo de computadores es
seleccionado por el usuario, de forma previa a la ejecución de un programa.
Independencia del hardware: Los programas serán ejecutados sobre un entorno
de procesamiento virtual. Si el usuario desea ejecutar una computación sobre
una plataforma concreta, podrá asociar ésta a una máquina en particular.
Independencia del lenguaje: Los servicios de PVM han sido desarrollados como
librerías estáticas para diversos lenguajes como C [Ritchie78], C++ [Stroustrup98]
o Fortran [Koelbel94].
CAPÍTULO 4
34
Computación basada en tareas: La unidad de paralelismo en PVM es una tarea:
un hilo de ejecución secuencial e independiente. No existe una traducción directa
de tarea a procesador: un procesador puede ejecutar diversas tareas.
Modelo de paso de mensajes explícitos: Las distintas tareas en ejecución realizan
una parte del trabajo global, y todas ellas se comunican entre sí mediante el
envío explícito de mensajes.
Entorno de computación heterogéneo: PVM soporta heterogeneidad respecto
al hardware, redes de comunicación y aplicaciones.
Soporte multiprocesador: Las plataformas multiprocesador puede desarrollar su
propia implementación de la interfaz PVM, obteniendo mayor eficiencia de un
modo estándar.
La arquitectura de PVM se muestra en la siguiente figura:
Hardware A
SSOO U UNNIXIX
Hardware B
SSOO U UNNIXIX
Librería
Estática
PVM
Fragmento A
Aplicación
PVM
Interfaz
Estándar
Librería
Estática
PVM
Fragmento B
Aplicación
PVM
Interfaz
Estándar
Ordenador A ppvvmmdd ppvvmmdd Ordenador B
Parallel Virtual Machine
Figura 4.2: Arquitectura de PVM.
Cada plataforma UNIX que desee utilizar PVM deberá ejecutar el proceso demonio
(daemon) de la máquina virtual. Eligiendo un lenguaje de programación, el acceso al sistema
heterogéneo y distribuido se realiza mediante una interfaz de invocación a una librería estática.
Esta librería ofrece un conjunto de rutinas, necesarias para la intercomunicación de las
distintas tareas de una aplicación. La implementación de dichas rutinas se apoya en el proceso
residente que intercomunica las distintas máquinas virtuales: pvmd (Parallel Virtual
Machine Daemon).
Coherent Virtual Machine
Coherent Virtual Machine (CVM) es una librería de programación en C que permite
al usuario acceder a un sistema de memoria compartida, para desarrollar aplicaciones distribuidas
entre distintos procesadores [Keleher96]. La librería se ha desarrollado para sistemas
UNIX y está enfocada principalmente para la intercomunicación de workstations.
CVM se ha implementado en C++ y consiste en un conjunto de clases que establecen
una interfaz básica e invariable de funcionamiento; definen un protocolo de compartición
de memoria flexible, un sistema sencillo de gestión de hilos y una comunicación entre
aplicaciones distribuidas, desarrollado sobre UDP [Raman98].
Panorámica de Utilización de Máquinas Abstractas
35
Es posible implementar cualquier protocolo de sincronización de memoria compartida,
derivando la implementación de las clases Page y Protocol. Todo funcionamiento
que deseemos modificar respecto al comportamiento base, se especificará en los métodos
derivados derogando su comportamiento [Booch94].
PerDiS
PerDis es un middleware que ofrece un entorno de programación de aplicaciones
orientadas a objetos, distribuidas, persistentes y transaccionales, desarrolladas de una forma
transparente, escalable y eficiente [Kloosterman98].
La creación de la plataforma PerDiS surge por la necesidad de desarrollar aplicaciones
cooperativas de ingeniería en “empresas virtuales”: un conjunto de compañías o departamentos
que trabajan conjuntamente en la implementación de un proyecto. Las distintas
partes de la empresa virtual cooperan y coordinan su trabajo mediante la compartición de
datos a través de su red de ordenadores.
PerDiS ofrece un entorno distribuido de persistencia, combinando características de
modelos de memoria compartida, sistemas de archivos distribuidos y bases de datos orientadas
a objetos. PerDiS permite, al igual que en los entornos de programación de memoria
compartida [Li89], desarrollar aplicaciones que accedan a objetos en memoria indistintamente
de su ubicación. Los objetos son enviados a la memoria local de un modo transparente
cuando son requeridos, haciendo que la programación distribuida sea menos explícita
en cuanto a la ubicación de los objetos.
Al igual que un sistema de archivos distribuido, PerDiS almacena en el disco bloques
de datos denominados clusters, almacenando en el cliente una caché de los mismos. Un
cluster es un grupo de objetos que define una unidad de almacenamiento, nombrado, protección
y compartición, del mismo modo que sucede en los sistemas de archivos [Sun89].
PerDiS facilita además un conjunto básico de funcionalidades propias de bases de
datos orientadas a objetos [Bancilhon92] como el almacenamiento nativo y manipulación
de objetos desde lenguajes orientados a objetos como C++, diferentes modelos de transacciones
y tolerancia a fallos.
Aplicación PerDiS
en Lenguaje A
API en A
Librería de Acceso
PerDiS Daemon PerDiS Daemon PerDiS Daemon
Aplicación PerDiS
en Lenguaje B
API en B
Librería de Acceso
Aplicación PerDiS
en Lenguaje A
API en A
Librería de Acceso
Máquina 1 Máquina 2 Máquina 3
Figura 4.3: Arquitectura de la plataforma PerDiS.
La plataforma PerDiS se basa en una arquitectura cliente/servidor simétrica: cada
nodo –ordenador del sistema– se comporta al mismo tiempo como cliente y servidor. La
Figura 4.3 muestra los componentes propios de la arquitectura del sistema:
CAPÍTULO 4
36
Aplicación desarrollada sobre la plataforma virtual, en un lenguaje de programación.
API (Application Programming Interface) de acceso a la plataforma, dependiente del
lenguaje de programación.
Una única librería, independiente del lenguaje, que sirve como nexo entre la plataforma
virtual y el API utilizado para una determinada aplicación.
El proceso demonio, “PerDiS Daemon”, encargado de interconectar todos los
ordenadores de la plataforma de forma independiente al lenguaje de programación,
y gestor del sistema de persistencia distribuido.
Como se muestra en la Figura 4.3, el sistema es independiente del lenguaje, y en cada
nodo existe parte del almacenamiento global del sistema –aunque no exista ninguna
aplicación en ejecución.
4.2.2 Aportaciones y Carencias de los Sistemas Estudiados
Los tres sistemas estudiados en el punto anterior son ejemplos de sistemas que tratan
de ofrecer una plataforma de computación virtual. Para ello, no definen una máquina
abstracta con todos los requisitos identificados en § 2.1, sino que establecen una interfaz de
acceso a cada máquina concreta.
La unión de todos los accesos desde las distintas plataformas existentes consigue la
abstracción de una única plataforma virtual, susceptible de ser utilizada como una sola entidad
para la resolución de un problema. Este requisito es definido como global del sistema
final –en § 2.4.6; si bien busca una única entidad de programación, carece de determinadas
características necesarias en ese tipo de sistema.
El principal inconveniente de los sistemas estudiados reside en que su diseño ha estado
enfocado a la resolución de un determinado problema. No se trata de construir una
plataforma flexible, requisito § 2.1.5, diseñada para la resolución de múltiples problemas,
sino que su diseño se ha llevado a cabo bajo el establecimiento previo del problema a resolver.
Van, por tanto, en contra del requisito § 2.1.3.
4.3 Plataformas Independientes
Dentro de esta clasificación, analizaremos casos prácticos de la utilización de máquinas
abstractas como instrumento para conseguir plataformas independientes del lenguaje,
sistema operativo y microprocesador. Los sistemas estudiados identifican una máquina
abstracta como base computacional y, sobre ésta, desarrollan el conjunto de su plataforma,
de modo independiente al entorno computacional real existente.
4.3.1 Estudio de Sistemas Existentes
Smalltalk-80
El sistema Smalltalk-80 tiene sus raíces en el centro de investigación de Xerox, Palo
Alto. Empezó en la década de los setenta, pasando por la implementación de tres sistemas
principales: Smalltalk 72, 76 y 80; el número corresponde al año en el que fueron diseñados
[Krasner83].
Panorámica de Utilización de Máquinas Abstractas
37
Los esfuerzos del grupo investigador estaban orientados a la obtención de un sistema
que fuese manejable por personas no informáticas. Para llegar a esto, se apostó por
un sistema basado en gráficos, interfaces interactivas y visuales, y una mejora en la abstracción
y flexibilidad a la hora de programar. La abstracción utilizada, más cercana al humano,
era la orientación a objetos, y, respecto a la flexibilidad, se podía acceder cómodamente y
en tiempo de ejecución a las clases y objetos que existían en el sistema [Mevel87].
El sistema Smalltalk-80 está dividido básicamente en dos grandes componentes
[Goldberg83]:
1. La imagen virtual: Colección de objetos, instancias de clases, que proporcionan
estructuras básicas de datos y control, primitivas de texto y gráficos, compiladores
y manejo básico de la interfaz de usuario.
2. La máquina virtual: Intérprete de la imagen virtual y de cualquier aplicación del
usuario. Dividida en:
El gestor de memoria: Se encarga de gestionar los distintos objetos en memoria,
sus relaciones y su ciclo de vida. Para ello implementa un recolector
de basura de objetos.
El intérprete de instrucciones: Analiza y ejecuta las instrucciones en tiempo
de ejecución. Las operaciones que se utilizan son un conjunto de primitivas
que operan directamente sobre el sistema.
Aunque exista una especificación formal de la máquina abstracta [Goldberg83], con
la existencia de ésta no se buscó directamente una plataforma independiente, sino aprovechar
determinadas características propias de un lenguaje interpretado11: flexibilidad del sistema
y un nivel de abstracción base adecuado –orientación a objetos.
Todas las aplicaciones constituyentes del sistema de Smalltalk-80 están escritas en el
propio lenguaje Smalltalk y, al ser éste interpretado, se puede acceder dinámicamente a todos
los objetos existentes en tiempo de ejecución (como señalábamos en § 3.3.3). El mejor
ejemplo es el Browser del sistema [Mevel87], mostrado en la Figura 4.4: aplicación que recorre
todas las clases (los objetos derivados de Class) del sistema (del diccionario
Smalltalk) y nos visualiza la información de ésta:
Categoría a la que pertenece la clase.
Un texto que describe la funcionalidad de ésta.
Sus métodos.
Sus atributos.
Código que define el comportamiento de cada método.
Esta información es modificable en todo momento; además tenemos la documentación
real, puesto que se genera dinámicamente, de todas las clases, objetos, métodos y
atributos existentes en el sistema.
11 Smalltalk no es interpretado directamente. Pasa primero por una fase de compilación a un código binario
en la que se detecta una serie de errores. Este código binario será posteriormente interpretado por el
simulador o procesador software de la máquina abstracta.
CAPÍTULO 4
38
Figura 4.4: Acceso al método “inspect” del grupo “user interface”, propio del objeto “Object” perteneciente
al grupo “Kernel-Objects”.
El aplicar estos criterios con todos los programas del sistema hace que la programación,
depuración y análisis de estos sea muy sencilla. La consecución de dichos objetivos es
obtenida gracias a la figura de una máquina abstracta, encargada de ejecutar el sistema.
Smalltalk-80 es pues, un sistema de computación que consigue, mediante una máquina
virtual, una integración entre todas sus aplicaciones, una independencia de la plataforma
utilizada y un nivel de abstracción orientado a objetos para su modelo de computación
base.
Self
Tomando originalmente la plataforma y lenguaje de Smalltalk-80, se creó el proyecto
Self [Ungar87]. Se especificó una máquina abstracta que reducía la representación en
memoria del modelo de objetos utilizado por Smalltalk. El criterio principal de diseño fue
representar el sistema sólo con objetos, eliminado el concepto de clase; este tipo de modelo
de objetos se define como basado en prototipos [Evins94] –lo estudiaremos con mayor
detenimiento en el capítulo 8.
La simplicidad y la pureza en los conceptos de esta máquina, hicieron que se realizasen
múltiples optimizaciones en la compilación llegando a resultados interesantes –como
la obtención tiempos de ejecución 50% superiores a código C compilado de forma optimizada
en velocidad [Chambers91].
En la búsqueda de optimización de máquinas virtuales, se descubrieron nuevos métodos
de compilación e interpretación como la compilación continua unida a la optimización
adaptable [Hölze94]. Estas técnicas de compilación y optimización avanzadas han sido
utilizadas en la mejora de máquinas abstractas comerciales como la de Java (The Java
“HotSpot” Virtual Machine) [Sun98].
El modo de programar en Self varía respecto a los entornos clásicos. Self se basa en
un sistema gráfico que permite crear aplicaciones de forma continua e interactiva. Posee un
Panorámica de Utilización de Máquinas Abstractas
39
conjunto de objetos gráficos denominados morphs, que facilitan la creación de interfaces,
inspeccionándolos, modificándolos y volcándolos a disco. La programación se lleva a cabo
accediendo, en tiempo de ejecución, a cualquier objeto existente en memoria –posea o no,
representación gráfica; esta facilidad es definida como “live editing”. Los objetos existentes
en el sistema son compartidos por todos los usuarios y aplicaciones.
Su implementación se llevó a cabo únicamente plataformas Solaris, y su utilización
práctica se limitó a la investigación y análisis para la creación de otros sistemas más comerciales,
como Java.
Uno de los proyectos para los que se utilizó fue el proyecto Merlin: un sistema que
trataba de acercar los ordenadores a la forma de pensar de los humanos, pudiendo ser éstos
fácilmente utilizables por cualquier persona [Assumpcao93]. Self fue ampliado para obtener
un grado de flexibilidad mayor, ofrecido por la capacidad de modificar parte de su comportamiento
mediante reflectividad computacional [Assumpcao95] –definiremos y estudiaremos
este concepto en el capítulo 6. La plataforma Self modificada pasó de ser un sistema
con un modelo muy básico, a ganar en flexibilidad y complejidad utilizando conceptos como
“mappings” y “reflectors”.
Java
En 1991, un grupo de ingenieros trabajaba en el proyecto Green: un sistema para
interconectar cualquier aparato electrónico. Se buscaba poder programar cualquier aparato
mediante un lenguaje sencillo, un intérprete reducido, y que el código fuese totalmente portable.
Especificaron una máquina abstracta con un código binario en bytes y, como lenguaje
de programación de ésta, intentaron utilizar C++ [Stroustrup98]. Se dieron cuenta de su
complejidad y dependencia de la plataforma de ejecución y lo redujeron al lenguaje Oak,
renombrándolo posteriormente a Java [Gosling96].
Ninguna empresa de electrodomésticos se interesó en el producto y en 1994 el proyecto
había fracasado. En 1995, con el extensivo uso de Internet, desarrollaron en Java un
navegador HTTP [Beners96] capaz de ejecutar aplicaciones Java en el cliente, descargadas
previamente del servidor –denominándose éste HotJava [Kramer96]. A raíz de esta implementación,
Netscape introdujo en su Navigator el emulador de la máquina abstracta permitiendo
añadir computación, mediante applets, a las páginas estáticas HTML utilizadas en
Internet [Beners93]; Java resultó mundialmente conocido.
Un programa en el lenguaje Java se compila para ser ejecutado sobre una plataforma
independiente [Kramer96]. Esta plataforma de ejecución independiente está formada
básicamente por:
La máquina virtual de Java (Java Virtual Machine) [Sun95].
La interfaz de programación de aplicaciones en Java o core API (Application Programming
Interface).
Esta dualidad, que consigue la independencia de una plataforma y el mayor nivel de
abstracción en la programación de aplicaciones, es igual que la que hemos identificado en
Smalltalk-80: imagen y máquina virtual. El API es un conjunto de clases que están compiladas
en el formato de código binario de la máquina abstracta [Sun95]. Estas clases son
utilizadas por el programador para realizar aplicaciones de una forma más sencilla.
La máquina virtual de Java es el procesador del código binario de esta plataforma.
El soporte a la orientación a objetos está definido en su código binario aunque no se define
CAPÍTULO 4
40
su arquitectura12. Por lo tanto, la implementación del intérprete y la representación de los
objetos en memoria queda a disposición del diseñador del simulador, que podrá utilizar las
ventajas propias de la plataforma real.
La creación de la plataforma de Java se debe a la necesidad existente de abrir el espacio
computacional de una aplicación. Las redes de computadores interconectan distintos
tipos de ordenadores y dispositivos entre sí. Se han creado múltiples aplicaciones, protocolos,
middlewares y arquitecturas cliente servidor para resolver el problema de lo que se conoce
como programación distribuida [Orfali96].
Una red de ordenadores tiene interconectados distintos tipos de dispositivos y ordenadores,
con distintas arquitecturas hardware y sistemas operativos. Java define una máquina
abstracta para conseguir implementar aplicaciones distribuidas que se ejecuten en
todas las plataformas existentes en la red (plataforma independiente). De esta forma, cualquier
elemento conectado a la red, que posea un procesador de la máquina virtual y el API
correspondiente, será capaz de procesar una aplicación –o una parte de ésta– implementada
en Java (característica estudiada previamente en § 3.3.4).
Para conseguir una este tipo de programación distribuida, la especificación de la
máquina abstracta de Java se ha realizado siguiendo fundamentalmente tres criterios [Venners98]:
1. Búsqueda de una plataforma independiente.
2. Movilidad del código a lo largo de la red.
3. Especificación de un mecanismo de seguridad robusto.
La característica de definir una plataforma independiente está implícita en la especificación
de una máquina abstracta. Sin embargo, para conseguir la movilidad del código a
través de las redes de computadores, es necesario tener en cuenta otra cuestión: la especificación
de la máquina ha de permitir obtener código de una aplicación a partir de una máquina
remota (§ 3.3.4.1).
La distribución del código de una aplicación Java es directamente soportada por la
funcionalidad de los “class loaders” [Gosling96]; se permite definir la forma en la que se obtiene
el software –en este caso las clases–, pudiendo éste estar localizado en máquinas remotas.
De esta forma la distribución de software es automática, puesto que se puede centralizar
todo el código en una sola máquina y ser cargado desde los propios clientes al principio
de cada ejecución.
Adicionalmente, cabe mencionar la importancia que se le ha dado a la seguridad y
robustez de la plataforma. Las redes representan un instrumento para aquellos programadores
que deseen destruir información, escamotear recursos o simplemente molestar. Un
ejemplo de este tipo de aplicaciones puede ser un virus distribuido, que se ejecute en nuestra
máquina una vez demandado por la red.
El primer módulo en la definición de la plataforma enfocado hacia su robustez, es
el “code verifier” [Sun95]. Una vez que el código ha sido cargado, entra en fase de verificación:
la máquina se asegura de que la clase esté correctamente codificada y de que no viole
la integridad de la máquina abstracta [Venners98].
El sistema de seguridad en la ejecución de código, proporcionado por la máquina
virtual, es ofrecido por el “security manager”; una aplicación cliente puede definir un security
manager de forma que se limite, por la propia máquina virtual, el acceso a todos los recursos
12 Si bien no define exactamente su arquitectura, supone la existencia de determinados elementos de ésta
como una pila [Sun95].
Panorámica de Utilización de Máquinas Abstractas
41
que se estime oportuno13. Se pueden definir así los límites del software obtenido; verbigracia:
la ejecución de un applet descargado de un servidor Web, no se le permitirá borrar archivos
del disco duro.
Compilador
Java
D.class C.class E.class
JVM para
Plataforma 1
Plataforma 1
A.class E.class
JVM para
Plataforma 2
Plataforma 2
A.class
Implementación
D.class física JVM
Aplicación Java
Servidor
A.class B.class
Java API
Java API
Java API
Cliente
Cliente
Cliente
Red de Ordenadores
Figura 4.5: Ejemplo de entorno de programación distribuida en Java.
En la Figura 4.5 se aprecia cómo se enlazan los distintos conceptos mencionados.
En un determinado equipo servidor, se diseña una aplicación y se compila al código especificado
como binario de la máquina abstracta. Este código es independiente de la plataforma
en la que fue compilado (característica de la utilización de máquinas abstractas, estudiada en
§ 3.3.2), pudiéndose interpretar en cualquier arquitectura que posea dicha máquina virtual.
Parte de esta aplicación es demandada por una máquina remota. La porción del
software solicitado es obtenida a través de la red de comunicaciones, y es interpretado por
la implementación del procesador en el entorno cliente. La forma en la que la aplicación
cliente solicita el código a la máquina remota es definida en su class loader (un ejemplo típico
es un applet que se ejecuta en el intérprete de un Web browser, demandando el código del
servidor HTTP).
El código obtenido puede ser “controlado” por un security manager definido en la
aplicación cliente. De esta forma la máquina virtual comprueba los accesos a recursos no
permitidos, lanzando una excepción en tiempo de ejecución si se produjese alguno [Gosling96].
El resultado es una plataforma independiente, capaz de distribuir de forma sencilla
sus aplicaciones en una red de computadores y con un mecanismo de control de código
correcto y seguro.
13 Este concepto ha sido definido como caja de arena o “sandbox”. Debido lo estricto que resultaba para el
desarrollo de applets, en Java2 se ha hecho más flexible mediante la utilización de archivos de políticas
en el “Java Runtime Environment” del cliente [Dageforde2000].
CAPÍTULO 4
42
Aunque en la especificación de la máquina virtual de Java [Sun95] no se identifica
una implementación, el comportamiento de ésta se describe en términos de subsistemas,
zonas de memoria, tipos de datos e instrucciones. En la Figura 4.6 se muestran los subsistemas
y zonas de memoria nombrados en la especificación.
Motor de Ejecución
Cargador de
Clases
Red de Ordenadores
Ficheros
Compilados
(.class)
Zona de
Métodos
Zona
Heap
Registros
Cont. Prog.
Thread 3
Thread 2
Thread 1
Pilas Java
Thread 3
Thread 2
Thread 1
Pilas
Métodos
Nativos
Thread 2
Zonas de Memoria Existentes en Tiempo de Ejecución
Ficheros
Compilados
(.class) Aplicación
en Disco
Figura 4.6: Arquitectura interna de la máquina virtual de Java.
La máquina virtual tiene un subsistema de carga de clases (class loader): mecanismo
utilizado para cargar en memoria tipos –clases e interfaces [Gosling96]. La máquina también
tiene un motor de ejecución (execution engine): mecanismo encargado de ejecutar las instrucciones
existentes en los métodos de las clases cargadas [Venners98].
La máquina virtual identifica básicamente dos zonas de memoria necesarias para
ejecutar un programa:
1. Las inherentes a la ejecución de la máquina virtual: una zona por cada ejecución
de la máquina.
2. Las inherentes a los hilos (threads) de ejecución dentro de una máquina: una zona
por cada hilo existente en la máquina.
En el primer grupo tenemos el área de métodos y el área heap. En la zona de métodos
se introduce básicamente la información y los datos propios de las clases de la aplicación.
En la zona heap se representan los distintos objetos existentes en tiempo de ejecución.
Por cada hilo en ejecución, se crea una zona de pila, un registro contador de programa
y una pila de métodos nativos –métodos codificados en la plataforma real mediante
el uso de una interfaz de invocaciones: JNI (Java Native Interface) [Sun97c]. En cada hilo se
va incrementando el contador de programa por cada ejecución, se van apilando y desapiPanorámica
de Utilización de Máquinas Abstractas
43
lando contextos o marcos en su pila, y se crea una pila de método nativo si se ejecuta un
método de este tipo.
Con respecto a la implementación de la máquina virtual, comentaremos que Javasoft
y Sun Microelectronicshan desarrollado la familia de procesadores JavaChip
[Kramer96]: picoJava, microJavay ultraJava[Sun97]. Estos microprocesadores son
implementaciones físicas de intérpretes de la máquina abstracta de Java, optimizados para
las demandas de esta plataforma como son la multitarea y la recolección de basura.
The IBM J9 Virtual Machine
Como una implementación comercial de la máquina abstracta de Java, la máquina
virtual J9 de IBM está enfocada al desarrollo de software empotrado [IBM2000b]. Mediante
la utilización de esta máquina abstracta y el entorno de desarrollo “VisualAge Micro Edition”,
se puede implementar software estándar que sea independiente de las siguientes variables
[IBM2000c]:
El dispositivo hardware destino (microprocesador).
El sistema operativo destino.
Las herramientas utilizadas en el desarrollo de la aplicación.
“VisualAge Micro Edition” ofrece un amplio abanico de librerías de clases para ser
implantadas: desde librerías de tamaño reducido para los dispositivos de capacidades reducidas,
hasta amplias APIs para los entornos más potentes.
El desarrollo de una aplicación en este entorno de IBM se lleva a cabo siguiendo la
arquitectura mostrada en la Figura 4.7:
Microprocesador / Dispositivos
SO Tiempo Real / Controladores
Máquina Virtual / JNI
Aplicación
Figura 4.7: Arquitectura de aplicaciones en VisualAge Micro Edition.
Con esta arquitectura, el código de la aplicación está aislado de las peculiaridades
existentes en la plataforma física destino. Mediante el “Java Native Interface” (JNI)
[Sun97c], se podrá acceder directamente a los controladores de los dispositivos y a funcionalidades
propias de sistemas operativos en tiempo real. Éste es el modo de asilar el código
nativo del portable.
Este sistema de IBM es un ejemplo práctico de la portabilidad de código para la
plataforma Java. Define un sistema estándar para la creación, ejecución y embebido de aplicaciones
empotradas en hardware [IBM2000d].
CAPÍTULO 4
44
.NET
.NET es la unión de una estrategia comercial de Microsoft y el diseño de una plataforma,
ambos enfocados a introducir la computación de cualquier ordenador en Internet
[Microsoft2000]. El objetivo principal es proporcionar una plataforma, en la que los usuarios
individuales y las empresas, posean un entorno de interoperabilidad sin fisuras a través
de Internet. Se podrán desarrollar y distribuir aplicaciones fácilmente mediante la red, de
forma independiente a la plataforma física utilizada.
La versión final de .NET está anunciada para el año 2002. Una primera publicación
de la herramienta de desarrollo “Visual Studio .NET” está disponible en el portal para desarrolladores
de Microsoft, desde Julio de 2000. Toda la información relativa al desarrollo de
esta plataforma es publicada en la página principal de .NET [Microsoft2000].
Con la plataforma .NET, Microsoft pasará de ser un proveedor de software a ser
un proveedor de servicios [Johnston2000]; desarrollará funciones que los usuarios obtendrán
mediante su conexión a Internet. Los consumidores de los servicios no seguirán el
ciclo: compra, instalación y mantenimiento de cada aplicación; con .NET, comprarán una
licencia de un servicio y éste se instalará y actualizará de forma automática en su máquina
cliente.
Los tres servicios principales que la compañía ha identificado son: almacenamiento,
autenticación y notificación. Con éstos, .NET proporcionará [Microsoft2000]:
La facilidad de interconectar y hacer que se comuniquen entre sí distintos sistemas
de computación, haciendo que la información de usuario sea actualizada
y sincronizada de forma automática.
Un mayor nivel de interacción para las aplicaciones Web, habilitando el uso de
información en XML (Extensible Markup Language) [W3C98].
Un servicio de suscripción en línea que permita acceder y obtener, de forma
personalizada, productos y servicios del ordenador servidor.
Un almacenamiento de datos centralizado, que aumentará la eficiencia y facilidad
de acceso a la información, al mismo tiempo que los datos estarán sincronizados
entre los distintos usuarios y dispositivos.
La posibilidad de interconexión de equipos mediante distintos medios, como
correo electrónico, faxes y teléfonos.
Para los desarrolladores de software, la posibilidad de crear módulos reutilizables
e independientes de la plataforma, que aumenten la productividad.
En la base de la arquitectura de la plataforma .NET (Figura 4.8), se encuentra una
máquina virtual denominada “Common Language Runtime” que proporciona un motor de
ejecución de código independiente del sistema en el que fue desarrollado, y compilado desde
cualquier lenguaje de programación.
Sobre la raíz de la plataforma (Common Runtime Language), se ofrecerá un marco de
trabajo base: código base, válido para cualquier plataforma, que puede ser utilizado desde
cualquier lenguaje de programación.
El entorno de programación está basado en XML [W3C98]. A éste se podrá acceder
desde una aplicación web –con un navegador de Internet– o bien desde una interfaz
gráfica. La comunicación entre cualquier aplicación se llevará a cabo mediante mensajes
codificados en XML, siendo así independiente del programa y sistema operativo emisor.
Panorámica de Utilización de Máquinas Abstractas
45
Servicios
Web
Interfaz
de Usuario
Datos y XML
Marco de Trabajo Base
Common Language Runtime
Figura 4.8: Arquitectura de la plataforma .NET.
Las ventajas propias de desarrollar aplicaciones en .NET, haciendo uso de la arquitectura
mostrada en la Figura 4.8, son [Johnston2000]:
Utilización del mismo código base en .NET, puesto que su codificación es independiente
de la máquina hardware.
La depuración de aplicaciones se realizará de forma indiferente al lenguaje de
implementación utilizado. Se podrán depurar aplicaciones, incluso si están codificadas
en distintos lenguajes de programación.
Los desarrolladores podrán reutilizar cualquier clase, mediante herencia o composición,
indistintamente del lenguaje empleado.
Unificación de tipos de datos y manejo de errores (excepciones).
La máquina virtual ejecutará código con un sistema propio de seguridad.
Se puede examinar cualquier código con una clase, obtener sus métodos, mirar
si el código está firmado, e incluso conocer su árbol de jerarquía14.
4.3.2 Aportaciones y Carencias de los Sistemas Estudiados
En todos los sistemas estudiados, se ha obtenido un sistema de computación multiplataforma
(requisito § 2.1.1). Si bien en Smalltalk no era el objetivo principal, la independencia
del sistema operativo y procesador hardware ha sido buscada en el diseño de todas
ellas.
Las máquinas abstractas de Smalltalk, Self y Java fueron desarrolladas para dar un
soporte portable a un lenguaje de programación. Su arquitectura está pensada especialmente
para trabajar con un determinado lenguaje. Sin embargo, aunque .NET está en fase de
desarrollo, parece que su arquitectura no va a ser dependiente de un determinado lenguaje
de programación; no obstante, parece que C# fue especificado especialmente para esta
14 Estas facilidades se obtienen al tener una máquina abstracta dotada de introspección; estudiaremos este
concepto en § 6.3.1.
CAPÍTULO 4
46
plataforma [Microsoft2000b]. De esta forma, .NET es la única plataforma pensada de
acuerdo con el requisito § 2.1.2.
Si bien, cuando se diseñaron las plataformas estudiadas, se trataba de obtener una
serie de resultados con su utilización, ninguna se desarrolló para solucionar un tipo específico
de problema. Al contrario de lo que ocurría en § 4.2, las máquinas virtuales no resuelven
un problema ad hoc.
El tamaño de todas las plataformas estudiadas es realmente elevado si lo evaluamos
con el requisito § 2.1.4. A modo de ejemplo, la máquina virtual más sencilla de las estudiadas,
Self, posee más de 100.000 líneas de código C++ y 3.000 líneas de código ensamblador
[Wolczko96]. Implementar una máquina virtual sobre una nueva plataforma, o en un sistema
empotrado es costoso; una ratificación de esto es la distribución de la máquina virtual
de Java por javasoft mediante un plug-in: las diversas modificaciones de la JVM y su
complejidad hace que se ralentice la implementación de ésta en los navegadores de
Internet, y por ello se distribuye con el sistema un plug-in [Javasoft99].
En cuanto a los grados de flexibilidad demandados en el requisito § 2.1.5, todas las
máquinas gozan de introspección: se puede analizar cualquier objeto en tiempo de ejecución.
Ninguna posee acceso al medio. Sólo en Smalltalk se pueden añadir primitivas, pero
para ello hay que modificar la implementación de la máquina virtual. El mayor grado de
flexibilidad lo consigue la modificación de la máquina Self, llevada a cabo en el proyecto
Merlin [Assumpcao93]; mediante reflectividad computacional se pueden modificar partes
del funcionamiento de la máquina (§ 2.1.5.3).
Otro de los requisitos necesarios en la búsqueda de una plataforma virtual era la
identificación y existencia de un interfaz de invocaciones a codificación nativa (§ 2.1.6). La
única máquina abstracta que lleva a cabo este requerimiento es Java, con su interfaz de invocaciones
nativas JNI [Sun97c]. Como demostración de sus posibilidades, hemos estudiado
en § 4.3 la plataforma J9 de IBM enfocada a implementar software empotrado. La interfaz
empleada es un sistema de enlazado (link) con una librería de código C. Un inconveniente
es que no se localizan los métodos de forma agrupada, sino que hay que analizar si
poseen la palabra reservada native en su declaración.
Respecto al nivel de abstracción de su modelo de computación, exceptuando Self,
todas poseen un modelo demasiado orientado al lenguaje de programación. Esto hace que
la traducción desde otros sistemas sea compleja, si éstos poseen un modelo de computación
diferente. Self se basa en un modelo basado en prototipos muy simple, uniforme y
puede representar a multitud de lenguajes orientados a objetos [Wolczko96]15.
Finalmente, a modo de conclusión, comentar que la investigación en la creación de
la plataforma .NET por Microsoft, indica, de alguna forma, que es necesario en el mundo
de la computación, una solución global para el desarrollo de aplicaciones distribuidas, portables
e interoperables. Actualmente existen diversas alternativas para crear este tipo de
aplicaciones, pero no existe una solución global. Los objetivos buscados por Microsoft con
.NET y los objetivos buscados en esta tesis, convergen en la necesidad desarrollar una nueva
plataforma de computación virtual.
15 Estudiaremos en profundidad este tipo de modelo de computación y sus posibilidades en el capítulo 8.
Panorámica de Utilización de Máquinas Abstractas
47
4.4 Desarrollo de Sistemas Operativos Distribuidos y
Multiplataforma
La portabilidad del código generado para una máquina abstracta (§ 3.3.2) y la distribución
e interoperabilidad de sus aplicaciones (§ 3.3.4) han sido los motivos principales
para desarrollar sistemas operativos sobre máquinas abstractas. Las aplicaciones para este
tipo de sistemas operativos adoptan todas las ventajas indicadas en § 3.4, y el código de las
rutinas propias del sistema operativo es distribuible a lo largo de un entorno de computación
heterogéneo e independientes de la plataforma.
En este apartado, estudiaremos un conjunto de sistemas operativos que hacen uso
de máquinas abstractas. Nos centraremos en el diseño de las máquinas, sin entrar en profundidad
en el desarrollo de los sistemas operativos. Finalmente, indicaremos las aportaciones
y limitaciones de las distintas máquinas virtuales.
4.4.1 Estudio de Sistemas Existentes
Merlin
El proyecto Merlin buscaba un sistema en el que se acercase el modelo de computación
de los ordenadores a la forma de pensar de los humanos, pudiendo ser éstos fácilmente
utilizados por cualquier persona [Assumpcao93].
El proyecto Merlin se inició con la utilización de una simplificación de la máquina
abstracta de Self [Ungar87] denominada “tinySelf” (las características de esta máquina virtual
han sido descritas en § 4.3). Ante las limitaciones de la arquitectura monolítica propia
de Self, desarrollaron un intérprete de este lenguaje sobre tinySelf añadiéndole reflectividad16
[Assumpcao99]; el resultado fue el lenguaje Self/R que heredaba las ventajas de Self
sumándole un grado de flexibilidad para obtener una semántica computacional abierta: es
posible modificar parte de la semántica del lenguaje.
La flexibilidad obtenida para el sistema hace que esta máquina, al no ser monolítica,
pueda clasificarse tanto en este apartado como en el § 4.5, “Máquinas Abstractas No Monolíticas”.
Inferno
El sistema operativo Inferno™ ha sido diseñado para ser implantado en elementos
de computación heterogéneos así como en los sistemas computaciones tradicionales. Las
ventajas que ofrece este sistema operativo son [Dorward97]:
Portabilidad a distintos procesadores. Existen versiones para Intel, SPARC,
MIPS, ARM, HP-PA, Power PC y AMD 29K.
Portabilidad a distintos entornos. Puede ejecutarse tanto como un sistema operativo
único para una máquina, como coexistir con otro sistema operativo como
Windows NT, Windows 95, Irix, Solaris, Linux, AIX, HP/UX y NetBSD.
Diseño distribuido. El mismo sistema es implantado tanto en la terminal del
cliente como en el servidor, pudiéndose acceder siempre desde uno a los recursos
del otro.
16 Especificaremos este concepto en el capítulo 6.
CAPÍTULO 4
48
Requerimientos hardware reducidos. Inferno se ejecuta en máquinas con tan
solo 1Mb de memoria y sin necesidad de que se implemente un sistema de traducción
de direcciones de memoria en hardware.
Aplicaciones portables. Las aplicaciones en Inferno se codifican en un lenguaje
denominado Limbo™ [Dorward97b], cuya representación binaria es independiente
de la plataforma.
Adaptabilidad dinámica. En función del hardware existente, las aplicaciones
podrán cargar un determinado módulo u otro. Se permite conocer las características
de la plataforma real existente.
Todas las características que aporta Inferno se consiguen estableciendo una máquina
abstracta como motor de ejecución: Dis, “the Inferno Virtual Machine” [Winterbottom97].
El diseño de esta máquina abstracta, al estar enfocado al desarrollo de un sistema operativo
multiplataforma, se basa principalmente en dos pilares:
1. La eficiencia es un aspecto crítico para el posterior desarrollo del sistema operativo.
El hecho de utilizar una máquina abstracta, ralentiza la ejecución de las rutinas
del operativo; el diseño de la máquina ha de estar orientado a reducir los
tiempos de ejecución al mínimo.
2. Su arquitectura debe cubrir las características generales de todas las plataformas
físicas en las que va a ser implantado. Su definición no se ha llevado a cabo como
un intérprete, sino como la raíz de todas las arquitecturas de los procesadores
modernos.
La arquitectura de Dis no es una máquina de pila, sino que sus instrucciones son
siempre de transferencia de memoria [Aho90]. La sintaxis de una instrucción para Dis es:
OP src1, src2, dst
Donde OP es el código de la operación, src1 y src2 son operandos en memoria,
y dst es el destino, también en memoria.
La memoria es un factor crítico al tratar de hacer el sistema lo más sencillo posible.
La máquina virtual utiliza un sistema de recolección de basura mediante un simple contador
de referencias [Cueva92]. Las referencias cíclicas son eliminadas con un algoritmo adicional.
El resultado es un sistema de gestión de memoria sencillo y eficiente, lo suficientemente
reducido como para poder implantarse en pequeños sistemas de computación.
Oviedo3
Oviedo317 [Cueva96] es un proyecto de investigación desarrollado por un grupo de
investigación en Tecnologías Orientadas a Objetos de la Universidad de Oviedo. El objetivo
del proyecto es construir un sistema computacional integral orientado a objetos. Las
características que soporta son las propias de un sistema orientado a objetos [Booch94]:
Abstracción e identidad de objetos.
Encapsulamiento.
Herencia.
Polimorfismo.
17 Oviedo Orientado a Objetos.
Panorámica de Utilización de Máquinas Abstractas
49
Sistema de comprobación de tipos en tiempo de ejecución.
Concurrencia.
Persistencia.
Distribución.
El motor computacional de todo el sistema es una máquina abstracta orientada a
objetos denominada Carbayonia [Izquierdo96]. Ésta constituye el núcleo del sistema estableciendo
el paradigma de la orientación a objetos desde la raíz del sistema.
El concepto básico en Oviedo3 es el objeto. Para manejar los objetos y alcanzar los
requisitos inherentes al sistema, surgen otros conceptos como el paso de mensajes, la definición
de métodos, la existencia de hilos, etc.
Sobre la plataforma virtual se desarrollan las distintas capas del sistema orientado a
objetos (Figura 4.9). La primera capa es un conjunto de objetos que desarrollan las funciones
propias de un sistema operativo: SO418 [Álvarez96, Álvarez97]; gestiona las tareas básicas
del sistema, dando un soporte de nivel de abstracción mayor al resto de los módulos
[Alvarez98].
Sobre ésta, se desarrollan compiladores y bases de datos orientadas a objetos –estos
últimos se apoyan directamente en el sistema de persistencia ofrecido por la máquina virtual
[Ortín97, Ortín97b] y en la noción de objeto persistente frente a tabla, cambiando la
identificación de la información propia de las bases de datos relacionales [Martínez98].
Aplicaciones de Usuario
Compiladores
Bases de Datos OO
Sistema Operativo OO
Máquina Abstracta
Orientada a Objetos
Independencia del Hardware
Figura 4.9: Capas del Sistema Integral Orienta a Objetos Oviedo3.
El diseño de aplicaciones en esta plataforma utiliza de forma íntegra el paradigma
de la orientación a objetos, reutilizando los objetos existentes en tiempo de ejecución –ya
sean propios del sistema o creados por el programador.
La máquina abstracta Carbayonia está compuesta fundamentalmente por cuatro
áreas mostradas en la Figura 4.10. En Carbayonia no se trabaja nunca con direcciones físicas
de memoria, sino que cada área se puede considerar a su vez como un objeto: se encarga
de la gestión de sus objetos, y se le envían mensajes para crearlos, obtenerlos y liberarlos.
El direccionamiento de un objeto se realiza a través de una referencia.
18 Sistema Operativo de Oviedo3.
CAPÍTULO 4
50
Referencias
del Sistema
Área de
Clases
Área de
Instancias
Área de
Threads
this rr exc
Figura 4.10: Áreas de la máquina abstracta Carbayonia.
Área de Clases. En este área se guarda la descripción de cada clase. Esta información
está compuesta por los métodos que tiene, las variables miembro que la
componen y de quién deriva.
Área de Instancias. Los objetos son ubicados en este área. Cuando se crea un
objeto se deposita aquí, y cuando éste se destruye es eliminado. Se relaciona con
el área de clases de manera que cada objeto puede acceder a la información de
la clase a la que pertenece, conociéndose así su tipo en tiempo de ejecución.
La única forma en la que se puede acceder a una instancia es mediante la utilización
de una referencia. Mediante la referencia, podremos pasarle al objeto los
mensajes pertinentes para que éste los interprete.
Referencias del Sistema. Son una serie de referencias que posee la máquina virtual
y tienen funciones específicas dentro el sistema:
this: dentro de un método, apunta al objeto implícito de la invocación.
exc: cuando se lanza una excepción, apunta al objeto que identifica la
excepción lanzada.
rr (return reference): referencia donde los métodos asignan el valor de retorno.
Área de hilos (threads). En la ejecución de una aplicación se crea un hilo (thread)
principal con un método inicial en la ejecución. En la ejecución de la aplicación
se podrán crear más threads, coordinarlos y finalizarlos. Cada hilo posee una pila
de contextos. El conjunto de threads existentes en una ejecución se almacena en
este área.
4.4.2 Aportaciones y Carencias de los Sistemas Estudiados
Las plataformas estudiadas, orientadas a crear un sistema operativo, se construyen
sobre una máquina abstracta multiplataforma (requisito § 2.1.1) y no dependen de un problema
determinado a resolver (requisito § 2.1.3) –el diseño de un sistema operativo no está
enfocado a la resolución de un tipo de problema, sino que ofrece unos servicios para que el
sistema pueda ser fácilmente accesible. Exceptuando el caso de Self/R, los sistemas son
también independientes del lenguaje: se puede acceder a sus servicios desde distintos lenguajes
de programación, mediante una compilación previa.
En lo referente al tamaño y semántica de la máquina virtual (requisito § 2.1.4), la
máquina virtual de Inferno™ es la única que posee un tamaño que permita implantarse en
Panorámica de Utilización de Máquinas Abstractas
51
pequeños sistemas de computación. El tamaño de las máquinas de Self y Oviedo3 hace,
además, muy difícil su migración a distintas plataformas físicas. Ninguna separa la semántica
computacional de la operacional.
Las máquinas virtuales Carbayonia, Dis y Self son monolíticas: definen un modelo
estático de computación, careciendo de flexibilidad computacional (requisito § 2.1.5). No
permiten modificar sus primitivas operacionales (§ 2.1.5.1) ni computacionales (§ 2.1.5.3).
Dis facilita un modo de acceso al entorno (§ 2.1.5.2) y Self posee introspección para conocer
el aspecto de sus objetos en tiempo de ejecución (§ 2.1.5.2.). Sin embargo, las carencias
en flexibilidad de Self son eliminadas con la interpretación de este lenguaje por él mismo,
consiguiendo un grado de reflectividad (ahondaremos en este concepto en el capítulo 6).
La separación del código dependiente de la plataforma, y la definición de una interfaz
de acceso a éste (requisito § 2.1.6), no son definidas para ninguna máquina abstracta. A
modo de ejemplo, Self/R entremezcla código Self y código ensamblador del microprocesador
utilizado.
En Dis, el nivel de abstracción utilizado (requisito § 2.1.7) es muy bajo puesto que
no define un modelo computacional base (es otro tipo de código ensamblador). Carbayonia
define un modelo basado en la orientación a objetos, pero aumenta demasiado la abstracción
de la máquina con características como distribución, seguridad o persistencia: la demanda
de una modificación en estas funcionalidades implica la modificación de la máquina
virtual. Como hemos señalado en § 4.3.2, y como ampliaremos en el capítulo 8, el modelo
computacional de Self, basado en prototipos, define una raíz computacional correcta en su
abstracción, y es independiente del lenguaje.
4.5 Máquinas Abstractas No Monolíticas
Las diferentes máquinas abstractas estudiadas poseen una arquitectura monolítica:
definen un sistema computacional invariable con una política y una semántica. El modo en
el que llevan a cabo la computación de las aplicaciones y sus diferentes características son
constantes y en ningún momento pueden modificarse. Este aspecto monolítico de las máquinas
abstractas, limita la obtención del grado de flexibilidad computacional buscado (requisito
§ 2.1.5).
La máquina virtual de Java [Sun95], permite modificar sólo un conjunto limitado de
sus características: el modo que carga las clases, mediante un ClassLoader, y el modo en
el que restringe la ejecución de un código por motivos de seguridad, mediante un
SecurityManager [Eckel2000]. Ésta es una solución de compromiso para determinados
casos prácticos, pero no define una arquitectura flexible.
Para implementar el proyecto Merlin, se amplió la máquina virtual de Self para añadirle
reflectividad [Assumpcao95]. Mediante la utilización de reflectividad podemos conseguir
plataformas flexibles (como veremos en el capítulo 6). Sin embargo, en este punto
estudiaremos las plataformas no monolíticas existentes sin que hagan uso de arquitecturas
reflectivas.
4.5.1 Estudio de Sistemas Existentes
Virtual Virtual Machine
Actualmente existen aplicaciones con multitud de funcionalidades que llegan a implementar
servicios propios de un sistema operativo; sin embargo, carecen de interoperabiCAPÍTULO
4
52
lidad con otras aplicaciones. Un entorno computacional más orientado hacia la reutilización,
debería dar soporte a un sistema que promueva la interacción entre aplicaciones (interoperabilidad
interna) y que sea accesible desde cualquier lenguaje, código o representación
de datos (interoperabilidad externa) [Folliot98]. Éstos fueron los objetivos buscados
en el diseño de virtual virtual machine (VVM) [Folliot97], una plataforma multilenguaje, independiente
del hardware y sistema operativo, y que es extensible y adaptable, de forma dinámica,
a cualquier tipo de aplicación.
Una aplicación que se vaya a ejecutar sobre la VVM, tendrá asociado un “tipo” –el
lenguaje en el que ha sido codificada. Cada “tipo” o lenguaje de programación tiene asociada
una descripción para la máquina virtual denominada “VMlet”. Esta descripción contiene
un conjunto de reglas que describen cómo traducir la aplicación, codificada en su propio
lenguaje, a la representación interna del modelo de computación de la VVM. La interoperabilidad
interna del sistema (reutilización de código, independientemente del lenguaje en el
que fue desarrollado) se consigue gracias a la traducción a un único modelo de computación.
La arquitectura propuesta se muestra en la Figura 4.11:
Java VMlet Smalltalk
VMlet
Aplicación 1
(Java)
Aplicación 2
(Java+Smalltalk)
Aplicación 3
(Smalltalk)
Reglas
Java
Reglas
Smalltalk
Herramientas de Lenguajes
Consola
Administración
Sistema Operativo Virtual
Procesador Virtual
Reglas
Globales
Módulo
Seguridad
Ejecución
Optimización
Validación
...
Sistema Operativo Real
Administración
de la VVM
Figura 4.11: Arquitectura de Virtual Virtual Machine.
El procesador virtual es la raíz básica del modelo de computación del sistema. Ejecuta
un lenguaje interno, al cuál se traduce cualquier lenguaje de programación utilizado
para acceder a la plataforma. Sobre este procesador virtual se construye el sistema operativo
virtual: implementa las rutinas propias de un sistema operativo, siendo éste independiente
de la plataforma y del lenguaje.
Las herramientas de lenguajes son un conjunto de librerías de programación que
proporcionan funcionalidades propias de un conjunto de lenguajes. Su representación también
es independiente de la plataforma y lenguaje, al haber sido desarrolladas sobre el procesador
virtual.
Las interoperabilidad interna se apoya en un módulo de seguridad, crítico para asegurar
la integridad del sistema. Las reglas que definen la seguridad del sistema están dentro
de este módulo. Las reglas propias de cada lenguaje, se encuentran en su VMlet.
Las aplicaciones codificadas para VVM son traducidas dinámicamente, utilizando
su VMlet, a la representación interna de computación. Esto permite que aplicaciones desarrolladas
en distintos lenguajes interactúen entre sí, y que una aplicación pueda desarrollarse
en distintos lenguajes de programación.
Panorámica de Utilización de Máquinas Abstractas
53
VVM posee una consola para poder administrar y acceder a la plataforma virtual
desde el sistema operativo real utilizado.
Adaptive Virtual Machine
El diseño de “Adative Virtual Machine” (AVM) está enfocado a encontrar una plataforma
de ejecución dotada de movilidad, adaptabilidad, extensibilidad y dinamicidad. Sobre
ella, las aplicaciones podrán distribuirse de forma portable, segura e interoperable. Las aplicaciones
se podrán adaptar a cualquier plataforma física, y se podrá extender su funcionalidad
de un modo sencillo. Además, el entorno de ejecución de una aplicación podrá ser
modificado de forma dinámica [Baillarguet98].
Se identifica el desarrollo de una máquina abstracta como mecanismo para lograr
los objetivos mencionados, puesto que éstas ofrecen una separación entre las aplicaciones
ejecutables y el hardware sobre las que se ejecutan [Baillarguet98]. Se diseña pues una máquina
abstracta adaptable (AVM) sobre la que se ofrece un entorno virtual de ejecución –
Virtual Execution Environment (VEE). En la Figura 4.12 se muestra la arquitectura del entorno
virtual de ejecución.
VMspec
jdfsakjsfa
asfkd jdsafjk
sdaj fads jafdsj
saj asdff
afsdkasj
jdfsakjsfa
asfkd jdsafjk
sdaj fads jafdsj
saj asdff
afsdkasj
VMlet
0001010
1111010
0101010
1010101
0001010
1111010
0101010
1010101
Interfaz del Sistema Operativo
Aplicación
en
Ejecución
Aplicación
Intérprete
Primitivas Objetos Ejecución
Compilador
de VMspec
Optimizador
de Código
Generador
de VMlet
Máquina Virtual Generada
Plataforma Física Real
Cargador de
aplicaciones
Cargador
de VMlet Adaptable Virtual Machine
Figura 4.12: Arquitectura de la Adaptable Virtual Machine.
La AVM separa la parte estática que representa el motor de ejecución o intérprete,
de la parte dinámica que define las características propias del entorno de computación en la
interpretación –por ejemplo, el planificador de hilos. La unión de ambas partes supone el
entorno virtual de computación (VEE): un sistema de computación adaptable.
La parte dinámica son especificaciones de máquina virtual (VMspec): constituyen
especificaciones de alto nivel de componentes de la máquina virtual, como sistemas de persistencia
o planificación de hilos. Cada VMspec incluye las definiciones de ejecución y un
modelo de objetos, así como un conjunto de primitivas para acceder al sistema operativo.
El compilador y optimizador de código transforman las VMspecs en una representación
binaria denominada VMlet.
El cargador de VMlets carga en memoria la especificación de la máquina virtual definida,
y el generador de la máquina virtual la introduce en el sistema de interpretación
adaptable (AVM). Una vez concluido este proceso, la máquina virtual se comporta con
unas determinadas características hasta que sean modificadas con la carga de una nueva
especificación.
CAPÍTULO 4
54
El intérprete de la máquina abstracta ha sido desarrollado como un framework orientado
a objetos que produce un entorno de computación adaptable.
Extensible Virtual Machine
El hecho de implementar un motor mínimo de ejecución dotado de unas interfaces
de acceso a bajo nivel, permite diseñar una plataforma de ejecución de un amplio espectro
de aplicaciones, adaptable a cualquier tipo de problema. Esta es la idea básica sobre la que
se diseña Extensible Virtual Machine (XVM) [Harris99].
La mayoría de las máquinas abstractas existentes siguen un esquema monolítico, en
el que la arquitectura de éstas poseen un esquema fijo de abstracciones, funcionalidades y
políticas de funcionamiento en la ejecución de sus aplicaciones. De forma contraria, un
sistema extensible debería permitir a los programadores de aplicaciones, modificar el entorno
de ejecución en función de las necesidades demandadas por del tipo de aplicación
que se esté desarrollando. Podemos poner la gestión de memoria heap como ejemplo: Determinados
tipos de aplicaciones hacen un uso extenso de un tamaño concreto de objetos y
establecen una determinada dependencia entre éstos. Para este tipo de aplicaciones, un
modo específico de creación y ubicación de objetos en la memoria heap implica una reducción
significativa en los tiempos de ejecución [Vo96].
Sceptre es el primer prototipo desarrollado para obtener la XVM. Su diseño, orientado
a la extensibilidad, utiliza una fina granularidad de operaciones primitivas, mediante las
cuales se desarrollarán las aplicaciones. Su arquitectura se basa en la utilización de componentes;
cada componente implementa un conjunto de operaciones. Una máquina virtual
concreta se crea como composición de un conjunto de componentes. Al más bajo nivel, los
componentes se implementan sobre la “core-machine” (motor base de ejecución), que proporciona
acceso a la memoria y a la pila. Los servicios que ofrece un componente se especifican
mediante una determinada interfaz denominada “port”.
Distributed Virtual Machine
Distributed Virtual Machine (DVM) establece una arquitectura de servicios distribuidos
que permite administrar y establecer un sistema global de seguridad, para un conjunto
heterogéneo de sistemas de computación sobre una misma máquina virtual [Sirer99]. Con
DVM, características como verificación del código, seguridad, compilación y optimización,
son extraídas de los clientes (máquinas virtuales) y centralizadas en potentes servidores.
En las máquinas virtuales monolíticas, como Java [Sun95], no es posible establecer
un sistema de seguridad y una administración para el conjunto de todas las máquinas
virtuales en ejecución. DVM ha sido diseñada e implementada partiendo de Java,
descentralizando todas aquellas partes de su arquitectura monolítica que deban estar
distribuidas para un control global, al nivel de sistema.
La funcionalidad centralizada de DVM se realiza mediante un procesamiento estático
(tiempo de compilación), y un análisis y gestión dinámica (en tiempo de ejecución). Este
esquema es mostrado en la Figura 4.13.
Panorámica de Utilización de Máquinas Abstractas
55
Código
Fuente
Verificador
Control
Seguridad
Auditor
Administrador
del Cliente
Compilador
Optimizador
Perfilador
Servicios Seguridad Servicios Ejecución
Servicios
Administración
Código
Binario
Servicios Estáticos
Llamadas a
servicios
dinámicos
Administración
Control
Seguridad
Acceso a
Librerías
Servicios Dinámicos
Administración
Centralizada
Compilación
Ejecución
Figura 4.13: Fases en la compilación y ejecución de una aplicación sobre DVM.
Los servicios estáticos son el verificador, compilador, auditor, perfilador y optimizador.
Analizan la aplicación antes de su ejecución y aseguran que cumplan todas las restricciones
del sistema. Los servicios dinámicos complementan la funcionalidad de los servicios
estáticos, proporcionando servicios en tiempo de ejecución dependientes del contexto
del cliente.
El enlace entre los servicios estáticos y dinámicos, se produce gracias a un código
adicional que se inyecta en los archivos binarios, en la fase de procesamiento estático.
Cuando, en el análisis estático, se encuentran operaciones que no pueden ser comprobadas
en tiempo de compilación, se inserta en el código llamadas al correspondiente servicio dinámico.
Mientras que los problemas propios de una arquitectura monolítica son resueltos
con DVM de una forma distribuida, ésta distribución puede llevar a costes de eficiencia en
tiempo de ejecución; la transmisión de la información a través de la red, puede llegar a ser
el cuello de botella de las aplicaciones en DVM. La resolución de este problema se puede
conseguir mediante servidores que utilicen replicación de su información.
4.5.2 Aportaciones y Carencias de los Sistemas Estudiados
Las distintas plataformas estudiadas son multiplataforma (requisito § 2.1.1) y no dependen
de forma directa de un lenguaje de programación (requisito § 2.1.2). Todas están
enfocadas a desarrollar una plataforma computacional de forma independiente a un problema
preestablecido (requisito § 2.1.3), aunque DVM está principalmente enfocado a la
administración de una plataforma distribuida.
La flexibilidad y extensibilidad (requisito § 2.1.5) es la característica común que las
agrupa. Rompen con las arquitecturas monolíticas típicas, y establecen mecanismos para
modificar características del entorno de computación. Excepto XVM, todas ofrecen un
mecanismo de extensibilidad basado en una arquitectura modular; se identifican las funcionalidades
susceptibles de ser modificadas, y se establece una interfaz para extender éstas.
XVM ofrece una expresividad de su flexibilidad basada en un lenguaje de acceso a su “coreCAPÍTULO
4
56
machine”; no se ofrece un conjunto de módulos variables, sino que el programador define
una máquina virtual concreta, apoyándose la expresividad de un lenguaje.
El mismo diseño modular identificado en el párrafo anterior, es el culpable de los
tamaños de las distintas plataformas (requisito § 2.1.4). VVM, AVM y DVM establecen un
framework de desarrollo y ejecución flexible respecto a un conjunto de módulos. La flexibilidad
modular hace que el tamaño del framework sea elevado, dificultando la implantación y
migración de la plataforma a distintos sistemas. En XVM, el tamaño de la plataforma está
en función de su nivel de abstracción, y la mayoría de su código se desarrolla sobre la máquina
abstracta –siendo así totalmente portable.
En VVM y ADM existe una interfaz única de acceso al sistema operativo utilizado
(requisito § 2.1.6).
4.6 Conclusiones
Hemos visto cómo inicialmente la utilización de una plataforma virtual, basada en
una máquina abstracta, buscaba la portabilidad de su código binario. Los sistemas desarrollados
conseguían este objetivo para un determinado lenguaje (§ 4.1.1); otras plataformas
más modernas, que gozan de éxito comercial, también han sido desarrolladas para dar soporte
principalmente a un lenguaje de programación (como es el caso de Java Virtual Machine).
Uno de los objetivos a alcanzar en el diseño de una plataforma de computación flexible
es que la construcción de ésta, no esté asociada a un determinado lenguaje de programación
de alto nivel (requisito § 2.1.2).
La portabilidad del código ofrecida por una máquina virtual, es explotada en entornos
distribuidos de computación. Si el código de una plataforma puede ser ejecutado en
cualquier sistema, éste puede ser fácilmente distribuido a través de una red de ordenadores.
Además, al existir un único modelo de computación y representación de datos, las aplicaciones
distribuidas en un entorno heterogéneo de computación pueden intercambiar datos
de forma nativa –sin necesidad de utilizar una interfaz de representación común.
Como hemos señalado en § 4.2.1, la creación de distintas máquinas abstractas ha sido
utilizada para resolver problemas concretos. Sin embargo, el objetivo de esta tesis es
encontrar una plataforma de computación independiente de un determinado problema
(requisito § 2.1.3).
Plataformas de diseño más avanzadas, con las ventajas ya mencionadas, han sido estudiadas
en § 4.3. Estos sistemas ofrecen programación portable, distribución de código,
interacción de aplicaciones distribuidas y un modelo de computación único, basado en el
paradigma de la programación orientada a objetos. La implementación de aplicaciones es
más homogénea y sencilla, para entornos heterogéneos distribuidos. Cabe destacar el proyecto
“.NET” de Microsoft (§ 4.3.1), actualmente en fase de desarrollo, que unifica sus
sistemas operativos en una única plataforma basada en una máquina abstracta.
En los sistemas analizados en el punto anterior, las distintas máquinas abstractas se
han diseñado para obtener entornos de programación. Sin embargo, las características ofrecidas
también se han utilizado para desarrollar sistemas operativos (§ 4.4.1). La única diferencia
entre unos y otros se centra en la semántica de los servicios implementados.
El principal inconveniente de todos los sistemas mencionados es su arquitectura
monolítica: se ofrece un modelo estático de computación, careciendo de flexibilidad computacional;
las características computacionales de la plataforma no pueden modificarse. Un
objetivo primordial en esta tesis es la obtención de flexibilidad del entorno computacional
(requisito § 2.1.5). Las arquitecturas monolíticas no pueden ofrecer esta característica.
Panorámica de Utilización de Máquinas Abstractas
57
El desarrollo de plataformas no monolíticas se centra en la descomposición modular
de sus características (§ 4.5). Mediante la selección dinámica de estas funcionalidades se
pueden obtener distintas máquinas virtuales, específicas para un determinado tipo de problema.
Sus limitaciones principales son el tamaño de los sistemas y el establecimiento a
priori de las características modificables (§ 4.5.2) –algo no identificado en el diseño no se
podrá modificar posteriormente.
A modo de conclusión, señalaremos la máquina virtual de Self como una plataforma
con muchas características positivas: Representa un nivel de abstracción adecuado (basado
únicamente en objetos), es independiente del lenguaje (en [Wolczko96] se utilizó a
través distintos lenguajes, como Java y Smalltalk) y Self/R otorga una flexibilidad basada en
reflectividad (desarrollada para el proyecto Merlin). Sin embargo, su desarrollo es complejo
(más de 100.000 líneas de C++ y 3.000 de ensamblador), limitando su portabilidad; entrelaza
código independiente de la plataforma con código ensamblador, dificultando su implementación
en nuevas plataformas; finalmente, el sistema de reflectividad de Self/R es complejo
y poco flexible.
59
CCAAPPÍÍTTUULLO 55:
SISTEMAS FLEXIBLES NO REFLECTIVOS
Buscando entornos computacionales de programación flexible, en este capítulo estudiaremos
los trabajos relacionados existentes exceptuando una técnica: la reflectividad
computacional –ésta será estudiada con detenimiento en el capítulo 6.
La mayoría de los sistemas estudiados se fundamentan en un paradigma de diseño y
programación denominado “separación de incumbencias” (separation of concerns) [Hürsch95],
que se centra en separar la funcionalidad básica de una aplicación de otros aspectos
especiales, como pueden ser la persistencia o distribución.
Cuando se desarrolla un sistema, comúnmente se entremezclan en su codificación
características tan dispares como su funcionalidad (aspecto principal) y aspectos adicionales
como los relativos a su almacenamiento de datos, sincronización de procesos, distribución,
restricciones de tiempo real, persistencia o tolerancia a fallos. El hecho de unir todas estas
incumbencias en la propia aplicación conlleva:
1. Elevada complejidad en el diseño y codificación de la aplicación.
2. El programa, al unir todos sus aspectos en una dimensión, es difícil de mantener.
Los cambios en una incumbencia implican el cambio global del sistema.
3. El código de la aplicación es poco legible; al combinar todos los aspectos, la
funcionalidad central de la aplicación no sobresale en un plano superior de abstracción.
Los sistemas a estudiar en este capítulo separan las distintas incumbencias o aspectos
de una aplicación y, mediante distintos mecanismos de implementación, forman el sistema
global a partir de las especificaciones realizadas. El objetivo final es siempre separar la
funcionalidad de una aplicación de sus distintas incumbencias, haciendo éstas variables y
consiguiendo así la adaptabilidad global del sistema.
En cada caso estableceremos una descripción y estudio del sistema, para posteriormente
analizar sus puntos positivos aportados y sus carencias, en función de los requisitos
definidos en el capítulo 3.
5.1 Implementaciones Abiertas
La construcción de software se ha apoyado tradicionalmente en el concepto de
módulo o caja negra que expone su funcionalidad ocultando su implementación. Este prinCAPÍTULO
5
60
cipio de “caja negra” ha facilitado la división de problemas reduciendo su complejidad, y ha
fomentado la portabilidad y reutilización de código. Sin embargo, el hecho de ocultar la
implementación de las funcionalidades expuestas puede llevar a carencias de eficiencia en la
reutilización del módulo.
La aproximación denominada “implementación abierta” (open implementation) trata de
construir software reutilizable al mismo tiempo que eficiente [Kiczales96]. Esta técnica
permite al cliente del módulo seleccionar distintas estrategias de implementación en función
de sus necesidades de eficiencia. La implementación sigue estando separada de la interfaz
del módulo, pero es posible parametrizarla en función de los contextos de utilización.
A modo de ejemplo, podemos pensar en la implementación de un módulo que
ofrece la computación propia de un conjunto. Su interfaz poseerá operaciones como “insertar”,
“eliminar”, “buscar” o “recorrer” sus elementos. Una vez implementado este módulo,
lo reutilizamos en distintas aplicaciones bajo diferentes contextos, variando significativamente
aspectos como:
El número de elementos que contiene.
La frecuencia con que se van a insertar dichos elementos.
El número de elementos y frecuencia con que van a ser eliminados en tiempo
de ejecución.
La implementación del contenedor se puede efectuar mediante distintas estructuras
de datos como tablas hash, listas enlazadas o árboles. En función del contexto de utilización,
la selección de una u otra estructura de datos producirá cambios en la eficiencia del
sistema. De esta forma, las implementaciones abiertas describen una funcionalidad mediante
una interfaz y la posibilidad de seleccionar una determinada estrategia de implementación
(ISC code) [Kiczales96b]. Se puede expresar una implementación abierta como la función:
f :C →S
Siendo C el conjunto de posibles perfiles de usuario, y S el conjunto de estrategias
de implementación ofrecidas por el módulo. A lo largo del tiempo, el conjunto de contextos
del cliente (C) podrá variar y, por lo tanto, deberá poder variar el abanico de estrategias
de implementación (S). En una implementación abierta, deberemos permitir la modificación
y ampliación de perfiles de usuario (C’), implementaciones (S’) y asociación entre
éstas (f’). Un módulo abierto deberá pues proporcionar cuatro interfaces distintas:
Sistemas Flexibles No Reflectivos
61
C’
f’
S’
Módulo Abierto
Interfaz de
Caja Negra
Interfaz de
Especificación de
Perfil de Usuario
Interfaz de
Selección de
Estrategia
Interfaz de
Estrategia de
Implementación
Perfiles de
Cliente
Estrategias de
Implementación
Selección de
Estrategia
Figura 5.1: Distintas interfaces ofrecidos en una implementación abierta.
1. Interfaz de caja negra: ofrece la funcionalidad del módulo para una estrategia de
implementación por defecto.
2. Interfaz de especificación de perfil de usuario: permite definir contextos de utilización
futuros por parte del cliente.
3. Interfaz de estrategia de implementación: facilita la adición de nuevas implementaciones
para conseguir los servicios ofrecidos por la interfaz de caja negra.
4. Interfaz de selección de estrategia: ofrece al cliente la posibilidad de elegir él
mismo la estrategia de implementación oportuna –eliminando así la selección
automática conseguida por f.
Sobre esta teoría se ha descrito un método para determinar el aspecto de las interfaces
ofrecidas al usuario del módulo, a partir de la especificación de un problema. Este método
ha sido bautizado con el nombre de “análisis y diseño de implementaciones abiertas”
(Open Implementation Analysis/Design) [Maeda97].
5.1.1 Aportaciones y Carencias
Esta primera aproximación para la generación de software flexible, consigue ofrecer
las ventajas de separar la implementación de la interfaz de un módulo, sin perder eficiencia
en la reutilización de código. Es una instancia de la teoría de “separación de incumbencias”
[Hürsch95]: se separa una funcionalidad de sus distintas implementaciones.
Este mecanismo ofrece una mera solución al problema de la eficiencia de un sistema
construido a través de módulos reutilizables, sin ofrecer ninguno de los grados de flexibilidad
indicados en los requisitos § 2.3. Como ejemplo, podemos indicar que el usuario
podría conocer las distintas estrategias de implementación existentes en un módulo, si el
sistema hubiese sido dotado de introspección (§ 2.3.1).
Este sistema fue la base para crear otros más avanzados como la programación
orientada a aspectos (§ 5.3) o la programación adaptable (§ 5.4), que estudiaremos posteriormente
en este capítulo.
CAPÍTULO 5
62
5.2 Filtros de Composición
Los filtros de composición fueron diseñados para aumentar la adaptabilidad del paradigma
de la orientación a objetos, añadiendo a éste extensiones modulares y ortogonales
[Bergmans94]:
La modularidad de las extensiones implica que los filtros de composición pueden
ser añadidos a cada objeto, sin necesidad de modificar la definición de éste.
La ortogonalidad de los filtros requiere que éstos tengan funcionalidades independientes
entre sí.
Un ejemplo comparativo de adaptabilidad de objetos mediante filtros de composición
puede ser la modificación del funcionamiento de una cámara de fotos. Si las condiciones
de luz son pobres y el cuerpo a fotografiar se encuentra alejado, podremos modificar el
funcionamiento de la cámara (objeto) añadiéndole filtros de color y lentes de aumento (filtros
de composición); estas dos extensiones son modulares porque no es necesario modificar
el funcionamiento de la cámara para acoplarlas, y son ortogonales porque sus funcionalidades
son independientes entre sí.
Los filtros de composición son objetos instanciados de una clase filtro. Su propósito
es acceder y modificar la forma en la que se envían y reciben los mensajes, especificando
condiciones para aceptarlo o rechazarlo, y determinando la acción a desencadenar en cada
caso. El sistema se asegura que un mensaje sea procesado por los filtros especificados antes
de que el método sea ejecutado (filtro de entrada) y antes de que un mensaje sea pasado
(filtro de salida). Se identifican así dos niveles de abstracción distintos: los métodos –de
mayor nivel de abstracción– y los filtros de composición –adaptables y dependientes del
contexto, y por lo tanto de menor nivel de abstracción.
Las acciones a llevar a cabo en la recepción o envío de un mensaje dependen de la
clase de la que el filtro es instancia. Existen filtros Dispatch (para implementar herencia
dinámica o delegación), RealTime (restricciones de tiempo real), Meta (coordinación de
comportamientos), Error (incorrección en el manejo de un mensaje) y Wait (sincronización
de procesos).
Los filtros de composición se han aplicado para adaptar la orientación a objetos a
determinadas necesidades como delegación [Aksit88], transacciones atómicas [Aksit91],
integración de acceso a bases de datos en un lenguaje de programación [Aksit92], coordinación
de comportamiento entre objetos [Aksit93], restricciones de tiempo real [Aksit94] y
especificaciones flexibles y reutilizables de sincronización de procesos [Bergmans94].
El primer lenguaje de programación de filtros de composición fue “Sina” [Koopmans95].
Posteriormente, los lenguajes de programación Smalltalk y C++ fueron extendidos
para poder añadir filtros de composición a sus objetos sin necesidad de modificar éstos
[Dijk95, Glandrup95].
5.2.1 Aportaciones y Carencias
Los filtros de composición permiten modificar el mecanismo computacional base
en la orientación a objetos: el paso de mensajes. Con esta técnica se permite modificar el
comportamiento de un grupo de objetos (los instanciados de una determinada clase). El
paso de mensajes es el único comportamiento susceptible de modificación, sin poder rectificar
cualquier otro aspecto computacional del sistema (§ 2.3.3).
Sistemas Flexibles No Reflectivos
63
La flexibilidad de las aplicaciones se consigue especificando su funcionalidad básica
mediante un lenguaje orientado a objetos, y posteriormente ampliando ésta mediante la
programación de filtros de composición. Para que un objeto pueda utilizar un filtro de
composición, deberá especificarlo a priori (en tiempo de compilación) en la interfaz de su
clase; esto limita la flexibilidad dinámica del sistema (§ 2.3.4).
El sistema final alcanza un compromiso flexibilidad-eficiencia: simplifica la adaptabilidad
de una aplicación a la modificación del paso de mensajes, para conseguir desarrollar
aplicaciones suficientemente eficientes en tiempo de ejecución.
5.3 Programación Orientada a Aspectos
Existen situaciones en las que los lenguajes orientados a objetos no permiten modelar
de forma suficientemente clara las decisiones de diseño tomadas previamente a la implementación.
El sistema final se codifica entremezclando el código propio de la especificación
funcional del diseño, con llamadas a rutinas de diversas librerías encargadas de obtener
una funcionalidad adicional (por ejemplo, distribución, persistencia o multitarea). El resultado
es un código fuente excesivamente difícil de desarrollar, entender, y por lo tanto mantener
[Kiczales97].
Cuando desarrollamos una aplicación, podemos hacerlo de dos forma bien distintas:
la primera, siguiendo una estructura computacional clara y organizada; la segunda, primando
la eficiencia de la aplicación, generalmente con un código fuente menos legible. La
programación orientada a aspectos permite separar la funcionalidad de la aplicación de la
gestión de memoria y eficiencia de los algoritmos utilizados, y compone el sistema final a
partir del conjunto de aspectos identificados por el programador. En la programación
orientada a aspectos se definen dos términos distintos [Kiczales97]:
Un componente es aquel módulo software que puede ser encapsulado en un
procedimiento (un objeto, método, procedimiento o API). Los componentes
serán unidades funcionales en las que se descompone el sistema.
Un aspecto es aquel módulo software que no puede ser encapsulado en un procedimiento.
No son unidades funcionales en las que se pueda dividir un sistema,
sino propiedades que afectan la ejecución o semántica de los componentes.
Ejemplos de aspectos son la gestión de la memoria o la sincronización de hilos.
Una aplicación desarrollada mediante programación orientada a aspectos está compuesta
por la programación de sus componentes funcionales en un determinado lenguaje, y
por la programación de un conjunto de aspectos que especifican características adicionales
del sistema. El “tejedor de aspectos” (aspect weaver) acepta la programación de aspectos y
componentes, y genera la aplicación final en un determinado lenguaje de programación.
Como se muestra en la Figura 5.2, la generación de la aplicación final se produce en
tres fases:
CAPÍTULO 5
64
Programación
de
Componentes
Programación
de
Aspectos
Tejedor de Aspectos
Aplicación
Final
Modificación del
grafo de ejecución
por los distintos
aspectos
1
2
3
Grafo del flujo
de ejecución de los
componentes
Figura 5.2: Fases en la generación de una aplicación orientada a aspectos.
1. En la primera fase, el tejedor construye un grafo del flujo de ejecución del
programa de componentes.
2. El segundo paso consiste en ejecutar la programación de cada uno de los aspectos.
Éstos actúan sobre el grafo que define la funcionalidad del sistema (punto
anterior) realizando las modificaciones o añadiduras oportunas.
3. La última fase consiste en recorrer el grafo modificado y generar el código final
de la aplicación; ésta cumple con la funcionalidad descrita por sus componentes
y ofrece los aspectos definidos por el programador.
Se han implementado distintos sistemas de programación orientada a aspectos,
siendo uno de los más avanzados el denominado AspectJ [Kiczales2001]; una extensión de
la plataforma de Java. Una aplicación en AspectJ consta de:
Puntos de unión (join points): Describen puntos de ejecución de una aplicación.
AspectJ proporciona un elevado grupo de puntos de unión, siendo un ejemplo
de éstos la invocación de métodos.
Designación de puntos de ruptura (pointcut designators): Identifican colecciones de
puntos de unión en el flujo de ejecución de un programa, para posteriormente
poder modificar su semántica.
Consejo (advice): Acciones que se ejecutan previamente, en el momento, o posteriormente
a que se alcance la ejecución de una designación de puntos de ruptura.
Aspecto (aspect): Módulo de implementación, definido como una clase, que
agrupa acciones en puntos de unión.
Utilizando este entorno de programación se han desarrollado sistemas de trazabilidad
de software, auditoría de invocación a métodos (logging), adición de precondiciones y
poscondiciones, así como utilización de programación por contratos [Meyer97] en fase de
desarrollo y su posterior eliminación sin necesidad de modificar el código fuente. También
se ha conseguido desarrollar aspectos reutilizables como por ejemplo determinadas políticas
de sincronización de hilos [Kiczales2001].
Sistemas Flexibles No Reflectivos
65
5.3.1 Aportaciones y Carencias
La programación orientada a aspectos permite separar la parte funcional de una
aplicación de otros aspectos propios de su implantación o despliegue. Esta facilidad otorga
al sistema cierta flexibilidad con ciertas limitaciones.
El proceso de especificación de aspectos para modificar la semántica de una aplicación
es un proceso estático (§ 2.3.3); una aplicación no puede modificar sus distintos aspectos
en tiempo de ejecución.
El modo en el que se crea, y posteriormente se trata, el grafo que define el flujo de
la ejecución de la aplicación, supone una elevada complejidad para el programador, e implica
la definición de un lenguaje distinto para la descripción de aspectos. La implementación
de AspectJ ha sido más sencilla, al hacer uso de la característica introspectiva de la plataforma
Java [Sun97d] (§ 2.3.1). De la misma forma, podría facilitarse el tratamiento del grafo
añadiendo un sistema de modificación estructural dinámica del sistema (§ 2.3.2).
Otra limitación en la flexibilidad de la programación orienta a aspectos es la restricción
a priori impuesta por la definición de puntos de unión (join points): si una característica
del lenguaje de programación de componentes no se identifica como un punto de unión,
no podrá ser modificada posteriormente (§ 2.3.4). Todos los sistemas desarrollados son
también dependientes del lenguaje utilizado (§ 2.3.5).
5.4 Programación Adaptable
La programación adaptable es una extensión de la programación orientada a objetos,
en la que se flexibiliza las relaciones entre la computación y los datos; el software se
puede adaptar para manejar cambios en los requerimientos del sistema. La programación
adaptable permite expresar la intención general de un programa sin necesidad de especificar
todos los detalles de las estructuras de los objetos [Lieberherr96].
El software adaptable toma el paradigma de la orientación a objetos y lo amplía para
que éste soporte un desarrollo evolutivo de aplicaciones. La naturaleza progresiva de la
mayor parte de los sistemas informáticos, hace que éstos sufran un número elevado de
cambios a lo largo de su ciclo de vida. La adaptabilidad de un sistema guía la creación de
éste a una técnica que minimice el impacto producido por los cambios. Software adaptable
es aquél que se amolda automáticamente a los cambios contextuales (por ejemplo, la implementación
de un método, la estructura de una clase, la sincronización de procesos o la
migración de objetos).
Un caso práctico de programación adaptable es el método Demeter (Demeter Method)
[Lieberherr96]. En este método se identifican inicialmente clases y su comportamiento
básico sin necesidad de definir su estructura. Las relaciones entre éstas y sus restricciones se
establecen mediante patrones de propagación (propagation patterns). En este nivel de programación
se especifica la intención general del programa sin llegar a realizar una especificación
formal.
Cada patrón de propagación de divide en las siguientes partes:
Signatura de la operación o prototipo de la computación a implementar.
Especificación de un camino; indicando la dirección en el grafo de relaciones
entre clases que va a seguir el cálculo de la operación.
CAPÍTULO 5
66
Fragmento de código; especificando algún tipo de restricción, o la realización
de la operación desde un elevado nivel de abstracción.
Una vez especificado el grafo de clases y los patrones de propagación, tenemos una
aplicación de mayor nivel de abstracción que una programada convencionalmente sobre un
lenguaje orientado a objetos. Esta aplicación podrá ser formalizada en distintas aplicaciones
finales mediante distintas personalizaciones (customizations). En cada personalización se especifica
la estructura y comportamiento exacto de cada clase en un lenguaje de programación
–en el caso de Demeter, en C++ [Stroustrup98].
Finalmente, una vez especificado el programa adaptable y su personalización, el
compilador Demeter genera la aplicación final en C++. La adaptabilidad del sistema final
se centra en la modificación de las personalizaciones a lo largo del ciclo de vida de la aplicación;
distintas personalizaciones de un mismo programa adaptable dan lugar a diferentes
aplicaciones finales, sin variar la semántica del nivel de abstracción más elevado.
5.4.1 Aportaciones y Carencias
La flexibilidad conseguida en este tipo de sistemas es similar a la otorgada por la
programación orientada a aspectos: es posible separar la funcionalidad central de la aplicación
de otras incumbencias no funcionales. Sin embargo, la principal diferencia entre ambos
es que los aspectos son una técnica de implementación, y la programación adaptable
está más enfocada a la ingeniería del software. Los programas adaptables tratan de representar
de algún modo la funcionalidad captada en tiempo de análisis y diseño.
La programación adaptable, más que buscar entornos de computación flexible (como
los requeridos para esta tesis), está enfocada a minimizar el impacto en los cambios
producidos en entornos de desarrollo evolutivos. Es por esta razón, por la que este tipo de
sistemas no cumple los requisitos impuestos de flexibilidad (§ 2.3).
5.5 Separación Multidimensional de Incumbencias
En la introducción de este capítulo “Sistemas Flexibles No Reflectivos”, identificábamos
la separación de incumbencias como un nuevo paradigma de diseño y programación,
basado en identificar, encapsular y manipular las partes de un sistema relevantes a sus
distintas competencias. La separación de estas competencias o incumbencias, evita la miscelánea
de código concerniente a la obtención de distintos objetivos, facilitando el desarrollo
de software y su mantenimiento.
El conjunto de incumbencias relevantes en un sistema varía a lo largo del ciclo de
vida de éste, añadiendo mayor complejidad a la separación de las competencias. El caso
más general es aquél en el que cualquier criterio para la descomposición de incumbencias
pueda ser aplicado. A modo de ejemplo, podemos identificar un sistema en el que se reconozcan
tres dimensiones de competencias (mostrado en la Figura 5.3): primero, el estado y
comportamiento de cada objeto, ubicados en su clase –modificar ésta implica modificar la
funcionalidad y estructura de sus instancias; segundo, las características de cada objeto –
capacidad de visualización, impresión, persistencia, etc.; tercero, reutilización de artefactos
software –middleware (COM [Brown98], CORBA [OMG95], etc.), acceso a bases de datos,
sistemas de seguridad, etc.
Sistemas Flexibles No Reflectivos
67
Comportamiento
y estructura del
objeto (clase)
Artefacto Software
(middleware, bases de
datos, seguridad, etc.)
Características
(impresión, visualización,
persistencia, etc.)
Figura 5.3: Ejemplo de sistema con tres dimensiones de incumbencias.
El término “separación multidimensional de incumbencias” (Multi-Dimensional Separation
of Concerns) se ha utilizado para designar a los sistemas capaces de proporcionar la
separación incremental, modularización e integración de artefactos software, basados en
cualquier número de incumbencias [IBM2000e]. Los principales objetivos de estos sistemas
son:
1. Permitir encapsular todos los tipos de incumbencias de un sistema, de forma
simultánea: El usuario podrá elegir y especificar cualquier dimensión de incumbencias
del sistema, sin estar restringido a un número determinado ni a un tipo
de competencias preestablecidas.
2. Utilización de incumbencias solapadas e interactivas: En los sistemas estudiados
previamente en este capítulo, las incumbencias separables de un sistema debían
ser independientes y ortogonales –en la práctica esto no ocurre. La separación
multidimensional de incumbencias permite que éstas se solapen y que la interacción
entre ellas sea posible.
3. Remodularización de incumbencias: El cambio o aumento de requisitos de un
sistema puede implicar nuevas apariciones de incumbencias o modificaciones
de las existentes. La creación o modificación de los módulos existentes deberá
ser factible sin necesidad de modificar los módulos no implicados.
Las distintas ventajas de la utilización de este paradigma son:
Promueve la reutilización de código; no sólo aspectos funcionales de computación,
sino competencias adicionales (persistencia, distribución, etc.).
Al separarse los distintos aspectos del sistema, se mejora la comprensión del
código fuente.
Reducción de los impactos en el cambio de requisitos del sistema.
Facilita el mantenimiento de los sistemas gracias al encapsulamiento individual
de las incumbencias identificadas.
Mejora la trazabilidad de una aplicación centrándonos en aquel aspecto que deseemos
controlar.
CAPÍTULO 5
68
Existen diversos sistemas de investigación construidos basándose en este paradigma
[OOPSLA99, ICSE2000]. Un ejemplo es Hyperspaces [Ossher99], un sistema de separación
multidimensional de incumbencias independiente del lenguaje, creado por IBM, así como la
herramienta Hyper/J [IBM2000f] que da soporte a Hyperspaces en el lenguaje de programación
Java [Gosling96].
5.5.1 Aportaciones y Carencias
La principal aportación de los sistemas basados en separación multidimensional de
incumbencias, en comparación con los estudiados en este capítulo, es el grado de flexibilidad
otorgado: mientras que el resto de sistemas identifican un conjunto limitado de aspectos
a describir, en este caso no existen límites.
Por otro lado, si bien el grado de flexibilidad aumenta al aumentar la posibilidad de
identificar más incumbencias, el mecanismo para obtener dicha flexibilidad no se modifica.
Dicho mecanismo sigue los pasos de descripción de competencias y procesamiento de éstas
para generar el sistema final. Aunque se llega a un compromiso de flexibilidad para obtener
eficiencia de la aplicación final, éste está lejos de ser el mecanismo flexible dinámico
(accesible en tiempo de ejecución) descrito en los requisitos de § 2.3.
5.6 Sistemas Operativos Basados en Micronúcleo
A la hora de diseñar sistemas operativos distribuidos, existen dos grandes tipos de
arquitecturas distintas [Tanenbaum95]:
Una se basa en que cada máquina debe ejecutar un núcleo tradicional (núcleo
monolítico) que proporcione la mayoría de los servicios.
Otra diseña el núcleo del sistema operativo de forma que sea lo más reducido
posible (micronúcleo), y el grueso de los servicios del sistema operativo se obtiene
a partir de los servidores al nivel de usuario.
El objetivo de los sistemas no monolíticos es mantener lo más reducido posible la
funcionalidad del micronúcleo. En términos básicos, suelen proporcionar primitivas de
comunicación entre procesos, administración de memoria y entrada/salida a bajo nivel. Los
servicios se construyen sobre estas primitivas, estableciéndose previamente una interfaz
bien definida de acceso al micronúcleo.
El carácter modular de estos sistemas facilita la implantación, eliminación y modificación
de los distintos servidores, obteniendo así mayor flexibilidad que los operativos monolíticos.
Además, los usuarios que necesiten un determinado servicio específico, podrán
codificarlo e implantarlo en el sistema ellos mismos.
Sistemas Flexibles No Reflectivos
69
Aplicaciones
Interfaz de Acceso
Procesos
Administrador
Gestión Memoria
Archivos
Pipes
Buffers
Dispositivo
Hardware
SO Monolítico
Acceso Usuario
Micronúcleo
Sistema Archivos A
Micronúcleo
Sistema Archivos B
Micronúcleo
Aplicaciones
Hardware Hardware Hardware
SO basado en Micronúcleo
Red Computadores
Figura 5.4: Comparación de acceso a sistemas de archivos de sistemas operativos monolíticos y
basados en micronúcleo.
En la figura anterior se muestra un ejemplo de utilización de un sistema basado en
micronúcleo en la que se implementan distintos sistemas de archivos. Se puede desarrollar
un servidor de archivos DOS, otro UNIX y un tercero propietario implementado por el
usuario. Los distintos programas desarrollados sobre este operativo, podrán seleccionar
cualquiera de los tres sistemas de archivos a utilizar, e incluso utilizar los tres simultáneamente.
La mayor desventaja de estos sistemas respecto a los monolíticos es el rendimiento.
El hecho de centralizar las operaciones sobre las primitivas del micronúcleo, supone un
mayor acoplamiento, y por lo tanto una pérdida de rendimiento. Sin embargo, mediante
distintas técnicas de implementación, se han realizado estudios en los que el rendimiento
no baja substancialmente y es aceptable gracias a la flexibilidad obtenida [Liedtke95].
La arquitectura de sistemas operativos basados en micronúcleo ha sido motivo de
investigación en los últimos años, existiendo multitud de ellos: Amoeba [Mullender87],
CHORUS [Rozier88], MOS [Barak85], Topaz [McJones88], V-System [Cheriton88] o Mach
[Rashid86]. Como caso práctico, estudiaremos brevemente dos de ellos.
Amoeba
Ameba es un sistema operativo distribuido que permite agrupar CPUs y equipos de
entrada/salida, haciendo que todo el conjunto se comporte como una única computadora
[Mullender90]. Facilita también elementos de programación paralela.
Se originó en Vrije Universiteit (Amsterdam) en 1981, como un proyecto de investigación
en el cómputo distribuido y paralelo [Tanenbaum95]. Fue diseñado por Andrew
Tanenbaum y tres de sus estudiantes de doctorado. En el año 1983, Amoeba 1.0 tenía un
primer nivel operacional. El sistema evolucionó durante algunos años, adquiriendo características
como emulación parcial de UNIX, comunicación en grupo y un nuevo protocolo
de bajo nivel.
Una de las mayores diferencias entre Amoeba y el resto de sistemas operativos distribuidos
es que Amoeba carece del concepto de máquina “origen”. Cuando un usuario
entra en el sistema, entra a éste como un todo y no a una máquina específica. Las máquinas
CAPÍTULO 5
70
no tienen propietarios. El shell ofrecido al usuario busca de manera automática la máquina
con la menor carga, para ejecutar cada en ella comando solicitado.
Amoeba consta de dos partes fundamentales: el micronúcleo que se ejecuta en cada
procesador, y una colección de servidores que proporcionan la mayor parte de la funcionalidad
global de un sistema operativo tradicional. La estructura general se muestra en la siguiente
figura:
Micronúcleo
Red Computadores
Servicios
Micronúcleo Micronúcleo
Servicios
Hilos
Servidores
Sistema Archivos
Aplicación
Cliente
Aplicación Cliente
Sistema
Archivos
Procesador A Procesador B Procesador C
Sistema Operativo Amoeba
Servicios Planificador
Procesos
Figura 5.5: Estructura de aplicaciones sobre el sistema operativo Amoeba.
El micronúcleo se ejecuta en todas las máquinas del sistema. Éste tiene cuatro funciones
básicas:
1. Manejo de procesos e hilos.
2. Soporte del manejo de memoria a bajo nivel.
3. Soporte de la comunicación.
4. Manejo de entrada/salida mediante primitivas de bajo nivel.
Mediante esta arquitectura se consiguen dos grandes objetivos: distribuir los servicios
a lo largo de una red heterogénea de computadores, y separar la interfaz e implementación
de un servicio para que éste pueda adaptarse a las distintas necesidades.
Mach
El principal objetivo del sistema operativo Mach era demostrar que se podían estructurar
los sistemas operativos de manera modular, como una colección de procesos que
se comuniquen entre sí mediante la transferencia de mensajes, incluso a través de una red
[Babao89].
La primera versión de Mach apareció en 1986 para un VAX 11/784, un multiprocesador
con cuatro CPUs. Poco después se instituyó la Open Software Foundation, un consocio
de vendedores de computadores, con el fin de arrebatarle el control del UNIX a su
creador AT&T. OSF eligió a Mach 2.5 como la base para su primer sistema operativo.
En 1988, el núcleo de Mach era grande y monolítico, debido a la presencia de una
gran parte del código UNIX de Berkeley. En 1989, se eliminó dicho código del núcleo y se
ubicó en el espacio de usuario, constituyendo Mach 3.0 como un sistema basado en micronúcleo
[Tanenbaum95]. Éste manejaba cinco abstracciones principales:
1. Procesos.
Sistemas Flexibles No Reflectivos
71
2. Hilos.
3. Objetos de memoria.
4. Puertos.
5. Mensajes.
El micronúcleo de Mach se diseñó como una base para emular cualquier tipo de sistema
operativo. La emulación se lleva a cabo mediante una capa software ejecutada fuera
del núcleo, en el espacio de usuario –como se muestra en la Figura 5.6. Con este sistema, es
posible ejecutar aplicaciones sobre distintos sistemas operativos, al mismo tiempo y sobre
una misma máquina.
Micronúcleo
Emulación
Sistema
Operativo
A
Emulación
Sistema
Operativo
B Emulación
Sistema
Operativo
C
Procesos Usuario
Espacio
de
Usuario
Espacio
del
Micronúcleo
Sistema Operativo Mach
Figura 5.6: Ejecución de aplicaciones sobre distintos sistemas operativos emulados en Mach.
La idea subyacente en el núcleo de Mach es proporcionar los mecanismos necesarios
para que el sistema funcione, dejando las distintas políticas para los procesos de usuario.
El sistema de micronúcleo no sólo permite flexibilizar los distintos servicios del operativo
–como veíamos en el caso de Amoeba–, sino que puede utilizarse para flexibilizar el
propio sistema operativo, implementando simultáneamente distintas emulaciones.
5.6.1 Aportaciones y Carencias
La idea principal de los sistemas operativos basados en micronúcleo se centra en
codificar la parte computacional mínima de forma monolítica, para identificar posteriormente
los servicios de un mayor nivel de abstracción como unidades modulares reemplazables.
Este tipo de sistemas aporta un modo sencillo de diseñar un primer mecanismo de
flexibilidad para una plataforma de computación (§ 2.1.4) –sistemas estructuralmente reflectivos,
como la máquina abstracta de Smalltalk [Goldberg83], utilizan esta técnica para
poder adaptar y extender su núcleo computacional [Krasner83].
Las primitivas ofrecidas por el micronúcleo pueden ser ampliadas a un mayor nivel
de abstracción implementándolas como servicios, y flexibilizando así los elementos básicos
del sistema computacional (§ 2.2.1). El usuario puede modificar, elegir y ampliar cualquier
característica del sistema (§ 2.2.2), siempre que haya sido desarrollada como un servicio.
Otra de las aportaciones positivas derivadas de la reducción del núcleo computacional
es la posibilidad de implantar éste en entornos heterogéneos (§ 2.4.5). Al haberse
CAPÍTULO 5
72
especificado previamente una interfaz de acceso, la selección de los distintos servicios que
se desarrollan sobre el micro núcleo puede ser llevada a cabo de forma dinámica (§ 2.2.2).
Una de sus mayores carencias para convertirse en entornos computacionales flexibles
es la falta de un sistema introspectivo que permita conocer (§ 2.2.3) y autodocumentar
(§ 2.2.4) dinámicamente todas las características de los servicios implantados en el sistema.
5.7 Conclusiones
En este capítulo hemos visto cómo la programación adaptable puede ser utilizada
en distintos campos, y cómo se han desarrollado distintas técnicas para conseguirla. Los
sistemas estudiados han sido desarrollados buscando un entorno en el que la parte funcional
de la aplicación se pudiese separar del resto de incumbencias. Estos trabajos de investigación
identifican, de algún modo, la necesidad de desarrollar entornos de programación
flexibles.
La principal diferencia entre los distintos sistemas estudiados reside en el punto de
vista que adoptan para conseguir la separación de incumbencias: desarrollo de componentes
(§ 5.1), modificación de parte de la semántica de un lenguaje de programación (§ 5.2),
separación de funcionalidades (§ 5.3), ciclo de vida de un sistema informático (§ 5.4) y
computaciones susceptibles de ser reemplazadas (§ 5.6).
A excepción de los sistemas operativos basados en micronúcleo, las herramientas
analizadas otorgan un reducido nivel de flexibilidad para resolver un problema concreto,
sin incurrir en elevadas carencias de eficiencia computacional.
En lo referente a los sistemas operativos basados en micronúcleo, el criterio de diseño
centrado en separar la computación primitiva de los servicios supone un mecanismo
para desarrollar un sistema con un elevado grado de flexibilidad: todo lo desarrollado como
un servicio es reemplazable.
La simplificación del micronúcleo facilita su implantación en entornos heterogéneos
y simplifica la recodificación de sus primitivas como servicios –para su posterior modificación.
Sus principales carencias son acarreadas por la ausencia de un mecanismo de acceso
dinámico19.
No es posible conocer de forma exacta y en tiempo de ejecución los servicios
existentes en el sistema.
La selección dinámica de distintos servicios no está permitida.
No es posible modificar un servicio de forma dinámica, sin necesidad de finalizar
su ejecución.
La utilización de un servicio por una aplicación es explícita, y por lo tanto es
necesario modificar la aplicación si queremos modificar algún aspecto de los
servicios que utiliza.
19 Veremos en el capítulo 10 cómo estos problemas se pueden solventar con la incorporación de características
reflectivas al sistema computacional base.
73
CCAAPPÍÍTTUULLO 66:
REFLECTIVIDAD COMPUTACIONAL
Previamente a este capítulo, hemos estudiado un conjunto de sistemas que dotaban
a distintos entornos computacionales de un cierto grado de flexibilidad. Cada uno de dichos
sistemas utilizaba distintas técnicas para conseguir adaptarse a las distintas necesidades
del usuario, así como para poder ampliar o extender sus características.
A lo largo de este capítulo, introduciremos los conceptos propios de una técnica
utilizada para conseguir un elevado grado de flexibilidad en determinados sistemas: la reflectividad20
computacional. Haremos un estudio genérico de las posibilidades de los sistemas
reflectivos y estableceremos distintas clasificaciones en función de distintos criterios.
En el siguiente capítulo, todos los términos definidos y estudiados aquí serán utilizados
para analizar los distintos sistemas dotados de reflectividad, así como para, posteriormente,
poder especificar y diseñar nuestro propio entorno computacional reflectivo.
6.1 Conceptos de Reflectividad
6.1.1 Reflectividad
Reflectividad o reflexión (reflection) es la propiedad de un sistema computacional que
le permite razonar y actuar sobre él mismo, así como ajustar su comportamiento en función
de ciertas condiciones [Maes87].
El dominio computacional de un sistema reflectivo –el conjunto de información
que computa– añade al dominio de un sistema convencional la estructura y comportamiento
de sí mismo. Se trata pues de un sistema capaz de acceder, analizar y modificarse a sí
mismo, como si de una serie de estructuras de datos propias de un programa de usuario se
tratase.
20 Traducción de reflection. Muchos autores la han traducido también como reflexión. Nosotros utilizaremos
el término reflectividad a lo largo de esta memoria.
CAPÍTULO 6
74
6.1.2 Sistema Base
Aquél sistema de computación que es dominio de otro sistema computacional distinto,
denominado metasistema [Golm97]. El sistema base es el motor de computación
(intérprete, software o hardware) de un programa de usuario.
6.1.3 Metasistema
Aquél sistema computacional que posee por dominio de computación a otro sistema
computacional denominado sistema base. Un metasistema es por lo tanto un intérprete
de otro intérprete.
6.1.4 Cosificación
Cosificación o concretización (reification) es la capacidad de un sistema para acceder
al estado de computación de una aplicación, como si se tratase de un conjunto de datos
propios de una aplicación de usuario [Smith82].
La cosificación es la posibilidad de acceso desde un sistema base a su metasistema,
en el que se puede manipular el sistema base como si de datos se tratase; es el salto del entorno
de computación de usuario al entorno de computación del intérprete de la aplicación
de usuario.
Si podemos cosificar21 un sistema, podremos acceder y modificar su estructura y
comportamiento y, por lo tanto, podremos añadir a su dominio computacional su propia
semántica; estaremos trabajando pues, con un sistema reflectivo.
21 Cosificar o concretizar: transformar una representación, idea o pensamiento en cosa.
Reflectividad Computacional
75
Sistema Base
Meta-Sistema
Reflectividad
(conexión causal)
Cosificación
Objeto en
sistema Base
Meta-Objeto
Objeto computado
en el Sistema Base
Objeto computado
en el Meta-Sistema
Interpreta
Interpreta
Programa
Usuario
Interpreta
Intérprete
Software o
Hardware
Figura 6.1: Entorno de computación reflectivo.
6.1.5 Conexión Causal
Un sistema es causalmente conexo si su dominio computacional alberga el entorno
computacional de otro sistema (permite la cosificación del sistema base) y, al modificar la
representación de éste, los cambios realizados causan un efecto en su propia computación
(la del sistema base) [Maes87b].
Un sistema reflectivo es aquél capaz de ofrecer cosificación completa de su sistema
base, e implementa un mecanismo de conexión causal. La forma en la que los distintos
sistemas reflectivos desarrollan esta conexión varía significativamente.
6.1.6 Metaobjeto
Identificando la orientación a objetos como paradigma del sistema reflectivo, puede
definirse un metaobjeto como un objeto del metasistema que contiene información y comportamiento
propia del sistema base [Kiczales91]. La abstracción del metaobjeto no es necesariamente
la de un objeto propio del sistema base, sino que puede representar cualquier
elemento que forme parte de éste –por ejemplo, el mecanismo del paso de mensajes.
CAPÍTULO 6
76
6.1.7 Reflectividad Completa
La conexión causal no es suficiente para un sistema reflectivo íntegro. Cuando la
cosificación de un sistema es total, desde el metasistema se puede acceder a cualquier elemento
del sistema base y la conexión causal se produce para cualquier elemento modificado,
entonces ese sistema posee reflectividad completa (completeness) [Blair97]. En este caso,
el metasistema debe ofrecer una representación íntegra del sistema base (todo podrá ser
modificado).
Como veremos en el estudio de sistemas reales (capítulo 7), la mayoría de los sistemas
existentes limitan a priori el conjunto de características del sistema base que pueden ser
modificados mediante el mecanismo de reflectividad implementado.
6.2 Reflectividad como una Torre de Intérpretes
Para explicar el concepto de reflectividad computacional, así como para posteriormente
implementar prototipos de demostración de su utilidad, Smith identificó el concepto
de reflectividad computacional como una torre de intérpretes [Smith82].
Diseñó un entorno de trabajo en el que todo intérprete de un lenguaje era a su vez
interpretado por otro intérprete, estableciendo una torre de interpretaciones. El programa
de usuario se ejecuta en un nivel de interpretación 0; cobra vida gracias a un intérprete que
lo anima desde el nivel computacional 1. En tiempo de ejecución el sistema podrá cosificarse,
pasando el intérprete del nivel 1 a ser computado por otro intérprete –la reflectividad
computacional implica una computación de la computación. En este caso (Figura 6.2.b) un
nuevo intérprete’, desde el nivel 2, pasa a computar el intérprete inicial. El dominio computacional
del intérprete’ –mostrado en la Figura 6.2 por un doble recuadro– estará formado
por la aplicación de usuario (nivel 0) y la interpretación directa de éste (nivel 1). En esta
situación se podrá acceder tanto a la aplicación de usuario (por ejemplo, para modificar sus
objetos) como a la forma en la que éste es interpretado (por ejemplo, para modificar la semántica
del paso de mensajes). El reflejo de dichos cambios en el nivel 1 es lo que se denomina
reflectividad o reflexión.
Programa de
Usuario
Intérprete
Interpreta
Programa de
Usuario
Intérprete
Interpreta
Interpreta
Intérprete’
Programa de
Usuario
Intérprete
Interpreta
Interpreta
Intérprete’
Interpreta
Intérprete’’
Nivel 0
Nivel 1
Nivel 2
Nivel 3
Dominios Computacionales
a)
b) c)
Cosificación Cosificación
Figura 6.2: Torre de intérpretes definida por Smith.
La operación de cosificación se puede aplicar tantas veces como deseemos. Si el intérprete’
procesa al intérprete del nivel 1, ¿podemos cosificar este contexto para obtener un
Reflectividad Computacional
77
nuevo nivel de computación? Sí; se crearía un nuevo intérprete’’ que procesare los niveles
0, 1 y 2, ubicándose éste en el nivel computacional 3. En este caso reflejaríamos la forma en
la que se refleja la aplicación de usuario (tendríamos una meta-meta-computación).
Un ejemplo clásico es la depuración de un sistema (debug). Si estamos ejecutando un
sistema y deseamos depurar mediante una traza todo su conjunto, podremos cosificar éste,
para generar información relativa a su ejecución desde su intérprete. Todos los pasos de su
interpretación podrán ser trazados. Si deseamos depurar el intérprete de la aplicación en
lugar de la propia aplicación, podremos establecer una nueva cosificación aplicando el
mismo sistema [Douence99].
Este entorno de trabajo fue definido por Smith como una torre de intérpretes
[Smith82]. Definió el concepto de reflectividad, y propuso la semántica de ésta para los
lenguajes de programación, sin llevar a cabo una implementación. La primera implementación
de este sistema fue realizada mediante la modificación del lenguaje Lisp [Steele90],
denominándolo 3-Lisp [Rivières84].
La torre infinita de intérpretes siempre tiene un nivel máximo de computación.
Siempre existirá un intérprete que no sea interpretado a su vez por otro procesador: un
intérprete hardware o microprocesador. Un microprocesador es un intérprete de un lenguaje
de bajo nivel, implementado físicamente.
En la Figura 6.3 mostramos un ejemplo real de una torre de intérpretes. Se trata de
la implementación de un intérprete de un lenguaje de alto nivel, en el lenguaje de programación
Java [Gosling96].
Binario 80x86
Código C
Java
Virtual
Machine
Java
Virtual
Machine
Ejecuta
Compilador
de C a 80x86
80x86
Código binario JVM
Código Java
Intérprete
del Lenguaje
alto nivel
Ejecuta
Compilador
de Java
Código Alto Nivel
Programa
de usuario
Ejecuta
Nivel 0
Nivel 1
Nivel 2
Nivel 3
Intérprete
del Lenguaje
alto nivel
Aplicaciones
Lenguajes Programación
Figura 6.3: Ejemplo de torre de intérpretes en la implementación de un intérprete en Java.
Java es un lenguaje que se compila a un código binario de una máquina abstracta
denominada “Java Virtual Machine” [Lindholm96]. Para interpretar este código –
previamente compilado– podemos utilizar un emulador de la máquina virtual programado
en C y compilado, por ejemplo, para un i80X86. Finalmente el código binario de este miCAPÍTULO
6
78
croprocesador es interpretado una implementación física de éste. En el ejemplo expuesto
tenemos un total de cuatro niveles computacionales en la torre de intérpretes.
Si pensamos en el comportamiento o la semántica de un programa como una función
de nivel n, ésta podrá ser vista realmente como una estructura de datos desde el nivel
n+1. En el ejemplo, el intérprete del lenguaje de alto nivel define la semántica de dicho
lenguaje, y la máquina virtual de Java define la semántica del intérprete del lenguaje de alto
nivel. Por lo tanto, el intérprete de Java podrá modificar la semántica computacional del
lenguaje de alto nivel, obteniendo así reflectividad computacional.
Cada vez que en la torre de intérpretes nos movamos en un sentido de niveles ascendente,
podemos identificar esta operación como cosificación22 (reification). Un movimiento
en el sentido contrario se identificará como reflexión (reflection) [Smith82].
6.3 Clasificaciones de Reflectividad
Pueden establecerse distintas clasificaciones en el tipo de reflectividad desarrollado
por un sistema, en función de diversos criterios. En este punto identificaremos dichos criterios,
clasificaremos los sistemas reflectivos basándonos en el criterio seleccionado, y describiremos
cada uno de los tipos.
Para cada clasificación de sistemas reflectivos identificado, mencionaremos algún
ejemplo existente para facilitar la comprensión de dicha división. Sin embargo, el estudio
detallado de los distintos entornos reflectivos existentes se llevará a cabo en el capítulo 7.
6.3.1 Qué se refleja
Hemos definido reflectividad en § 6.1 como “la propiedad de un sistema computacional
que le permite razonar y actuar sobre él mismo”. El grado de información que un
sistema posee acerca de sí mismo –aquello susceptible de ser cosificado y reflejado– establece
la siguiente clasificación.
6.3.1.1 Introspección
Capacidad de un sistema para poder inspeccionar u observar, pero no modificar, los
objetos de un sistema [Foote92]. En este tipo de reflectividad, se facilita al programador
acceder al estado del sistema en tiempo de ejecución: el sistema ofrece la capacidad de conocerse
a sí mismo.
Esta característica se ha utilizado de forma pragmática en numerosos sistemas. A
modo de ejemplo, la plataforma Java [Kramer96] ofrece introspección mediante su paquete
java.reflect [Sun97d]. Gracias a esta capacidad, en Java es posible almacenar un objeto
en disco sin necesidad de implementar un solo método: el sistema accede de forma introspectiva
al objeto, analiza sus atributos y los convierte en una secuencia de bytes para su
posterior envío a un flujo23 [Eckel2000]. Sobre este mismo mecanismo Java implementa
además su sistema de componentes JavaBeans [Sun96]: dado un objeto, en tiempo de ejecución,
se determinará su conjunto de propiedades y operaciones.
Otro ejemplo de introspección, en este caso para código nativo compilado, es la
adición de información en tiempo de ejecución al estándar ISO/ANSI C++ (RTTI, Run-
Time Type Information) [Kalev98]. Este mecanismo introspectivo permite conocer la clase de
22 Ciertamente hacemos datos (o cosa) un comportamiento.
23 Este proceso se conoce como “serialización” (serialization).
Reflectividad Computacional
79
la que es instancia un objeto, para poder ahormar éste de forma segura respecto al tipo
[Cardelli97].
6.3.1.2 Reflectividad Estructural
Se refleja el estado estructural del sistema en tiempo de ejecución –elementos tales
como las clases, el árbol de herencia, la estructura de los objetos y los tipos del lenguaje–
permitiendo tanto su observación como su manipulación [Ferber88].
Mediante reflectividad estructural, se puede acceder al estado de la ejecución de una
aplicación desde el sistema base. Podrá conocerse su estado, acceder las distintas partes del
mismo, y modificarlo si se estima oportuno. De esta manera, una vez reanudada la ejecución
del sistema base (después de producirse la reflexión), los resultados pueden ser distintos
a los que se hubieren obtenido si la modificación de su estado no se hubiera llevado a
cabo.
Ejemplos de sistemas de naturaleza estructuralmente reflectiva son Smalltalk-80
[Goldberg83] y Self [Ungar87]. La programación sobre estas plataformas se centra en ir
creando los objetos mediante sucesivas modificaciones de su estructura. No existe diferencia
entre tiempo de diseño y tiempo de ejecución; al poder acceder a toda la estructura del
sistema, el programador se encuentra siempre en tiempo de ejecución modificando dicha
estructura (§ 4.3).
6.3.1.3 Reflectividad Computacional
También denominada reflectividad de comportamiento (behavioural reflection). Se refleja
el comportamiento exhibido por un sistema computacional, de forma que éste pueda
computarse a sí mismo mediante un mecanismo de conexión causal (§ 6.1) [Maes87]. Un
sistema dotado de reflectividad computacional puede modificar su propia semántica; el
propio comportamiento del sistema podrá cosificarse para su posterior manipulación.
Un ejemplo de abstracción de reflectividad computacional es la adición de las
“Proxy Classes” [Sun99] a la plataforma Java2. Apoyándose en el paquete de introspección
(java.reflect), se ofrece un mecanismo para modificar el paso de mensajes: puede
manipularse dinámicamente la forma en la que un objeto interpreta la recepción de un
mensaje24. De esta forma la semántica del paso de mensajes puede ser modificada.
Otros ejemplos reducidos de reflectividad computacional en la plataforma Java son
la posibilidad de sustituir el modo en el que las clases se cargan en memoria y el grado de
seguridad de ejecución de la máquina abstracta. Gracias a las clases
java.lang.ClassLoader y java.lang.SecurityManager [Gosling96], se puede
modificar en el sistema la semántica obtención del código fuente (de este modo se consigue
cargar un applet de un servidor web) y el modo en el que una aplicación puede acceder a su
sistema nativo (el sistema de seguridad frente a virus, en la ejecución de applets, conocido
como sandbox [Orfali98]).
6.3.1.4 Reflectividad Lingüística
Un lenguaje de programación posee unas especificaciones léxicas [Cueva93], sintácticas
[Cueva95] y semánticas [Cueva95b]. Un programa correcto es aquél que cumple las
tres especificaciones descritas. La semántica del lenguaje de programación identifica el significado
de cualquier programa codificado en dicho lenguaje, es decir cuál será su comportamiento
al ejecutarse.
24 Estudiaremos este mecanismo, en mayor profundidad, en el siguiente capítulo.
CAPÍTULO 6
80
La reflectividad computacional de un sistema nos permite cosificar y reflejar su semántica;
la reflectividad lingüística [Ortín2001] nos permite modificar cualquier aspecto del
lenguaje de programación utilizado: mediante el lenguaje del sistema base se podrá modificar
el propio lenguaje (por ejemplo, añadir operadores, construcciones sintácticas o nuevas
instrucciones).
Un ejemplo de sistema dotado de reflectividad lingüística es OpenJava [Chiba98].
Ampliando el lenguaje de acceso al sistema, Java, se añade una sintaxis y semántica para
aplicar patrones de diseño [GOF94], de forma directa, por el lenguaje de programación –
amolda el lenguaje a los patrones Adapter y Visitor [Tatsubori98].
6.3.2 Cuándo se produce el reflejo
En función de la clasificación anterior, un sistema determina lo que puede ser cosificado
para su posterior manipulación. En este punto clasificaremos los sistemas reflectivos
en función del momento en el que se puede llevar a cabo dicha cosificación.
6.3.2.1 Reflectividad en Tiempo de Compilación
El acceso desde el sistema base al metasistema se realiza en el momento en el que el
código fuente está siendo compilado, modificándose el proceso de compilación y por lo
tanto las características del lenguaje procesado [Golm97].
Tomando Java como lenguaje de programación, OpenJava es una modificación de
éste que le otorga capacidad de reflejarse en tiempo de compilación [Chiba98]. En lugar de
modificar la máquina virtual de la plataforma para que ésta posea mayor flexibilidad, se
rectifica el compilador del lenguaje con una técnica de macros que expande las construcciones
del lenguaje. De esta forma, la eficiencia perdida por la flexibilidad del sistema queda
latente en tiempo de compilación y no en tiempo de ejecución.
Lo que pueda ser modificado en el sistema está en función de la clasificación §
6.3.1. En el ejemplo de OpenJava se pueden modificar todos los elementos; verbigracia, la
invocación de métodos, el acceso a los atributos, operadores del lenguaje o sus tipos. El
hecho de que el acceso al metasistema se produzca en tiempo de compilación implica que
una aplicación tiene que prever su flexibilidad antes de ser ejecutada; una vez que esté corriendo,
no podrá accederse a su metasistema de una forma no contemplada en su código
fuente.
6.3.2.2 Reflectividad en Tiempo de Ejecución
En este tipo de sistemas, el acceso del metasistema desde el sistema base, su manipulación
y el reflejo producido por un mecanismo de conexión causal, se lleva a cabo en
tiempo de ejecución [Golm97]. En este caso, una aplicación tendrá la capacidad de ser
flexible de forma dinámica, es decir, podrá adaptarse a eventos no previstos25 cuando esté
ejecutándose.
El sistema MetaXa, previamente denominado MetaJava, modifica la máquina virtual
de la plataforma Java para poder ofrecer reflectividad en tiempo de ejecución [Kleinöder96].
A las diferentes primitivas semánticas de la máquina virtual se pueden asociar metaobjetos
que deroguen el funcionamiento de dichas primitivas: el metaobjeto define la
nueva semántica. Ejemplos clásicos de utilización de esta flexibilidad son la modificación
del paso de mensajes para implementar un sistema de depuración, auditoría o restricciones
de sistemas en tiempo real [Golm97b].
25 No previstos en tiempo de compilación –cuando la aplicación se esté siendo implementada.
Reflectividad Computacional
81
6.3.3 Cómo se expresa el acceso al metasistema
En función del modo en el que se exprese el metasistema desde el sistema base,
podremos establecer la siguiente clasificación.
6.3.3.1 Reflectividad Procedural
La representación del metasistema se ofrece de forma directa por el programa que
implementa el metasistema [Maes87]. En este tipo de reflectividad, el metasistema y el sistema
base utilizan el mismo modelo computacional; si nos encontramos en un modelo de
programación orientado a objetos, el acceso al metasistema se facilitará utilizando este paradigma.
El hecho de acceder de forma directa a la implementación del sistema ofrece dos
ventajas frente a la reflectividad declarativa:
1. La totalidad del sistema es accesible y por lo tanto cualquier parte de éste podrá
ser manipulada. No existen restricciones en el acceso.
2. La conexión causal es automática [Blair97]. Al acceder directamente a la implementación
del sistema base, no es necesario desarrollar un mecanismo de actualización
o reflexión.
6.3.3.2 Reflectividad Declarativa
En la reflectividad declarativa, la representación del sistema en su cosificación es
ofrecida mediante un conjunto de sentencias representativas del comportamiento de éste
[Maes87]. Haciendo uso de estas sentencias, el sistema podrá ser adaptado.
La ventaja principal que ofrece esta clasificación es la posibilidad de elevar el nivel
de abstracción, a la hora de representar el metasistema. Las dos ventajas de la reflectividad
procedural comentadas previamente, se convierten en inconvenientes para este tipo de
sistemas.
La mayor parte de los sistemas existentes ofrecen un mecanismo de reflectividad
declarativa. En el caso de MetaXa [Kleinöder96], se ofrece una representación de acceso a
las primitivas de la máquina abstracta modificables.
6.3.4 Desde Dónde se puede modificar el sistema
El acceso reflectivo a un sistema se puede llevar a cabo desde distintos procesos. La
siguiente clasificación no es excluyente: un sistema puede estar incluido en de ambas clasificaciones.
6.3.4.1 Reflectividad con Acceso Interno
Los sistemas con acceso interno (inward) a su metasistema permiten modificar una
aplicación desde la definición de éste (su codificación) [Foote92]. Esta característica define
aquellos sistemas que permiten modificar una aplicación desde sí misma.
La mayoría de los sistemas ofrecen esta posibilidad. En MetaXa [Kleinöder96], sólo
la propia aplicación puede modificar su comportamiento.
6.3.4.2 Reflectividad con Acceso Externo
El acceso externo de un sistema (outward) permite la modificación de una aplicación
mediante otro proceso distinto al que será modificado [Foote92]. En un sistema con esta
característica, cualquier proceso puede modificar al resto. La ventaja es la flexibilidad obteCAPÍTULO
6
82
nida; el principal inconveniente, la necesidad de establecer un mecanismo de seguridad en
el acceso entre procesos (§ 6.4).
El sistema Smalltalk-80 ofrece un mecanismo de reflectividad estructural con acceso
externo [Mevel87]: el programador puede acceder, y modificar en su estructura, cualquier
aplicación o proceso del sistema.
6.3.5 Cómo se ejecuta el sistema
La ejecución del sistema reflectivo puede darse de dos formas: mediante código nativo
o mediante la interpretación de un código intermedio.
6.3.5.1 Ejecución Interpretada
Si el sistema se ejecuta mediante una interpretación software, las características de reflectividad
dinámica se pueden ofrecer de una forma más sencilla. Ejemplos de este tipo de
sistemas son Self [Smith95], Smalltalk-80 [Krasner83] o Java [Kramer96]. Estas plataformas
de interpretación ofrecen características reflectivas de un modo sencillo, al encontrase el
intérprete y el programa ejecutado en el mismo espacio de direcciones (§ 3.3.3).
Su principal inconveniente es la pérdida de eficiencia por la inclusión del proceso de
interpretación.
6.3.5.2 Ejecución Nativa
Se produce cuando se compila código nativo, capaz de ofrecer cualquier grado de
reflectividad indicado en § 6.3.1. Un primer ejemplo es el mecanismo RTTI del ANSI/ISO
C++, que ofrece introspección en tiempo de ejecución para un sistema nativo [Kalev98].
Esta clasificación es distinta a la indicada en § 6.3.2. Puede producirse el reflejo del
sistema en tiempo de ejecución, tanto para sistemas compilados (nativos) como interpretados.
Un ejemplo de ello es el sistema Iguana [Gowing96]. Este sistema implementa un
compilador de C++ que ofrece reflectividad computacional en tiempo de ejecución. El
código generado es una ampliación del estándar RTTI.
La principal ventaja de este tipo de sistemas es su eficiencia; sin embargo, su implementación
es más costosa que la de un intérprete, puesto que debemos generar código
capaz de ser modificado dinámicamente.
6.4 Hamiltonians versus Jeffersonians
Durante el desarrollo del sistema democrático americano, existían dos escuelas de
pensamiento distintas: los Hamiltonians y los Jeffersonians. Los Jeffersonians pensaban que cualquier
persona debería tener poder para llevar a cabo decisiones como la elección del presidente
de los Estados Unidos. Por el contrario, los Hamiltonians eran de la opinión de que
sólo una elite educada para ello, y no todo el pueblo, debería influir en tales decisiones.
Esta diferencia de pensamiento ha sido utilizada para implementar distintas políticas en
sistemas computacionales reflectivos [Foote92].
La vertiente Jeffersoniana otorga al programador de un sistema reflectivo la posibilidad
de poder modificar cualquier elemento de éste. No restringe el abanico de posibilidades
en el acceso al metasistema. El hecho de permitir modificar cualquier característica del
sistema, en cualquier momento y por cualquier usuario, puede desembocar en una semántica
del sistema prácticamente incomprensible. Un ejemplo de esto, es un programa escrito
Reflectividad Computacional
83
en C++ con uso intensivo de sobrecarga de operadores (reflectividad del lenguaje): el resultado
puede ser un código fuente prácticamente ininteligible.
Muchos autores abogan por el establecimiento de un mecanismo de seguridad que
restrinja el modo en el una aplicación pueda modificar el sistema [Foote90]. Un ejemplo
clásico es establecer una lista de control de acceso en el que sólo los administradores del
sistema puedan cambiar la semántica global. Por el contrario, el resto de usuarios podrán
modificar únicamente la semántica de sus aplicaciones.
Como mencionamos en el capítulo 1, esta tesis trata de buscar un entorno computacional
con un elevado grado de flexibilidad. No es nuestro objetivo la implementación de
un sistema de seguridad encargado de controlar el nivel de reflexión permitido –esto formaría
parte del desarrollo de una plataforma comercial. Por esta razón, nos centraremos en
la vertiente Jeffersoniana, teniendo siempre en cuenta los riesgos que ésta puede llegar a
suponer.
6.5 Formalización
Las bases de la computación han sido formalizadas mediante matemáticas, basándose
principalmente en -calculus [Barendregt81] y extensiones de éste. Un ejemplo es la
formalización realizada por Mendhekar en la que define una pequeña lógica para lenguajes
funcionales reflectivos [Mendhekar96].
Mendhekar propone un cálculo reducido de computación, denominado
v −calculus , y define sobre él una lógica ecuacional. Se aprecia cómo es realmente difícil
formalizar un sistema reflectivo y las limitadas ventajas que proporciona. La investigación
acerca de reflectividad computacional, tiene una rama relativa a su formalización y estudio
matemático, pero su complejidad hace que vaya por detrás de las implementaciones y
estudios de prototipos computacionales.
En esta tesis, el estudio y diseño de las distintas técnicas para obtener reflectividad
computacional serán abordadas desde el punto de vista pragmático y empírico; no formal.
85
CCAAPPÍÍTTUULLO 77:
PANORÁMICA DE UTILIZACIÓN DE REFLECTIVIDAD
En el capítulo anterior clasificábamos la reflectividad en función de distintos criterios.
Utilizando dicha clasificación y los conceptos definidos en el mencionado capítulo,
estableceremos un estudio de un conjunto de sistemas existentes que utilizan de algún modo
reflectividad.
Agruparemos los sistemas en función de ciertas características comunes, especificaremos
brevemente su funcionamiento, y, en el ámbito de grupo, detallaremos los beneficios
aportados y carencias existentes de los sistemas estudiados. A modo de conclusión y apoyándonos
en los requisitos definidos en el capítulo 2, analizaremos las principales carencias
del conjunto total de sistemas existentes.
7.1 Sistemas Dotados de Introspección
En el capítulo anterior, definíamos introspección como la capacidad de acceder a la
representación de un sistema en tiempo de ejecución. Mediante introspección únicamente
se puede conocer el sistema, sin permitir la modificación de éste y sin producirse, por lo
tanto, su reflejo mediante un sistema de conexión causal. Por esto determinados autores
definen introspección como reflectividad estructural “de sólo lectura” [Foote90].
La introspección es probablemente el tipo de reflectividad más utilizado actualmente
en los sistemas comerciales. Se ha desarrollado en áreas como la especificación de componentes,
arquitecturas de objetos distribuidos y programación orientada a objetos.
ANSI/ISO C++ RunTime Type Information (RTTI)
El lenguaje de programación C++ es un lenguaje compilado que genera código nativo
para cualquier tipo de plataforma [Cueva98]. En la fase de ejecución de dicho código
nativo, existe una carencia del conocimiento de los tipos de los objetos en tiempo de ejecución.
El estándar ANSI/ISO de este lenguaje amplió su especificación añadiendo cierta
información respecto al tipo (RTTI) en tiempo de ejecución, para poder dar seguridad respecto
al tipo al programador de aplicaciones [Kalev98].
El caso típico en la utilización de este mecanismo es el ahormado descendente
(downcast) en una jerarquía de herencia [Eckel2000b]. Un puntero o referencia a una clase
base se utiliza genéricamente para englobar cualquier objeto de esa clase o derivado (Figura
7.1). Si tenemos un puntero a dicha clase, ¿qué mensajes le podemos pasar? Sólo aquéllos
CAPÍTULO 7
86
definidos en la clase base; por lo tanto, en el caso de un objeto instancia de una clase derivada,
se pierde su tipo –no se puede invocar a los métodos propios de la clase derivada.
ClaseBase
método1()
método2()
método3()
DerivadaA
medodoDerivadaA()
DerivadaB
metodoDerivadaB()
Figura 7.1: Punteros o referencias “Base” permiten la utilización genérica de objetos derivados.
La resolución de este problema se lleva a cabo añadiendo a los objetos información
dinámica relativa a su tipo (introspección). Mediante el estándar RTTI podemos preguntarle
a un objeto cuál es su tipo y recuperar éste si lo hubiésemos perdido [Stroustrup98]. El
siguiente fragmento de código referente a la jerarquía de clases mostrada en la Figura 7.1,
recupera el tipo del objeto gracias al operador dynamic_cast que forma parte del estándar
RTTI.
Base *ptrBase;
DerivadaA *ptrDerivadaA=new DerivadaA;
// * Tratamiento genérico mediante herencia
ptrBase=ptrDerivadaA;
// * SÓLO se pueden pasar mensajes de la clase Base
ptrBase->metodoBase();
// * Solicitamos información del tipo del objeto
ptrDerivadaA=dynamic_cast(ptrBase);
// * ¿Es de tipo DerivadaA?
if (ptrDerivadaA)
// * Recuperamos el tipo; se pueden pasar mensajes de Derivada
ptrDerivadaA->mentodoDerivadaA();
Al acceder al objeto derivado mediante el puntero base, se pierde el tipo de éste no
pudiendo invocar a los mensajes derivados. Gracias a la utilización de RTTI, el programador
puede recuperar el tipo de un objeto, e invocar a sus propios mensajes.
Plataforma Java
Java define para su plataforma [Kramer96] una interfaz de desarrollo de aplicaciones
(API, Application Programming Interface) que proporciona introspección, denominada The
Java Reflection API [Sun97d]. Este API permite acceder en tiempo de ejecución a la representación
de las clases, interfaces y objetos existentes en la máquina virtual. Las posibilidades
que ofrece al programador son:
Determinar la clase de la que un objeto es instancia.
Obtener información sobre los modificadores de una clase [Gosling96], sus métodos,
campos, constructores y clases base.
Encontrar las declaraciones de métodos y constantes pertenecientes a un interface.
Panorámica de Utilización de Reflectividad
87
Crear una instancia de una clase totalmente desconocida en tiempo de compilación.
Obtener y asignar el valor de un atributo de un objeto en tiempo de ejecución,
sin necesidad de conocer éste en tiempo de compilación.
Invocar un método de un objeto en tiempo de ejecución, aun siendo éste desconocido
en tiempo de compilación.
Crear dinámicamente un array26 y modificar los valores de sus elementos.
La restricción de utilizar sólo este tipo de operaciones pasa por la imposibilidad de
modificar el código de un método en tiempo de ejecución. La única tarea que se puede
realizar con respecto a los métodos es su invocación dinámica. La inserción, modificación y
borrado de miembros (métodos y atributos) no es posible en la plataforma Java.
Sobre este API de introspección se define el paquete java.beans, que ofrece un
conjunto de clases e interfaces para desarrollar la especificación de componentes software de
la plataforma Java: los JavaBeans [Sun96].
Cabe preguntarse por qué es necesaria la introspección para el desarrollo de un sistema
de componentes. Un componente está constituido por métodos, propiedades y eventos.
Sin embargo, una aplicación que vaya a utilizar un conjunto de componentes no puede
saber a priori la estructura que éstos van a tener. Para conocer el conjunto de estas tres
características, de forma dinámica, se utiliza la introspección: permite acceder a los métodos
de instancia públicos y conocer su signatura.
Aplicación
Clases para
Introspección
Bean
Reflection
API
- Obtener Métodos
- Buscar Método
- Comprobar Parámetros
- Invocar al Método
Conocimiento
Propiedades
Operaciones
Eventos
Figura 7.2: Utilización de introspección en el desarrollo de un sistema de componentes.
El paquete de introspección identifica una forma segura de acceder al API de
reflectividad, y facilita el conocimiento y acceso de métodos, propiedades y eventos de un
componente en tiempo de ejecución. En la Figura 7.2 se aprecia cómo una aplicación solicita
el valor de una propiedad de un Bean. Mediante el paquete de introspección se busca el
método apropiado y se invoca, devolviendo el valor resultante de su ejecución.
Otro ejemplo de utilización de la introspección en la plataforma Java es la posibilidad
de convertir cualquier objeto a una secuencia de bytes27, para posteriormente poder
almacenarlo en disco o transmitirlo a través de una red de computadores. Un objeto en
Java que herede del interface java.io.Serializable, sin necesidad de implementar
ningún método, podrá ser convertido a una secuencia de bytes. ¿Cómo es posible conocer
el estado del objeto en tiempo de ejecución? Mediante el acceso introspectivo a éste [Eckel2000].
26 En Java los arrays son objetos primitivos.
27 También denominado “serialización” (serialization).
CAPÍTULO 7
88
objeto
agregado:Clase
n:int
Agregado:Clase
id:int
descripción:String
Serialización
Acceso
Introspectivo
Representación en
series de bytes
n id
agregado
objeto
descripción
Figura 7.3: Conversión de un objeto a una secuencia de bytes, mediante un acceso introspectivo.
Mediante el paquete java.reflect se va consultando el valor de todos los atributos
del objeto y convirtiendo éstos a bytes. Si un atributo de un objeto es a su vez otro
objeto, se repite el proceso de forma recursiva. Del mismo modo es posible crear un objeto
a raíz de un conjunto de bytes –siguiendo el proceso inverso.
CORBA
CORBA (Common Object Request Broker Architecture) es un ambicioso proyecto del
consorcio OMG (Object Management Group) enfocado a diseñar un middleware que proporcione
una arquitectura de objetos distribuidos, en la que puedan definirse e interoperar distintos
componentes, sin tener en cuenta el lenguaje y la plataforma en la que éstos hayan sido
implementados [OMG97].
En el desarrollo de CORBA, se utilizó el concepto de introspección para permitir
crear aplicaciones distribuidas portables y flexibles, capaces de reaccionar a futuros cambios
producidos en el sistema y, por lo tanto, no dependientes de una interfaz de acceso monolítica.
Para ello, CORBA define un modo de realizar invocaciones dinámicas a métodos de
objetos remotos –DII, Dynamic Invocation Interface [OMG95]– sin necesidad de conocer éstos
en el momento de crear la aplicación cliente.
Objetos Cliente
Invocación
Dinámica
DDI
Stubs Interfaz
del ORB
Skeletons
Estáticos
Skeletons
Dinámicos
SDI
Adaptador
Objetos
Implementación de Objetos Servidor
Almacén de
Implementación
Almacén de
Interfaces
Object Request Broker
Figura 7.4: Elementos de la arquitectura de objetos distribuidos CORBA.
Panorámica de Utilización de Reflectividad
89
Como se muestra en la Figura 7.428, CORBA proporciona metadatos de los objetos
existentes en tiempo de ejecución, guardando en el almacén de interfaces (Interface Repository)
las descripciones de las interfaces de todos los objetos CORBA. El módulo de invocación
dinámica, DII, apoyándose en el almacén de interfaces, permite al cliente conectarse al
ORB, solicitar la descripción de un objeto, analizarla, y posteriormente invocar al método
adecuado.
El acceso dinámico a métodos de objetos distribuidos tiene una serie de ventajas.
No es necesaria la existencia de stubs para el cliente, el acceso se desarrolla en su
totalidad en tiempo de ejecución.
Para compilar la aplicación cliente que realiza las llamadas a los objetos servidores,
no es necesario haber implementado previamente el servidor.
En las modificaciones del servidor no es necesario recompilar la especificación
IDL para el cliente; éste es capaz de amoldarse a los cambios.
La depuración del cliente es más sencilla puesto que podremos ver el aspecto
del servidor desde la plataforma cliente, sin necesidad de llevar una traza paralela
de otro proceso.
Los mensajes de error no producen una parada en la ejecución sino que pueden
ser controlados desde la aplicación cliente.
El control de versiones de objetos servidores puede ser llevado a cabo mediante
el descubrimiento de los nuevos objetos, coexistiendo así distintas versiones de
aplicaciones CORBA.
Es la base para crear un sistema de componentes CORBA [OMG98].
Almacén de
Interfaces
ORB
Petición de
Interfaz de Objeto Localización
del Objeto
Obtención
de la Interfaz
Devolución
de la Interfaz
Selección
del Método
Petición de
Invocación Método
Ejecución
Método
Devolución de
la invocación del
método
Cliente de
Objetos CORBA
Servidor de
Objetos CORBA
1 Red de
Computadores 2
3
4
5
6
7
8
Figura 7.5: Invocación dinámica de un método en CORBA utilizando DII.
En la Figura 7.5 se aprecia cómo un cliente, mediante la utilización del ORB, solicita
al módulo DII del servidor de objetos CORBA la interfaz de un objeto. Éste localiza la
descripción del objeto en el almacén de interfaces y se lo pasa al cliente. Finalmente, el
cliente selecciona el método deseado enviándole al objeto servidor el mensaje oportuno.
Todo este proceso se puede llevar a cabo de forma independiente a la implementación del
28 Para obtener una descripción más detallada del funcionamiento de cada uno de los módulos de la arquitectura
CORBA, consúltese [OMG95] o [Orfali98].
CAPÍTULO 7
90
objeto, puesto que en todo momento se trabaja con las descripciones de sus interfaces en
único un lenguaje: IDL (Interface Definition Language) [OMG96].
Bajo el mismo escenario de la Figura 7.5, se puede suponer el caso de no encontrar
el objeto, o método de éste, deseado por el cliente. En este caso no se producirá un error
en tiempo de ejecución, sino que el cliente podrá tener una respuesta adicional a esta situación.
El inconveniente del uso introspectivo de CORBA es, como en todos los sistemas
que aumentan su grado de flexibilidad, la pérdida de eficiencia en tiempo de ejecución producida
por la computación adicional necesaria para obtener la información introspectiva.
COM
COM (Component Object Model) es un modelo binario de interacción de objetos construido
por Microsoft [Microsoft95], cuyo principal objetivo es la creación de aplicaciones
mediante la unión de distintas partes –componentes– creadas en distintos lenguajes de programación
[Kirtland99]. El acceso a los componentes se realiza, al igual que en CORBA, a
través de interfaces (interface).
Al tratarse de código binario, nativo de una plataforma tras su compilación previa,
no puede obtenerse directamente la descripción de cada componente. La información necesaria
para obtener introspección, al igual que en CORBA, se añade al propio objeto para
que pueda ofrecerla en tiempo de ejecución. La gran diferencia frente a la postura de
CORBA es que en COM la descripción del componente la posee él mismo y no un almacén
de interfaces externo.
En COM un objeto puede implementar un número indefinido de interfaces, pero
¿cómo podemos conocer y obtener éstos en tiempo de ejecución? ¿Cómo se ofrece la introspección?
Mediante la utilización de un interface predefinido, denominado IUnknown.
Todo interface en COM hereda de IUnknown y éste obliga a implementar tres métodos:
AddRef, Release y QueryInterface [Box99]. Este último método,
QueryInterface, permite conocer a partir de un objeto el conjunto de interfaces que
éste implementa. De esta forma un objeto es capaz de ofrecer información acerca de sí
mismo: introspección.
Panorámica de Utilización de Reflectividad
91
MétodoA1
MétodoA2
MétodoA3 MétodoB1
MétodoB2
MétodoB3
IUnknown
InterfaceA
InterfaceB
Se accede al
Interface IUnknown
Se invoca a
QueryInterface
Se obtiene el
Interface A o B
Se invoca a cualquier
método de los
Interfaces A o B
QueryInterface
AddRef
Release
Objeto
COM
Componente
COM
Interfaces COM
1
2
3
4
Figura 7.6: Sistema de introspección en el modelo de componentes COM.
El cliente de un componente COM le pide a éste su interface por defecto, obteniendo
una referencia a un interface IUnknown –todos los interfaces heredan de éste. Mediante
la referencia obtenida, le pregunta al objeto si implementa un determinado interface invocando
a su método QueryInterface. En el caso afirmativo, el cliente consigue una nueva
referencia al interface deseado, pudiendo pasarle cualquier mensaje que éste especifique.
Microsoft aumentó el sistema de componentes COM haciéndolo distribuido –
DCOM [Brown98]–, apoyándose para ello en la introspección descrita. Posteriormente le
añadió características propias de sistemas de componentes transaccionales, rebautizándolo
como COM+ [Kirtland99b].
7.1.1 Aportaciones y Carencias de los Sistemas Estudiados
En este punto hemos visto cómo es importante la característica introspectiva en el
desarrollo de una plataforma. Desde el punto de vista del programador, es necesaria para
realizar operaciones seguras respecto al tipo (RTTI de C++) o para almacenar objetos en
disco (“Serialización” de la plataforma Java). Desde el punto de vista de un sistema operativo,
es necesaria para desarrollar un sistema de componentes (COM). Desde el punto de
vista de entornos distribuidos, es necesaria para desarrollar aplicaciones flexibles (CORBA
y Java).
La utilización de la introspección queda patente con numerosos ejemplos prácticos
de sistemas actuales reales. En el desarrollo de aplicaciones de comercio electrónico, es
común utilizar la transferencia de información independiente de la plataforma y autodescriptiva
en formato XML [W3C98]. Mediante la utilización de una librería –DOM
[W3C98b] o SAX [Megginson2000]– es posible conocer los datos en tiempo de ejecución
(introspección), eliminando el acoplamiento que se produce entre el cliente y el servidor en
muchos sistemas distribuidos. De esta forma, las aplicaciones ganan en flexibilidad pudiéndose
adaptar a los futuros cambios.
CAPÍTULO 7
92
En el desarrollo de una plataforma de computación flexible, la introspección es una
característica fundamental a ser otorgada (§ 2.1.5.2) a su motor computacional, obteniéndose
así un primer grado de flexibilidad gracias al conocimiento dinámico de la totalidad del
sistema (§ 2.3.1).
7.2 Sistemas Dotados de Reflectividad Estructural
Fijándonos en la clasificación realizada en el capítulo anterior, en § 6.3.1 se define
un sistema dotado de la capacidad de acceder y modificar su estructura como estructuralmente
reflectivo. Estudiaremos casos reales de este tipo de sistemas y analizaremos sus
ventajas e inconvenientes en función de los requisitos definidos en el capítulo 2.
Smalltalk-80
Es uno de los primeros entornos de programación que se han basado en reflectividad
estructural. El sistema Smalltalk-80 se puede dividir en dos elementos:
La imagen virtual, que es una colección de objetos que proporcionan funcionalidades
de diversos tipos.
La máquina virtual, que interpreta la imagen virtual y las aplicaciones de usuario.
En un principio se carga la imagen virtual en memoria y la máquina va interpretando
el código. Si deseamos conocer la estructura de alguna de las clases existentes, su descripción,
el conjunto de los métodos que posee o incluso la implementación de éstos, podemos
utilizar una aplicación desarrollada en el sistema denominada browser [Mevel87]
(Figura 7.7). El browser es una aplicación diseñada en Smalltalk que accede a todos los objetos
clase29 existentes en el sistema, y muestra su estructura y descripción. De la misma forma,
se puede acceder a los métodos de éstas. Este es un caso de utilización de la introspección
que ofrece el sistema. Gracias a la introspección, se consigue una documentación dinámicamente
actualizada de las clases del sistema.
29 En Smalltalk-80 todas las clases se identifican como un objeto en tiempo de ejecución. Los objetos que
identifican clases son instancias de clases derivadas de la clase Class.
Panorámica de Utilización de Reflectividad
93
Figura 7.7: Análisis del método inspect del objeto de clase Object, con la aplicación Browser de
Smalltalk-80.
De la misma forma que las clases, haciendo uso de sus características reflectivas, los
objetos existentes en tiempo de ejecución pueden ser inspeccionados gracias al mensaje
inspect [Goldberg89]. Mediante esta aplicación también podremos modificar los valores de
los distintos atributos de los objetos.
Vemos en la Figura 7.8, cómo es posible hacer uso de la reflectividad estructural
para modificar la estructura de una aplicación en tiempo de ejecución. Una utilidad dada al
mensaje inspect es la depuración de una aplicación sin necesidad de generar código intruso a
la hora de compilar.
CAPÍTULO 7
94
Figura 7.8: Invocando al método inspect del objeto Object desde un espacio de trabajo de Smalltalk-
80, obtenemos un acceso a las distintas partes de la instancia.
Lo buscado en el diseño de Smalltalk era obtener un sistema fácilmente manejable
por personas que no fuesen informáticos. Para ello se identificó el paradigma de la orientación
a objetos y la reflectividad estructural. Una vez diseñada una aplicación consultando la
información necesaria de las clases en el browser, se puede depurar ésta accediendo y modificando
los estados de los objetos en tiempo de ejecución. Se accede al error de un método
de una clase y se modifica su código, todo ello haciendo uso de la reflectividad estructural
en tiempo de ejecución del sistema.
Hemos visto cómo en Smalltalk-80 se utilizaron de forma básica los conceptos de
reflectividad estructural e introspección, para obtener un entorno sencillo de manejar y
autodocumentado.
La semántica del lenguaje Smalltalk viene dada por una serie de primitivas básicas
de computación de la máquina virtual, no modificables por el programador; carece por lo
tanto de reflectividad computacional.
Self, Proyecto Merlin
El sistema Self fue construido como una simplificación del sistema Smalltalk y también
estaba constituido por una máquina virtual y un entorno de programación basado en
el lenguaje Self [Ungar87]. La reducción principal respecto a Smalltalk se centró en la eliminación
del concepto de clase, dejando tan sólo la abstracción del objeto. Este tipo de lenguajes
orientados a objetos han sido denominados “basados en prototipos” –
profundizaremos en el estudio de éstos en el capítulo 8.
Este sistema, orientado a objetos puro e interpretado, fue utilizado en el estudio de
desarrollo de técnicas de optimización propias de los lenguajes orientados a objetos
[Chambers91]. Fueron aplicados métodos de compilación continua y compilación adaptable
[Hölzle94], métodos que actualmente han sido implantados a plataformas comerciales
como Java[Sun98].
Panorámica de Utilización de Reflectividad
95
El proyecto Merlin se creó para hacer que los ordenadores fuesen fácilmente utilizados
por los humanos, ofreciendo sistemas de persistencia y distribución implícitos [Assumpcao93].
El lenguaje seleccionado para desarrollar este sistema fue Self.
Para conseguir flexibilidad en el sistema, trataron de ampliar la máquina virtual con
un conjunto de primitivas de reflectividad (regions, reflectors, meta-spaces) [Cointe92]. La complejidad
de ésta creció de tal forma que su portabilidad a distintas plataformas se convirtió
en algo inviable [Assumpcao95]. Assumpçao propone en [Assumpcao95] el siguiente conjunto
de pasos para construir un sistema portable con capacidades reflectivas:
1. Implementar, sobre cualquier lenguaje de programación, un pequeño intérprete
de un subconjunto del lenguaje a interpretar. En su caso un intérprete de “tiny-
Self”. El requisito fundamental es que éste tenga reflectividad estructural.
2. Sobre lenguaje (tinySelf), se desarrolla un intérprete del lenguaje buscado (Self),
con todas sus características.
3. Implementamos una interfaz de modificación de las operaciones deseadas. La
codificación de un intérprete en su propio lenguaje ofrece total flexibilidad: todo
su comportamiento se puede modificar –incluso las primitivas computacionales–
puesto que tinySelf posee reflectividad estructural dinámica. Sólo debemos
implementar un protocolo de acceso a la modificación de las operaciones
deseadas.
Self
Lenguaje C
Intérprete
TinySelf
TinySelf
Intérprete
Self
Interpreta
Aplicación
Self
Interpreta
Mediante Reflectividad
Estructural una aplicación
accede y modifica el
modo en el que es
interpretada
Primer
Nivel
Computacional
Segundo
Nivel
Computacional
Lenguajes de
Programación
Figura 7.9: Consiguiendo reflectividad mediante la introducción de un nuevo intérprete.
El resultado es un intérprete de Self que permite modificar determinados elementos
de su computación, siempre que éstos hayan sido tenidos en cuenta en el desarrollo del
intérprete.
El principal problema es la eficiencia del sistema. El desarrollar dos niveles computacionales
(intérprete de intérprete) ralentiza la ejecución de la aplicación codificada en Self.
Lo que se propone para salvar este obstáculo [Assumpcao95] es implementar un traductor
de código tinySelf a otro lenguaje que pueda ser compilado a código nativo. Una vez desCAPÍTULO
7
96
arrollado éste, traducir el código propio del intérprete de Self –codificado en el lenguaje
tinySelf– a código nativo, reduciendo así un nivel computacional.
Self
Lenguaje C
Intérprete
TinySelf
TinySelf
Intérprete
Self
Aplicación
Self
Interpreta
Primer
Nivel
Computacional
Segundo
Nivel
Computacional
Interpreta
Compilador
TinySelf
Intérprete
Nativo
Self
Self
Aplicación
Self
Interpreta
Único
Nivel
Computacional
Figura 7.10: Reducción de un nivel de computación en la torre de intérpretes.
El resultado se muestra en la Figura 7.10: un único intérprete de Self en lugar de los
dos anteriores. Sin embargo, el producto final tendría dos inconvenientes:
1. Una vez que traduzcamos el intérprete, éste no podrá ofrecer la modificación
de una característica que no haya sido prevista con anterioridad. Si queremos
añadir alguna, deberemos volver al sistema de los dos intérpretes, codificarla,
probarla y, cuando no exista ningún error, volver a generar el intérprete nativo.
2. La implementación de un traductor de tinySelf a un lenguaje compilado es una
tarea difícil, puesto que el código generado deberá estar dotado de la información
necesaria para ofrecer reflectividad estructural en tiempo de ejecución.
Existen sistemas que implementan estas técnicas como por ejemplo Iguana
[Gowing96].
Al mismo tiempo, la ejecución de un código que ofrece información modificable
dinámicamente –sus objetos– ocupa más espacio, y añade una ralentización
en sus tiempos de ejecución.
ObjVlisp
ObjVlisp es un sistema creado como una evolución de Smalltalk-76 [Ingalls78],
desarrollado como un sistema extensible capaz de modificar y ampliar determinadas funcionalidades
de los lenguajes orientados a objetos [Cointe88].
El modelo computacional del sistema es definido por la implementación de una
máquina virtual en el lenguaje Lisp [Steele90]; ésta está dotada de un conjunto de primitivas
que trabajan con la abstracción principal del objeto. El modelo computacional del núcleo
del sistema se define con seis postulados [Cointe88]:
1. Un objeto está compuesto de un conjunto de miembros que pueden representar
información de éste (atributos) y su comportamiento (métodos). La diferencia
entre ambos es que los métodos son susceptibles de ser evaluados. Los miemPanorámica
de Utilización de Reflectividad
97
bros de un objeto pueden conocerse y modificarse dinámicamente (reflectividad
estructural).
2. La única forma de llevar a cabo una computación sobre un objeto es enviándole
un mensaje indicativo del método que queramos ejecutar.
3. Todo objeto ha de pertenecer a una clase que especifique su estructura y comportamiento.
Los objetos se crearán dinámicamente como instancias de una clase.
4. Una clase es también un objeto instancia de otra clase denominada metaclase.
Por lo tanto, aplicando el punto 3, una metaclase define la estructura y el comportamiento
de sus clases instanciadas. La metaclase inicial primitiva se denomina
CLASS, y es construida como instancia de sí misma.
5. Una clase puede definirse como subclase de otra(s) clase(s). Este mecanismo de
herencia permite compartir los miembros de las clases base por sus clases derivadas.
El objeto raíz en la jerarquía de herencia se denomina OBJECT.
6. Los miembros definidos por una clase describen un ámbito global para todos
los objetos instanciados a partir de dicha clase; todos sus objetos pueden acceder
a dichos miembros.
A partir de los seis puntos anteriores, se deduce que son establecidos, en tiempo de
ejecución, dos grafos de objetos en función de dos tipos de asociaciones existentes entre
ellos: la asociación “hereda de” –en la que el objeto raíz es OBJECT– y la asociación “instancia
de” –con objeto raíz CLASS. Un ejemplo de ambos grafos se muestra en la siguiente
figura:
OBJECT
CLASS MetaClaseB
ClaseA ClaseB
ObjetoA1
ObjetoA2
ObjetoB1
ObjetoB2
Objeto
Clase
Metaclase
Instancia de
Hereda de
Figura 7.11: Doble grafo de objetos en el sistema ObjVlisp.
La plataforma permite extender el sistema computacional gracias a la creación dinámica
de objetos y a la extensión de los existentes. Los nuevos objetos ofrecen un mayor
nivel de abstracción al usuario del sistema. Esto es una consecuencia directa de aplicar la
reflectividad estructural en tiempo de ejecución.
El sistema soporta un mecanismo de adaptabilidad basado en el concepto de metaclase.
Si queremos modificar el comportamiento de los objetos instancia de una clase, podemos
crear una nueva metaclase y establecer una asociación entre la clase y ésta, mediante
la relación “es instancia de”. El resultado es la modificación de parte de la semántica de los
objetos de dicha clase. En la Figura 7.11, la semántica de los objetos instancia de la clase B
es definida por su metaclase B.
CAPÍTULO 7
98
Las distintas modificaciones que puede hacer una metaclase en el funcionamiento
de los objetos de una clase son:
Modificación del funcionamiento del mecanismo de herencia.
Alteración de la forma en la que se crean los objetos, implementando un nuevo
comportamiento de la primitiva make-object.
Cambio de la acción llevada a cabo en la recepción de un mensaje.
Utilización de otro tipo de contenedor para coleccionar los miembros de un objeto.
Modificación del acceso a los miembros, estableciendo un mecanismo de ocultación
de la información.
Linguistic Reflection in Java
Para Graham Kirby y Ron Morrison Linguistic Reflection es la capacidad de un programa
para, en tiempo de ejecución, generar nuevos fragmentos de código e integrarlos en
su entorno de ejecución30 [Kirby98]. Implementaron un entorno de trabajo en el que añadían
al lenguaje de programación Java esta característica.
La capacidad de un sistema reflectivo para adaptarse a un contexto en tiempo de
ejecución es opuesta al concepto de tipo, que limita, en tiempo de compilación, el conjunto
de operaciones aplicables a un objeto [Cardelli97]. El objetivo del prototipo implementado
es ofrecer la generación dinámica de código sin perder el sistema de tipos del lenguaje Java
[Gosling96].
Como se muestra en la Figura 7.12, los pasos llevados a cabo en la evaluación dinámica
de código son:
1. La aplicación inicial es ejecutada por una implementación de la máquina virtual
de Java.
2. La aplicación genera código dinámicamente en función de los requisitos propios
de su contexto de ejecución.
3. Este nuevo fragmento de código es compilado a código binario de la máquina
virtual, mediante un compilador de Java codificado en Java. Este proceso se
realiza con la comprobación estática de tipos propia del lenguaje.
4. La implementación de una clase derivada de la clase ClassLoader [Gosling96]
permite cargar dinámicamente el código generado, para pasar a formar
parte del entorno computacional de la aplicación inicial.
30 El concepto definido como “linguistic reflection” no es el mismo que definíamos en § 6.3.1 como “reflectividad
lingüística”.
Panorámica de Utilización de Reflectividad
99
1
Java Virtual
Machine
Aplicación en
Ejecución
Aplicación
Java
class MiClase {
...
}
2 Compilador
Java
Aplicación
Java
ClassLoader
3
4
Ejecución de
la aplicación
Java
Creación dinámica
de código Java
Compilación
a código binario
Carga dinámica de la
nueva aplicación creada
Figura 7.12: Evaluación dinámica de código creado en tiempo de ejecución.
La aportación principal del sistema es soportar la codificación de aplicaciones genéricas
en tiempo de ejecución y seguras respecto al tipo. Éste es un concepto similar a las
plantillas (templates) del lenguaje C++ [Stroustrup98], pero resueltas dinámicamente en lugar
de determinar su tipo en tiempo de compilación. Como caso práctico, se han implementado
productos cartesianos de tablas de una base de datos relacional, así como contenedores
de objetos seguros respecto al tipo, ambos genéricos dinámicamente.
En lo referente a reflectividad, el sistema no aporta nada frente a la introspección
ofrecida por la plataforma Java [Sun97d]. Sin embargo, la posibilidad de crear código dinámicamente
ofrece facilidades propias de plataformas dotadas de reflectividad estructural
como Smalltalk. La implementación mediante un intérprete puro [Cueva98] hubiese mucho
más sencilla, pero menos eficiente en tiempo de ejecución.
Python
Python es un lenguaje de programación orientado a objetos, portable e interpretado
[Rossum2001]. Su desarrollo comenzó en 1990 en el CWI de Amsterdam. El lenguaje posee
módulos, paquetes, clases, excepciones y un sistema de comprobación de tipos dinámico.
La implementación del intérprete de Python ha sido portada a diferentes plataformas
como UNIX, Windows, DOS, OS/2, Mac y Amiga.
El entorno de programación de Python ofrece en tiempo de ejecución determinadas
funcionalidades de reflectividad estructural [Andersen98]:
Modificación dinámica de la clase de un objeto. Todo objeto posee un atributo
denominado __class__ que hace referencia a su clase (las clases también son
objetos). La modificación de esta referencia implica la modificación del tipo del
objeto.
Acceso al árbol de herencia. Toda clase posee un atributo __bases__ que referencia
una colección de sus clases base modificables dinámicamente.
Acceso a los atributos y métodos de un objeto. Tanto los objetos como las clases
poseen un diccionario de sus miembros (__dict__) que puede ser consultado
y modificado dinámicamente.
Control de acceso a los atributos. El acceso a los atributos de una clase puede
ser modificado con la definición de los métodos de clases __getattr__ y
__setattr__.
Además, el intérprete del lenguaje ofrece numerosa información del sistema en
tiempo de ejecución: introspección. Sobre la reflectividad estructural del lenguaje se han
construido módulos que aumentan las características reflectivas del entorno de programaCAPÍTULO
7
100
ción [Andersen98], ofreciendo un mayor nivel de abstracción basándose en el concepto de
metaobjeto [Kiczales91].
7.2.1 Aportaciones y Carencias de los Sistemas Estudiados
Los sistemas dotados de reflectividad estructural ofrecen una mayor flexibilidad que
los que únicamente ofrecen introspección. En Java, una aplicación puede modificar su flujo
en función de su estado –conociéndolo mediante introspección–; en Smalltalk, además
puede modificar su estructura dinámicamente –gracias a la reflectividad estructural. Si analizamos
la dicotomía anterior desde el punto de vista de la seguridad, para una plataforma
comercial sería peligroso ofrecer reflectividad estructural, puesto que cualquier aplicación
podría acceder y modificar la estructura del sistema. Esta discusión se detalla en § 6.4.
Los sistemas estudiados ofrecen conocimiento automático de su estructura (§ 2.3.1)
y su modificación (§ 2.3.2). Si se codifican todas las primitivas computacionales sobre el
mismo lenguaje, al ofrecer éste reflectividad estructural, se podrán adaptar a cualquier contexto
dinámicamente. Solo tendremos que acceder a éstas y modificar su implementación.
Mediante este mecanismo implementado en el proyecto Merlin y en ObjVLisp, obtenemos
la adaptabilidad del entorno de programación (§ 2.2).
Una de las características analizadas tanto en el lenguaje Python como en el desarrollo
del sistema ObjVlisp, es la modificación de la semántica del lenguaje para un objeto o
una clase. En estos ejemplos, mediante el uso de la reflectividad estructural, es posible modificar
el mecanismo de interpretación de la recepción de un mensaje para una clase; modificando
estructuralmente una metaclase en ObjVlisp se puede conseguir este objetivo. Sin
embargo, no estamos obteniendo la modificación del comportamiento para el sistema global
–característica propia de la reflectividad computacional. Estos sistemas carecen por
tanto de la modificación dinámica de su semántica (§ 2.3.3).
Finalmente comentaremos cómo el proyecto Merlin propuso implementar un intérprete
de Self codificado en tinySelf para ofrecer reflectividad computacional. Aunque
ésta no es una tarea sencilla, sí que supone un mecanismo de implementación para obtener
un sistema computacionalmente reflectivo (§ 2.3.3). Utilizaremos esta idea para llevar a
cabo el diseño de nuestro entorno computacionalmente reflectivo sin restricciones
(capítulo 11).
7.3 Reflectividad en Tiempo de Compilación
Basándonos en la clasificación realizada en el capítulo anterior, la reflectividad en
tiempo de compilación se produce en fase de traducción independientemente de lo que el
sistema permita reflejar. En este tipo de sistemas una aplicación puede adaptarse a un contexto
dinámicamente, siempre y cuando los cambios hayan sido tenidos en cuenta en su
código fuente –puesto que toda su información se genera tiempo de compilación. Si en
tiempo de ejecución aparecieren exigencias no previstas en fase de desarrollo del sistema,
éstas no podrían solventarse dinámicamente; debería recodificarse y recompilarse el sistema.
OpenC++
OpenC++ [Chiba95] es un lenguaje de programación que ofrece características reflectivas
a los programadores de C++. Su característica principal es la eficiencia dinámica
de las aplicaciones creadas: no existe una sobrecarga computacional en tiempo de ejecuPanorámica
de Utilización de Reflectividad
101
ción. Está enfocado a amoldar el lenguaje de programación y su semántica al problema para
poder desarrollar las aplicaciones a su nivel de abstracción adecuado.
El modo en el que son generadas las aplicaciones se centra en un preproceso de código
fuente. Una aplicación se codifica en OpenC++ pudiendo hacer uso de sus características
reflectivas. Ésta es traducida a código C++ que, tras compilarse mediante cualquier
compilador estándar, genera la aplicación final –adaptable en el grado estipulado cuando
fue codificada.
La arquitectura presenta el concepto de metaobjeto –popularizado por Gregor Kiczales
[Kiczales91]– para modificar la semántica determinados elementos del lenguaje. En
concreto, para las clases y funciones miembro, se puede modificar el modo en el que éstas
son traducidas al código C++ final.
Aplicación
Final
Metaobjetos
1
Fichero
C++
Fichero
C++
Fichero
C++
Fichero
C++
Fichero
C++
Fichero
C++
2
Aplicación C++
3
Código
Fuente
OpenC++
Compilador
C++
Compilador
OpenC++
Traducción
metaobjetos
Metaobjetos
por omisión
Modificación de
metaobjetos Traducción sin
modificación
Modificación
del lenguaje
Fuente
Fichero
C++
Figura 7.13: Fases de compilación de código fuente OpenC++.
Tomando código OpenC++ como entrada, el traductor de este lenguaje genera
C++ estándar como salida. Este proceso consta de 2 fases mostradas en la Figura 7.13. En
la primera fase, se crea el árbol sintáctico relativo a las clases y funciones miembros de la
aplicación, para crearse un metaobjeto por cada uno de dichos elementos sintácticos.
Por omisión, los metaobjetos son instancias de una clase Class (si el elemento sintáctico
es una clase), o bien de una clase Function (cuando se trate de un método). Estas
clases traducen el código sin añadir ninguna modificación. En el caso de que una clase o un
método hayan sido definidos mediante un metaobjeto en OpenC++, éstos serán instancias
de clases derivadas de las dos mencionadas anteriormente; dichas clases definirán la forma
en la que se traducirá el código fuente asociado a los metaobjetos. Ésta es la segunda fase
de traducción que concluye con la generación de la aplicación final por un compilador de
C++.
El sistema ofrece un mecanismo de preproceso para poder amoldar el lenguaje de
programación C++ [Stroustrup98] a las necesidades del programador. Además de poder
modificar parte de la semántica de métodos y clases (reflectividad computacional),
OpenC++ ofrece la posibilidad de añadir palabras reservadas al lenguaje, para implementar
modificadores de tipos, clases y el operador new (reflectividad lingüística). Está enfocado a
la creación de librerías para poder crear un sistema de separación de aspectos en programación
(§ 5.3).
OpenJava
Una vez analizadas y estudiadas las técnicas y ventajas en la utilización de reflectividad
en tiempo de compilación, Shigeru Chiba aumentó las características reflectivas del
lenguaje de programación Java, denominándose éste OpenJava [Chiba98].
CAPÍTULO 7
102
El lenguaje Java ofrece introspección por medio de la clase java.lang.Class
[Sun97d], entre otras. Mediante la creación de una nueva clase, OJClass, y el preproceso
de código OpenJava realizado por el OpenJava compiler, diseñaron un lenguaje capaz de ofrecer
reflectividad en tiempo de compilación de las siguientes características:
Modificación del comportamiento de determinadas operaciones sobre los objetos,
como invocación a métodos o acceso a sus atributos (reflectividad computacional
restringida).
Reflectividad estructural. A la introspección ofrecida por la plataforma de Java
(§ 7.1), se añaden a los objetos y clases funcionalidades propias de reflectividad
estructural (§ 7.2).
Modificación de la sintaxis del lenguaje. Pueden crearse nuevas instrucciones,
operadores y palabras reservadas (reflectividad lingüística).
Modificación de los aspectos semánticos del lenguaje. Es posible modificar la
promoción o coerción de tipos [Cueva95b].
OpenJava mejora las posibilidades ofrecidas por su hermano OpenC++, sin necesidad
de modificar la implementación de la máquina abstracta –como sucede, por ejemplo,
en el caso de MetaXa [Kleinöder96]. Esto supone dos ventajas:
1. Al no modificarse la máquina virtual, no se generan distintas versiones de ésta
con la consecuente pérdida de portabilidad de su código fuente [Ortin2001].
2. Al no ofrecer adaptabilidad dinámica, no supone una pérdida de eficiencia en
tiempo de ejecución.
El lenguaje de programación OpenJava se ha utilizado para describir la solución de
problemas mediante patrones de diseño [GOF94] en el nivel de abstracción adecuado [Tatsubori98];
el lenguaje se modificó para amoldar su sintaxis a los patrones adapter y visitor
[GOF94].
Java Dynamic Proxy Classes
En la edición estándar de la plataforma Java2 (Java Standard Edition) versión 1.2.3 se
ha aumentado el paquete java.lang.reflect, para ofrecer un mecanismo de modificación
del paso de mensajes de una determinada clase; este tipo de clase se denomina proxy
(apoderadas) [Sun99].
Una clase proxy especifica la implementación de un conjunto ordenado de interfaces.
La clase es creada dinámicamente especificando su manejador de invocaciones
(InvocationHandler). Cada vez que se invoque a un método de las interfaces implementadas,
la clase apoderada ejecutará el método invoke de su manejador; éste podrá
modificar su semántica en función del contexto dinámico existente. El diagrama de clases
que ofrece este marco de trabajo se muestra a continuación:
Panorámica de Utilización de Reflectividad
103
java.lang.reflect.InvocationHandler
invoke()
java.lang.reflec.Proxy
1 newProxyInstance()
InterfaceA
<>
InterfaceB
<>
InterfaceC
<>
InterfacesImplementation
Figura 7.14: Diagrama de clases de la implementación de un manejador de invocaciones.
La clase es creada dinámicamente especificando las interfaces a implementar, un
cargador de clases (ClassLoader [Gosling96]) y una implementación del manejador de
invocaciones (InvocationHandler); esta funcionalidad la ofrece el método de clase
newProxyInstance de la clase Proxy.
La adaptabilidad del sistema se centra en la implementación del método invoke
del manejador. Este método recibe el objeto real al que se ha pasado el mensaje, el identificador
del método y sus parámetros. Haciendo uso del sistema introspectivo de la plataforma
Java [Sun97d], podremos conocer e invocar el método que deseemos en función de los
requisitos existentes en tiempo de ejecución.
Con este mecanismo, la plataforma de Java ofrece un grado de flexibilidad superior
a la introspección existente en versiones anteriores; permite modificar la semántica del paso
de mensajes. Sin embargo, las distintas interpretaciones de dicha semántica han de ser especificadas
en tiempo de compilación, para ser seleccionadas posteriormente de forma dinámica.
El resultado es un conjunto de clases que, haciendo uso de la introspección de la
plataforma, permite simular la modificación de una parte del comportamiento del sistema.
No obstante, como indicábamos en § 7.2.1, no estamos en un grado computacional de
reflectividad, al no permitir la modificación de dicho comportamiento para todo el sistema
–se modifica tan solo para los objetos instancia de una clase.
7.3.1 Aportaciones y Carencias de los Sistemas Estudiados
La principal carencia de los sistemas reflectivos en tiempo de compilación es la incapacidad
de poder amoldarse a situaciones no previstas en fase de desarrollo. Toda la
adaptabilidad de un sistema, deberá definirse cuando éste es construido y, una vez que esté
ejecutándose, no podrá adecuarse de otra manera (§ 2.3.3). Por otro lado, al determinarse el
grado de flexibilidad en tiempo de compilación, estos sistemas son más eficientes que los
adaptables dinámicamente.
Teniendo en cuenta el nivel de reflexión ofrecido, los sistemas estudiados presentan
una mayor información a reflejar que los que estudiaremos en el siguiente punto. La reflecCAPÍTULO
7
104
tividad del lenguaje –o modificación de éste– se ofrece comúnmente en sistemas reflectivos
estáticos, cuando en los dinámicos supone una carencia (§ 2.3.5).
La compilación previa del código fuente, permite utilizar lenguajes seguros respecto
al tipo [Cardelli97], eliminando una elevado número de errores en tiempo de ejecución
propio de sistemas de naturaleza interpretada; por ejemplo Python [Rossum2001].
Como veremos en el estudio de los sistemas reflectivos dinámicos basados en Meta-
Object Protocols (MOPs) (§ 7.4.1), existe una restricción previa de lo que puede ser modificado.
En el estudio de los sistemas reflectivos en tiempo de compilación, vemos cómo también
sucede lo mismo: el lenguaje define qué partes de él mismo pueden ser adaptadas (§
2.3.4). La diferencia es que en los sistemas analizados, dicha restricción se produce en fase
de compilación, frente a la restricción dinámica producida en el uso de MOPs.
7.4 Reflectividad Computacional basada en Meta-Object
Protocols
Los sistemas reflectivos que permiten modificar parte de su semántica en tiempo de
ejecución son comúnmente implementados utilizando el concepto de protocolo de
metaobjeto (Meta-Object Protocol, MOP). Estos MOPs definen un modo de acceso
(protocolo) del sistema base al metasistema, permitiendo modificar parte de su propio
comportamiento dinámicamente.
En este punto analizaremos brevemente un conjunto de sistemas que utilizan
MOPs para modificar su semántica dinámicamente, para posteriormente hacer una síntesis
global de sus aportaciones y carencias.
Closette
Closette [Kiczales91] es un subconjunto del lenguaje de programación CLOS [Steele90].
Fue creado para implementar un MOP del lenguaje CLOS, permitiendo modificar
dinámicamente aspectos semánticos y estructurales de este lenguaje. La implementación se
basó en desarrollar un intérprete de este subconjunto de CLOS sobre el propio lenguaje
CLOS, capaz de ofrecer un protocolo de acceso a su metasistema.
Existen dos niveles computacionales: el nivel del intérprete de CLOS (metasistema),
y el del intérprete de Closette (sistema base) desarrollado sobre el primero. El acceso del
sistema base al metasistema se realiza mediante un sistema de macros; el código Closette se
expande a código CLOS que, al ser evaluado, puede acceder a su metasistema. La definición
de la interfaz de estas macros constituye el MOP del sistema.
El diseño de este MOP para el lenguaje CLOS se centra en el concepto de metaobjeto.
Un metaobjeto es cualquier abstracción, estructural o computacional, del metasistema
susceptible de ser modificada por su sistema base. Un metaobjeto no es necesariamente
una representación de un objeto en su sistema base; puede representar una clase o la semántica
de la invocación a un método. El modo en el que se puede llevar a cabo la modificación
de los metaobjetos es definido por el MOP.
La implementación del sistema fue llevada a cabo en tres capas, mostradas en la
Figura 7.1:
1. La capa de macros; define la forma en la que se va a interpretar el subconjunto
de CLOS definido, estableciéndolo mediante traducciones a código CLOS. En
el caso de que no existiese un MOP, esta traducción sería la identidad; el código
Closette se traduciría a código CLOS sin ningún cambio.
Panorámica de Utilización de Reflectividad
105
2. La capa de “pegamento” (glue). Son un conjunto de funciones desarrolladas en
CLOS que facilitan el acceso a los objetos del metasistema, para así facilitar la
implementación de la traducción de las macros. Como ejemplo, podemos citar
la función find-class que obtiene el metaobjeto representativo de una clase,
cuyo identificador es pasado como parámetro.
3. La capa de nivel inferior. Es la representación en CLOS (metasistema) de la estructura
y comportamiento del sistema base. Todas aquellas partes del sistema
que deseen ser reflectivas, deberán ser implementadas como metaobjetos.
Código
Closette
Capa Macros:
Traducción
Closette/CLOS
Capa
“pegamento”
Metaobjetos
Intérprete
Closette
Intérprete
CLOS
MOP
Invocación
Acceso
Sistema Base
(closette)
Metasistema
(CLOS)
Ejecuta
Ejecuta
Capa de nivel inferior
Figura 7.15: Arquitectura del MOP desarrollado para el lenguaje CLOS.
MetaXa31
MetaXa es un sistema basado en el lenguaje y plataforma Java que añade a éste características
reflectivas en tiempo de ejecución, permitiendo modificar parte de la semántica
de la implementación de la máquina virtual [Golm97]. El diseño de la plataforma está
orientado a dar soporte a la demanda de creación de aplicaciones flexibles, que puedan
adaptarse a requerimientos dinámicos como distribución, seguridad, persistencia, tolerancia
a fallos o sincronización de tareas [Kleinöder96].
El modo en el que se deben desarrollar aplicaciones en MetaXa se centra en el concepto
de metaprogramación (meta-programming) [Maes87]: separación del código funcional
del código no funcional. La parte funcional de una aplicación se centra en el modelado del
dominio de la aplicación (nivel base), mientras que el código no funcional formaliza la supervisión
o modelado de determinados aspectos propios código funcional (metasistema).
MetaXa permite separar estos dos niveles de código fuente y establecer entre ellos un mecanismo
de conexión causal en tiempo de ejecución.
El sistema de computación de MetaXa se apoya sobre la implementación de objetos
funcionales y metaobjetos conectados a los primeros –a un metaobjeto se le puede conectar
objetos, referencias y clases; de forma genérica utilizaremos el término objeto. Cuando
un metaobjeto está conectado a un objeto sobre el que sucede una acción, el sistema provoca
un evento en el metaobjeto indicándole la operación solicitada en el sistema base. La
implementación del metasistema permite modificar la semántica de la acción provocadora
31 Anteriormente conocido como MetaJava.
CAPÍTULO 7
106
del evento. La computación del sistema base es suspendida de forma síncrona, hasta que el
metasistema finalice la interpretación del evento capturado.
Lenguaje Java
Programación
de MetaObjetos
Programación
de Objetos Base
Conexión
entre ambos
Lenguaje Binario
Máquina Virtual
MetaXa
Compilación
Máquina Virtual
MetaXa
Interpretación
Metasistema
Sistema
base
Sistema en Ejecución
Figura 7.16: Fases en la creación de una aplicación en MetaXa.
Si un metaobjeto recibe el evento method-enter, la acción por omisión será ejecutar el
método apropiado. Sin embargo, el metaobjeto puede sobreescribir el método
eventMethodEnter para modificar el modo en el que se interpreta la recepción de un
mensaje.
En MetaXa, para hacer el sistema lo más eficiente posible, inicialmente ningún objeto
está conectado a un metaobjeto. En tiempo de ejecución se producen las conexiones
apropiadas para modificar los comportamientos solicitados por el programador.
La implementación de la plataforma reflectiva de MetaXa toma la máquina virtual
de Java[Sun95] y añade sus nuevas características reflectivas mediante la implementación
de métodos nativos residentes en una librería de enlace dinámico [Sun97c].
Uno de los principales inconvenientes del diseño abordado es el de la modificación
del comportamiento individual de un objeto. Al residir el comportamiento de todo objeto
en su clase, ¿qué sucede si deseamos modificar la semántica de una sola instancia de dicha
clase? MetaXa crea una nueva clase para el objeto denominada clase sombra (Shadow Class)
con las siguientes características [Golm97c]:
Una clase y su clase sombra asociada son iguales para el nivel base.
La clase sombra difiere de la original en las modificaciones realizadas en el metasistema.
Los métodos y atributos de clase (static) son compartidos por ambas clases.
Panorámica de Utilización de Reflectividad
107
Clase C
Objeto A
Objeto B
Tipo
Tipo
Clase
Clase
Clase C
Objeto A
Objeto B
Tipo
Tipo
Clase
Clase
Clase Sombra C
Sistema
Base
Modificación
del comportamiento
del objeto A
Figura 7.17: Creación dinámica de una clase sombra en MetaXa para modificar el comportamiento
de una instancia de una clase.
Hay que solventar un conjunto de problemas para mantener coherente el sistema
en la utilización de las clases sombra [Golm97c]:
1. Consistencia de atributos. La modificación de los atributos de una clase ha de
mantenerse coherente con su representación sombra.
2. Identidad de clase. Hay que tener en cuenta que el tipo de una clase y el de su
clase sombra han de ser el mismo, aunque tengan distinta identidad.
3. Objetos de la clase. Cuando el sistema utilice el objeto representante de una clase
(en Java una clase genera un objeto en tiempo de ejecución) han de tratarse
paralelamente el de la clase original y el de su sombra. Un ejemplo puede ser las
operaciones monitorenter y monitorexit de la máquina virtual [Venners98],
que tratan los objetos clase como monitores de sincronización; ha de
ser el mismo monitor el de la clase sombra que el de la real.
4. Recolector de basura. Las clases sombra deberá limpiarse cuando el objeto no
tenga asociado un metaobjeto.
5. Consistencia de código. Tendrá que ser mantenida cuando se produzca la creación
de una clase sombra a la hora de estar ejecutándose un método de la clase
original.
6. Consumo de memoria. La duplicidad de una clase produce elevados consumos
de memoria; deberán idearse técnicas para reducir estos consumos.
7. Herencia. La creación de una clase sombra, cuando la clase inicial es derivada de
otra, obliga a la producción de otra clase sombra de la clase base original. Este
proceso ha de expandirse de forma recursiva.
La complejidad surgida por el concepto de clase en un sistema reflectivo hace afirmar
a los propios autores del sistema, cómo los lenguajes basados en prototipos como Self
[Ungar87] o Moostrap solucionan estos problemas de una forma más coherente
[Golm97c].
Iguana
La mayoría de los sistemas reflectivos, adaptables en tiempo de ejecución y basados
en MOPs, son desarrollados mediante la interpretación de código; la ejecución de código
nativo no suele ser común en este tipo de entornos. La principal razón es que los intérpretes
tienen que construir toda la información propia de la estructura y la semántica de la
aplicación a la hora de ejecutar ésta. Si un entorno reflectivo trata de modificar la estructura
o comportamiento de un sistema, será más sencillo ofrecer esta información si la ejecución
de la aplicación es interpretada.
CAPÍTULO 7
108
En el caso de los compiladores, la información relativa al sistema es creada en
tiempo de compilación –y generalmente almacenada en la tabla de símbolos [Cueva92b]–
para llevar a cabo todo tipo de comprobación de tipos [Cueva95b] y generación de código
[Aho90]; una vez compilada la aplicación, dicha información deja de existir. En el caso de
un depurador (debugger), parte de la información es mantenida en tiempo de ejecución para
poder conocer el estado de computación (introspección) y permitir modificar su estructura
(reflectividad estructural dinámica). El precio a pagar en este caso es un aumento de tamaño
de la aplicación, y una ralentización de su ejecución.
El sistema Iguana ofrece reflectividad computacional en tiempo de ejecución basada
en un MOP, compilando código C++ a la plataforma nativa destino [Gowing96]. En la
generación de código, de forma contraria a un depurador, Iguana no genera información de
toda la estructura y comportamiento del sistema. Por omisión, compila el código origen
C++ a la plataforma destino sin ningún tipo de información dinámica. El programador ha
de especificar qué parte del sistema desea que sea adaptable en tiempo de ejecución, de
modo que el sistema generará el código oportuno para que sea reflectivo.
Iguana define dos conceptos para especificar el grado de adaptabilidad de una aplicación:
1. Categorías de cosificación (Reification Categories). Indican al compilador dónde
debe producirse la cosificación del sistema. Son elementos susceptibles de ser
adaptados en Iguana; ejemplos son clases, métodos, objetos, creación y destrucción
de objetos, invocación a métodos o recepción de mensajes, entre otros.
2. Definición múltiple de MOPs (Multiple fine-grained MOPs). El programador ha de
definir la forma en la que el sistema base va a acceder a su información dinámica,
es decir se ha de especificar el MOP de acceso al metasistema.
La implementación de Iguana está basada en el desarrollo de un preprocesador que
lee el código fuente Iguana –una extensión del C++– y traduce toda la información especifica
del MOP, a código C++ con información dinámica adicional adaptable en tiempo de
ejecución (el código C++ no reflectivo no sufre proceso de traducción alguno). Una vez
que la fase de preproceso haya sido completada, Iguana invocará a un compilador de C++
para generar la aplicación final nativa, adaptable dinámicamente.
Cognac
Cognac [Murata94] es un sistema orientado a objetos basado en clases, cuya intención
es proporcionar un entorno de programación de sistemas operativos orientados a objetos
como Apertos [Yokote92]. El lenguaje de programación de Cognac es intencionalmente
similar a Smalltalk-80 [Goldberg89]; para aumentar su eficiencia, se le ha añadido
comprobación estática de tipos [Cardelli97].
Los principales objetivos del sistema son:
Uniformidad y simplicidad. Para el programador sólo debe haber un tipo de objeto
concurrente, sin diferenciar ejecución síncrona de asíncrona.
Eficiencia. Necesaria para desarrollar un sistema operativo.
Seguridad. Deberá tratarse de minimizar el número de errores en tiempo de ejecución;
de ahí la introducción de tipos estáticos al lenguaje.
Migración. En el sistema los objetos deberán poder moverse de una plataforma
física a otra, para seleccionar el entorno de ejecución que más les convenga.
Panorámica de Utilización de Reflectividad
109
Metaprogramación. El sistema podrá programarse separando las distintas incumbencias
y aspectos de las aplicaciones, diferenciando entre el código funcional
del no funcional.
La arquitectura del sistema está compuesta de cinco elementos:
1. El front-end del compilador. El código fuente Cognac es traducido a un código
intermedio independiente de la plataforma destino seleccionada. El compilador
selecciona la información propia de las clases y la almacena, para su posterior
uso, en el sistema de clases (quinto elemento).
2. El back-end del compilador. Esta parte del compilador toma el código intermedio
y lo traduce a código binario propio de la plataforma física utilizada. Crea
un conjunto de funciones traducidas de cada una de las rutinas del lenguaje de
alto nivel.
3. Librería de soporte dinámico (Run-time Support Library). Es el motor principal de
ejecución. Envía una petición al sistema de clases para conocer el método apropiado
del objeto implícito a invocar; una vez identificado éste en el código nativo,
carga la función apropiada y la ejecuta.
4. Intérprete. Aplicación nativa capaz de ejecutar el código intermedio de la aplicación.
Será utilizado cuando el sistema requiera reflejarse dinámicamente. La
ejecución del código en este modo se ralentiza frente a la ejecución nativa.
5. Sistema de clases. Información dinámica relativa a las clases y métodos del sistema.
El proceso de reflexión dinámica y los papeles de las distintas partes del sistema se
muestran en la Figura 7.18. La ejecución del sistema es controlada por la librería de soporte
dinámico, que lee la información relativa al mensaje solicitado, busca éste entre el código
nativo y lo ejecuta. Cuando se utiliza un metaobjeto dinámicamente, el motor de ejecución
pasa a ser el intérprete, que ejecuta el código intermedio de dicho metaobjeto. El comportamiento
del sistema es derogado por la interpretación del metaobjeto creado; éste dicta la
nueva semántica del sistema.
Font-End del
Compilador
Back-End del
Compilador
Código
Intermedio
Código fuente
Cognac
Intérprete
Código
Nativo
Sistema
de Clases
Librería de
Soporte
Dinámico
Leer Información
Selección y
Ejecución
Ejecución
Monolítica
Inserción de
Información
Lectura
Ejecución Ejecución
Reflectiva
Figura 7.18: Arquitectura y ejecución de una aplicación en Cognac.
CAPÍTULO 7
110
Guanará
Para el desarrollo de la librería MOLDS [Oliva98] de componentes de metasistema
reusables, enfocados a la creación de aplicaciones de naturaleza distribuida –ofreciendo
características de persistencia, distribución y replicación, indiferentemente de la aplicación
desarrollada (separación de incumbencias) –, se desarrolló la plataforma computacionalmente
reflectiva Guanará [Oliva98b]. Guanará es una ampliación de la implementación de
la máquina virtual de Java, Kaffe OpenVM, otorgándole la capacidad de ser reflectiva
computacionalmente en tiempo de ejecución mediante un MOP [Oliva98c].
El mecanismo de reflectividad ofrecido al programador está centrado en el concepto
de metaobjeto. Cuando se enlaza un metaobjeto a una operación del sistema, la semántica
de dicha operación es derogada por la evaluación del metaobjeto. El modo en el que sea
desarrollado este metaobjeto definirá dinámicamente el nuevo comportamiento de la operación
modificada.
El concepto de metaobjeto es utilizado por multitud de MOPs y, la combinación de
éstos, se suele llevar a cabo mediante el patrón de diseño “Cadena de Responsabilidad”
(Chain of Responsibility) [GOF94]: cada metaobjeto es responsable de invocar al siguiente
metaobjeto enlazado con su misma operación, y devolver el resultado de éste. Este esquema
de funcionamiento es poco flexible y todo metaobjeto necesita modificar su comportamiento
en función del resto de metaobjetos (Figura 7.19).
cliente servidor
mo2 :
MetaObject
mo1 : MetaObject moN : MetaObject
Operación
Operación
Modificación del
Significado de la operación
Operación1 Operación2 OperaciónN
Respuesta2 Respuesta2
Respuesta1
Operación
Respuesta
Sistema Base
MetaSistema
Cadena de Responsabilidad
Figura 7.19 :Utilización del patrón Chain of Responsibility para asociar múltiples metaobjetos.
Guanará aborda este problema con el uso del patrón “Composición” (Composite)
[GOF94], permitiendo establecer combinaciones de comportamiento más independientes y
flexibles [Oliva99]. Cada operación puede tener enlazado un único metaobjeto primario,
denominado compositor (composer). Éste, haciendo uso del patrón “Composición”, podrá
acceder a una colección de metaobjetos mediante una estructura de grafo. Como se muestra
en la Figura 7.20, cada uno de los elementos de un compositor puede ser un metaobjeto
u otro compositor.
Panorámica de Utilización de Reflectividad
111
MetaObject Compositor
Interfaz
*
: Compositor : Compositor
: Compositor
: MetaObject
: MetaObject
: MetaObject
: MetaObject
objeto1 objeto2 objeto3
: MetaObject
Diagrama de Clases
Diagrama de Objetos
Sistema Base
MetaSistema
MetaMetaSistema
Especificación
Comportamiento
Figura 7.20: Utilización del patrón Composite para asociar múltiples metaobjetos.
El modo en el que cada compositor establece el nuevo comportamiento de su operación
asociada en función de los metaobjetos utilizados, viene definido por una configuración
adicional denominada metaconfiguración. De esta forma, separamos la estructura de
los metaobjetos de su secuencia de evaluación, haciendo el sistema más flexible y reutilizable.
La parte realmente novedosa de este sistema radica en la forma en la que se pueden
combinar múltiples comportamientos a una operación reflectiva, así como el establecimiento
de metacomportamientos de un metaobjeto (metametaobjetos). Establece un mecanismo
escalable y flexible, basado en el patrón de diseño Composite.
Dalang
Dalang [Welch98] es una extensión reflectiva del API de Java [Kramer96], que añade
reflectividad computacional para modificar únicamente el paso de mensajes del sistema,
de un modo restringido. Implementa dos mecanismos de reflectividad: en tiempo de compilación
(estática) y en tiempo de ejecución (dinámica).
El conjunto de clases utilizadas en el esquema estático se muestra en la Figura 7.21.
Dada una clase C cuyo paso de mensajes deseemos modificar, Dalang sustituye dicha clase
por otra nueva con la misma interfaz y nombre, renombrando la original –esto es posible
gracias a la introspección de la plataforma Java [Sun97d]. La nueva clase posee un objeto de
la clase original en la que delegará la ejecución de todos sus métodos. Sin embargo, poseerá
la capacidad de ejecutar, previa y posteriormente a la invocación del método, código adicional
que pueda efectuar transformaciones de comportamiento (en la Figura 7.21, este
código reside en la implementación de los métodos beforeMethod y afterMethod).
CAPÍTULO 7
112
C
method()
beforeMethod()
objeto.method()
afterMethod()
C
method()
beforeMethod()
afterMethod()
C Renombrada
1 method()
-objeto
Clase antes de
la transformación
Esquema creado
para la modificación
del paso de mensajes
a la clase C
Figura 7.21: Transformación de una clase en Dalang para obtener reflectividad.
La reflectividad dinámica del sistema se obtiene añadiendo al esquema anterior la
implementación de un cargador de código capaz de realizar la carga dinámica de las nuevas
clases en el sistema. La plataforma Java ofrece fácilmente esta posibilidad mediante la implementación
de una clase derivada de ClassLoader [Gosling96].
Esta arquitectura posee un conjunto de inconvenientes:
Transparencia. Se permite modificar el paso de mensajes de un conjunto de objetos
(las instancias de la clase renombrada); sin embargo, no es posible modificar
la semántica del paso de mensajes para todo es sistema.
Grado de reflectividad. La modificación de la semántica se reduce al paso de
mensajes y se realiza en un grado bastante reducido –ejecución anterior y posterior
de código adicional.
Eficiencia. La creación dinámica de clases requiere una compilación al código
nativo de la plataforma, con la consecuente ralentización en tiempo de ejecución.
Su principal ventaja es que no modifica la máquina virtual de Java ni el código existente
en su plataforma; de esta forma, el sistema no pierde la portabilidad del código Java y
es compatible con cualquier aplicación desarrollada para esta plataforma virtual.
NeoClasstalk
Tras los estudios de reflectividad estructural llevados a cabo con el sistema ObjVlisp
(analizado en § 7.2), la arquitectura evolucionó hacia una ampliación de Smalltalk
[Goldberg83] denominada Classtalk [Mulet94]; su principal objetivo era utilizar esta plataforma
como medio de estudio experimental en el desarrollo de aplicaciones estructuralmente
reflectivas. Continuando con el estudio de aplicaciones basadas en reflectividad, el
desarrollo de un MOP que permitiese modificar el comportamiento de las instancias de una
clase hizo que el sistema se renombrase a NeoClasstalk [Rivard96].
La aproximación que utilizaron para añadir reflectividad computacional a Neo-
Classtalk fue la utilización del concepto de metaclase32, propio del lenguaje Smalltalk. Una
metaclase define la forma en la que van a comportarse sus instancias, es decir, sus clases
asociadas –el concepto de metaclase es tomado como un mecanismo para definir la semántica
computacional de las clases instanciadas. Sobre este sistema, se desarrollaron metacla-
32 Al igual que en los lenguajes orientados a objetos basados en clases, un objeto es una instancia de una
clase, el concepto de metaclase define una clase como instancia de una metaclase –la cual define el comportamiento
de todas sus clases asociadas.
Panorámica de Utilización de Reflectividad
113
ses genéricas para poder ampliar las características del lenguaje [Ledoux96] tales como la
definición de métodos con pre y poscondiciones, clases de las que no se pueda heredar o la
creación de clases que tan sólo puedan tener una única instancia –patrón de diseño Singleton
[GOF94].
El modo en el que se modifica el comportamiento se centra en la modificación dinámica
de la metaclase de una clase. Como se muestra en la Figura 7.22, existe una metaclase
por omisión denominada StandardClass; esta metaclase define el comportamiento
general de una clase en el lenguaje de programación Smalltalk. El nuevo comportamiento
deseado deberá implementarse en una nueva metaclase, derivada de StandardClass.
Metaclase que define
el comportamiento
genérico de toda clase
en Smalltalk
Nueva metaclase
que modifica el
comportamiento de
sus instancias
ClaseB Metaclase
ClaseA StandardClass
Clase cuyas
instancias poseen el
comportamiento por
defecto
Las instancias de
la clase B, tendrán
una modificac ión de
su comportamiento
inicial
instanceof
ins tanceof
Figura 7.22: MOP de NeoClasstalk utilizando metaclases.
En NeoClasstalk, los objetos pueden cambiar de clase dinámicamente, modificando
su asociación instanceof. Si modificamos esta referencia en el caso de una clase, estaremos
modificando su metaclase y, por tanto, modificando su comportamiento.
Una de las utilizaciones prácticas de este sistema fue el desarrollo de OpenJava [Ledoux99],
un ORB capaz de adaptarse dinámicamente a los requisitos del programador
CORBA [OMG98]. Mediante la modificación del comportamiento basado en metaclases,
añade una mayor flexibilidad al middleware CORBA consiguiendo:
Modificación dinámica del protocolo de comunicaciones. La utilización de objetos
delegados (proxy) permite ser configurada para modificar el comportamiento
del paso de mensajes, seleccionando dinámicamente el protocolo deseado.
Migración de objetos servidores dinámicamente.
Replicación de objetos servidores.
Implementación de un sistema dinámico de caché.
Gestión dinámica de tipos. Accediendo dinámicamente a las especificaciones de
los interfaces de los objetos servidores (archivos IDL), se pueden implementar
comprobaciones de tipo en tiempo de ejecución.
El desarrollo de todo el sistema fue llevado a cabo siguiendo la separación de incumbencias:
la parte de una aplicación que modele el dominio del problema se deberá separar
del resto de código que pueda ser reutilizado para otras aplicaciones, y modele un aspecto
global a varios sistemas.
Moostrap
Moostrap [Mulet93] es un lenguaje orientado a objetos reflectivo basado en prototipos,
implementado como un intérprete desarrollado en Scheme [Abelson2000].
CAPÍTULO 7
114
Acorde a la definición de la mayor parte de los lenguajes basados en prototipos, define
un número reducido de primitivas computacionales que va extendiendo mediante la
utilización de sus características estructuralmente reflectivas, para ofrecer un mayor nivel de
abstracción en la programación de aplicaciones. La abstracción del objeto –la entidad básica
en los lenguajes basados en prototipos– queda definida con primitivas de reflectividad
estructural:
Un objeto viene definido como un conjunto de miembros (slots). Éstos pueden
constituir datos representativos del estado dinámico del objeto (atributos) o su
comportamiento (métodos). La diferencia entre los dos tipos de miembros, es
que los segundos pueden ser evaluados, describiendo la ejecución de un comportamiento33.
Adición dinámica de miembros a un objeto. Todo objeto posee el miembro
computacional primitivo addSharedSlots capaz de añadir dinámicamente
un slot a un objeto.
Eliminación dinámica de miembros de un objeto, mediante la primitiva
removeSlot.
Mediante sus capacidades estructuralmente reflectivas, extiende las primitivas iniciales
para ofrecer un mayor nivel de abstracción al programador de aplicaciones. Un ejemplo
de esto es la definición del mecanismo de herencia, apoyándose en su reflectividad estructural
dinámica [Mulet93]: si un objeto recibe un mensaje y no tiene un miembro con dicho
nombre, se obtiene su miembro parent y se le envía dicho mensaje a este objeto, continuando
este proceso de un modo recursivo.
Además de reflectividad estructural, Moostrap define un MOP para permitir modificar
el comportamiento de selección y ejecución de un método de un objeto, ante la recepción
de un mensaje. La semántica de la derogación de estas dos operaciones viene definida
por el concepto de metaobjeto [Kiczales91], utilizado en la mayor parte de los MOPs existentes.
El paso de un mensaje puede modificarse en dos fases: primero, ejecutándose el
metaobjeto asociado a la selección del miembro y, posteriormente, interpretando el comportamiento
definido por el metaobjeto que derogue la ejecución del método.
Utilizando Moostrap, se trató de definir una metodología para crear metacomportamientos
mediante la programación de metaobjetos en Moostrap [Mulet95].
7.4.1 Aportaciones y Carencias de los Sistemas Estudiados
En la totalidad de los MOPs estudiados, se permite la modificación dinámica de
parte del comportamiento del mismo (§ 2.3.3), y, en casos como Dalang, dicha modificación
se puede realizar en fase de compilación para obtener una mayor eficiencia en tiempo
de ejecución.
El concepto de MOP establece un modo de acceso del sistema base al metasistema,
identifica el comportamiento que puede ser modificado. El establecimiento de este protocolo
previamente a la ejecución de la aplicación supone una restricción a priori de la semántica
que podrá ser modificada dinámicamente. De esta forma, un MOP implica una restricción
en la que un sistema puede modificar su propio comportamiento (§ 2.3.4).
33 Este proceso de convertir datos en computación, lo definimos “descosificación” al tratarse del proceso
contrario de “cosificación” –definido en el capítulo 6.
Panorámica de Utilización de Reflectividad
115
Una proposición para resolver esta limitación de los MOPs pasa por ampliar éste
cuando sea necesario [Golm98]; verbigracia, si un MOP no contempla la modificación de la
semántica de la creación de objetos, podemos modificarlo para que sea adaptable. Sin embargo,
como se muestra en la Figura 7.23, la modificación del MOP supone la modificación
del intérprete, dando lugar a distintas versiones del mismo y a la pérdida de la portabilidad
del código existente para las versiones anteriores [Ortín2001].
Código Fuente v.1
Acceso al
Metasistema
mediante un
MOP
Ejecuta
Resto
Intérprete
Implementación
del MOP
Código Fuente v.2
Ejecuta
Resto
Intérprete
Modificación
del MOP
Intérprete v. 1 Intérprete v. 2
Modificación
del MOP
¿Es posible ejecutar
código v.1 con el
intérprete v.2?
Figura 7.23: Pérdida de la portabilidad código de un intérprete, al modificar su MOP.
Los sistemas basados en MOPs otorgan una flexibilidad dinámica de su comportamiento
pero, de forma contraria a las técnicas estudiadas en § 7.2, carecen de la posibilidad
de modificar el lenguaje con el que son desarrolladas sus aplicaciones (§ 2.3.5) –se limitan a
modificar la semántica de éstos.
La mayor carencia de los sistemas reflectivos es su eficiencia en ejecución. La utilización
de intérpretes es más común, pero las aplicaciones finales poseen tiempos de ejecución
más elevados que si hubieren sido compiladas a código nativo. A raíz de analizar los
sistemas estudiados podemos decir:
1. La implementación de un MOP en un sistema interpretado (por ejemplo, MetaXa)
es más sencilla y menos eficiente que el desarrollo de un traductor a un
lenguaje compilable, como por ejemplo Iguana. En este caso, el sistema debe
poseer información dinámica adicional para poder acceder y modificar ésta en
tiempo de ejecución.
Un ejemplo de la complejidad mencionada es la posibilidad de conocer dinámicamente
el tipo de un objeto en C++ (RTTI, RunTime Type Information) [Stroustrup98].
Esta característica supone modificar la generación de código, para que
todo objeto posea la información propia de su tipo –generalmente la ejecución
de una aplicación nativa no necesita esta información y por tanto no se genera.
2. Puesto que los sistemas compilados poseen mayor eficiencia frente a los interpretados,
que ofrecen una mayor sencillez a la hora de implementar sistemas
flexibles, la unión de las dos técnicas de generación de aplicaciones puede dar
lugar a un compromiso eficaz.
En el caso de Cognac, todo el sistema se ejecuta en tiempo dinámico excepto
aquella parte que se identifica como reflectiva; en este momento un intérprete
ejecuta el código intermedio que define el nuevo comportamiento. Para el sistema
Iguana todo el código es traducido sin información dinámica, salvo aquél
que va a ser adaptado.
3. El desarrollo de un MOP en dos niveles de interpretación (Closette) es más
sencillo que si sólo elegimos uno (MetaXa). Si necesitamos modificar el MOP
CAPÍTULO 7
116
de Closette, deberemos hacerlo sobre el primer intérprete; en el caso de MetaXa,
deberemos recodificar la máquina virtual.
Finalmente comentar, poniendo por ejemplo a Moostrap, que la reflectividad estructural
y computacional son dos mecanismos útiles a la hora de desarrollar un sistema
extensible y adaptable, a partir de un conjunto de primitivas reducidas (§ 2.2.1). Los modelos
computacionales orientados a objetos basados en prototipos, facilitan su implementación.
7.5 Intérpretes Metacirculares
Dentro del capítulo anterior, en el § 6.2, razonábamos acerca del concepto de reflectividad
computacional utilizando la metáfora de una torre de intérpretes. Cuando una
aplicación pueda acceder a su nivel inferior, podrá modificar su comportamiento. Si lo que
desea es modificar la semántica de su semántica (el modo en el que se interpreta su comportamiento),
deberá acceder en la torre a un nivel computacional dos unidades inferior.
El proceso de acceder, desde un nivel en la torre de intérpretes definida por Smith
[Smith82], a niveles inferiores puede, teóricamente, extenderse hasta el infinito –al fin y al
cabo, todo intérprete será ejecutado o animado por otro. Las implementaciones de intérpretes
capaces de ofrecer esta abstracción se han denominado intérpretes metacirculares
(metacircular interpreters) [Wand88].
3-Lisp
La idea de la torre infinita de intérpretes propuesta por Smith [Smith82] en el ámbito
teórico tuvo distintas implementaciones en un futuro inmediato [Rivières84].
El desarrollo de los prototipos de intérpretes metacirculares comenzó por la implementación
de lenguajes de computación sencilla. El diseño de un intérprete capaz de
ejecutar un número infinito de niveles computacionales, supone una elevada complejidad
que crece a medida que aumentan las capacidades computacionales del lenguaje a interpretar.
El primer prototipo metacircular desarrollado, denominado 3-Lisp [Wand88], interpreta
un subconjunto del lenguaje Lisp [Steele90], y permite cosificar y reflejar un número
indefinido de niveles computacionales.
El estado computacional que será reflejado en este lenguaje está formado por tres
elementos [Wand88]:
Entorno (environment): Identifica el enlace entre identificadores y sus valores en
tiempo de ejecución.
Continuación (continuation): Define el contexto de control. Recibe el valor devuelto
de una función y lo sitúa en la expresión que se está evaluando, en la posición
en la que aparece la llamada a la función ya ejecutada.
Almacén (store): Describe el estado global de la computación en el que se incluyen
contextos de ejecución e información sobre los sistemas de entrada y salida.
De esta forma, el estado de computación de un intérprete –denominado metacontinación
(metacontinuation) [Wand88]– queda definido formalmente por tres valores (e, r, k),
que podrán ser accedidos desde el metasistema como un conjunto de tres datos (cosificación).
La capacidad de representar formalmente y mediante datos el estado computacional
de una aplicación en tiempo de ejecución, aumenta en complejidad al aumentar el nivel de
abstracción del lenguaje de programación en el que haya sido codificada. Por esta causa, la
Panorámica de Utilización de Reflectividad
117
mayoría de los prototipos de intérpretes metacirculares desarrollados computan lenguajes
de semántica reducida.
Intérprete
3-Lisp
Código
3-Lisp
(e,r,k)
(e,r,k)
(e,r,k)
Pila de
Estados
Computacionales
Sistema
Base
Meta
Sistema
Cosificación
(reification)
Reflexión
(reflection)
Nivel 0
Nivel 1
Nivel 2
Modifica
Ejecuta
Accede
Intérprete
3-Lisp
Código
3-Lisp
(e,r,k)
(e,r,k)
(e,r,k)
Pila de
Estados
Computacionales
Sistema
Base
Meta
Sistema
Nivel 0
Nivel 1
Nivel 2
Modifica
Ejecuta
Accede
Nivel 3 (e’,r’,k’)
Figura 7.24: Implementación de un intérprete metacircular de 3-Lisp.
En la Figura 7.24 se aprecia el funcionamiento de los prototipos de interpretación
de 3-Lisp. Existe un programa codificado en 3-Lisp que posibilita el cambio de nivel con
las operaciones reify (aumento de nivel) y reflect (reducción de nivel). Un intérprete del subconjunto
de Lisp definido va leyendo y ejecutando el lenguaje fuente. La ejecución de la
aplicación supone la modificación de los tres valores que definen el estado computacional
de la aplicación (e, r, k). En la interpretación se puede producir un cambio de nivel de
computación:
Cosificación (reify): Se apila el valor del estado computacional existente (e, r, k) y
se crea un nuevo estado de computación (e’, r’, k’). Ahora el intérprete trabaja
sobre este nuevo contexto y la aplicación puede modificar los tres valores de
cualquier nivel inferior, como si de datos se tratase.
Reflexión (reflect): Se desapila el contexto actual volviendo al estado anterior
existente en la pila. La ejecución continúa donde había cesado antes de hacer la
última cosificación.
Las condiciones necesarias para implementar un prototipo de estas características
son básicamente dos:
1. Expresividad computacional mediante un único lenguaje. Puesto que realmente
existe un único intérprete (ver Figura 7.24), éste estará obligado a animar un solo
lenguaje de programación. En todos los niveles computacionales deberá utilizarse
por tanto el mismo lenguaje de programación.
2. Identificación formal del estado computacional. La semántica computacional
del lenguaje deberá representarse como un conjunto de datos manipulables por
el programa. La complejidad de este proceso es excesivamente elevada para la
mayoría de los lenguajes de alto nivel.
ABCL/R2
La familia de lenguajes ABCL fue creada para llevar a cabo investigación relativa al
paralelismo y orientación a objetos. Inicialmente se desarrolló un modelo de computación
concurrente denominado ABCM/1 (An object-Based Concurrent computation Model) y su lenguaje
asociado ABCL/1 (An object-Based Concurrent Language) [Yonezawa90].
CAPÍTULO 7
118
En la implementación de un modelo computacional concurrente, la reflectividad
computacional ofrece la posibilidad de representar la estructura y la computación concurrente
mediante datos (cosificación), utilizando abstracciones apropiadas. En la definición
del lenguaje ABCL/R (ABCL reflectivo) [Watanabe88], a partir de todo objeto “x” puede
obtenerse su metaobjeto “↑x” que representa, mediante su estructura, el estado computacional
de “x”. Se implementa un mecanismo de conexión causal, para que los cambios del
metaobjeto se reflejen en el objeto original. La operación “↑” puede aplicarse tanto a objetos
como a metaobjetos, tratándose pues de una implementación de una torre infinita de
intérpretes (intérprete metacircular).
Para facilitar la coordinación entre metaobjetos del mismo tipo, y para definir comportamientos
similares de un grupo de objetos, la definición del lenguaje ABCL/R2 [Matsuoka91]
añadía el concepto de “metagrupo”: metaobjeto que define el comportamiento de
un conjunto de objetos del sistema base. Para implementar la adición de metagrupos, surgen
determinadas ampliaciones del sistema:
1. Nuevos objetos de núcleo (kernel objects) para gestionar los grupos: The Group
Mangager, The Primary Metaobject Generator y The Primary Evaluator.
2. Se crea un paralelismo entre dos torres de intérpretes: la torre de metaobjetos
(activada mediante la operación “↑”) y la torre de metagrupos (activada mediante
la operación “⇑”).
3. Objetos no cosificables (non-refying objects). Se ofrece la posibilidad de definir objetos
no reflectivos para eliminar la creación de su metaobjeto adicional y obtener
así mejoras de rendimiento.
MetaJ
MetaJ [Doudence99] es un prototipo que trata de ofrecer las características propias
de un intérprete metacircular para un subconjunto del lenguaje de programación Java [Gosling96].
Expresado siempre en Java, el intérprete permite cosificar objetos para acceder a su
propia representación interna (reflectividad estructural) y a la representación interna de su
semántica (reflectividad computacional).
Inicialmente el intérprete procesa léxica y sintácticamente el código fuente, creando
un árbol sintáctico con nodos representativos de las distintas construcciones sintácticas del
lenguaje. El método eval de cada uno de estos nodos representará la semántica asociada a
cada uno de sus elementos sintácticos.
Conforme la interpretación del árbol se va llevando a cabo, se van creando objetos
representativos de los creados por el usuario en la ejecución de la aplicación. Todos los
objetos creados poseen el método reify que nos devuelve un metaobjeto: representación
interna del objeto que nos permite acceder a su estructura (atributos, métodos y clase), así
como a su comportamiento (por ejemplo, la búsqueda de atributos o la recepción de mensajes).
Podremos obtener así:
1. Reflectividad estructural: Accediendo y modificando la estructura del metaobjeto,
se obtiene una modificación estructural del objeto; existe un mecanismo de
conexión causal que refleja los cambios realizados en todo metaobjeto.
2. Reflectividad computacional: Se consigue modificando la clase de una instancia
por una clase derivada que derogue el método que especifica la semántica a alterar.
En la Figura 7.25 se muestra cómo se modifica la clase del metaobjeto para
ser otra con la redefinición del método lookupMethod, encargada de gestionar
la recepción de mensajes.
Panorámica de Utilización de Reflectividad
119
BaseInstance
lookupMethod()
...
ModificaciónRecepciónMensajes
lookupMethod()
Modificación de
la semántica de
la gestión de
mensajes
objeto metaObjeto
: Attribute
class : BaseInstance class :
ModificaciónRecepciónMensajes
Lista de Atributos
Clase de la que es instancia
Se rompe la asociación
con la clase anterior y
se asocia a la nueva que
modifica la recepción de
mensajes
Figura 7.25: Modificación de la clase de un objeto, para obtener la modificación de la semántica de
la recepción de mensajes.
La parte novedosa de MetaJ sobre el resto de sistemas estudiados a lo largo de este
capítulo reside en la capacidad de poder cosificar metaobjetos en el grado que deseemos. Si
invocamos al método reify de un metaobjeto, obtendremos la representación de un metaobjeto
pudiendo modificar así la semántica de su comportamiento. El acceso reflectivo
no posee un límite de niveles, constituyéndose así como un caso particular de un intérprete
metacircular.
7.5.1 Aportaciones y Carencias de los Sistemas Estudiados
Los sistemas estudiados ofrecen el mayor nivel de flexibilidad computacional respecto
al dominio de niveles computacionales a modificar. El acceso a cualquier elemento
de la torre de intérpretes permite modificar la semántica del sistema en cualquier grado. Sin
embargo, aunque teóricamente facilita la comprensión del concepto de reflectividad, en un
campo más pragmático puede suponer determinados inconvenientes. La posibilidad de
acceso simultáneo a distintos niveles puede producir la pérdida del conocimiento de la semántica
del sistema, sin conocerse realmente cuál es el significado del lenguaje de programación
[Foote90]. Un sistema de seguridad en la utilización de la reflectividad sería imprescindible
en este caso –§ 6.4.
En los sistemas estudiados, se ha dado precedencia a ofrecer un número indefinido
de niveles de computación accesibles, frente a ofrecer un mayor grado de información a
cosificar. Si tomamos 3-Lisp como ejemplo, ofrece la cosificación del estado computacional
de la aplicación, pero no permite modificar la semántica del lenguaje (§ 2.3.3); el intérprete
es monolítico e invariable.
Para conseguir este requisito mediante la implementación de infinitos niveles, debería
especificarse la semántica del lenguaje en el propio estado de computación, extrayéndola
del intérprete monolítico.
CAPÍTULO 7
120
En el caso de MetaJ, se restringe a priori el número de operaciones semánticas a
modificar, puesto que han de estar predefinidas como métodos de una clase de comportamiento.
No existe pues, una flexibilidad no restringida a priori (§ 2.3.4). Además, la modificación
a llevar a cabo en el comportamiento ha de especificarse en tiempo de compilación
(§ 2.4.8).
Como conclusión, cabe mencionar que a la hora de desarrollar un sistema reflectivo,
es más útil ahondar en la cantidad de información a cosificar y el modo en el que ésta
pueda ser expresada, que aumentar el número de niveles computacionales cosificables.
Un punto adicional a destacar, propio del sistema ABCL/R2, es su definición de
metagrupos. Vemos cómo se utiliza este concepto para agrupar el comportamiento de un
conjunto de objetos en una sola abstracción. Para conseguirlo, se introducen un conjunto
de entidades adicionales y dos torres de interpretación paralelas. Si bien la agrupación de
metaobjetos puede ser atrayente para reducir la complejidad del metasistema, deberíamos
buscar un mecanismo auxiliar más sencillo para conseguir dicha funcionalidad –los objetos
rasgo o trait de los sistemas orientados a objetos basados en prototipos (capítulo 8), nos
pueden ofrecer esta abstracción de un modo sencillo.
7.6 Conclusiones
A lo largo de este capítulo, estudiando distintos tipos de sistemas reflectivos, hemos
visto cómo la reflectividad es una técnica que puede ser empleada para obtener flexibilidad
en un sistema computacional –en el capítulo 8 vimos un conjunto de técnicas alternativas.
En este punto, analizaremos qué puede aportar esta técnica a los objetivos buscados en esta
tesis (§ 1.2), así como las limitaciones encontradas.
Puesto que el concepto de sistema reflectivo puede ser catalogado de diversos modos
(clasificación realizada en el capítulo anterior), analizaremos globalmente los sistemas
reflectivos teniendo en cuenta tres criterios: cuándo se produce la reflexión, qué se refleja y
el número de niveles computacionales utilizado.
7.6.1 Momento en el que se Produce el Reflejo
La reflectividad en tiempo de ejecución otorga un elevado grado de flexibilidad al
sistema, puesto que éste puede adaptarse a contextos impredecibles en fase de diseño.
Cuando una aplicación necesita poder especificar nuevos requisitos dinámicamente, la reflectividad
en tiempo de compilación no es suficiente.
Sin embargo, la reflectividad estática posee una ventaja directa sobre la dinámica: la
eficiencia de las aplicaciones en tiempo de ejecución. La adaptabilidad dinámica de un sistema
produce una ralentización del mismo en su ejecución.
Desde el punto de vista empírico, podemos ver cómo los sistemas estáticos ofrecen
reflectividad del lenguaje de programación, cuando esto no ocurre en ningún sistema dinámico;
el lenguaje de programación en estos casos se mantiene inamovible.
7.6.2 Información Reflejada
El primer nivel de información a reflejar es la estructura del sistema en un modo de
sólo lectura: introspección (§ 7.1). La característica práctica de este nivel de reflectividad
queda patente en el número de sistemas comerciales que la utilizan. Permite desarrollar
Panorámica de Utilización de Reflectividad
121
fácilmente sistemas de componentes, de persistencia, comprobaciones de tipo dinámicas, o
middlewares de distribución.
Para el sistema de computación flexible buscado en esta tesis, además de todas las
utilidades prácticas estudiadas en § 7.1, ofrece un mecanismo de autodocumentación real y
dinámica, y un conocimiento dinámico exacto del estado del sistema.
El segundo grado de información a reflejar es la reflectividad estructural, en la que
se permite tanto el acceso como la modificación dinámica de la estructura del sistema. A
nivel práctico existen muchas posibilidades para este tipo de sistemas, muchas de ellas todavía
no explotadas. Ejemplos pueden ser interfaces gráficas adaptables mediante la incrustación,
eliminación y modificación dinámica de la estructura de los objetos gráficos, aplicaciones
de bases de datos que trabajen con un abanico de información adaptable en tiempo
de ejecución, o la apertura a un nuevo modo de programación adaptable dinámicamente
[Golm98] y creación de nuevos patrones de diseño [Ferreira98].
Para el desarrollo de nuestro sistema, al igual que fue utilizada en Smalltalk, Self y
Moostrap, la reflectividad estructural puede emplearse para hacer un sistema extendido de
un conjunto reducido de primitivas computacionales, programando la mayor parte de éste
en su propio lenguaje y consiguiendo así adaptabilidad –el sistema posee la capacidad de
modificarse a sí mismo, al estar escrito en su propio lenguaje– y portabilidad –cualquier
intérprete de las primitivas computacionales básicas podrá ejecutar el sistema en su totalidad.
Cuando la semántica del sistema puede modificarse, nos encontramos en el tercer
nivel de esta clasificación: reflectividad computacional. Éste ofrece una flexibilidad elevada
para todo el sistema en su conjunto. Se ha utilizado en la mayoría de casos a nivel de prototipo,
aplicándose a depuradores (debuggers), compilación dinámica (JIT, Just In Time compilation),
desarrollo de aplicaciones en tiempo real, o creación de sistemas de persistencia y distribución.
En los sistemas estudiados aparecen limitaciones respecto al grado de modificación
de la semántica adaptable, y a la imposibilidad de modificar el lenguaje de programación (§
7.4.1). Ambas restricciones han sido especificadas como necesaria su superación en esta
tesis –requisitos § 2.3.4 y § 2.3.5). Por lo tanto, nuestro sistema deberá tener un mecanismo
no restrictivo de modificación de su semántica, así como la capacidad de modificar y seleccionar
dinámicamente su lenguaje de programación (cuarto y último nivel de información a
reflejar).
7.6.3 Niveles Computacionales Reflectivos
En la torre de intérpretes enunciada por Smith [Smith82], analizada en el capítulo
anterior, el acceso de un sistema a su metasistema se representaba como el salto al intérprete
que ejecutaba dicha aplicación. En un sistema reflectivo, podemos preguntarnos cuántos
niveles computacionales necesitamos y qué ganaríamos introduciendo más.
Coincidiendo con la mayoría de los autores, un sistema altamente flexible y manejable
es aquél que ofrece una elevada “anchura” y no “altura” de su torre de intérpretes. Esto
quiere decir que es más útil obtener una forma sencilla y potente de modificación del metasistema
desde el sistema base –anchura de la torre–, que la capacidad de modificar el comportamiento
del lenguaje que especifica el comportamiento, denominado metacomportamiento
–altura de la torre.
Como hemos especificado en § 7.5.1, la posibilidad de acceder un número infinito
de niveles de computación, puede hacer al programador perder la semántica real del sisteCAPÍTULO
7
122
ma con el que está trabajando. Por esto, reduciremos el nivel computacional necesario para
nuestro sistema a dos y trataremos de darle la mayor expresividad posible; sin restricciones.
123
CCAAPPÍÍTTUULLO 88:
LENGUAJES ORIENTADOS A OBJETOS BASADOS EN
PROTOTIPOS
Según la noción utilizada para agrupar objetos de igual comportamiento en un modelo
computacional orientado a objetos, es posible establecer la siguiente clasificación de
lenguajes [Evins94]:
Lenguajes orientados a objetos basados en clases.
Lenguajes orientados a objetos basados en prototipos.
Estudiaremos las semejanzas y disimilitudes existentes entre ambos, y analizaremos
las ventajas e inconvenientes que cada uno de los modelos aporta.
8.1 Clases y Prototipos
El modelo computacional de objetos basado en clases se apoya en la agrupación de
objetos de igual estructura y comportamiento, como instancias de una misma clase
[Booch94]. Se establece así una relación necesaria de instanciación entre objetos y clases;
no es posible crear y utilizar un objeto, si no se ha definido previamente la clase a la que
pertenece.
La estructura estática de un objeto y su comportamiento en función de su estado,
son definidos por una clase mediante sus atributos y sus métodos respectivamente. Cuando
se crea un objeto como instancia de una clase, su estado quedará definido por un conjunto
dinámico de valores para cada uno de los atributos de la estructura definida por su clase,
variando éste dinámicamente.
En el modelo computacional basado en objetos, no existe el concepto de clase; la
única abstracción existente es el objeto [Borning86]. Un objeto describe su estructura (conjunto
de atributos), su estado (los valores de éstos) y su comportamiento (la implementación
de los métodos que pueda interpretar).
La herencia es un mecanismo jerárquico de delegación de mensajes existente también
en el modelo de prototipos. Si se le envía un mensaje a un objeto, se analiza si éste
posee un método que lo implemente y, si así fuere, lo ejecuta; en caso contrario se repite
este proceso para sus objetos padre, en el cado de que los hubiere.
CAPÍTULO 8
124
La relación de herencia entre objetos basados en prototipos es una asociación más,
dotada de una semántica adicional –la especificada en el párrafo anterior. En el caso del
lenguaje de programación Self [Ungar87], la identificación de esta semántica especial es
denotada por la definición del miembro parent. El objeto asociado mediante este miembro
es realmente el objeto padre. Al tratarse la herencia como una asociación, es posible
modificar en tiempo de ejecución el objeto padre al que se hace referencia, obteniendo así
un mecanismo de herencia dinámica o delegación.
Vemos como, al eliminar el concepto de clase en el modelo, la aproximación de
prototipos resulta más sencilla. Sin embargo, ¿es posible agrupar objetos con el mismo
comportamiento, al igual que lo hacen las clases?
Utilizando únicamente el concepto de objeto también podremos agrupar comportamientos.
Si se crean objetos que únicamente posean métodos, éstos describirán el comportamiento
común de todos sus objetos derivados. Este tipo de objetos se denomina de
característica o rasgo (trait) [Lieberman86]. En la Figura 8.1, el objeto trait Object define
el comportamiento toString de todos los objetos. Del mismo modo, Point define el
comportamiento de sus dos objetos derivados.
Object
id:String
toString()
clone()
Point
x,y:Integer
add(p:Point)
p:Point
id=“p”
x=245
y=-23
Object
clone()
toString()
Point
add()
PointPrototype
x=0 y=0
ObjectPrototype
id=“none”
p
x=245
y=-23
id=“p”
Object
clone()
toString()
Point
add()
PointPrototype
id=“none”
x=0 y=0
ObjectPrototype
id=“none”
p
id=“p”
x=245
y=-23
b) Modelo basado en prototipos
con herencia múltiple
c) Modelo basado en prototipos
con herencia simple
a) Modelo basado
en clases
Clonación de Objetos
Clonación de Objetos
Objetos
Derogación
Figura 8.1: Representación de clases y objetos en los dos modelos.
Del mismo modo que hemos agrupado objetos de igual comportamiento, podemos
cuestionarnos la posibilidad de agruparlos por estructura. Un prototipo es un objeto descriptor
de una estructura común, utilizado para hacer copias exactas de él –clonaciones. En
este modelo computacional, mediante la utilización de prototipos y la primitiva de clonación,
se obtiene la misma funcionalidad que la de instanciación o creación de objetos a través
de una clase en su modelo homólogo.
En la Figura 8.1, la creación de un punto pasa por la clonación de su prototipo. Éste
posee la estructura común de todos los puntos (atributos x e y), su estado inicial (ambos
valores iguales a cero) y el comportamiento común definido por el objeto trait
Object. Vemos en la figura también, cómo es posible representar el diseño mediante la
utilización de un sistema computacional dotado tan solo de herencia simple.
Lenguajes Orientados a Objetos Basados en Prototipos
125
Como ejemplos de lenguajes orientados a objetos basados en prototipos podemos
mencionar a Self [Ungar87, Chambers89], Moostrap [Mulet93], ObjectLisp, Cecil [Chambers93],
NewtonScript [Swaine94] o PROXY [Leavenworth93].
8.2 Utilización de Lenguajes Orientados a Objetos Basados en
Prototipos
En este apartado evaluaremos las ventajas existentes en la utilización del modelo
orientado a objetos basado en prototipos, dejando para el próximo sus inconvenientes
frente al que utiliza clases.
8.2.1 Reducción Semántica
Una de las características destacables en el modelo de prototipos, al llevarse a cabo
una comparación con su homólogo, reside en la simplicidad obtenida al:
1. Eliminar el concepto de clase. No es necesario crear una clase para todo objeto.
2. Suprimir la dependencia necesaria existente entre un objeto y una clase. Siempre
ha de existir entre ellos una relación de instanciación, y el estado del objeto ha
de ser coherente con la estructura defina por su clase.
3. Eliminar la comprobación estática de tipos. Un objeto sólo puede recibir los
mensajes implementados por su clase y superclases. En un sistema basado en
clases, esta comprobación es realizada en tiempo de compilación.
En el caso del modelo basado en prototipos, al constituirse la herencia como un
mecanismo dinámico (delegación), es imposible llevar a cabo esta validación estáticamente;
el conocimiento de esta información sólo puede efectuarse dinámicamente34.
8.2.2 Inexistencia de Pérdida de Expresividad
La sencillez del modelo computacional que se basa únicamente en objetos puede
hacernos creer que supone una pérdida en la expresividad de los lenguajes que lo utilizan.
Sin embargo, se han realizado estudios que demuestran que la utilización de prototipos no
supone pérdida alguna de expresividad [Ungar91, Evins94]: toda semántica representable
mediante el modelo de clases puede ser traducida al modelo de prototipos. Su demostración
ha quedado patente en la implementación de compiladores de lenguajes basados en
clases, como Self y Java, a la plataforma Self que utiliza prototipos [Wolczko96].
Sin ánimo de ahondar en la traducción de conceptos de un modelo a otro, mostraremos
un ejemplo de traducción de la noción de miembro de clase, existente en la mayoría
de los lenguajes de programación basados en clases.
34 Es por esta razón por la que los sistemas basados en prototipos utilizan un sistema dinámico de tipos
[Chambers89], o bien carecen de su inferencia [Cardelli97].
CAPÍTULO 8
126
Point
x,y:Integer
nPoints:Integer
add(p:Point)
incPoints()
p:Point
x=245
y=-23
Point
nPoints=2
add()
incPoints()
PointPrototype
id=“none”
x=0 y=0
p
x=245
y=-23
a) Modelo basado b) Modelo basado en prototipos
en clases
Figura 8.2: Miembros de clase en ambos modelos orientados a objetos.
Como se muestra en la figura anterior, la clase Point posee un atributo de clase
que cuenta el número de instancias creadas, y un método de clase que permite incrementar
éste. La ubicación de ambos en el modelo de prototipos se sitúa en el objeto trait. Éste, y
no uno de sus derivados, será el que reciba los mensajes de clase y será su propio estado el
que determine el resultado de su ejecución (puesto que el método incPoints incrementa
el atributo nPoints que pertenece al objeto trait). La utilización de este servicio se demandará
mediante la clase (objeto Point) y no mediante una de sus instancias.
8.2.3 Traducción Intuitiva de Modelos
La traducción del modelo basado en clases –de un mayor nivel de abstracción– al
modelo que utiliza prototipos se produce de un modo intuitivo para el programador. Por
ejemplo, el programador de aplicaciones que selecciona un lenguaje de programación como
Python [Rossum2001] o Java [Gosling96], ha de tener en mente esta traducción.
Aunque los dos lenguajes mencionados en el párrafo anterior poseen un modelo
computacional basado en clases, una vez compilado su código fuente, en fase de ejecución,
el modelo computacional empleado está basado en prototipos. En tiempo de ejecución las
abstracciones de clases son substituidas por objetos que representan éstas. Así, en Python,
el atributo __class__ de todo objeto nos devuelve su objeto-clase asociado; en Java, el
método getClass nos devuelve un objeto del tipo Class.
Por esta traducción entre modelos intuitiva, la sencillez del modelo de prototipos y
la carencia de pérdida en su expresividad, Wolczko propone los prototipos como modelo
universal de computación de lenguajes orientados a objetos [Wolczko96]. Llevando a cabo
la traducción de cualquier lenguaje a un único modelo, la interacción intuitiva entre aplicaciones
se podría lograr independientemente del lenguaje que hay sido utilizado para su
construcción.
8.2.4 Coherencia en Entornos Reflectivos
Dos problemas existentes en el campo de las bases de datos son la evolución y
mantenimiento de versiones de esquemas. Pueden definirse de la siguiente forma [Roddick95]:
Evolución de esquemas (schema evolution): Capacidad de una base de datos para
permitir la modificación de su diseño, sin incurrir en pérdida de información.
Lenguajes Orientados a Objetos Basados en Prototipos
127
Mantenimiento de versiones de esquemas (schema versioning): Se produce cuando,
tras haberse llevado a cabo un proceso de evolución del esquema, la base de datos
permite acceder a la información tanto de un modo retrospectivo, como
mediante la estructura actual, manteniendo así la información para cada versión
de esquema existente.
Con el surgimiento de las bases de datos orientadas a objetos, este problema es trasladado
al concepto de clase: el esquema –estructura y comportamiento– de un objeto queda
determinado por la clase de la que es instancia. ¿Cómo se deberá adaptar un objeto si es
modificada su clase?
La cuestión surgida en el punto anterior brota de igual modo en el caso de utilizar
un modelo computacional basado en clases que esté dotado de reflectividad estructural (§
6.3.1). ¿Qué sucede con los estados de los objetos si modifico la estructura de su clase?
¿Cómo puedo modificar la estructura de un único objeto? Existen distintas aproximaciones.
Una de las soluciones aportadas, sin necesidad de eliminar el concepto de clase, es
la enfocada a obtener mantenimiento de versiones del esquema. El sistema MetaXa
[Golm97], estudiado en § 7.4, implementa esta solución mediante la creación de las denominadas
clases sombra (shadow classes). En este sistema, siempre que se modifique una clase
se crea una copia (sombra) de la clase antigua, se modifica ésta, y se asocian los objetos
nuevos a esta clase, manteniendo los existentes como instancias de la versión anterior.
Clase Clase A Clase A
Clase
ObjetoA Tipo
ObjetoB
Clase Clase A Clase A
Clase
ObjetoA Tipo
ObjetoB
Clase A’ Tipo
La modificación del comportamiento del
objeto B, mediante la reimplementación
de uno de sus métodos, produce la creación
de una clase sombra A’, de igual tipo para
el programador
Tipos existentes
para el programador
Modelo de computación
existente para el intérprete
Figura 8.3: Creación de una clase sombra para la modificación del comportamiento de un objeto.
La implementación de este mecanismo ha de tener en cuenta que la clase original y
la sombra, aun siendo distintas, deberán poseer la misma identidad, es decir, el programador
de aplicaciones no deberá tener noción de la existencia de ambas. La implementación
de este mantenimiento de versiones de clases es complejo y conlleva a un determinado
número de incoherencias del modelo computacional [Golm97c]. Los propios autores del
sistema, en [Golm97c], mencionan que sería más sencilla su implementación si se hiciese
uso del modelo basado en prototipos.
La modificación de la estructura y comportamiento de objetos en un sistema basado
en prototipos es más sencilla de implementar, y no genera incoherencias en dicho modelo
–por ejemplo, el sistema Moostrap [Mulet93]. Distintos escenarios son:
CAPÍTULO 8
128
1. La modificación de la estructura de un único objeto se obtiene al modificar directamente
éste.
2. La modificación de la estructura de los nuevos objetos a crear se lleva a cabo
mediante la modificación del prototipo utilizado.
3. La modificación del comportamiento de todos los objetos de un grupo, se obtiene
mediante la manipulación del objeto trait correspondiente.
4. La modificación de los nuevos objetos a crear, o de únicamente uno de los existentes,
se puede alcanzar mediante la clonación del objeto de comportamiento
seleccionado y su posterior modificación.
En este modelo, es el programador el que lleva el peso de agrupar los objetos por
comportamiento y estructura; de este modo también puede identificar lo que realmente
desea modificar (el grupo, uno en concreto o los sucesivos).
8.3 Conclusiones
En función de la clasificación establecida al comienzo de este capítulo, veíamos
cómo los modelos computacionales orientados a objetos podían utilizar clases o simplemente
objetos. Como veíamos en el punto anterior, la utilización del modelo computacional
basado en prototipos aporta un conjunto de ventajas. Sin embargo, los lenguajes que
utilizan clases también poseen virtudes:
El nivel de abstracción ofrecido por las clases es más elevado, permitiendo expresar
al programador un modelo más cercano al problema a resolver.
La agrupación obligada de objetos mediante la definición de clases exige al programador
la división de las aplicaciones a desarrollar en abstracciones a modelar.
La definición de los tipos de objetos mediante sus clases permite detectar en
tiempo de compilación errores de tipo, reduciendo así del número de errores a
producidos en tiempo de ejecución y facilitando la labor del desarrollador.
En función del estudio realizado de ambos modelos, podemos afirmar como conclusión
que los sistemas basados en clases están más orientados a la programación, mientras
que la utilización de prototipos está más acorde con el desarrollo de una plataforma
computacional. Como ejemplo pragmático de esto, podemos mencionar cómo determinados
lenguajes basados en clases, como Java o Python, traducen su semántica de programación
basada en clases a un modelo computacional de objetos, en su fase de ejecución.
129
CCAAPPÍÍTTUULLO 99:
ARQUITECTURA DEL SISTEMA
Una vez descritos todos los objetivos y requisitos a conseguir, estudiado los distintos
sistemas y técnicas existentes en la consecución de éstos, y evaluadas las aportaciones y
carencias de los mismos, describiremos la arquitectura general del sistema innovador propuesto
en esta tesis, analizando brevemente su estructura y los objetivos generales a cumplir
por cada uno de sus elementos.
Profundizaremos en capítulos posteriores en la descripción de cada uno de los elementos
del sistema, así como en la justificación de las técnicas seleccionadas y el cumplimiento
de los objetivos marcados.
9.1 Capas del Sistema
El sistema está dividido en tres capas o elementos bien diferenciados, que pueden
apreciarse en la siguiente figura:
CAPÍTULO 9
130
Máquina Abstracta
Intérprete
Genérico
Aplicación
en “A”
Aplicación
en “B”
ejecución
ejecución
Especificación
Lenguaje “A”
Especificación
Lenguaje “B”
Funcionalidades
de Persistencia
Funcionalidades
de Distribución
modificación
utilización
modificación
utilización
Entorno de Programación
Sistema Reflectivo
No Restrictivo
ejecución
Figura 9.1: Arquitectura del sistema.
9.1.1 Máquina Abstracta
El motor computacional del sistema es la implementación de una máquina abstracta
–máquina virtual. Todo el código ejecutado por ésta es portable y, por tanto, independiente
de la plataforma física empleada.
Posteriormente ahondaremos en las ventajas de codificar el sistema sobre una máquina
abstracta (§ 9.3) y en la arquitectura y diseño de ésta (capítulo 10 y capítulo 12), mas
la ventaja a destacar en este momento es que todo el sistema comparte el mismo modelo
computacional y éste es independiente de la plataforma.
9.1.2 Entorno de Programación
Sobre la máquina abstracta se desarrolla un código que facilite la labor del programador.
Este código es portable, independiente del lenguaje (cualquier aplicación en el sistema,
codificada en cualquier lenguaje, puede utilizarlo) e independiente de la plataforma.
La máquina abstracta ha de poseer la característica de ser extensible para, sin necesidad
de modificar su implementación, pueda desarrollarse sobre ella un entorno de programación
con funcionalidades de un mayor nivel de abstracción, como distribución o persistencia.
9.1.3 Sistema Reflectivo No Restrictivo
Hasta esta tercera y última capa, todas las aplicaciones del sistema se desarrollan
sobre el lenguaje nativo de la máquina abstracta. Este lenguaje posee una semántica fija
para sus aplicaciones. Mediante esta capa se otorga independencia del lenguaje al sistema y
flexibilidad dinámica, sin restricciones previas, de los lenguajes a utilizar.
Arquitectura del Sistema
131
Un intérprete genérico toma la especificación de un lenguaje y ejecuta una aplicación
codificada en éste. La aplicación puede hacer uso del entorno de programación e interactuar
con otras aplicaciones codificadas en otros lenguajes. Adicionalmente podrá modificar
la especificación del lenguaje utilizado, reflejándose estos cambios en la adaptación
de la semántica de la propia aplicación, de forma instantánea.
9.2 Único Modelo Computacional de Objetos
Adicionalmente al conjunto de requisitos obtenidos por la utilización de una máquina
abstracta –véase el capítulo 16–, la utilización de ésta está justificada por la selección
de un único modelo computacional de objetos, cualesquiera sean el lenguaje y plataforma
seleccionados.
La idea es que todo el código se ejecute sobre el modelo de objetos soportado por
la máquina virtual35, y que las aplicaciones puedan interactuar entre sí independientemente
de su lenguaje de programación. La codificación del entorno de programación sobre el
lenguaje propio de la máquina, facilita la utilización de éste sin dependencia alguna del lenguaje
a utilizar.
Una de las tareas a llevar a cabo por el intérprete genérico del sistema reflectivo es
traducir las aplicaciones codificadas mediante un lenguaje de programación, a su correspondencia
en el modelo computacional de la máquina abstracta. Una vez este proceso haya
sido llevado a cabo, la aplicación podrá interaccionar con el resto del sistema como si
hubiese sido codificada sobre su lenguaje nativo.
35 Para saber más acerca del modelo computacional de objetos utilizado por la máquina abstracta, así
como la justificación de su elección, consúltese el capítulo 10.
CAPÍTULO 9
132
Aplicación en el
Lenguaje “A”
Aplicación en el
Lenguaje “B”
Objetos
primitivos
Entorno de
computación
Intérprete Genérico
Traducción Traducción
Aplicación B Aplicación A
Máquina
Abstracta
Entorno Computacional
de la Máquina Abstracta
Interacción entre las
distintas aplicaciones
Figura 9.2: Interacción entre distintos objetos de un mismo espacio computacional.
Como se muestra en la figura anterior, la máquina abstracta parte de un conjunto
mínimo de objetos primitivos –funcionalidad básica; la reducción del conjunto de éstos
facilita su portabilidad. Haciendo uso de la extensibilidad de esta plataforma virtual, se desarrolla
código que eleva el nivel de abstracción en la programación de la plataforma –
entorno de programación–, facilitando la tarea del programador.
El programador elige un lenguaje de programación y, en la interpretación de éste, su
modelo es traducido al propio de la plataforma abstracta. Con el único modelo de objetos
existente, la interacción de aplicaciones es directa, reutilizando en todo momento las funcionalidades
desarrolladas, indistintamente del lenguaje de programación utilizado.
El resultado es un único espacio computacional de interacción de objetos que ofrecen
sus servicios al resto del sistema. La procedencia de cada uno de ellos es intrascendente,
puesto que la máquina computa éstos de modo uniforme.
9.3 Máquina Abstracta
La máquina abstracta supone el motor computacional del conjunto del sistema. La
migración de éste a una plataforma pasa por la recompilación de su implementación para el
nuevo sistema nativo. El modelo computacional de objetos definido por ésta representará
el propio del sistema y la forma en la que las distintas aplicaciones interactúen entre sí.
Éstos son los objetivos generales a alcanzar:
Arquitectura del Sistema
133
9.3.1 Conjunto Reducido de Primitivas
La máquina abstracta ha de poder implantarse en entornos heterogéneos. Cualquier
sistema computacional, por reducido que éste sea, deberá ser capaz de instalar una implementación.
La reducción del número de primitivas computacionales facilita su implantación
en plataformas heterogéneas.
El diseño de una plataforma de computación reducida facilita la migración del sistema.
Si el código de la máquina es portable, la portabilidad total del sistema se reduce a la
implantación de la máquina en distintas plataformas. Por lo tanto, la reducción de su tamaño
minimiza el esfuerzo para llevar a cabo nuevas implantaciones.
9.3.2 Mecanismo de Extensibilidad
Dado que el número de primitivas computacionales de la máquina abstracta debe
ser lo más reducido posible, el lenguaje de programación poseerá un bajo nivel de abstracción.
Para ofrecer al programador de aplicaciones un mayor conjunto de funcionalidades y
un mayor nivel de abstracción, es necesario que la máquina abstracta posea un mecanismo
de extensibilidad.
Un problema típico en la creación de máquinas abstractas es la ampliación de sus
funcionalidades mediante la inclusión de instrucciones, aumentando así la funcionalidad e
implementación de ésta. Un ejemplo es el desarrollo de la máquina abstracta del sistema
integral orientado a objetos Oviedo3 [Álvarez97]. Inicialmente ofrecía las características
propias de una plataforma orientada a objetos pura. Posteriormente, distintas versiones
fueron implementándose añadiendo características de persistencia [Ortin97], seguridad
mediante capacidades [Díaz2000], planificadores genéricos de tareas [Tajes2000] y un sistema
de distribución [Álvarez2000].
La existencia de múltiples versiones de implementaciones de la máquina abstracta,
la pérdida de portabilidad de su código, la complejidad de implementar una única máquina
virtual con el conjunto total de sus características, y la pérdida de sencillez en su implementación
para poder implantarla en entornos heterogéneos, son los resultados de seguir este
diseño erróneo.
La ampliación de la abstracción del sistema ha de codificarse en el propio lenguaje
de programación de la máquina, a partir de sus primitivas computacionales. El hecho de
que éste sea código propio de la máquina, hace que sea portable a cualquier plataforma. El
resultado es una máquina virtual de sencilla implementación y, sobre ella, un código portable
que amplia el nivel de abstracción para el programador, cualquiera que sea la plataforma
existente.
CAPÍTULO 9
134
Primitivas
Entorno de
computación
Máquina
Abstracta
Aplicación
de Usuario
Extensión de Funcionalidades
Utilización de Funcionalidades
Figura 9.3: Extensibilidad de las funcionalidades primitivas de la máquina abstracta.
Para que lo propuesto pueda llevarse a cabo sin necesidad de modificar la implementación
de la máquina abstracta, es necesario que ésta posea un mecanismo de extensibilidad.
Un ejemplo de esta extensión del nivel de abstracción, es la codificación de la segunda
capa de este sistema: el entorno de programación.
9.3.3 Selección del Modelo Computacional
El nivel de abstracción proporcionado por la máquina abstracta es propio del modelo
computacional utilizado por todo el sistema. La selección del correcto nivel de abstracción
que ofrezca el modelo de computación es una tarea difícil: un bajo nivel de abstracción
hace más compleja la interoperabilidad de aplicaciones, mientras que si éste es
demasiado elevado, podrá suponer que plataforma sea dependiente del lenguaje.
El modelo a elegir ha de permitir representar la computación de cualquier lenguaje
de programación, ser sencillo para poder ser implementada su computación de un modo
reducido, y no su suponer un cambio de expresividad elevado para que puedan interactuar
entre sí las aplicaciones codificadas en distintos lenguajes de un modo natural.
9.3.4 Interacción Directa entre Aplicaciones
Puesto que el objetivo de esta tesis es conseguir un entorno de programación de
aplicaciones flexible, que permita su interacción indistintamente del lenguaje de programación
elegido para desarrollarlas, el motor computacional del mismo –la máquina abstracta–
ha de facilitar la comunicación entre ellas.
Si tomamos por ejemplo la máquina virtual de Java [Sun95], cada aplicación que se
ejecuta necesita un proceso con la ejecución de la máquina virtual que interprete este código,
suponiendo así la interacción entre estas aplicaciones una comunicación entre dos procesos
distintos dentro del sistema operativo existente. Mecanismos adicionales como el uso
de sockets o RPCs (Remote Procedure Calls), son comúnmente utilizados en la intercomunicación
de estos procesos.
Arquitectura del Sistema
135
Nuestra máquina abstracta deberá ser capaz de ejecutar aplicaciones en paralelo, facilitando
la intercomunicación entre éstas, y estableciendo un espacio de nombres único en
el que el paso de mensajes entre las distintas aplicaciones sea similar a la invocación de métodos
de un objeto del mismo programa.
9.3.5 Evaluación Dinámica de Datos como Código
Esta característica es utilizada en la implementación de la tercera capa del sistema;
supone la posibilidad de crear y modificar dinámicamente una información –datos–, y
hacer posteriormente que la máquina abstracta los interprete como computación –código.
Si, como mencionábamos en § 6.1, el hecho de representar el comportamiento como
datos manipulables por un proceso se define como “cosificación”, el proceso contrario
en el que éstos se evalúan, puede definirse como “descosificación”. Ejemplos de esta característica
en lenguajes de programación conocidos son la posibilidad de evaluar una lista
como código mediante la función eval de Lisp [Steele90], o la función exec de Python
[Rossum2001] tomando cadenas de caracteres.
Esta peculiaridad de la máquina es utilizada por el intérprete genérico de la capa del
sistema reflectivo. Como se muestra en la Figura 9.1, una aplicación deberá ser capaz de
modificar la especificación de su lenguaje. Para ello, generará dinámicamente un código
evaluable por la máquina –no por el intérprete– que, al ejecutarse en el entorno de ésta,
podrá modificar la especificación del lenguaje –existente en el mismo nivel computacional.
Este sistema será detallado en el capítulo 11.
9.4 Entorno de Programación
Respecto al software flexible desarrollado sobre la plataforma abstracta, enfocado a
elevar el nivel de abstracción del sistema, debe ser desarrollado siguiendo un conjunto de
objetivos básicos. Enunciaremos éstos para posteriormente estudiar su diseño en el
capítulo 10.
9.4.1 Portabilidad e Independencia del Lenguaje
La codificación del entorno de programación ha de ser independiente de toda plataforma
física y lenguaje de programación. Para codificar una única vez éste e instalarlo en
cualquier sistema, el código no deberá tener dependencia alguna de la plataforma física
donde se implante. Del mismo modo, cualquier aplicación desarrollada sobre cualquier
lenguaje, deberá poder utilizar los servicios ofertados por este código; no deberá existir
restricción alguna al respecto.
La única tarea a tener en cuenta a la hora de implantar el entorno de programación
en una plataforma, es la selección del subconjunto de funcionalidades que deseemos instalar.
En función de las necesidades del sistema, de su potencia de cómputo y de la memoria
disponible, el sistema demandará una parte o la totalidad de las funcionalidades del entorno
de programación.
9.4.2 Adaptabilidad
Puesto que el objetivo principal de esta tesis es el desarrollo de un sistema computacional
flexible, la adaptabilidad de sus funcionalidades es una característica primordial. El
CAPÍTULO 9
136
entorno de programación deberá aumentar el nivel de abstracción del sistema, ofreciendo
nuevas funcionalidades; sin embargo, es importante que éstas sean adaptables.
Si el entorno de programación ofrece funcionalidades de persistencia, éstas deberán
ser flexibles respecto al entorno físico utilizado para almacenar los objetos. Además, si el
programador deseare introducir su propio sistema de persistencia, su diseño debería estar
enfocado a minimizar el número de pasos necesarios para implantarlo.
La adaptabilidad también ha de aplicarse a las primitivas de la máquina abstracta. Si
codificamos todo el software utilizando estas primitivas, las aplicaciones perderán adaptabilidad
al ser variables de computaciones constantes –la semántica de las primitivas se mantiene
invariable. Sin embargo, del mismo modo que fue diseñada la imagen computacional
de Smalltalk [Ingalls78], la semántica de las primitivas pueden extenderse con nuevas rutinas
adaptables36. Si se programa haciendo uso de las segundas, el código generado podrá
adaptar su funcionalidad modificando la extensión de las primitivas.
El objetivo buscado es que la totalidad de las funcionalidades ofrecidas por esta capa
del sistema sean adaptables a las distintas necesidades de los programadores.
9.4.3 Introspección
La implantación del entorno de programación en sistemas computacionales heterogéneos
requiere el conocimiento dinámico del subconjunto de funcionalidades instaladas en
cada plataforma. Si una plataforma no ofrece las características de persistencia por limitaciones
de espacio, el código de usuario deberá tener la posibilidad de consultar si esta funcionalidad
ha sido implantada, antes de hacer uso de ella.
El objetivo de desarrollo del sistema en entornos heterogéneos, y la posibilidad de
crear aplicaciones en el conjunto de plataformas existentes como si de un único ordenador
se tratase, requiere la existencia de un mecanismo que permita analizar y conocer un sistema
desde sí mismo –introspección.
9.5 Sistema Reflectivo No Restrictivo
En el análisis de cualquier sistema reflectivo, siempre se ha de tener en cuenta el
debate “Hamiltonians versus Jeffersonians” [Foote92] que expresábamos en § 6.4. Por un lado
está la flexibilidad otorgada al programador para hacer sus aplicaciones lo más adaptables y
extensibles posible –Jeffersonians; por otro lado, la posibilidad de modificar indefinidamente
el sistema puede conllevar a estados incoherentes de computación y semánticas ininteligibles
[Foote90] –Hamiltonians.
Nuestro principal objetivo es desarrollar un estudio para la creación de un sistema
flexible con un elevado grado de adaptabilidad. Por esta razón, nos centraremos en la vertiente
Jeffersoniana, tratando de encontrar el mayor nivel de adaptabilidad posible. Una vez
obtenido éste, podría diseñarse un sistema de seguridad encargado de controlar el nivel de
reflexión permitido a cada uno de los usuarios. Sin embargo, este propósito queda fuera de
los objetivos marcados dentro de esta tesis.
A continuación analizaremos los puntos más significativos de esta capa del sistema.
36 En Smalltalk, para acceder a un atributo indexado de un objeto la primitiva a utilizar es basicAt. Sin
embargo, su utilización es desaconsejada frente al mensaje at [Mevel87]; este segundo método se implementa
haciendo uso de la primitiva, teniendo en cuenta la jerarquía objetos establecida por la relación
de herencia.
Arquitectura del Sistema
137
9.5.1 Características Adaptables
Basándose en la clasificación de adaptabilidad descrita en § 6.3, deberá otorgarse al
sistema las siguientes características de adaptabilidad:
1. Conocimiento dinámico del entorno. El conjunto del sistema ha de ser introspectivo,
ofreciendo la posibilidad de conocer su estado y descripción dinámicamente.
2. Acceso y modificación de su estructura. La estructura de los objetos existentes
deberá ser manipulable en tiempo de ejecución, para conseguir adaptabilidad estructural
dinámica.
3. Semántica computacional. La semántica computacional del sistema deberá poder
conocerse, modificarse y ampliarse. De esta forma, una aplicación en ejecución
podrá ser adaptada sin necesidad de modificar su código fuente ni finalizar
su ejecución.
4. Configuración del lenguaje de programación. Cualquier aplicación deberá ser
capaz de modificar su propio lenguaje de programación para amoldarlo a sus
necesidades específicas de expresividad.
Como estudiamos en el capítulo 7, no existe sistema alguno dotado de la capacidad
de ser adaptable en todas estas características.
9.5.2 Independencia del Lenguaje
Para esta capa del sistema, las aplicaciones deberán constituir procesos computacionales
susceptibles de interactuar con el resto de aplicaciones existentes, sin que el
lenguaje de programación sea una variable adicional del programa. Mediante algún
mecanismo de implementación, el programador deberá ser capaz de desarrollar
aplicaciones –o fracciones de aplicaciones– en el lenguaje que él desee, sin que ello restrinja
el modo en el que se interactúe con el resto del sistema.
La programación de aplicaciones no deberá verse limitada a los lenguajes de programación
más conocidos; el sistema deberá constituir un entorno de desarrollo e interacción
de lenguajes de propósito específico.
Deberá contemplarse la posibilidad de que la propia aplicación describa su lenguaje
haciéndola autosuficiente para su ejecución, permitiendo el desarrollo de aplicaciones que
realmente expresen computación por sí solas –no exista dependencia de la implementación
de un intérprete del lenguaje empleado.
9.5.3 Grado de Flexibilidad de la Semántica Computacional
Como hemos definido en § 9.5.1, una de las características a hacer adaptable en
nuestro sistema es su semántica computacional. Esta faceta implica la posibilidad de adaptar
dinámicamente una aplicación, sin necesidad de modificar su código fuente.
El modo en el que expresamos la semántica de un lenguaje es mediante otro
lenguaje de especificación de semánticas [Mosses92]. Si accedemos y modificamos la
semántica del lenguaje de programación, lo haremos mediante el lenguaje de especificación
de semánticas. Podemos observar pues, como éste es un concepto recursivo: ¿cómo
definimos la semántica del lenguaje de especificación de semánticas?
CAPÍTULO 9
138
La flexibilidad de modificar la semántica de un lenguaje puede pasar por modificar,
a su vez, la semántica del lenguaje de especificación de su semántica. La pregunta que debemos
hacernos es si esta facultad es o no realmente necesaria para la consecución de los
requisitos de nuestro sistema.
Tras el estudio en § 7.5 de los sistemas que permiten modificar la semántica de
cualquier lenguaje de un modo recursivo en infinitos niveles (intérpretes metacirculares),
concluimos las siguientes afirmaciones:
La posibilidad de modificar la semántica de semánticas en un grado indefinido
puede llevar a la pérdida del conocimiento de la semántica real existente en el
sistema –no conoceremos el comportamiento real de la ejecución de una instrucción.
Los beneficios aportados por la utilización de elevar la flexibilidad a más de un
nivel computacional son muy limitados [Foote90].
La mayoría de los sistemas limitan el espectro de la semántica computacional a
flexibilizar, es decir, no son capaces de modificar la totalidad de su comportamiento.
Intérprete Semántica
Lenguaje
Aplicación
Intérprete
Semántica
Semántica
Semántica
Lenguaje
Intérprete
Semántica
Semántica
Semántica
Semántica
Semántica
Lenguaje
…
Ejecuta
Ejecuta
Ejecuta
Utiliza
Utiliza
Utiliza
Adapta
Adapta
Adapta
Adaptabilidad
de una parte
de la semántica
Infinitos niveles
computacionales
Intérprete Semántica
Lenguaje
Aplicación
Ejecuta
Utiliza
Adapta
Adaptabilidad
total de la
semántica
Aproximación Horizontal
Aproximación Vertical
Figura 9.4: Distintas aproximaciones en la consecución de adaptabilidad en la semántica del sistema.
En función del resumen del estudio realizado en este punto, y buscando la mayor
flexibilidad computacional de nuestro sistema, limitaremos la modificación de la semántica
computacional a un nivel –dos grados de altura–, pero obligando a que ésta permita modificar
la totalidad de sus computaciones –grado de anchura ilimitado.
9.5.4 Adaptabilidad No Restrictiva
En el análisis efectuado en el punto anterior, requeríamos la necesidad de modificar
la semántica de cualquier faceta computacional del sistema. Para este punto, se demanda
que el mecanismo de modificación de la semántica no imponga restricciones previas la ejecución
de dichas modificaciones.
Arquitectura del Sistema
139
Existen sistemas que permiten modificar cualquier semántica computacional, estableciendo
previamente el protocolo de acceso a ellas (§ 7.4.1). Si para una aplicación, una
vez codificada y ejecutada, se desea modificar algún aspecto no previsto con anterioridad,
su adaptación dinámica no será factible. En este tipo de sistemas no es posible desarrollar
aplicaciones adaptables a contextos desconocidos en fase de desarrollo. Su flexibilidad está
condicionada al conocimiento de a qué deberá adaptarse previamente a su ejecución.
La adaptabilidad de nuestro sistema no deberá necesitar el conocimiento previo de
aquello que sea susceptible de ser modificado. Deberá desarrollar un mecanismo de flexibilidad
no restrictivo, en el que cualquier característica podrá ser adaptada sin necesidad de
estimarlo previamente.
141
CCAAPPÍÍTTUULLO 1100:
ARQUITECTURA DE LA MÁQUINA ABSTRACTA
Partiendo de los requisitos impuestos en el capítulo 2 y los objetivos globales de esta
tesis, analizaremos las alternativas estudiadas en la sección del estado del arte y, basándonos
en su evaluación, adoptaremos los criterios de diseño generales (arquitectura) en la
construcción de la máquina abstracta.
Fundamentalmente, los principales objetivos a obtener en el diseño de la plataforma,
se pueden reducir a los siguientes: portabilidad, heterogeneidad, extensibilidad y adaptabilidad.
Centrándonos en estos pilares, describiremos su arquitectura para posteriormente,
en el capítulo 12, presentar su diseño.
En el apéndice A se describe finalmente un diseño para la implementación de una
máquina virtual acorde a la máquina abstracta descrita. Se debe recordar la diferencia existente
entre máquina abstracta y virtual, descrita en capítulo 3: una máquina virtual es un
intérprete software de la especificación de una máquina abstracta.
10.1 Características Principales de la Máquina Abstracta
En este apartado, enunciaremos los requisitos de la máquina abstracta, evaluaremos
posibles alternativas, y justificaremos y explicaremos la solución a emplear.
10.1.1 Reducción de Primitivas Computacionales
Buscando la portabilidad global del sistema y la implantación de éste en entornos
computacionales heterogéneos, el número de primitivas de la plataforma deberá reducirse
al mínimo. Seleccionando la cantidad mínima de primitivas a implementar por la máquina,
obtenemos dos beneficios:
1. La implantación de ésta en múltiples plataformas, al ser reducida la implementación
de su máquina virtual, se llevará a cabo de un modo sencillo, obteniendo
así una elevada portabilidad.
2. Su tamaño reducido implica la posibilidad de implantación en sistemas computacionales
de capacidad restringida; tanto en dispositivos físicos de capacidad
CAPÍTULO 10
142
limitada de cómputo, como en otras aplicaciones que deseen interpretar su lenguaje37.
La obtención de los dos beneficios anteriores producidos al reducir las operaciones
primitivas de nuestro sistema de computación, quedó demostrada empíricamente en el desarrollo
de los sistemas operativos basados en micronúcleo, estudiados en § 5.6.
En el diseño de la máquina abstracta distinguiremos entre dos tipos distintos de
primitivas, siendo un conjunto de éstas variable mientras que el otro se deberá mantener
inalterable:
1. Primitivas computacionales. Definen el sistema computacional de la máquina
abstracta. Supone la semántica del lenguaje de la máquina que toda máquina virtual
deberá implementar. El conjunto de estas primitivas es invariable. Ejemplos
de estas operaciones pueden ser el modo en el que se crea un objeto o se interpreta
el paso de un mensaje.
2. Primitivas operacionales. Suponen la semántica primitiva de una operación independiente
del funcionamiento de la máquina. Un ejemplo puede ser la forma
en la que se suman dos enteros o el almacenamiento de un byte en un sistema
persistente.
Este conjunto de primitivas operacionales ha de reducirse al mínimo; sin embargo,
en ocasiones puede ser necesaria su ampliación. Un ejemplo puede ser la
inclusión de números racionales en el álgebra operacional de la máquina. Esta
ampliación debe llevarse a cabo sin necesidad de modificar la implementación
de la máquina, de forma que el código existente para nuestra plataforma no
pierda nunca su capacidad de ser ejecutado –la máquina virtual no deberá sufrir
modificación alguna.
Máquina Virtual
Primitivas
Computacionales
Primitivas
Operacionales
v.1
Op1
Op2
Op3
Op4
Primitivas
Operacionales
v.2
Op1
Op2
Op3
Op4
Código
Fuente
Inicial
Código Fuente
Utiliza nueva
Primitiva
Ejecución
Ejecución
Aplicación 1
Aplicación 2
Acceso
Aplicación 1
Acceso
Aplicación 2
Reemplazamiento
del componente
Plataforma Física
de Ejecución
Una vez reemplazada
la nueva semántica
operacional, ambas
aplicaciones siguen
siendo válidas
Figura 10.1: Ampliación de la semántica operacional de la máquina virtual sin perder la portabilidad
de sus aplicaciones.
37 Como ejemplo real podemos tomar la implementación de la máquina virtual de Java (JVM, Java Virtual
Machine) [Lindholm96] como parte de un navegador Web, dedicada a ejecutar código Java descargado
dinámicamente de la red.
Arquitectura de la Máquina Abstracta
143
La ampliación del sistema debe llevarse a cabo sin modificar la interfaz del módulo
de primitivas operaciones existente con anterioridad. De este modo, la plataforma
es adaptable a cualquier entorno, no existirán distintas versiones del intérprete,
y las aplicaciones no quedarán desfasadas.
10.1.2 Extensibilidad
El hecho de diseñar la máquina abstracta con el menor número de primitivas posible
ayuda a que ésta sea fácilmente implantada en diversas plataformas. Sin embargo, el
bajo nivel de abstracción de su lenguaje hace complejo el desarrollo de aplicaciones sobre
ésta. Un mecanismo de extensibilidad de la plataforma computacional será necesario para
obtener un mayor nivel de abstracción.
En el estudio de los sistemas que hacen uso de la introspección (§ 7.1) y de la
reflectividad estructural (§ 7.2), vimos cómo ambos conceptos pueden ser utilizados para
implementar sistemas extensibles. En concreto, en el caso de los sistemas Smalltalk, Self y
ObjVlisp, esta extensibilidad se ha empleado en la creación de entornos computacionales
diseñados sobre máquinas abstractas.
Mediante introspección, podremos conocer dinámicamente la estructura dinámica
de un objeto (sus métodos, atributos y el número de veces que es referenciado por otros
objetos, por ejemplo). Haciendo uso de esta información, se podrá desarrollar un sistema
de persistencia, un tratamiento de excepciones, o incluso un recolector de basura, gracias al
conocimiento estructural de cada objeto.
En el caso de haber implementado un sistema de persistencia o un sistema de distribución
con movilidad de objetos, la reflectividad estructural puede ser utilizada para
crear dinámicamente un objeto y añadirle sus atributos y métodos oportunos. El desarrollo
de estas funcionalidades eleva el nivel de abstracción computacional existente inicialmente
significando la extensión del mismo.
El hecho de implementar un entorno de programación haciendo uso del propio
lenguaje de la máquina y de su facultad extensiva, implica que la totalidad del código de
dicho entorno será independiente de la plataforma, y que no será necesario modificar la
implementación de la máquina virtual dando lugar a distintas versiones – se perdería así la
posibilidad de ejecutar código de versiones anteriores.
10.1.3 Definición del Modelo Computacional
La selección correcta del modelo computacional a seguir por la máquina abstracta
es una tarea compleja. Por un lado, ha de ser lo suficientemente sencilla como para satisfacer
el criterio de reducir al mínimo su número de primitivas computacionales (§ 10.1.1); por
el otro, deberá ofrecer un nivel de abstracción que permita la interacción entre aplicaciones
de un modo sencillo, independientemente del lenguaje de programación seleccionado por
el programador.
Los modelos computacionales orientados a objetos están siendo ampliamente utilizados
en la actualidad. Sin embargo, la implementación del conjunto global de sus características
[Booch94] supondría que la máquina virtual poseyese un tamaño demasiado elevado
para implantarlo en plataformas con poca capacidad de procesamiento. Tomando el modelo
de objetos de Smalltalk y reduciéndolo computacionalmente, se creó un modelo de objetos
más limitado basado en el concepto de prototipo (capítulo 8). Este modelo de objetos
era más sencillo de implementar y, lo más importante, no perdía expresividad frente al moCAPÍTULO
10
144
delo basado en clases: todo lo representable en un lenguaje orientado a objetos basado en
clases, se puede expresar mediante el uso de prototipos [Ungar91].
Como explicábamos en el capítulo 8, el modelo orientado a objetos basado en prototipos
elimina el concepto de clase. Además de reducirse la semántica de sus operaciones
sin pérdida alguna de expresividad, la eliminación del concepto de clase es útil y coherente
en entornos estructuralmente reflectivos. La utilización de reflectividad estructural con el
modelo basado en clases produce la aparición de un problema denominado evolución de
esquemas (scheme evolution) [Roddick95].
¿Qué sucede si queremos modificar la estructura de un único objeto de una clase
sin alterar el resto de sus instancias? ¿Cómo se amoldan los objetos al eliminar, modificar o
aumentar alguno de los atributos de su clase? Existen soluciones de compromiso como la
creación de clases sombra (shadow classes), implementada por la plataforma reflectiva MetaXa
(§ 7.4), de elevada complejidad de implementación y resultados poco satisfactorios.
En la utilización del modelo de prototipos, estas modificaciones no conllevan a incoherencias
en el lenguaje de programación –véase el capítulo 8.
Conocidos lenguajes de programación, como Smalltalk, Java y Python, utilizan este
modelo computacional en tiempo de ejecución. Aunque en tiempo de diseño exista el concepto
de clase, éste es traducido en la ejecución de la aplicación al de objeto, convirtiéndose
así toda clase en un objeto.
La elección del modelo computacional orientado a objetos basado en prototipos
para la máquina abstracta, y por tanto para el conjunto del sistema, se justifica por varias
razones:
1. El modelo computacional es más sencillo que el basado en clases.
2. Existe una traducción directa y evidente del modelo de clases al de prototipos,
utilizado por la mayoría de los lenguajes actuales –véase el capítulo 8.
3. La utilización del modelo no supone pérdida de expresividad alguna.
4. Se amolda mejor a un esquema computacional estructuralmente reflectivo.
10.1.4 Adaptabilidad
Los sistemas operativos basados en micronúcleo (§ 5.6) obtienen una adaptabilidad
de sus servicios, que ofrecen mediante la definición de una interfaz para cada tipo de servicio.
Cualquier rutina que implemente las operaciones definidas por la interfaz, puede formar
parte del sistema operativo y sustituir a otro ya existente, obteniéndose así un grado de
adaptabilidad respecto a sus servicios.
La idea de separar el núcleo computacional de los mayores niveles de abstracción es
adoptada en nuestro sistema, separando la implementación de la máquina virtual de la codificación
del entorno de programación. Sin embargo, la especificación previa de las interfaces
de los distintos servicios, limita el número y tipo de servicios que pueden implantarse
en nuestra plataforma –entorno de programación.
La introspección y reflectividad estructural de nuestra máquina, serán utilizadas
también como mecanismos de adaptabilidad. En un entorno de computación heterogéneo,
una aplicación puede hacer uso de la introspección para conocer los servicios existentes en
la plataforma existente. En función de los servicios existentes, podrá llevar a cabo una funcionalidad
u otra, adaptándose así al contexto de ejecución presente.
Arquitectura de la Máquina Abstracta
145
Haciendo uso de la reflectividad estructural ofrecida, la estructura de un objeto que
ofrezca un determinado tipo de servicio podrá modificarse para derogar dicha funcionalidad
en otros, sin necesidad de establecer previamente la interfaz de funcionamiento –como
sucede en los sistemas basados en micronúcleo.
Siguiendo con los criterios reflectivos de diseño del proyecto Merlin [Assumpcao95]
desarrollado sobre el lenguaje Self, los objetos primitivos se extienden con la implementación
de nuevos objetos en el mismo lenguaje. La totalidad del sistema será codificado
utilizando estos nuevos objetos. Si queremos modificar el significado de una de sus
primitivas, modificamos la estructura del objeto –alguno de sus métodos–, o bien lo sustituimos
por otro nuevo. De esta forma, todo el software es adaptable dinámicamente al no
utilizar de forma directa las funcionalidades de los objetos primitivos.
Como ejemplo de lo comentado en el párrafo anterior, podemos codificar un objeto
encargado de crear objetos haciendo uso de la primitiva existente para esto. Todo el
entorno de programación se codificará haciendo uso de este nuevo objeto. Si se desea implementar
un sistema de persistencia, podrá modificarse la funcionalidad de éste para hacer
que la creación de dichos objetos implique su escritura en disco.
Otro modo de utilizar la reflectividad estructural para el desarrollo de aplicaciones
adaptables es mediante la codificación de éstas en función de la estructura de sus objetos.
La información de alta de elementos en una base de datos del menú de una aplicación, podrá
adaptarse dinámicamente haciendo uso de esta capacidad.
10.1.5 Interacción Directa entre Aplicaciones
La adaptabilidad del sistema ha ofrecerse tanto para una misma aplicación, como
entre distintas aplicaciones, de forma que la ejecución de un programa pueda adaptar el
funcionamiento de otro. Del mismo modo, la interacción entre aplicaciones es requerida
para que la intercomunicación entre ellas, indiferentemente del lenguaje de programación
utilizado, sea factible –ya que el motor computacional base de todo el sistema es la máquina
abstracta.
El acceso desde una aplicación a los objetos de otra ha de producirse de igual modo
que el acceso a sus propios objetos, sin necesidad de utilizar una capa intermedia de software.
En el caso de la máquina virtual de Java [Sun95], la ejecución de cada aplicación implica
la creación de una nueva instancia de la máquina que interprete su código. Para intercomunicar
estos dos procesos, es necesaria una capa software adicional como por ejemplo
RMI (Remote Method Invocation) [Sun97b].
Para minimizar la complejidad en la interacción de aplicaciones, el diseño de la máquina
abstracta ha de llevarse a cabo teniendo en cuenta que ésta supondrá un único proceso
en el sistema operativo en el que esté hospedada. Las aplicaciones en una misma plataforma
se ejecutarán en el mismo espacio de direcciones, interaccionando a través de un
espacio de nombres que tenga en consideración, para cada objeto, la aplicación en la que
fue creado.
10.1.6 Manipulación Dinámica del Comportamiento
La introspección y reflectividad estructural de la máquina nos permite conocer y
modificar dinámicamente la estructura de un objeto. En el modelo computacional basado
en prototipos, la estructura de un objeto comprende tanto sus propiedades (atributos) como
sus métodos. Por lo tanto, para ofrecer conocimiento dinámico real de la estructura de
un objeto, es necesario acceder y manipular su comportamiento.
CAPÍTULO 10
146
La representación de la computación como datos manipulables recibe el nombre de
cosificación (§ 6.1). El hecho de tratar dinámicamente instrucciones como datos, conlleva
la necesidad de que la plataforma computacional implemente un mecanismo de evaluación
o descosificación de dicha información. La posibilidad de manipular dinámicamente código
como si de datos se tratase supone:
1. La viabilidad de modificación dinámica del comportamiento de los objetos.
2. El desarrollo de aplicaciones que se adapten a comportamientos imprevisibles
en tiempo de diseño, surgidos cuando ésta ya esté desarrollada y en ejecución.
3. La factibilidad de depurar, modificar y ampliar una aplicación en ejecución,
puesto que se puede especificar dinámicamente la creación y modificación de
nuevos comportamientos.
Vemos cómo esta característica impuesta a la máquina abstracta está enfocada hacia
la obtención de un sistema computacional más flexible.
10.2 Técnicas de Diseño Seleccionadas
Como hemos justificado en este capítulo, el diseño de la máquina abstracta, raíz
computacional del conjunto del sistema, deberá diseñarse siguiendo los siguientes criterios:
1. Ha de reducirse al mínimo el número de primitivas computacionales y operacionales.
2. La implementación de las primitivas operacionales ha de separarse de las computacionales,
siguiendo un mecanismo que permita al usuario extenderlas, sin
modificar la implementación de la máquina virtual existente.
3. El sistema computacional deberá ofrecer funcionalidades introspectivas.
4. La reflectividad estructural permitirá al usuario modificar los atributos y métodos
existentes para cada objeto.
5. El sistema computacional seguirá un modelo orientado a objetos, basado en
prototipos.
6. La ejecución de cada una de las aplicaciones en una misma plataforma física se
llevará a cabo en el mismo espacio de direcciones, y la interacción entre objetos
de distintas aplicaciones se producirá sin ninguna capa software adicional.
7. Deberá permitirse el tratamiento del comportamiento de cada objeto como si
de datos se tratase, implementándose un mecanismo de evaluación o descosificación
de datos dinámico.
147
CCAAPPÍÍTTUULLO 1111:
ARQUITECTURA DEL SISTEMA REFLECTIVO NO
RESTRICTIVO
A raíz de la estructuración de nuestro sistema en capas, presentada en el capítulo 9,
estudiaremos en este título la arquitectura de la tercera y última que representaba un sistema
computacional adaptable sin restricción alguna, independiente del lenguaje.
La arquitectura presentada deberá cumplir todos los objetivos generales impuestos
en el capítulo 9, analizando y justificando la utilización de las técnicas existentes descritas
en capítulos anteriores, y definiendo el esquema general del funcionamiento del sistema –
sin entrar en consideraciones propias de diseño (capítulo 14).
Puesto que el desarrollo de esta última capa del sistema se llevará a cabo sobre el
sistema computacional descrito por la máquina abstracta, la arquitectura propuesta demandará
unos requisitos computacionales a la plataforma base. Finalmente, enunciaremos estos
requisitos que deberán satisfacerse en el diseño de la primera capa.
11.1 Análisis de Técnicas de Obtención de Sistemas Flexibles
En el capítulo 5 estudiamos un conjunto de distintas técnicas utilizadas por sistemas
para obtener distintos grados de flexibilidad computacional. Por el contrario, en el capítulo
7 todos los casos analizados utilizaban la misma idea para conseguir adaptabilidad: reflectividad.
Analizaremos las aportaciones de estos dos grupos de sistemas.
11.1.1 Sistemas Flexibles No Reflectivos
Exceptuando los sistemas operativos basados en micronúcleo, el conjunto de
aproximaciones descritas en el capítulo 5 poseen un tratamiento estático de las aplicaciones,
buscando siempre la separación de incumbencias o aspectos. En el conjunto de métodos
existentes, se desarrolla por un lado la funcionalidad central de una aplicación y por otro se
especifican aspectos como por ejemplo persistencia, distribución o planificación de hilos.
Se trata de separar el código funcional de cada aplicación de las incumbencias propias de
múltiples aplicaciones, susceptibles de ser reutilizadas.
El resultado es el desarrollo de software reutilizable y flexible respecto a un conjunto
de aspectos carentes de dinamicidad. Estas técnicas están demasiado enfocadas a la creaCAPÍTULO
11
148
ción de software susceptible de ser adaptado por diversas incumbencias, careciendo de la
posibilidad de crear aplicaciones flexibles en tiempo de ejecución.
Si tomamos, por ejemplo, la técnica de desarrollo de aplicaciones orientadas a aspectos,
observamos que es necesario desarrollar la funcionalidad de la aplicación por un
lado, y por otro definir los aspectos reutilizables a cualquier aplicación –por ejemplo, los
aspectos “persistencia” y “optimización en velocidad”. Una vez creadas las distintas partes
de la aplicación, se crea el código final de ésta mediante el tejedor de aspectos (aspect weaver)
–traductor de aplicaciones en función de los aspectos definidos.
La principal carencia del funcionamiento descrito reside en la imposibilidad de modificar
dinámicamente los aspectos de una aplicación. De forma añadida, la aplicación no
puede adaptar su funcionalidad principal en tiempo de ejecución. Estas limitaciones se deben
a la orientación distinta de objetivos de este tipo de sistemas respecto buscado en esta
tesis: la separación de incumbencias está más enmarcada en campos de reutilización de
código, ingeniería del software o creación de un paradigma de programación, que en la
flexibilidad dinámica.
Como comentaremos en el capítulo 15, el sistema reflectivo desarrollado puede ser
utilizado como una plataforma en la que sea factible el conocimiento, selección y modificación
dinámica de los distintos aspectos de una aplicación, sin necesidad de finalizar su ejecución.
11.1.2 Sistemas Reflectivos
La reflectividad supone más una técnica de lenguajes de programación que de ingeniaría
del software o desarrollo de aplicaciones. Los lenguajes de programación reflectivos
permiten modificar o acceder, en un determinado grado, a un conjunto de sus propias características
o de sus aplicaciones, utilizando para ello distintos mecanismos de implementación.
Nos centraremos en una de las clasificaciones de reflectividad identificadas en § 6.3:
“Cuándo se produce el reflejo”. Los sistemas reflectivos que son adaptables en tiempo de
compilación generan las mismas carencias que los que acabamos de analizar (§ 11.1.1): limitan
su adaptabilidad al momento en el que la aplicación es creada, careciendo de flexibilidad
dinámica.
Es por tanto necesario centrarse en los sistemas reflectivos adaptables en tiempo de
ejecución, capaces de ofertar adaptabilidad dinámica mediante el empleo de técnicas de
procesamiento de lenguajes –en la mayoría de los casos, mediante intérpretes. Este tipo de
sistemas se distingue de los estáticos fundamentalmente en:
Ofrecen la adaptabilidad de las aplicaciones en tiempo de ejecución.
La adaptabilidad dinámica supone una ralentización de la ejecución de las aplicaciones
al no producirse ésta en fase de compilación.
De algún modo, la flexibilidad dinámica ganada se paga con tiempos de ejecución
más elevados.
11.1.2.1 Reflectividad Dinámica
Siguiendo con el estudio desarrollado en el capítulo 7 y centrándonos en los objetivos
principales de esta tesis (§ 1.2), las técnicas estudiadas más cercanas a ofrecer la flexibilidad
buscada son las reflectivas en tiempo de ejecución. Dentro de esta clasificación teneArquitectura
del Sistema Reflectivo No Restrictivo
149
mos dos grupos: intérpretes metacirculares y sistemas basados en MOPs (Meta-Object Protocols).
Los intérpretes metacirculares ofrecen una torre infinita [Wand88] de intérpretes reflectivos,
en el que sólo parte de la semántica del sistema es adaptable. Como hemos comentado
y justificado en § 9.5.3, estos sistemas no ofrecen un grado total de adaptación
semántica y pueden conllevar a la pérdida del conocimiento real de la semántica del sistema,
escapándose así de los objetivos marcados.
Los entornos computacionales basados en MOPs limitan el número de niveles
computacionales a dos, ofreciendo un protocolo de acceso desde el nivel superior al subyacente.
Esta idea es similar a la buscada, pero, como se muestra en la Figura 11.1, posee un
conjunto de limitaciones:
IInnttéérrpprreettee
Metaobjetos
Aplicación de Usuario
MOP
La ampliación del MOP
o de los metaobjetos
implica una nueva
versión del intérprete
Ejecuta
Figura 11.1: Estructura general de un MOP.
1. La especificación del MOP supone una restricción a priori. El protocolo debe
contemplar lo que será adaptable dinámicamente. Esta especificación a realizar
previamente a la ejecución de una aplicación implica que ésta no podrá adaptarse
a cualquier requisito surgido dinámicamente –ha de estar contemplado en el
protocolo.
2. La expresividad de los metaobjetos es restringida. El modo en el que una característica
del sistema ofrece su adaptabilidad es mediante el concepto de metaobjeto
[Kiczales91]: si algo es adaptable, se representará mediante un metaobjeto y
sus métodos ofrecerán la forma en la que puede adaptarse.
3. No existe una expresividad sin restricción de la adaptabilidad del sistema. Sólo
podrá adaptarse dinámicamente aquello que posea un metaobjeto, y éste podrá
adaptarse tan sólo en función de los métodos que ofrezca.
4. Uno de los procesos de desarrollo de MOPs propuesto por Michael Golm
[Golm98] se basa en la ampliación del protocolo y de los servicios de los metaobjetos
bajo demanda: si algo no es adaptable, se modifica la especificación
del protocolo o del metaobjeto y se vuelve a generar la aplicación.
CAPÍTULO 11
150
5. El criterio propuesto por Golm elimina la adaptabilidad dinámica sin restricciones
buscada en esta tesis y, adicionalmente, supone la generación de distintas
versiones del intérprete –y por tanto del lenguaje–, perdiendo así la portabilidad
del código existente con anterioridad (§ 7.4.1).
6. La estructura establecida en los sistemas que utilizan MOPs es siempre dependiente
de un único lenguaje de programación.
11.2 Sistema Computacional Reflectivo Sin Restricciones
Apoyándonos en la definición de un sistema reflectivo como aquél que puede acceder
a niveles computacionales inferiores dentro de su torre de interpretación [Wand88],
hemos justificado en § 9.5.3 la necesidad de una altura de dos niveles –interpretación de un
intérprete– y una anchura absoluta –acceso al metasistema no restrictivo. Esto quiere decir
que sólo son necesarios dos niveles de interpretación, pero que el nivel superior ha de poder
modificar cualquier característica computacional del nivel subyacente que le da vida.
Recordando los objetivos marcados para esta última capa del sistema (§ 9.5), podemos
abreviar enunciando las principales características que ha de aportar frente a un sistema
basado en un MOP:
1. No debe ser necesario estipular lo que va a adaptarse previamente a la ejecución
de la aplicación.
2. Debe existir un mecanismo de expresividad en el que cualquier característica del
sistema pueda adaptarse dinámicamente –horizontalidad total.
3. El sistema debe ser totalmente independiente del lenguaje.
4. Los grados de flexibilidad a conseguir son: introspección, estructural, semántica
y lenguaje.
El esquema a seguir se muestra en la siguiente figura38:
38 En este apartado sólo estudiaremos la funcionalidad de cada una de las partes del esquema. En puntos
sucesivos, profundizaremos acerca de cómo desarrollar éstas.
Arquitectura del Sistema Reflectivo No Restrictivo
151
Especificación Lenguaje A Especificación
Lenguaje B
Tabla de símbolos
de aplicación B
Tabla de símbolos
de aplicación A
Aplicación A Aplicación B
Ejecuta
Accede y
Accede y modifica
modifica
Accede Accede
Intérprete Genérico
Reflectividad
Computacional
y Lingüística
Introspección y Reflectividad Estructural Reflectividad Computacional y Lingüística
Introspección y
Reflectividad
Estructural
Nivel Computacional 0 Nivel
Computacional 1
Figura 11.2: Esquema general del sistema computacional no restrictivo.
Una de las diferencias frente a los intérpretes convencionales es la creación de un
intérprete genérico independiente del lenguaje. Este procesador ha de ser capaz de ejecutar
cualquier aplicación escrita sobre cualquier lenguaje de programación. De este modo, la
entrada a este programa no se limita, como en la mayoría de los intérpretes, a la aplicación a
ejecutar; está parametrizado además con la especificación del lenguaje en el que dicha aplicación
haya sido codificada.
Implementando un intérprete genérico en el que las dos entradas son la aplicación a
procesar y la especificación del lenguaje en el que ésta haya sido escrita, conseguimos hacer
que el sistema computacional sea independiente del lenguaje.
Para cada aplicación evaluada, el intérprete genérico deberá crear un contexto de
ejecución o tabla de símbolos dinámica, en el que aparezcan todos los objetos39 creados a
raíz de ejecutar el programa. Se trata de ubicar todos los objetos creados en el contexto de
ejecución de la aplicación en un único espacio de nombres.
La flexibilidad del sistema se obtiene cuando, en tiempo de ejecución, la aplicación
accede a la especificación de su lenguaje, o bien a su tabla de símbolos existente. El resultado
de estos accesos supone distintos grados de flexibilidad:
1. Si analiza, sin modificar, su tabla de símbolos supondrá introspección.
2. En el caso de modificar los elementos de su tabla de símbolos, la flexibilidad
obtenida es reflectividad estructural.
3. El acceso y modificación de la semántica propia de la especificación de su lenguaje
de programación significará reflectividad computacional.
4. La modificación de las características léxicas o sintácticas de su lenguaje producirán
una adaptación lingüística.
39 El término objeto aquí denota cualquier elemento o símbolo existente en el contexto de ejecución de la
aplicación. Un objeto puede ser, por tanto, una función, variable, clase u objeto propiamente dicho.
CAPÍTULO 11
152
El modo en el que las aplicaciones de usuario accedan a su tabla de símbolos y a la
especificación de su lenguaje, no deberá poseer restricción alguna. La horizontalidad de este
acceso ha de ser plena.
Para conseguir lo propuesto, la principal dificultad radica en la separación de los
dos niveles computacionales mostrados en la Figura 11.2. El nivel computacional de aplicación
poseerá su propio lenguaje y semántica, mientras que el nivel subyacente que le da
vida –el intérprete– poseerá una semántica y lenguaje no necesariamente similar. El núcleo
de nuestro sistema se centra en un salto computacional del nivel de aplicación al de interpretación;
¿cómo puede una aplicación modificar, sin restricción alguna, el intérprete que lo
está ejecutando?40
11.2.1 Salto Computacional
La raíz computacional de nuestro sistema está soportada por una implementación,
hardware o software, de nuestra máquina abstracta. Sobre su dominio computacional, se
desarrolla el intérprete genérico independiente del lenguaje de programación.
La especificación de cada lenguaje a interpretar se llevará a cabo mediante una estructura
de objetos, representativa de su descripción léxica, sintáctica y semántica. Este
grupo de objetos sigue el modelo computacional descrito por la máquina y, por tanto, pertenece
también a su espacio computacional.
En la ejecución de una aplicación por un intérprete, éste siempre debe mantener dinámicamente
una representación de sus símbolos en memoria. Como describíamos en el
punto anterior, el intérprete genérico ofrecerá éstos al resto de aplicaciones existentes en el
sistema. De esta forma, por cada aplicación ejecutada por el intérprete, se ofrecerá una lista
de objetos representativos de su tabla de símbolos.
Siguiendo este esquema, y como se muestra en la Figura 11.3, el dominio computacional
de la máquina abstracta incluye el intérprete genérico y, por cada aplicación, el conjunto
de objetos que representan la especificación de su lenguaje y la representación de sus
símbolos existentes en tiempo de ejecución.
40 Es importante darse cuenta cómo una primera aproximación para solucionar el problema planteado es
la utilización de un MOP. Sin embargo, como hemos comentado en § 11.1.2.1, los MOPs establecen
restricciones de acceso en el salto computacional.
Arquitectura del Sistema Reflectivo No Restrictivo
153
a=10*2;
b=a/-18;
a;
b;
Reify {
vars["a"]=1
vars["a"]=2
}
a;
b;
Reify {
code=“...”
language.["asignment"].
actions.append(code)
}
a=10*2;
Tabla de
Símbolos
vars["a"]=1
vars["a"]=2
code=“...”
language.["asignment"].
actions.append(code)
Intérprete
Genérico
Lenguaje de alto
nivel codificado
en el lenguaje de
programación
seleccionado
Lenguaje de la
máquina abstracta
a ejecutar por el
nivel computacional
inferior
El intérprete toma
el código de máquina
abstracta y lo evalúa
en el entorno computacional
de la máquina: salto
computacional
Entorno Computacional de la Máquina Abstracta
Entorno Computacional Aplicación
Ejecuta
Especificación
del Lenguaje
Figura 11.3: Salto computacional producido en la torre de intérpretes.
En el espacio computacional del intérprete se encuentran las aplicaciones de usuario
codificadas en distintos lenguajes de programación. Cualquiera que sea el lenguaje de programación
utilizado, una aplicación de usuario tiene siempre una instrucción de acceso al
metasistema –reify. Dentro de esta instrucción –entre llaves– el programador podrá utilizar
código de su nivel computacional inferior: código propio de la máquina abstracta. Así,
una aplicación poseerá la expresividad de su lenguaje más la propia de la máquina abstracta.
Siempre el que intérprete genérico analice una instrucción reify en un programa
de usuario, en lugar de evaluarla como una sentencia propia del lenguaje interpretado, seguirá
los siguientes pasos:
1. Tomará la consecución de instrucciones codificadas en el lenguaje de la máquina
abstracta, como una cadena de caracteres.
2. Evaluará o descosificará los datos obtenidos, para que sean computados como
instrucciones por la máquina abstracta.
El resultado es que el código de usuario ubicado dentro de la instrucción de cosificación
es ejecutado por el nivel computacional inferior al resto de código de la aplicación.
Es en este momento en el que se produce un salto computacional real en el sistema.
Para que la implementación del mecanismo descrito anteriormente sea factible, la
posibilidad de evaluar o descosificar dinámicamente cadenas de caracteres como computación
será uno de los requisitos impuestos a la máquina abstracta.
La interacción directa entre aplicaciones es otro de los requisitos impuestos a la plataforma
virtual en la descripción global de la arquitectura del sistema. Cualquier aplicación,
desarrollada en cualquier lenguaje, podrá interaccionar directamente –sin necesidad de una
capa software adicional– con el resto de aplicaciones existentes. De este modo, el código de
la aplicación propio de la instrucción reify, al ser ejecutado en el entorno computacional
de la máquina, podrá acceder a cualquier aplicación dentro de este dominio, y en concreto a
su tabla de símbolos y a la especificación de su lenguaje:
CAPÍTULO 11
154
1. Si analiza, mediante la introspección ofrecida por la máquina abstracta, su propia
tabla de símbolos, estará obteniendo información acerca de su propia ejecución:
introspección de su propio dominio computacional.
2. Si modifica la estructura de alguno de sus símbolos, haciendo uso de la reflectividad
estructural de la máquina, el resultado es reflectividad estructural de su
nivel computacional.
3. La modificación, mediante la utilización de la reflectividad estructural de la máquina,
de las reglas semánticas del lenguaje de programación supone reflectividad
computacional o de comportamiento.
4. Si la parte a modificar de su lenguaje es su especificación léxica o sintáctica, el
resultado obtenido es reflectividad lingüística del nivel computacional de usuario.
Vemos como otros dos requisitos necesarios en la máquina abstracta para llevar a
cabo los procesos descritos son introspección y reflectividad estructural de su dominio
computacional.
Finalmente comentaremos que la evaluación de una aplicación debe realizarse por
el intérprete genérico analizando dinámicamente la especificación de su lenguaje, de forma
que la modificación de ésta conlleve automáticamente al reflejo de los cambios realizados.
De este modo, no es necesaria la implementación de un mecanismo de conexión causal (§
6.1) ni la duplicación de información mediante metaobjetos, puesto que el intérprete ejecuta
la aplicación derogando parte de su evaluación en la representación de su lenguaje.
11.2.2 Representación Estructural de Lenguajes y Aplicaciones
El salto computacional real ofrecido por nuestro sistema cobra importancia en el
momento en el que la aplicación de usuario accede a las estructuras de objetos representantes
de su lenguaje de programación o de su tabla de símbolos. Indicaremos brevemente
cómo pueden representarse éstos para que su manipulación, gracias a la reflectividad estructural
de la máquina abstracta, sea posible. Una descripción más detallada puede obtenerse
del diseño del prototipo realizado en el capítulo 14, “Diseño de un Prototipo de
Computación Reflectiva Sin Restricciones”.
La representación de los lenguajes de programación a utilizar en nuestro sistema se
lleva a cabo mediante estructuras de objetos que representan gramáticas libres de contexto,
para las descripciones léxicas y sintácticas, y reglas semánticas expresadas mediante código
de la plataforma virtual. Su expresividad es la propia de una definición dirigida por sintaxis
[Aho90], utilizando el lenguaje de la máquina abstracta como lenguaje de descripción semántica.
Para liberar al usuario de la necesidad de crear estas estructuras de objetos, se diseña
un metalenguaje de descripción de lenguajes de programación con las características
mencionadas; la especificación de la gramática de una implementación de este metalenguaje
se describe en el 283apéndice B. El procesamiento de código expresado mediante este metalenguaje
supondrá la creación de la estructura de objetos representativa del lenguaje especificado.
A modo de ejemplo, mostraremos la traducción de la siguiente definición dirigida
por sintaxis:
S ::= B S r1: Regla en Código Máquina Abstracta
Arquitectura del Sistema Reflectivo No Restrictivo
155
r2: Regla en Código Máquina Abstracta
| C D r3: Regla en Código Máquina Abstracta
B ::= “B”
C ::= “C”
D ::= “D”
Los símbolos terminales se han mostrado entre comillas dobles. Las reglas semánticas
r1, r2 y r3 se codificarán mediante el lenguaje de la máquina abstracta. Las producciones
cuyo símbolo gramatical a su izquierda es S, forman parte de la descripción sintáctica
del lenguaje. Las tres últimas reglas representan su especificación léxica.
El procesamiento de la descripción del lenguaje anterior, crea su especificación mediante
objetos siguiendo la estructura mostrada en la Figura 11.4. Cada una de las reglas
posee un objeto que representa el símbolo gramatical no terminal de su parte izquierda.
Éste está asociado a tantos objetos como partes derechas posea dicha producción. Cada
una de las partes derechas hace referencia a una lista de símbolos gramaticales –parte derecha
de la producción– y a una lista con todas las regla semánticas asociadas.
S:NoTerminal
:ParteDerecha
:ParteDerecha
B:NoTerminal S:NoTerminal
r1:ReglaSemántica r2:ReglaSemántica
C:NoTerminal D:NoTerminal
r3:ReglaSemántica
B:NoTerminal :ParteDerecha B:Terminal
C:NoTerminal :ParteDerecha C:Terminal
D:NoTerminal :ParteDerecha D:Terminal
partesDerechas
partesDerechas
partesDerechas
partesDerechas
reglasSemánticas
reglasSemánticas
símbolosGramaticales
símbolosGramaticales
símbolosGramaticales
símbolosGramaticales
símbolosGramaticales
Figura 11.4: Especificación de un lenguaje de programación mediante una estructura de objetos.
Una vez creada esta estructura, se analizará una aplicación codificada mediante el
lenguaje descrito, utilizando un algoritmo descendente de análisis sintáctico [Aho90]. Conforme
se vaya examinando ésta, se irá creando su árbol sintáctico. Cada nodo de este árbol,
guardará una referencia a la representación de su lenguaje de programación. En concreto,
para cada nodo propio de un símbolo no terminal, se establecerá una relación con la parte
derecha de la producción elegida en la creación del árbol –véase la Figura 11.5.
La evaluación de la aplicación supone el recorrido del árbol, ejecutando las reglas
semánticas asociadas a la descripción de su lenguaje. La relación establecida entre el árbol,
representante de la aplicación, y la estructura que especifica su lenguaje de programación,
supone que la modificación del lenguaje implique automáticamente su actualización o reflejo
en la aplicación de usuario; esta conexión es conocida como conexión causal [Maes87b].
CAPÍTULO 11
156
S:NoTerminal
:ParteDerecha
:ParteDerecha
B:NoTerminal S:NoTerminal
r1:ReglaSemántica r2:ReglaSemántica
C:NoTerminal D:NoTerminal
r3:ReglaSemántica
reglasSemánticas
reglasSemánticas
símbolosGramaticales
símbolosGramaticales
S:Nodo
B:Nodo
S:Nodo
C:Nodo
D:Nodo
D:NoTerminal
:ParteDerecha B:Terminal
C:NoTerminal
:ParteDerecha C:Terminal
B:NoTerminal
:ParteDerecha D:Terminal
partes
derechas
símbolosGramaticales
símbolosGramaticales
símbolosGramaticales
“C”:Nodo “D”:Nodo “B”:Nodo
Árbol Sintáctico
Especificación
Lenguaje de
Programación
Figura 11.5: Creación del árbol sintáctico asociado a la especificación de un lenguaje.
El modo en el que se recorre el árbol no sigue un algoritmo predeterminado, como
por ejemplo el propio de un esquema descendente de traducción [Aho90]. La regla semántica
del símbolo inicial de la gramática, ha de indicar cómo se evalúa la ejecución del primer
nodo del árbol; este proceso se extiende de un modo recursivo al resto de elementos del
árbol41.
Por cada aplicación en nuestro sistema existirá un objeto representante de ésta. Cada
objeto de aplicación poseerá una referencia a su árbol sintáctico, a su lenguaje de programación,
y a su tabla de símbolos dinámica –contexto de ejecución.
Tabla de Símbolos Lenguaje
Aplicación
Contexto de Ejecución
Árbol Sintáctico Especificación del Lenguaje
Objeto Representante
de una aplicación
Figura 11.6: Principal información existente para cada aplicación en ejecución.
Accediendo a la lista de objetos representante de su tabla de símbolos –mediante la
introspección y reflectividad estructural ofrecida por la máquina abstracta–, se le brinda al
41 Para obtener más información acerca de cómo describir la evaluación del árbol, consúltese el diseño
(capítulo 14) o manual de usuario (apéndice B) del prototipo implementado.
Arquitectura del Sistema Reflectivo No Restrictivo
157
programador de aplicaciones introspección y reflectividad estructural del lenguaje de programación
que haya seleccionado.
11.3 Beneficios del Sistema Presentado
Enlazando con el análisis realizado en § 11.1 de técnicas utilizadas para obtener sistemas
computacionales flexibles, éste es el conjunto de beneficios aportados frente a los
sistemas basados en MOPs:
1. No se establece un protocolo que restringe previamente las características susceptibles
de ser adaptadas. Cualquier aplicación puede modificar cualquier característica
de su lenguaje de programación y de su contexto de ejecución, sin
restricción alguna, en tiempo de ejecución.
2. No existe una expresividad limitada. El modo en el que podemos modificar una
aplicación se expresa mediante un lenguaje de computación: el lenguaje de la
máquina abstracta. De este modo, la expresividad ofrecida es en sí un lenguaje
de acceso y modificación de lenguajes, cuya evaluación supone un salto real en
los dos niveles de computación existentes.
3. Independencia del lenguaje. La separación de la especificación del lenguaje de
programación a interpretar, del intérprete en sí, supone una independencia del
lenguaje en el esquema ofrecido. Las características ofrecidas por el sistema no
implican la utilización de un lenguaje de programación específico. Sin embargo,
el lenguaje de adaptación dinámica siempre es el mismo: el lenguaje de la máquina
abstracta.
4. Se ofrece cuatro niveles de adaptabilidad: introspección, estructura dinámica,
comportamiento y lenguaje.
5. Existe una conexión causal directa sin necesidad de duplicar información. La
utilización de metaobjetos supone:
Duplicidad de información para ofrecer características del sistema. Cualquier
característica adaptable es ofrecida al programador a través de un metaobjeto.
Un objeto adaptable implica así la creación paralela de un metaobjeto.
Implementación de un mecanismo de conexión causal. Los reflejos de actualización
de metaobjetos en el sistema base han de implementarse mediante
un mecanismo adicional.
Ambas restricciones no aparecen en el sistema presentado.
6. Adaptabilidad cruzada. Cualquier aplicación puede adaptar dinámicamente a otra
existente en el sistema, sin necesidad de que ambas utilicen el mismo lenguaje de
programación. Esta posibilidad atribuye a nuestro sistema la capacidad de constituirse
como un entorno computacional de separación de incumbencias (concerns)
en tiempo de ejecución y sin restricción alguna.
Como punto negativo, los sistemas basados en MOPs poseen una mayor eficiencia
en tiempo de ejecución. El hecho de duplicar la información a reflejar mediante el uso de
metaobjetos y la restricción impuesta por el uso de un MOP, se justifica mediante el diseño
seguido en el que sólo se usa un nivel computacional –un único intérprete. Si el usuario de
un MOP no identifica ningún elemento como adaptable, no existe casi penalización en los
tiempos de ejecución. Sin embargo, nuestro esquema de dos niveles de computación hace
CAPÍTULO 11
158
que el sistema posea tiempos de ejecución más elevados, indistintamente de la utilización o
no de las características reflectivas.
11.4 Requisitos Impuestos a la Máquina Abstracta
Analizando los requisitos necesarios impuestos a la máquina abstracta para poder
desarrollarse el sistema reflectivo propuesto, podemos enumerarlos del siguiente modo:
1. Introspección. Para que el programador pueda conocer la tabla de símbolos
existente en la ejecución de su aplicación y la especificación del lenguaje de programación
utilizado, la plataforma computacional deberá poseer características
introspectivas.
2. Evaluación o descosificación de datos. El intérprete genérico toma el código de
la máquina abstracta a evaluar como una cadena de caracteres. Éste deberá ser
interpretado por la máquina como instrucciones, no como datos. Este mecanismo
de conversión de datos a computación es necesario, pues, para desarrollar
el sistema presentado.
3. Reflectividad estructural. El código de la máquina abstracta evaluado en el nivel
computacional subyacente al propio de la aplicación, requiere esta característica
para poder adaptar la especificación de su lenguaje y los objetos existentes en su
contexto de ejecución.
4. Interacción directa entre aplicaciones. Puesto que el código a descosificar por la
máquina abstracta accede a otras aplicaciones desarrolladas sobre ésta –
especificación del lenguaje y tabla de símbolos–, la interacción entre aplicaciones
computadas por la máquina es necesaria. Su uso también queda patente en
la adaptación de una aplicación por otra existente en el sistema.
Como tesis propuesta, afirmaremos que un entorno computacional que ofrezca estas
características será válido para desarrollar esta capa del sistema reflectivo42. La demostración
de esta proposición ha sido llevada a cabo mediante la implementación de un
prototipo cuyo diseño es descrito en el capítulo 14, y su utilización en el apéndice B.
42 Tan solo nos referimos al desarrollo de esta capa del sistema. Para obtener la totalidad de los beneficios,
deberemos desarrollar ésta sobre una plataforma virtual poseedora de las características descritas en
el capítulo 9 y en el capítulo 10.
159
CCAAPPÍÍTTUULLO 1122:
DISEÑO DE LA MÁQUINA ABSTRACTA
Basándonos en los criterios de diseño especificados en la arquitectura de la máquina
abstracta, capítulo 10, diseñaremos ésta mediante la especificación léxica y sintáctica de
cada una de sus instrucciones, definiendo su semántica mediante los cambios producidos
en su estado computacional.
Describiremos la plataforma virtual sin profundizar en la implementación de la
misma. El diseño de un prototipo de su implementación –máquina virtual– será mostrado
como un apéndice en el apéndice A.
Una vez descritas las funcionalidades básicas de la máquina, haciendo uso de su capacidad
de extensibilidad, ampliaremos levemente su abstracción mediante la codificación
de funcionalidades adicionales, como operaciones lógicas, aritméticas, de comparación,
iteración, y creación de abstracciones y entidades.
12.1 Especificación de la Máquina Abstracta
La principal característica a definir dentro del modelo computacional ofrecido por
la máquina abstracta, es su noción de objeto. Identificábamos como criterio de diseño a
seguir en su arquitectura el modelo computacional de objetos basado en prototipos. De
esta forma, el objeto supone la abstracción base de su modelo computacional.
12.1.1 Noción de Objeto
En tiempo de ejecución, la máquina alberga un conjunto de objetos. El modo de
acceder y utilizar éstos, es mediante referencias: si queremos utilizar los servicios ofrecidos
por un objeto o conocer su estado, debemos utilizar una referencia a éste.
Un objeto está constituido por un conjunto de referencias a otros objetos. Estas referencias
entre objetos denotan distintas semánticas de relaciones como general-específico,
asociación o todo-parte.
Dada una referencia a un objeto, podemos conocer el conjunto de referencias que
éste posee, observando su estado, estructura y comportamiento –introspección. Del mismo
modo, el conjunto de referencias que posee puede modificarse dinámicamente permitiendo
la inserción, borrado y actualización de nuevas referencias en tiempo de ejecución –
reflectividad estructural.
CAPÍTULO 12
160
Un objeto puede denotar en sí información (datos) o comportamiento. La representación
de ambos se produce del mismo modo, mas la representación del comportamiento
puede evaluarse o descosificarse43, es decir, traducir dinámicamente su información en una
computación para la máquina abstracta. El comportamiento de un objeto viene denotado
por sus objetos asociados que representan computación; su estado es descrito por aquellos
asociados que simbolizan únicamente información.
12.1.2 Entorno de Programación
La máquina virtual interpretará el lenguaje de la máquina abstracta que definiremos
en este documento. El estado computacional inicial de la máquina abstracta viene dado por
un conjunto de objetos primitivos, descritos en § 12.1.3, que ofrecen su funcionalidad computacional
básica. Uno de los criterios definidos en la arquitectura de la máquina, capítulo
10, requiere reducir al mínimo el número de objetos primitivos, para otorgar el mayor
grado de heterogeneidad posible a la plataforma base, facilitando así la implantación de ésta
en sistemas dotados de una capacidad computacional restringida.
Al igual que sucede en la plataforma de Smalltalk, al inicializar la máquina abstracta
ésta interpreta una imagen del entorno de computación: codificación, en el propio lenguaje
de la máquina, de un entorno de programación que eleve el nivel de abstracción inicial de la
plataforma. Partiendo de la posibilidad de acceso a todos los objetos existentes en tiempo
de ejecución, se irá creando nuevas abstracciones para conseguir un mayor nivel de abstracción
–extensibilidad.
Introspección y reflectividad estructural son las técnicas ofrecidas para obtener el
mecanismo de extensibilidad buscado –capítulo 10. El hecho de codificar la extensión
computacional de la máquina en su propio lenguaje supone los siguientes beneficios:
1. El entorno de programación al completo es portable a cualquier plataforma.
2. No se crean distintas versiones de la máquina virtual, perdiendo la portabilidad
de aplicaciones de versiones anteriores.
3. El tamaño de la implementación de la plataforma virtual no es excesivo y puede
implantarse en un número elevado de entornos computacionales.
Para cada una de las funcionalidades desarrolladas en el entorno de programación
(persistencia, distribución, planificación genérica de hilos y recolección de basura), se deberá
proporcionar un grado de adaptabilidad que permita modificar y adaptar dinámicamente
los mecanismos, técnicas y protocolos utilizados, en función de los requerimientos surgidos.
12.1.3 Objetos Primitivos
Inicialmente la máquina abstracta posee el siguiente conjunto de objetos primitivos,
así como las referencias propias para acceder a ellos:
Objeto nil. Objeto ancestro que ofrece las funcionalidades básicas de cualquier
objeto de nuestra plataforma.
Objetos cadenas de caracteres. Representan la información indivisible de nuestro
sistema. Esta información puede representar simplemente datos, o computación
a ser evaluada.
43 Proceso inverso de cosificar: convertir en datos una computación.
Diseño de la Máquina Abstracta
161
Objeto Extern. Identifica el mecanismo de adición de primitivas operacionales
a nuestra plataforma, cuyo requerimiento fue impuesto en el capítulo 10.
Objeto System. Representa la colección de todos los objetos existentes en la
plataforma real utilizada. Implica un mecanismo introspectivo de conocimiento
de los objetos existentes en la ejecución de cada máquina.
Referencias
El acceso a un objeto se ha de realizar siempre mediante una referencia. Un objeto
como tal no tiene un nombre propio, pero sí una referencia a éste. Por abreviar, a lo largo
de este documento expresaremos “el objeto R” en lugar de “el objeto referenciado por R”,
aunque realmente R sea una referencia al objeto.
Una referencia ofrece un mecanismo de acceso a los servicios propios de un objeto:
su estado (datos) y comportamiento (computación). Mediante la utilización de una referencia
a un objeto, podremos conocer y modificar su estado, así como solicitar el desencadenamiento
de cualquiera de los comportamientos que éste desarrolle.
12.1.3.1 Objeto nil
Inicialmente existe el objeto nil, ancestro de todo objeto, al que se puede acceder
mediante la referencia nil. Este objeto, al igual que cualquier otro, posee un conjunto de
referencias a otros objetos. Todo objeto tendrá al menos dos referencias miembro:
id: Referencia a un objeto cadena de caracteres, que identifica de forma única
el objeto. Esta unicidad ha de ser universal, es decir, no puede existir colisión de
identificadores con otro objeto, aunque se encuentre en otra plataforma
super: Referencia al objeto padre o base del objeto actual.
El objeto al que se puede acceder a través de nil, posee como referencia super
una referencia a él mismo, y como referencia id una referencia al objeto .
Pero, ¿qué es el objeto ? Un objeto cadena de caracteres. Cada vez que se
necesite un objeto cadena de caracteres, se podrá utilizar una referencia a él identificando la
cadena de caracteres entre los delimitadores léxicos < y >. De esta forma, este tipo de objetos
se van creando implícitamente por el programador, mediante la utilización de referencias
nombradas entre < y >.
12.1.3.2 Objetos Cadenas de Caracteres
Para la máquina abstracta, un objeto cadena de caracteres significará la forma básica
de representar cualquier dato. Además, la plataforma permite evaluar o descosificar44 estos
objetos, representando así su computación o comportamiento.
Hemos visto cómo todo objeto posee una referencia a otro objeto cadena de caracteres
que identifica al primero de forma única. Para el objeto nil, su referencia id lo asocia
al objeto . Este objeto, a su vez, posee como referencia id una referencia a sí
mismo, y como referencia super una referencia al objeto de rasgo (trait) String.
En los lenguajes orientados a objetos basados en prototipos, el concepto de clase es
sustituido por el concepto de objeto trait –véase el capítulo 8. Estos objetos no son tratados
computacionalmente de un modo especial; son un objeto más. El programador es el
encargado de utilizarlos para agrupar objetos con comportamiento y estructura similares.
44 Al igual que sucede con la invocación de la función eval del LISP o exec de Python.
CAPÍTULO 12
162
De este modo, el objeto trait String define el comportamiento de todos los objetos
de tipo cadena de caracteres. Para obtener una mayor claridad de código, la plataforma
y el entorno de computación han sido desarrollados siguiendo la notación de identificar los
objetos de rasgo con su primera letra en mayúscula.
En la siguiente figura mostramos las asociaciones existentes entre los objetos primitivos
mencionados.
super
id
super
id
nil
String
super
id
super
id
Objetos
Referencias
Figura 12.1: Objetos primitivos “nil” y “String”.
Vemos cómo, siguiendo los criterios de diseño especificados, el identificador del
objeto String es el objeto , así como su objeto padre es el objeto nil.
Las cadenas de caracteres de este lenguaje han de poder anidarse. Existirán cadenas
de caracteres dentro de otras cadenas de caracteres. El autómata que las reconoce y su gramática
equivalente no puede ser de tipo 3 o regular, como las clásicas, comprendidas entre
una pareja del mismo delimitador [Cueva91]. Por lo tanto, las cadenas de caracteres son
declaradas como sentencias de un lenguaje de tipo 2, libre de contexto, y necesitan un elemento
léxico de apertura y cierre distinto: < y > respectivamente.
Los objetos cadenas de caracteres son el elemento primitivo de representación de
datos y computación para nuestra plataforma. Haciendo uso de la capacidad de reflectividad
estructural, podremos acceder en tiempo de ejecución a la estructura y estado de cualquier
objeto, así como a la implementación de todos los métodos no primitivos de la máquina
y del entorno de computación45.
Las cadenas de caracteres poseerán las siguientes de características:
Podrán anidarse. Dentro de una cadena representativa de código, podremos necesitar
representar a su vez otra cadena de caracteres. De este modo, deberá
permitirse la introducción de cualquier conjunto de cadenas de caracteres, de
forma recursiva.
Podrán contener caracteres especiales sin necesidad de utilizar códigos de escape.
Podremos utilizar cadenas de caracteres de más de una línea así como introducir
tabuladores dentro de ésta.
Representación de delimitadores. ¿Cómo se introducen los caracteres < y > dentro
de la propia cadena, sin que indiquen una subcadena? Con el carácter de
escape \, teniendo así dos caracteres distintos \> y \< que no indicarán anida-
45 Esta capacidad hace que el sistema sea dinámicamente introspectivo.
Diseño de la Máquina Abstracta
163
miento de cadenas de caracteres, y cuya representación a la hora de visualizarlos
será > y < respectivamente.
Utilización de caracteres de escape. Se podrán utilizar, si se desea, los caracteres
de escape \n, \\ y \t, en lugar de su propia representación.
Caracteres desoídos. Puesto que el sistema será autodocumentado mediante introspección,
será necesario representar dicha documentación de forma elegante.
Sin embargo, en la codificación de aplicaciones, puede surgir la necesidad de indentar
(sangrar) el código. No obstante, no se desea que aparezcan los tabuladores
de indentación en la documentación. Debemos identificar un modo de
ignorar caracteres para su posterior documentación.
Se establecen bloques de caracteres que ignorará el analizador léxico y no aparecerán
al acceder posteriormente a ellos. Estos caracteres serán los comprendidos
entre \*. Por ejemplo, podremos evaluar lo siguiente:
nil:(<==>,<\*
\* o <- params:<0>;
\* \* sID <- o:;
\* \* return <- sID:<==>();\*
\* >);
Posteriormente, si accedemos al objeto asociado a nil mediante la referencia ==
tendremos:
< o <- params:<0>;
sID <- o:;
return <- sID:<==>(); >
Para mostrar el código con mayor legibilidad, la indentación de cadenas de caracteres
con los caracteres de escape se obviará a partir de aquí, a lo largo de este capítulo.
12.1.3.3 Objeto Extern
Como se identificó en el capítulo 10, separaremos las primitivas computacionales
de las operacionales, permitiendo ampliar las segundas mediante un mecanismo de interconexión
estándar con el sistema operativo hospedado.
La máquina abstracta posee las siguientes primitivas:
Primitivas Computacionales. Propias de la semántica del funcionamiento de la
máquina. Podemos mencionar el mecanismo de acceso a los miembros, la evaluación
o descosificación de objetos, y el mecanismo de delegación o herencia
dinámica. Estas primitivas son implementadas en la máquina virtual y no puede
modificarse su semántica.
Primitivas Operacionales. Se representan mediante objetos evaluables que especifican
la semántica de operaciones, no representables mediante las existentes
inicialmente en la plataforma. Este tipo de semántica podrá ampliarse en función
de los requisitos de la plataforma.
Ejemplos de este tipo de primitivas son: operaciones aritméticas, tratamiento de
cadenas de caracteres, acceso a disco, primitivas de distribución o interfaces gráficas.
Para desarrollar un mecanismo de ampliación de primitivas operacionales, se necesita
un protocolo de comunicación con las implementaciones externas de los objetos primiCAPÍTULO
12
164
tivos operacionales, utilizando un mecanismo de interconexión estándar que sea coherente
con el diseño de la máquina. En función del sistema operativo existente en la implantación
de la plataforma virtual, se utilizará un mecanismo de interconexión que permita ampliar la
semántica operacional, de un modo estándar –ejemplos de esto pueden ser sistemas de
componentes o librerías de enlace dinámico.
Mediante la utilización de este sistema de ampliación de primitivas, la máquina no
tendrá nunca que reemplazarse, no se perderá portabilidad de su código, y la plataforma
computacional poseerá la posibilidad de ampliar sus operaciones concretas de una plataforma46.
La referencia Extern apunta a un objeto primitivo que posee un miembro id con
valor , un miembro super haciendo referencia a nil, y un miembro invoke.
Este último objeto es el encargado de representar todas las primitivas operacionales externas;
mediante el paso parámetros, indicaremos qué primitiva externa debe evaluar.
super
id
super
id
nil
String
super
Extern id
invoke
Máquina Abstracta
Sistema Operativo
Implementaciones
Externas
Mecanismo de
intercomunicación
estándar para el sistema
operativo seleccionado
en la implantación de la
máquina
Referencias a
Objetos String
Figura 12.2: Acceso a las primitivas operacionales externas mediante el método “invoke” del objeto
“Extern”.
La sintaxis de invocaciones al método invoke tendrá como parámetros la referencia
al objeto donde se ubica el miembro, la referencia al nombre del miembro, la referencia
al parámetro implícito y los parámetros de la invocación –veremos en § 12.1.4.4 la sintaxis
de invocación de métodos.
12.1.3.4 Objeto System
La reflectividad estructural de la plataforma nos permite acceder a los objetos y a la
estructura de éstos, de forma dinámica. Para conocer y acceder a su estructura, veremos
cómo los miembros de nil ofrecen las primitivas de contenedor. El objeto trait System
será el encargado de ofrecer el conocimiento dinámico de los objetos existentes en la plataforma,
así como una referencia para trabajar con ellos –introspección global del sistema.
El objeto System nos permite acceder al entorno de ejecución de la plataforma.
Es un objeto que posee por padre a nil, y que tiene un miembro objects que hace
46 Las distintas versiones de máquinas virtuales de la plataforma de Java [Kramer96], supone que el desarrollo
de applets para las últimas versiones actuales de ésta, no puedan ser ejecutados por aquellos clientes
que posean navegadores con versiones anteriores.
Diseño de la Máquina Abstracta
165
referencia a un objeto contenedor: posee, además de los miembros super e id, un miembro
por cada objeto existente en tiempo de ejecución, siendo cada referencia miembro el
id del objeto asociado.
super
id
super
id
nil
String
super
System id
objects super
id
nil
String
System
...
super
Extern id
invoke
Extern
Resto de Objetos como las
cadenas de caracteres ,
, o
Figura 12.3: Acceso a los objetos de la máquina abstracta mediante el objeto “System”.
Todas las primitivas de acceso a la plataforma física real o acceso al medio (la plataforma
concreta en la que se esté ejecutando la máquina abstracta), se ubicarán como miembros
de este objeto. Un ejemplo es el método localhost del sistema de distribución (§
13.4) que nos devuelve el identificador de la máquina en la red de computadores.
12.1.4 Posibles Evaluaciones
Esta máquina abstracta posee un conjunto inicial de objetos con una estructura establecida.
A raíz de éstos, crearemos la computación básica de la plataforma y posteriormente
un entorno de computación. El modo de construir computaciones es llevado a cabo
por el programador, mediante el siguiente conjunto de instrucciones:
1. Creación de referencias. Podremos declarar una referencia en el contexto actual
de ejecución para su posterior utilización.
2. Asignación de referencias. Podremos hacer que una referencia apunte (se quede
con la identidad) al objeto asociado a otra referencia existente.
3. Acceso a los miembros. Podremos acceder a cualquier miembro de un objeto
en tiempo de ejecución.
4. Descosificación o evaluación de un miembro sobre un objeto. Podremos evaluar
como comportamiento (instrucciones) un dato que sea miembro de un objeto.
Este objeto será sobre el que actuará la computación: el objeto implícito.
5. Comentarios. Como hemos mencionado, la autodocumentación, y por tanto el
acceso a los comentarios utilizados, es una característica propia de la plataforma.
Los comentarios no se eliminarán por el analizador léxico, aunque su computación
resulte nula.
CAPÍTULO 12
166
6. Descosificación o evaluación de objetos cadena de caracteres. Cualquier secuencia
de instrucciones de las anteriormente enumeradas, podrá tratarse como
datos (objetos cadenas de caracteres) y evaluarse como computación.
Una instrucción finalizará siempre con el carácter punto y coma.
12.1.4.1 Creación de Referencias
Una referencia es el mecanismo de acceso a un objeto. La ejecución de la máquina
parte de la existencia de un conjunto inicial de referencias. El usuario podrá utilizar éstas y
crear otras para acceder a los distintos objetos del sistema.
Cada vez que se evalúa o descosifica un objeto, se crea un nuevo contexto de ejecución
sobre el contexto existente –véase § 12.2.2. En éste contexto se pueden crear nuevas
referencias, además de acceder a las referencias del contexto padre. Si aplicamos esta propiedad
de modo transitivo, en realidad es factible acceder a cualquier contexto padre.
Una referencia ha de ser única en su contexto. Al destruirse un contexto (finalizar
su ejecución) se liberan sus referencias, pero no los objetos a los que éstas acceden47. La
creación de una nueva referencia en el contexto de ejecución se realiza mediante el componente
léxico “->”. Si la referencia en ese contexto ya existía, la evaluación es nula. Inicialmente
una referencia posee la identidad –apunta al objeto– nil.
-> nuevaRef;
-> otraRef;
La creación de referencias en un contexto, unido a la posibilidad de acceder desde
un contexto a otro padre, puede provocar conflictos de nombres. Si una referencia existe
en un contexto y en alguno de los contextos padre, no se podrá acceder desde el contexto
actual a las referencias de igual nombre en contextos padre. Esto representará un problema
en la evaluación de miembros que toman un código como parámetro que accede a referencias
del contexto padre; veremos posteriormente una solución a este problema.
Al tratar con un modelo de objetos basado en prototipos, no necesitamos declarar
tipos para las referencias. Todo el proceso de validación semántica, así como la inferencia
de tipos, se produce en tiempo de ejecución.
12.1.4.2 Asignación de Referencias
Hemos visto cómo crear nuevas referencias en un contexto. Una referencia es el
mecanismo de acceso a un objeto. Podremos asignar referencias con el token <-
nuevaRef <- String;
otraRef <- ;
La asignación de referencias produce un nuevo modo de acceso al objeto que estaba
siendo utilizado por la referencia situada a la derecha de la flecha. El sentido de la asignación
es el indicado por la flecha. La referencia que obtiene un nuevo valor, pierde la
identidad del objeto que estaba utilizando.
12.1.4.3 Acceso a los Miembros
Hemos visto qué aspecto inicial tiene la máquina abstracta, y cómo se estructura y
representa un objeto en memoria. Ahora mostraremos cómo se accede a los objetos existentes
para su posterior manipulación.
47 La liberación de las referencias de un contexto no implica la destrucción de los objetos referenciados,
pero sí supone el decremento en una unidad de su miembro refCount.
Diseño de la Máquina Abstracta
167
Como hemos comentado en la introducción de este capítulo, hay que tener en
cuenta que la abstracción base de la máquina es el objeto (orientación a objetos mediante
prototipos), y que un objeto se define como un conjunto de referencias a otros objetos a
los que podremos acceder en tiempo de ejecución. Un objeto es, por tanto, un contenedor
indirecto de otros objetos48.
Dado un objeto, accedemos a él mediante una referencia. Mediante ésta, accedemos
al conjunto de objetos asociados al objeto referenciado. La sintaxis consiste en separar la
referencia al objeto y la referencia al objeto que indica el nombre del miembro, mediante el
componente léxico dos puntos:
nil:;
String:;
:;
Todas las expresiones anteriores devuelven referencias a objetos: el objeto
en el primer caso, el objeto nil para el segundo ejemplo, y el objeto en la tercera
línea. La referencia devuelta puede asignarse a una referencia existente en el contexto, mediante
la asignación de referencias, para la posterior utilización del objeto referenciado:
ancho <- rectangulo:;
En el contexto de ejecución actual se pueden utilizar referencias locales a este contexto.
En el ejemplo anterior, la referencia devuelta en el acceso al miembro se asigna a la
referencia local ancho. Si ésta no existía previamente, se crea. Ahora existe otro medio
(otra referencia), para acceder al objeto miembro ancho.
12.1.4.4 Evaluación o Descosificación de Objetos Miembro
Mediante la adición de miembros que denotan secuencias de instrucciones, puede
añadirse comportamiento a los objetos del sistema. La invocación de un método se representará
mediante la evaluación de un miembro descosificable, indicando el objeto o parámetro
implícito sobre el que se actuará –al que se le enviará el mensaje.
Referencia al objeto
sobre el que se actúa
Nombre del miembro Parámetros
Invocación
Síncrona
RefObj : refNomMiembro ( refParams )
Invocación
Asíncrona
RefObj : refNomMiembro ) refParams (
Una vez accedido el objeto a evaluar, especificando siempre el parámetro implícito
u objeto sobre el que se actúa, se podrá evaluar éste de modo síncrono (esperando el hilo
actual a la finalización de su ejecución) o asíncrono (creándose un nuevo hilo sobre el contexto
existente). Los parámetros se pasan en ambos casos mediante un conjunto de referencias,
separadas por comas.
En la evaluación síncrona de un de un objeto podrá devolverse una referencia a un
objeto. Sin embargo, puesto que en la evaluación asíncrona no existe una espera a la finalización
de la invocación, no podrá asignarse ninguna referencia devuelta.
perimetro <- rectangulo:();
48 No contiene los propios objetos (composición) supeditando el ciclo de vida de éstos al suyo, sino que
contiene referencias a otros objetos que siguen existiendo aunque finalice la existencia del contenedor.
CAPÍTULO 12
168
rectangulo:)(;
12.1.4.5 Comentarios
Una instrucción puede ser una referencia a un objeto cadena de caracteres. La utilización
de esta posibilidad es de carácter documental para dicha instrucción. Si mantenemos
la política común en procesadores de lenguajes de eliminar los comentarios en el análisis
léxico, al acceder a la computación o instrucciones de la plataforma como datos, perderemos
los comentarios. Para la máquina abstracta, un comentario posee entidad léxica y sintáctica,
pero no semántica. La evaluación de un comentario es nula, pero no es eliminada
por el analizador léxico.
Accediendo a los miembros de un objeto, podrán conocerse los comentarios que el
programador haya identificado como necesarios. Si queremos dejar constancia de un comentario
únicamente en el archivo de entrada, sin que aparezca posteriormente, podremos
utilizar los caracteres a desoír en las cadenas de caracteres (§ 12.1.3.2):
< * Comentario para la posteridad * >;
...
< Comentario temporal que sólo queda en el archivo fuente >;
...
12.1.4.6 Evaluación de Objetos Cadena de Caracteres
No siempre es necesario evaluar un objeto como miembro. Puede interesar crear
una instrucción o conjunto de instrucciones como datos, para posteriormente evaluarlas.
La forma de representar instrucciones como datos, es utilizando objetos cadena de caracteres.
Una vez creada la cadena, se podrá descosificar o evaluar ésta de forma síncrona o
asíncrona.
< perimetro <- rectangulo:(); > ();
< rectangulo:)(; > )(;
La evaluación de un objeto cadena de caracteres se produce en un contexto nuevo
creado sobre el contexto actual (el contexto en el que se evalúa dicho objeto). Veremos
posteriormente cómo estas evaluaciones pueden devolver valores a los contextos padre, y
cómo acceder a las referencias de éstos desde el contexto creado.
12.1.5 Delegación o Herencia Dinámica
El mecanismo de herencia ha sido utilizado en el paradigma de la orientación a objetos
para reutilizar código e implementar aplicaciones adaptables. En nuestro lenguaje, la
evaluación de un objeto indicando el parámetro implícito, no implica que el objeto miembro
deba estar en ese objeto; puede formar parte de un objeto base –padre.
La especificación de una referencia a un objeto mediante el miembro super, indica
dónde se deberá buscar el miembro no encontrado el paso de un mensaje. En el caso de
que esto ocurra, se seguirá el proceso de búsqueda de modo recursivo en el objeto referenciado
por super. Esta búsqueda finalizará cuando se cumpla que la referencia super del
objeto examinado es una referencia a sí mismo, tratándose entonces del objeto ancestro
nil.
Este mecanismo de herencia es dinámico, es decir, se puede modificar la referencia
al objeto padre en tiempo de ejecución. La flexibilidad que esta capacidad otorga, hace que
el mecanismo obtenido se defina como delegación y, en alguna bibliografía, como herencia
dinámica.
Diseño de la Máquina Abstracta
169
12.1.6 Reflectividad Estructural y Computacional
La flexibilidad es el principal objetivo a obtener en el diseño de esta plataforma. Se
trata de especificar una máquina abstracta computacionalmente simple y, apoyándonos en
la extensibilidad y adaptabilidad otorgadas, construir un entorno de computación de un
mayor nivel de abstracción. La consecución de este objetivo se debe llevar a cabo sin necesidad
de modificar la máquina, aumentando su funcionalidad mediante su propio lenguaje.
Como hemos comentado a lo largo de esta tesis, la propiedad de reflectividad estructural
es un mecanismo útil para conseguir los objetivos mencionados. Mediante un
conjunto de primitivas, podremos acceder a la estructura y comportamiento de cualquier
objeto existente en tiempo de ejecución, para conocer, modificar o ampliar dinámicamente
cualquiera de sus miembros.
Como identificamos en el capítulo 6, la reflectividad computacional es un mecanismo
utilizado para otorgar flexibilidad en la semántica computacional de la máquina abstracta.
Al igual que multitud de sistemas (estudiados en el capítulo 7), para nuestra máquina
utilizaremos un protocolo de metaobjetos (MOP) reducido, obteniendo así un mecanismo
dinámico de flexibilidad computacional, con una expresividad restringida.
Determinadas primitivas computacionales de la máquina podrán modificarse mediante
el propio lenguaje del sistema haciendo uso de un pequeño MOP. Mediante un mecanismo
de reflectividad computacional restringido49, la máquina facilitará al programador
la posibilidad de modificar una serie de primitivas computacionales. De esta forma, no es
necesario modificar la máquina para introducir nuevas características, pudiendo adaptar
éstas mediante su propio lenguaje.
La máquina abstracta ofrece un MOP para modificar la semántica de las siguientes
primitivas computacionales:
Acceso a los miembros de un objeto, mediante la modificación del significado
del operador dos puntos.
Descosificación o evaluación síncrona y asíncrona de objetos, mediante la modificación
de los operadores paréntesis; tanto para objetos miembro evaluables,
como para objetos cadena de caracteres.
Veremos posteriormente en este documento la sintaxis del MOP que nos permite
adaptar estas primitivas en el lenguaje –en § 13.3.3 introducimos la adaptación del acceso al
miembro y en § 13.2.3 mostramos la modificación de la evaluación de código.
12.1.7 Objetos Miembro Primitivos
Existe un conjunto de objetos primitivos relativos al funcionamiento de la máquina
abstracta. Estos objetos poseen una semántica definida por la propia máquina, y por tanto
no están implementados en su propio lenguaje.
Podemos clasificarlos así:
Creación, destrucción y referencias de objetos.
Objeto Miembro Parámetros Descripción
nil new - Crea un objeto nuevo con dos referen-
49 Esta restricción supone establecer un protocolo a priori para identificar una sintaxis de modificación de
la semántica de un conjunto reducido de primitivas.
CAPÍTULO 12
170
Objeto Miembro Parámetros Descripción
cias miembro: “id” a un objeto derivado
de String con su identificación
única50 y super a nil.
nil delete - Libera la memoria del objeto implícito.
nil getRefCount - Devuelve un objeto cadena de caracteres,
indicando el número de referencias
existentes dinámicamente a este objeto51.
Acceso a los objetos mediante reflectividad estructural.
Objeto Miembro Parámetros Descripción
nil set 1: Referencia a
la clave
2: Referencia
al contenido
Asigna un nuevo miembro con un nombre
(clave) al objeto implícito. Si la referencia
no existía, la crea.
nil has 1: Referencia a
la clave
Devuelve un objeto lógico indicando si
existe o no la clave indicada.
nil firstKey - Devuelve una referencia a un objeto cadena
de caracteres que indica el nombre
del primer miembro.
nil nextKey 1:Referencia a
la clave
Devuelve la referencia a la siguiente clave
o nil si no hay más.
nil remove 1: Referencia a
la clave
Elimina la entrada con la clave pasada. El
objeto al que apunta no es liberado.
nil get 1: Referencia a
la clave
Devuelve la referencia asociada al nombre
de la clave pasada52 o nil si no existe.
Sincronización de hilos.
Objeto Miembro Parámetros Descripción
nil enter - Trata de acceder al monitor propio del objeto
implícito. Si ya está utilizado, se bloquea
en espera.
50 El hecho de otorgar identificadores únicos no es una tarea sencilla. Hay que tener en cuenta que puede
haber multitud de máquinas en distintas plataformas, y los objetos de cada una deberán tener identificadores
globalmente únicos. Para ello, utilizaremos los identificadores universalmente únicos especificados
por el Open Software Foundation’s Distributed Computing Environment (OSF DCE) [Kirtland99], representando
los IDs como cadenas de caracteres representativas de números de 32 bytes entre caracteres {} y
en base 16.
51 La utilidad principal de este miembro introspectivo, es facilitar al programador la implementación de
distintos recolectores de basura en el propio lenguaje de la máquina, sin necesidad de modificar su implementación
(§ 13.1).
52 Esta primitiva tiene la misma función que el acceso a un miembro mediante el uso del operador dos
puntos. Sin embargo, se utilizará una u otra en los sistemas de persistencia y distribución para diferenciar
entre objetos locales y en RAM, frente a objetos remotos y persistentes –véase el diseño de los servicios
de distribución y persistencia.
Diseño de la Máquina Abstracta
171
Objeto Miembro Parámetros Descripción
nil exit - Libera el recurso propio del objeto implícito.
El siguiente hilo esperando, entrará a él.
Si no existiese ninguno, la evaluación es
nula.
A continuación se presentarán ejemplos del empleo de estos objetos miembro primitivos,
para ampliar el nivel de abstracción de la plataforma, expresándolo en su propio
lenguaje.
12.2 Extensión de la Máquina Abstracta
Hasta este punto, hemos descrito el funcionamiento primitivo de la máquina abstracta,
mencionando las primitivas computacionales ofrecidas y los mecanismos a utilizar
para extender sus abstracciones –mediante los miembros primitivos del objeto nil. A partir
de este punto, extenderemos, haciendo uso del propio lenguaje de la máquina, su capacidad
computacional, sin necesidad de modificar el código fuente de la máquina virtual.
Al igual que la plataforma de Smalltalk [Krasner83], la programación llevada a cabo
será un conjunto de sentencias codificadas en un archivo imagen. Inicialmente la plataforma
ejecutará el código de este archivo para extender su abstracción.
12.2.1 Creación de Nuevas Abstracciones
Vamos a ver cómo podemos crear nuevos objetos en el sistema. Inicialmente tenemos
los objetos String, nil, System y Extern. Para poder añadir funcionalidades al
conjunto de todos los objetos existentes en la plataforma, podremos crear un objeto ancestro
Object que implemente estas funcionalidades. Este diseño ha sido adoptado por numerosos
lenguajes como Smalltalk, Object Pascal o Java.
Podríamos introducir estas funcionalidades en el objeto ancestro existente nil. Sin
embargo, dejaremos en nil aquellas capacidades semánticas primitivas que nos ofrece la
plataforma53.
Creamos inicialmente un objeto, almacenándolo en una referencia global (de contexto
0) Object:
-> Object;
Object <- nil:();
Object:(,