lunes, 19 de noviembre de 2007

Llamada dinámica a métodos

No es dinámico como en "dinámico de Bajo nivel" sino llamar métodos por su nombre. La Run-Time Type Information (RTTI) es la forma en que Delphi se vuelve mucho mas poderoso al combinarse con herencia y polimorfismo, sin embargo es el punto ahora, el problema es distinto.

Digamos que queremos hacer un pequeño motor de script (básico para empezar). Podemos guardar las instrucciones del script en un archivo de texto, en un campo de una tabla, etc. Nuestro programa lee esas instrucciones y las va ejecutando una por una, haciendo diversas cosas. ¿Dinámico, no?

Bueno es fácil con Delphi. Los métodos tienen una dirección de memoria donde esta el código (las instrucciones en lenguaje máquina) y una dirección de memoria donde esta los datos (digamos el ambiente del método), en este caso son los datos de la clase a la que el método pertenece.

Ahora, ¿Como guardamos una dirección de memoria en Delphi? Un puntero es un tipo de variable que guarda una dirección de memoria, en dicha dirección puede haber cualquier cosa: una cadena, un numero, un arreglo, o... un método. Como toda clase deriva de la Clase TObject, y dicha clase TObject tiene un método llamado MethodAddress con el que obtenemos el puntero hacia un método que sepamos su nombre, entonces si tenemos una clase llamada MiClase que tiene un método llamado Alerta, obtenemos la dirección de memoria donde esta el código que ejecuta ese método de la siguiente manera:

MiClase.MethodAddress("Alerta");


Para representar un método, Delphi tiene una estructura llamada TMethod. Dicha estructura tiene dos componentes: Code y Data (Código y Datos como lo mencionamos antes). En Code va la dirección de memoria de el método, y en Data guardamos la dirección de memoria de la instancia de la clase (es decir, el objeto) a la cual pertenece el método.

Entonces para llamar ejecutar nuestro Script básico, necesitamos una clase que contenga métodos con el nombre de las instrucciones de nuestro script, y después crear una instancia de esa clase (crear un objeto de esa clase), leer nuestro script y por cada linea del script (una instrucción) crear un TMethod obteniendo el puntero hacia esa función. Lo codifíco aquí en un solo procedimiento por facilidad:

// EjecutaScript con el parámetro del nombre
// del archivo "script"
procedure TMiClase.EjecutaScript(ArchScript: string);
var
p:
procedure of Object; // Un puntero a un metodo
Script: TStringList; // Aqui cargaremos el script
instrucc: string; // para iterar por
// las instrucciones
begin
if FileExists(ArchScript) then
begin
Script :
= TStringList.Create();
try
// Si existe el archivo del script
// Cargamos el script a una lista de strings
Script.LoadFromFile(NombreArchivoScript);
// Por cada instrucción (una linea
// en el archivo):
for instrucc in Script do
begin
// Si la linea no es vacía
if trim(instrucc) <> '' then
begin
// Obtenemos la dirección del método y la
// asignamos al elemento Code de
// p como TMethod
TMethod(p).Code := Self.MethodAddress(instrucc);
// Asignamos el objeto mismo
// como Data del TMethod
TMethod(p).Data := Self;
// Si resulto asignado p (no apunta a nil)
if Assigned(p) then
begin
p();
// Ejecutamos p!!
end;
end;
end;
finally
FreeAndNil(Script);
// liberamos el TStringList
// Como debe de ser :P
end;
end;
end;

Ejemplo: Si tenemos un método llamado Alerta, un BorraCache y un Procesa en la clase TMiClase:

// Actualizado 20/nov/2007 - ver nota al pie
TMiClase = class
...
public
procedure EjecutaScript(ArchScript:
string);
...
published
procedure Alerta;
procedure Procesa;
procedure BorraCache;
...
end;


y nuestro script consiste en un archivo llamado por ejemplo 'haznuevo.txt' que contiene:


alerta
borracache
procesa


al invocar:
var
MiObjeto: TMiClase;
begin
MiObjeto :
= TMiClase.Create;
try
MiObjeto.EjecutaScript(
'haznuevo.txt');
finally
FreeAndNil(MiObjeto);
end;
end;

Entonces se ejecutaran los métodos Alerta, BorraCache y Procesa del Objeto MiObjeto (de clase MiClase :)

También hay forma de pasar parámetros, lo mostrare en un post mas adelante.


Actualización 20/nov/2007: Como bien lo señala Al Gonzalez (¡gracias Al!), el método MethodAddress solo funciona para obtener la dirección de métodos declarados como published!! por lo que declararlos en la sección private no funciona.

5 comentarios:

  1. ¡Hola Salvador!

    Antes que nada, déjame puntualizar que, por lo menos hasta la versión 7, sólo es posible usar el método MethodAddress para obtener la dirección de memoria de métodos publicados (sección "Published") y no así privados, protegidos o públicos.

    Noté que el código que escribiste pertenece a una versión superior (quizá 2006 o 2007), por lo que quizá ahora ya sea posible, cuando menos en .NET, aunque tengo mis dudas.

    MethodAddress fue creado principalmente como elemento auxiliar del mecanismo de carga de DFMs ("streaming"). Ya que, al guardarse en archivos .dfm los nombres de los manejadores de eventos (que son métodos de una clase) y no sus direcciones de memoria (por que esto es imposible de conocer en tiempo de diseño), es necesario entonces que en tiempo de ejecución se obtengan las direcciones de memoria de dichos métodos a partir de su nombre.

    No obstante, podrían resultar útiles, en algunos casos, implementaciones de meta lenguaje como la de tu ejemplo.

    Saludos.

    Al González.

    ResponderBorrar
  2. Gracias Al! Es cierto, para que los métodos sean incluidos en la RTTI deben ser declarados como published.

    ¡Saludos!

    ResponderBorrar
  3. Muy buena la informacion y muy bueno tu codigo tambien ;)

    ResponderBorrar
  4. Hola Salvador:

    Es muy interesante este código, solo una pregunta... como es que puedes pasar parámetros a métodos con este método?.

    Gracias

    ResponderBorrar
  5. K:
    Una manera de pasarle parametros es que tus metodos declaren un solo parametro de tipo
    array of TVarRec
    que es un arreglo dinamico de valores (enteros, cadenas, chars, flotantes)
    por ejemplo:

    procedure Operacion(params: array of TVarRec);

    De esa manera puedes pasar cualquier cantidad de parametros de cualquier tipo primitivo, y por su puesto, dentro del procedimiento debes asumir que el elemento 1 es integer, el elemento 2 es cadena, etc. Esto es como si fuera dinamico y te pueden pasar cualquier cosa.

    Eso es solo una idea de como se podrian pasar parametros de forma dinamica segun el interprete de las instrucciones vaya obteniendo. Hay alternativas pero creo que esa es una muy flexible.

    ResponderBorrar