IDL (Interface Definition Language) es un lenguaje declarativo utilizado para definir las interfaces CORBA. Para hacer uso de los servidores CORBA es necesario invocar métodos de sus interfaces IDL.
Tal y como ya se ha comentado en el tema 1, se trata de un lenguaje que no contiene sentencias de programación, ya que su propósito es únicamente definir las signaturas de las interfaces.
Existen mappings estándar para convertir interfaces IDL en clases C++, código C, y clases Java, entre otros. Estas clases generadas usan el marco CORBA subyacente para comunicarse con los clientes remotes y proporcionan las bases para implementar y exportar nuestros propios objetos distribuidos. Java IDL es una implementación del estándar IDL-to-Java mapping y es proporcionado por Sun con es estándar Java SDK en los paquetes org.omg.CORBA y org.omg.CosNaming y sus subpaquetes.
Al igual que RMI, Java IDL permite acceder a objetos remotos a través de la red. También proporciona las herramientas necesarias para hacer que los objetos Java sean también accesibles a otros clientes CORBA. Si exportamos una clase usando Java IDL, se puede crear una instancia de esa clase y publicarla a través del servicio de nombres. Un cliente remoto puede entontrar dicho objeto, llamar a sus métodos, y recibir datos desde ellos, igual que si se estuviesen ejecutando en la máquina local del cliente. Sin embargo, a diferencia de RMI, los objetos que son exportados usando CORBA pueden ser accedeidos por clientes implementados en cualquier lenguaje que tenga asociado un IDL binding (C, C++, Ada, etc).
Las principales diferencias entre IDL y Java son:
structs
, unions
, y ennumerations.
Todas las declaraciones hechas con IDL están disponibles mediante el repositorio de interfaz (Interfaz Repository: IR). El IR es un servicio de CORBA que proporciona información en tiempo de ejecución sobre los tipos de interfaces de objetos soportados en una instalación de ORB particular. Puede verse como un conjunto de objetos que encapsulan las definiciones IDL de todos los tipos CORBA disponibles en un dominio particular.
Los módulos se declaran en IDL usando la palabra reservada module
,
seguida de un nombre para el módulo y una llave de apertura inicia el
ámbito del módulo. Cada elemento definido dentro del ámbito
del módulo (interfaces, constantes, otros módulos) pertenece a
dicho módulo y es referenciado en otros módulos IDL usando la
sintaxis module-name::x
. Supongamos que se quiere que determinadas
clases estén contenidas en un módulo llamado corba, que es parte
de un módulo mayor llamado jen. En IDL esto se declara como sigue:
//IDL module jen { module corba { interface NeatExample ...; }; };
Si se quiere referenciar la interfaz NeatExample en otros ficheros IDL se usa
la sintaxis jen::corba::netaExample
. Cabe destacar los punto y
coma después de las llaves de cierre de las definiciones del módulo,
que son requeridos en IDL, pero no en Java. También se requiere un punto
y coma después de una definición de interfaz.
Las interfaces declaradas en IDL son convertidas en clases o interfaces en Java. Como se ha mencionado anteriormente, IDL se usa solamente para declarar módulos, interfaces y sus métodos. Los métodos de las interfaces IDL se dejan siempre abstractos, para ser definidos en el lenguaje de programación que se quiera usar para implementar la interfaz.
La declaración de una interfaz include una cabecera de interfaz y un cuerpo de interfaz. La cabecera especifica el nombre de la interfaz y las interfaces de la cual es heredera (si hay alguna). Un ejemplo de cabecera de interfaz IDL es:
interface PrintServer : Server { ...
Esta cabecera comienza con la declaración de una interfaz llamada PrintServer
que hereda todos los métodos y miembros de datos de la interfaz Server
.
Una interfaz IDL puede heredar de múltiples interfaces; simplemente hay
que separar los nombre de interfaces con comas en la parte de herencia de la
cabecera.
Los módulos pueden anidarse dentro de otros módulos, y sus contenidos pueden ser referenciados de forma relativa a su ámito de nombres en el que se encuentran. Por ejemplo:
module outer { module inner { //módulo anidado interface inside{}; }; interface outside {//puede referirse a su clase anidada con su nombre local inner::inside get_inside(); }; };
La operación get_inside()
devuelve una referencia a objeto
de tipo ::outer::inner::inside
, pero puede usar la forma relativa
del nombre debido a su posición en el mismo ámbito que el módulo
interno.
Las interfaces pueden referenciarse mutuamente. Es decir, las declaraciones en una interfaz pueden usar el nombre de otra como tipo de objeto. Para evitar errores de compilación, un tipo de interfa debe ser declarado por anticipado antes de ser usado.
interface A; //declaración forward interface B {//B puede usar A como nombre de tipo A get_an_A(); }; interface A { B get_an_B(); };
Cuando una declaración en un módulo necesita alguna referencia mutua en una declaración en otro módulo, se puede cerrar el primer módulo y "reabrirlo" después de algunas otras declaraciones.
module X { interface A; //declaración forward }; module Y { interface B { X::A get_an_A(); }; }; module X { interface C { A get_an_A(); }; interface A { Y::B get_a_B(); }; };
El cuerpo de la interfaz declara todos los miembros de datos (o atributos)
y métodos de una interfaz. Los miembros de datos se declaran usando la
palabra clave attribute
. Como mínimo, la declaración
incluye un nombre y un tipo. Las declaraciones pueden especificar opcionalmente
si el atributo es de solo lectura o no, usando la palabra reservada readonly
.
Por defecto, cada atributo que se declara se puede leer y escribir (para Java
esto significa que el compilador IDL genera métodos públicos de
lectura y escritura para dicho atributo). Un ejemplo de declaración de
atributo string
de sólo lectura es:
readonly attribute string myString;
Un método se declara especificando su nombre, tipo de resultado y parámetros, como mínimo. Opcionalmente se pueden declarar excepciones que el método prodría provocar, la semántica de invocación del método, y el contexto para el método de llamada. Una declaración para un metódo sencillo que devuelve un string podría ser:
string parseString(in string buffer);
esto declara un método llamado parseString()
que acepta
un string
como argumento y devuelve un valor string
.
A continuación mostramos un ejemplo completo de IDL.
module OS { module services { interface Server { readonly attribute string serverName; boolean init (in string sName); }; interface Printable { boolean print(in string header); }; interface PrintServer: Server { boolean printThis(in Printable p); }; }; };
La primera interfaz, Server
, tiene un único atributo string
de solo lectura y un método init()
que acepta un string
y devuelve un boolean
. La interfaz Printable
tiene
un único método print()
que acepta una cabecera string
.
Finalmente, la interfaz PrintServer
"extiende" la interfaz
Server
(y por lo tanto hereda todos sus métodos y atributos)
y añade un método printThis
que acepta un objeto
Printable
y devuelve un boolean
. En todos los casos
hemos declarado los argumentos de los métodos solamente de entrada (es
decir, se pasan por valor), usando la palabra reservada in
.
El nombre de cualquier interfaz declarada en IDL constityue un nombre de tipo
de objeto que puede usarse como el tipo de cualquier parámetro de operación
o valor de retorno, o como un miembro en una declaración de tipo estructurado.
IDL proporciona un conjunto de tipos básicos para representar números,
strings, caracteres, y booleanos. Las definiciones de los mismos son
bastante precisas para permitir codificaciones ambiguas. Los tipos estructurados
en IDL son las estructuras, uniones con discriminante, arrays, y secuencias.
Las excepciones pueden considerarse un caso especial de estructuras que pueden
usarse solamente en cláusulas raise
de las operaciones.
Los tipos básicos proporcionados por IDL son:
[unsigned] short
, [unsigned] long
, [unsigned]
long long
, entero con complemento a dos (con o sin signo) de 16, 32
y 64 bits respectivamente.float
, double
, long double
, número
en punto flotante de 16, 32 y 64 bits respectivamente.fixed
, número decimal de punto fijo hasta 31 dígitos.boolean
, tipo booleano con valores TRUE
y FALSE
.wchar
, string
, wstring
, caracter
de otros juegos de caracteres para soportar internacionalización, cadena
de caracteres de longitud variable, y cadena de caracteres de wchar caracteres
de longitud variable, respectivamente.octet
, enum
, tipo no interpretado de 8 bits, y
tipo enumerado con valores enteros con nombre, respectivamente.any
, puede representar cualquier valor de los posibles IDL,
básico o no básico.native
, es un tipo opaco, cuya representación es especificada
por el tipo de lenguaje destino.Se puede utilizar la palabra reservada typedef
para crear tipos
de datos. En el caso de tipos "plantilla" (tipos que requieren un
parámetro para determinar su longitud o contenidos) se requiere un typedef
antes del tipo que puede usarse en una declaración de atributo o de operación.
Los strings pueden ser limitados o ilimitados. Los tipos string limitados son tipos "plantilla", indicándose su longitud máxima como parámetro.
interface StringProcessor { typedef octstring string <8>; typedef centastring string <100>; //... octstring MiddleEight(in string str); centastring PadOctString(in octstring ostr, char pad_char); };
Los tipos de punto fijo también son tipos "plantilla" y deben ser introducidos con un typedef, especificando el número de dígidos decimales y la escala, expresada como potencia de 10. Así por ejemplo para representar el número 123.45, se indicaría como un número con 5 dígitos, y una escala de 2.
typedef fixed <5,2> three_point_two;
Los tipos enumerados se declaran con un nombre, y una coma separa la lista de identificadores.
enum glass_color {gc_clear, gc_red, gc_blue, gc_green}
Las estructuras se declaran con la palabra reservada struct
, seguida
de un nombre.
interface HardwareStore { struct window_spec { glass_color color; height float; width float; };
Las uniones con discriminante se declaran con la palabra reservada union
,
seguida de un nombre y de la palabra reservada switch
, y parametrizada
por un tipo escalar (integer
, char
, Boolean
o enum
) que actúa como discriminante.
enum fitting_kind {door_k, window_k, shelf_k, cupboard_k}; union fitting switch (fitting_kind) { case door_k, cupboard_k: door_spec door; case window_k: window_spec win; default: float width; };
Las secuencias son tipos plantilla. Una secuencia es una colección ordenada de items que puede crecer en tiempo de ejecución. Sus elementos son accedidos mediante un índice. Las secuencias pueden ser limitadas o no. Todas las secuencias tienen dos características en tiempo de ejecución: su longitud máxima y la actual. La longitud máxima de las secuencias limitadas se determina en tiempo de compilación. La ventaja de las secuencias es que solamente se pasa a un objeto remoto el número de elementos actuales de la secuencia cuando se utiliza una secuencia como argumento.
//union type fitting declarada anteriormente typedef sequence <fitting> HardwareOrderSeq; typedef sequence <fitting,10> LimitedHWOrderSeq; typedef sequence <sequence <<fitting>,3> ThreeStoreHWOrderSeq; typedef sequence <sequence <fitting> ManyStoreHWOrderSeq; default: float width; };
Los arrays también se declaran normalmente con un typedef. Los arrays tendrán una longitud fija en tiempo de ejecución. Independientemente de su longitud y contenido, el array completo será codificado y enviado por la red.
typedef window WindowVec10[10]; typedef fitting FittingGrid[3][10]; struct bathroom { float width; float length; float heigth; boolean has_toilet; fitting fittings[6]; };
Las excepciones se declaran de la misma forma que las estructuras, usando la
palabra reservada exception
, en lugar de struct
.
exception OrderTooLarge { long max_items; long num_items_submitted; }; exception ColorMismatch { sequence <color> other_window_colors; color color_submitted; };
Las constantes pueden declararse con un ámbito global o dentro de los
módulos e interfaces. Se utiliza la palabra reservada const
.
const short max_storage_bays = 200; const windows_per_bay=45; const long max_windows = max_storage_bays * windows_per_bay; const string initial_quote= "fox in socks on knox on bloks"; const HarcwareStore::CashAmount balance = (max_storage_bays -3)/1.45;
Las declaraciones de las operaciones contienen un nombre de operación,
un tipo de retorno (o void
para indicar que no se espera ningún
valor), y una lista de parámetros, que puede ser vacía. Una operación
puede tener cláusulas raise
, para indicar las excepciones.
typedef float CashAmount; typedef sequence <window_spec> WindowSeq; CashAmount OrderFittings(in HardwareOrderSeq order) raises (OrderTooLarge); void OrderWindows( in WindowSeq order, in CashAmount willing_to_pay, out CashAmount total_price, out short order_number) raises (OrderTooLarge, ColorMismatch);
El modo de paso de parámetros es por valor para los tipos básicos y para estructuras, uniones y enumeraciones. Los argumentos de tipo objeto se pasan siempre por referencia.
Un atributo es equivalente lógicamente a un par de funciones de acceso,
una para acceder al valor, y otra para modificarlo. Los atributos de solo lectura
solamente requieren una función de acceso. Los atributos consisten en
la palabra reservada attribute
seguida del tipo del atributo y
de una lista de nombres de atributos. Pueden ir precedidos de la palabra reservada
readonly
.
readonly attribute CashAmount min_order, max_order; readonly attribute FittingSeq new_fittings; attribute string quote_of_the_day;
Los atributos previos pueden reemplazarse por el siguiente IDL:
CashAmount min_order(); CashAmount max_order(); FittingSeq new_fittings(); string get_quote_of_the_day(); void set_quote_of_the_day(in string quote);
Tal y como están declaradas, las operaciones y los atributos son equivalentes.
Los valuetypes son un tipo especial de datos en IDL introducidos para permitir que las entidades fueran pasadas por valor en lugar de por referencia en una invocación de operación, para que de esta forma el acceso al valor fuese siempre una operación local.
Otra razón de ser de los valuetypes es el proporcionar semánticas compartidas y "null" para valores IDL tales como structs y secuencias. Usando valuetypes, por ejemplo, es posible pasar un grafo entero en una invocación de una operación de forma que se preserven los valores compartidos en el grafo. O por ejemplo, una estructura IDL puede estar vacía (es decir, sus valores pueden estar no inicializados), pero nunca puede ser "null"; los valuetypes sí permiten esto.
Los valuetypes son una mezcla entre interfaces y estructuras, y se declaran
con la palabra reservada valuetype
. Existen varias clases de valuetypes.
Los regular valuetypes, como las interfaces, pueden contener definiciones
de atributos, operaciones y declaraciones de tipos locales. A diferencia de
las interfaces, pueden tener miembros que reflejan el estado, e inicializadores.
interface Observer { void notify(); } valuetype ShoppingCart supports Observer { readonly attribute long current_value; private HardwareOrderSeq selected; void addToCart(in fitting item); factory init(in HardwareOrderSeq initial); };
Los miembros que reflejan el estado (state members) constituyen el estado
que es codificado y enviado al receptor. Los miembros estado pueden declararse
public
o private
. Si se declara private debería
ser colamente accesible por la implementación de las operaciones valuetype.
Para crear nuevas instancias, puede definirse un inicializador usando la palabra
reservada factory
.
Los abstract valuetypes son un tipo de valuetypes que no pueden ser instanciados. No tienen inicializadores, ni valores mismbros, solamente se permiten operaciones o definiciones de tipos de datos.
abstract valuetype Cart supports Observer { void add(in Any item); }
Una vez que se han especificado las interfaces en IDL, es necesario generar las clases Java que sirven como punto de partida para implementar las interfaces remotas en Java usando un compilador Idl-to-Java. Cada compilador estándar Idl-to-Java genera las siguientes clases a partir de una interfaz IDL:
narrow()
que puede
realizar un cast desde referencias Object de CORBA a tipos de interfaz
en Java. La clase helper también proporciona otros métodos
estáticos útiles como read()
y write()
que permiten leer y escribir un objeto del correspodiente tipo usando streams
de entrada/salida.out
o inout
en métodos CORBA
remotos. En luegar de pasarse directamente al método remoto, el objeto
es "envuelto" dentro de su holder antes de pasarse como parámetro.
Cuando un método remoto tiene parámetros declarados como out
o inout
, el método tiene que ser capaz de actualizar el
argumento que se ha pasado y devolver el valor actualizado. La única
forma de garantizar esto es, incluso para tipos de datos primitivos Java,
forzando a que los argumentos out
e inout
sean "envueltos"
en clases holder Java, que toman como valor el valor de salida del
argumento cuando el método devuelve el resultado.Con respecto a la clase helper, la implementación del método
narrow()
convierte una referencia a un Object
de CORBA
en una referencia a un tipo específico. Cada stub CORBA para un
objeto remoto contiene un objeto Delegate
interno (obtenido del
paquete org.omg.CORBA.portable
) que es usado por el stub
para invocar peticiones remotas. Si el objeto no contiene un objeto Delegate
interno, el método narrow()
producirá la excepción
BAD_PARAM
.
Una clase holder contiene una única instancia de un objeto CORBA.
Cuando un objeto holder es pasado en una llamada a un método remoto
como un argumento inout
, se invoca a su método _write()
.
Este método toma el objeto contenido en la clase holder, lo serializa,
y lo envía como un flujo de datos a través del ORB al servidor
del objeto remoto. Cuando la llamada al método remoto termina, se invoca
al método _read()
de la clase holder para leer el
(posiblemente actualizado) objeto desde el servidor del objeto remoto, y el
objeto holder reemplaza su valor interno con el objeto actualizado.
La herramienta idlj
proporcionada por Sun puede generar dos clases
adicionales:
_
nombre-interfazStub
,
que actúa como una implementación en la parte del cliente de la interfaz
y conoce como convertir las peticiones a los métodos en peticiones
ORB que son enviadas al objeto remoto real. Por ejemplo la clase stub
para una interfaz llamada Server
es _ServerStub
._
nombre-interfazImplBase
,
que es una clase base para la implementación en el lado del servidor.
La clase base puede aceptar peticiones para el objeto desde el ORB y devolver
valores a través del ORB al cliente remoto. Por ejemplo la clase skeleton
para una interfaz llamada Server
es _ServerImplBase
.La clase base para la implementación del servidor acepta peticiones destinadas a la implementación del servidor desde el ORB. La clase base converte una petición en una llamada a un método en el objeto servidor y entonces toma el resultado de la llamada y la devuelve al ORB para enviarla al stub del cliente. Todo este trabajo se realiza por el método del skeleton del servidor invoque(). El método invoque() "averigua" el método que está siendo llamado, decodifica los argumentos del método (si los hay) desde la petición, y llama al método directamente.
Una vez que se ha escrito la interfaz IDL y generado la interfaz Java y las
clases de soporte, incluyendo las clases stub y skeleton, es necesario
concretar las implementaciones del servidor para todos los métodos de
la interfaz. Esto lo podemos hacer creando subclases a partir de la clase _xxxImplBase
generada por el compilador IDL.