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:

javieralso@avalon:~$ apt-get source zeroc-ice33

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:

#include <IceUtil/OutputUtil.h>
#include <IceUtil/Options.h>
#include <Slice/Parser.h>
#include <vector>

#include "OurParser.h"
#include "OurVisitor.h"

OurParser::OurParser(const string& path, const string& filename)
{
  vector<string> dummyArgs;
  _pp = new Preprocessor(path,filename,dummyArgs);
  FILE* cppHandle = _pp->preprocess(false);

  UnitPtr u = Unit::createUnit(false, true, true, true);
  int parseStatus = u->parse("", cppHandle, false);

  OurVisitor visitor;
  u->visit(&visitor, cppHandle);
}

OurParser::~OurParser()
{}

int
main(int argc, char* argv[])
{
  OurParser* sp = new OurParser("", argv[1]);
  return 0;
}

¿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:

  vector<string> dummyArgs;
  _pp = new Preprocessor(path,filename,dummyArgs);
  FILE* cppHandle = _pp->preprocess(false);

Después se instancia el parser propiamente dicho, y se parsea el Slice

  UnitPtr u = Unit::createUnit(false, true, true, true);
  int parseStatus = u->parse("", cppHandle, false);

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:

  OurVisitor visitor;
  u->visit(&visitor, cppHandle);

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:

#include <Slice/Parser.h>

using namespace Slice;

class OurVisitor: public ParserVisitor
{
public:
  OurVisitor(SymbolTable&);
  ~OurVisitor();

  virtual bool visitModuleStart(const ModulePtr&);
  virtual void visitModuleEnd(const ModulePtr&);
  virtual void visitClassDecl(const ClassDeclPtr&);
  virtual bool visitClassDefStart(const ClassDefPtr&);
  virtual void visitClassDefEnd(const ClassDefPtr&);
  virtual bool visitExceptionStart(const ExceptionPtr&);
  virtual void visitExceptionEnd(const ExceptionPtr&);
  virtual bool visitStructStart(const StructPtr&);
  virtual void visitStructEnd(const StructPtr&);
  virtual void visitOperation(const OperationPtr&);
  virtual void visitParamDecl(const ParamDeclPtr&);
  virtual void visitDataMember(const DataMemberPtr&);
  virtual void visitSequence(const SequencePtr&);
  virtual void visitDictionary(const DictionaryPtr&);
  virtual void visitEnum(const EnumPtr&);
  virtual void visitConst(const ConstPtr&);
}

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:

bool
OurVisitor::visitModuleStart(const ModulePtr& p)
{
   cout "Nombre del modulo: " << p->name() << endl;
   cout "Ambito de declaracion: " << p->scope() <<endl;
   return true;
}

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:

bool
OurVisitor::visitClassDefStart(const ClassDefPtr& p)
{
   cout "Nombre del interfaz: " << p->name() << endl;
   cout "Ambito de declaracion: " << p->scope() <<endl;
   return true;
}

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:

void
OurVisitor::visitOperation(const OperationPtr& p)
{

  size_t strIndex;
  string metadataName, metadataValue;
  StringList operationMetaData;

  cout "Nombre: " << p->name() << endl;
  cout "Ambito de declaracion: " << p->scope() << endl;
  TypePtr ret = p->returnType();
  string retS = returnTypeToString(ret, "", p->getMetaData());
  cout << "Tipo de retorno: " << retS << endl;

  if(p->mode() == Operation::Idempotent || p->mode() == Operation::Nonmutating){
    // Idempotent operation
    cout << "Funcion idempotente" << endl;
  }

  // Looking for metadata.
  operationMetaData = p->getMetaData();
  cout << "Metadatos:" << endl;
  for(StringList::iterator it = operationMetaData.begin(); it != operationMetaData.end(); it++) {
    cout << *it << endl;
    }
  }

  // Looking for parameters.
  ParamDeclList paramList = p->parameters();
  cout << "Parametros:" << endl;
  for(ParamDeclList::const_iterator q = paramList.begin();\
       q != paramList.end(); ++q){
    cout << "Nombre: " << (*q)->name();
    StringList metaData = (*q)->getMetaData();
    if (!(*q)->isOutParam()){
      cout << "Parametro de salida" << endl;
      typeString = inputTypeToString((*q)->type(), false, metaData);
      cout << "Tipo del  parametro: " << typeString << endl;
    }
}

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)

    string retS = returnTypeToString(ret, "", p->getMetaData());

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:

string
Slice::outputTypeToString(const TypePtr& type, bool useWstring, const StringList& metaData)
{
    static const char* outputBuiltinTable[] =
    {
        "Byte",
        "bool",
        "Short",
        "Int",
        "Long",
        "Float",
        "Double",
        "string",
        "ObjectPtr",
        "ObjectPrx",
        "LocalObjectPtr"
    };

    BuiltinPtr builtin = BuiltinPtr::dynamicCast(type);
    if(builtin)
    {
        if(builtin->kind() == Builtin::KindString)
        {
            string strType = findMetaData(metaData, true);
            if(strType != "string" && (useWstring || strType == "wstring"))
            {
                if(featureProfile == IceE)
                {
                    return "Wstring&";
                }
                else
                {
                    return "wstring&";
                }
            }
        }
        return outputBuiltinTable[builtin->kind()];
    }

    ClassDeclPtr cl = ClassDeclPtr::dynamicCast(type);
    if(cl)
    {
        return fixKwd(cl->scoped() + "Ptr");
    }

    StructPtr st = StructPtr::dynamicCast(type);
    if(st)
    {
        if(findMetaData(st->getMetaData(), false) == "class")
        {
            return fixKwd(st->scoped() + "Ptr");
        }
        return fixKwd(st->scoped());
    }

    ProxyPtr proxy = ProxyPtr::dynamicCast(type);
    if(proxy)
    {
        return fixKwd(proxy->_class()->scoped() + "Prx");
    }

    SequencePtr seq = SequencePtr::dynamicCast(type);
    if(seq)
    {
        string seqType = findMetaData(metaData, false);
        if(!seqType.empty())
        {
	  return seqType;
        }
        else
        {
	  return fixKwd(seq->scoped());
        }
    }
    ContainedPtr contained = ContainedPtr::dynamicCast(type);
    if(contained)
    {
      return fixKwd(contained->scoped());
    }

    return "???";
}

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.



blog comments powered by Disqus