ZeroC Ice: Parseando un fichero Slice
Cómo parsear un archivo Slice (el lenguaje de especificación de interfaces de ICE) para obtener toda la información semántica que contiene utilizando libSlice (el Parser proporcionado por ZeroC)
Introducción
Los archivos slice todos sabemos lo que son: Archivos que proporcionan una descripción de las clases, módulos, interfaces y operaciones que implementará un sistema distribuido basado en ICE.
La gente de ZeroC proporciona el código fuente de su middleware, pero éste no está muy bien documentado (siendo generosos), así que la manera de parsear un archivo slice y obtener toda la semántica que se necesitaba, ha pasado por hacer un poco de ingenería inversa del código de una de las utilidades que da ZeroC (en concreto slice2cpp). Así que para evitar tener que repetir el trabajo, aquí dejo un poco documentado el funcionamiento del parser para Slice que la gente de ZeroC montó.
Prerequisitos
Antes de empezar, no estaría de mas conocer un poco el patrón de programación visitor, ya que se utilizará para acceder a la información proporcionada por el parser. También se supone que se tienen nociones básicas de C++.
Yo he utilizado ice-3.3-beta así que tampoco estará de mas que además de tener ice instalado, se instalen los fuentes:
Parseando que es gerundio…
Para parsear un archivo slice, necesitaremos utilizar la librería Slice/Parser proporcionada por ZeroC. Un ejemplo de función que llama al parser sería la siguiente:
¿y como funciona esto? Bueno, la clase OurParser será quien realice todo el trabajo. Básicamente, su cometido consiste en crear una instancia del preprocesador pasándole el archivo a parsear y un conjunto de opciones (en nuestro caso, como no queremos pasarle nada, se le pasa un vector de cadenas vacío, dummyArgs). Después de instanciar el preprocesador, se preprocesa el archivo:
Después se instancia el parser propiamente dicho, y se parsea el Slice
u es quien realiza todo el trabajo de parseado. Las opciones que se pasan cuando se crea se utilizan entre otras cosas para propósitos de depuración y las que se dan en el ejemplo son válidas en general. Si se quiere, se puede jugar con los valores a ver qué pasa o directamente echar un ojo al código de slice2cpp donde están un poco mas comentadas.
la variable parseStatus almacena el resultado del parseo.
Una vez que se ha terminado de parsear el archivo, ya se cuenta con toda la información semántica, así que solo nos resta obtener dicha información y procesarla a nuestro antojo. Como se ha intentando que el parser sea lo mas general posible, de modo que pueda ser reutilizado para lo que sea necesario (vamos, que haya buena separación entre frontend y backend), se ha hecho uso del patron de diseño visitor que se comentó anteriormente. En nuestro caso creamos una clase que contendrá una serie de métodos que serán llamados en determinados momentos, como por ejemplo cuando se inicie la declaración de un módulo, su finalización, cuando se declare una función, sus parámetros, metadatos, se defina una estructura de datos o una clase, etc….
En nuestro caso, después del parseo, “visitamos” al parser para pedirle toda la información:
visitor es una instancia de nuestra clase visitante, que implementará los métodos necesarios para procesar la información.
Ésta clase viene a ser algo como ésto:
Bueno, como se puede ver, nuestro “visitante” tiene que heredar de la clase ParserVisitor, además, debe implementar todos los métodos que se ven. Éstos métodos serán llamados por el parser y será en ellos en los que nosotros definamos el procesado que queremos hacer de la información.
“Extrayendo” información
Bueno, aquí voy a comentar qué he hecho yo para obtener cierta información que necesitaba del Slice. Si se necesitase otra información, pues nada, a mirar el código de los conversores de código que da ZeroC, que es muy entrentenido :-P …
Modulos
Cada vez que se inicia el parseado de un módulo, se invoca la función visitModuleStart de nuestro visitor. Entre otras cosas, la información que puede ser necesaria en este punto es el nombre del módulo y su ámbito de declaración. Para acceder a esa información, algo como esto:
Cuando se abandona la declaración de un módulo, se invoca a visitModuleEnd, pasandole exactamente la misma información que a su homólogo de inicio de módulo.
Clases e Interfaces
Cuando se declara una clase o una interfaz en el Slice, el parser visita la misma función: visitClassDefStart para el inicio de la declaración y visitClassDefEnd para finalizar la declaración. Si la declaración se encuentra dentro de un módulo, estaremos en la declaración de una interfaz. En caso contrario, se tratará de una clase.
Aquí la información que leí fue la misma que en el módulo, así que el código es casi idéntico:
Operaciones
Para el caso de las operaciones necesitaremos leer mas información aparte del nombre y el ámbito. Ésta información se refiere al tipo de retorno, tipo de la función (por ejemplo puede ser idempotent), y los argumentos (nombre, tipo y dirección).
El procedimiento encargado de obtener la información de las operaciones es VisitOperation y un ejemplo de implementación que simplemente imprime información sería el siguiente:
Vayamos por partes: Lo primero que hemos obtenido, ha sido el nombre y el ámbito de declaración de la operación. Después de ésto, se obtiene el tipo de retorno. Éste tipo es un enumerado, por lo que deberemos transformarlo a una cadena con el nombre adecuado para poderlo imprimir (o hacer cualquier otro tipo de proceso que nos interese)
Después se consulta el modo de la operación, que nos dirá si es o no idempotente.
A continuación yo necesitaba los metadatos de las operaciones. getMetaData() devuelve una lista de cadenas, cada una de las cuales es un metadato, así que con un iterador recorremos dicha lista y vamos imprimiendo todos los metadados de cada operación. Ésto también se puede hacer con módulos, clases, interfaces, etc….
Finalmente, se consultan los parámetros: parameters() devuelve una lista de parámetros. Una vez que se tiene esa lista, hay que iterar sobre ella e ir obteniendo toda la información sobre cada uno de los parámetros:
- Nombre: (q)→name()
- Dirección: (q)→isOutParam(), devuelve verdadero si el parametro es de salida.
- Tipo: (*q)→type(), enumerado con el tipo del parametro.
Sobre los tipos de retorno y de los parámetros
returnTypeToString, inputTypeToString y outputTypeToString son funciones que son llamadas para convertir los enumerados que determinan el tipo de una función o parámetro en la cadena que los representa textualmente. En la implementación que tenemos, éstas funciones se encuentran dentro de un módulo llamado Slice/CPlusPlusUtil que hay que incluir. Cuando se detecte un string, int o bool por ejemplo, nuestro programita responderá con algo como ::ICE::string o ::ICE::int pero ¿y si queremos que, por ejemplo, cuando detecte un entero nos diga “cacho entero” (por ejemplo ;-))? Bueno, pues tendremos que sobreescribir las funciones de las que hablé antes para que nos devuelva lo que nosotros queramos. Yo en mi caso, modifiqué las tres. Aquí muestro mi outputTypeToString:
Lo único que modifiqué fue la tabla de nombres del principio, dándole los que yo quise. Si los nombres que vamos a dar a los parámetros de entrada, de salida y al tipo de retorno van a ser los mismos (es decir, no vamos a distinguir por ejemplo un entero de salida, un entero de entrada y un tipo de retorno entero, llamandolos por el mismo nombre de forma interna en los tres casos) podemos utilizar solo una de éstas tres funciones y llamarla en todos los casos. En caso contrario, habrá que utilizar una función distinta para cada caso y llamarla cuando sea necesaria.