Ver Mensaje Individual
  #9  
Antiguo 14-08-2006
Avatar de Al González
[Al González] Al González is offline
In .pas since 1991
 
Registrado: may 2003
Posts: 5.604
Reputación: 29
Al González Es un diamante en brutoAl González Es un diamante en brutoAl González Es un diamante en brutoAl González Es un diamante en bruto
Smile Con _CopyRecord, pero...

¡Hola a todos!

He leído con interés el tema sin poder evitar remontarme a marzo de 2000, cuando buscaba algo similar a lo planteado por xEsk. En aquel entonces, indagando en la unidad System.pas de Delphi 3 o 4 (no recuerdo la versión exacta), encontré un par de funciones que llamaron mucho mi atención: _CopyRecord y _CopyObject (si, la “famosa” _CopyObject, el mayor misterio de la unidad System). Bien, resulta que sí es posible hacer una clonación superficial (shallow) o profunda (deep) —con mucho más esfuerzo— de un objeto en Delphi Win32, sin importar su clase, sin tener que redefinir la implementación de métodos como Assign o AssignTo. Pero el precio es, digamos, bastante incómodo.

Habiendo aprendido que las funciones de la bien llamada magia del compilador, cuyos nombres inician con un guión bajo (“_”), y que generalmente fueron escritas en lenguaje ensamblador, son las rutinas que el compilador encadena como resultado de ciertas sentencias de código fuente, me pregunté «¿dónde utiliza el compilador la función _CopyRecord?». Encontrar la respuesta no fue difícil. Cada vez que el compilador encuentra una sentencia como «Registro1 := Registro2;», siendo Registro1 y Registro2 dos variables de un mismo tipo record (llamado struct en otros lenguajes), el código máquina resultante es una llamada a la función _CopyRecord, la cual recibe como parámetros el registro destino, el registro origen y la información estructural del tipo de dato:

Cita:
Empezado por Borland
Código Delphi [-]
procedure       _CopyRecord{ dest, source, typeInfo: Pointer };
_CopyRecord utiliza la información estructural dada por el tercer parámetro para copiar adecuadamente los campos de la estructura origen a la estructura destino. El proceso sería relativamente simple si no existieran los jóvenes tipos de datos que requieren un tratamiento especial (cadenas largas, variantes, arreglos dinámicos e interfaces). _CopyRecord copia el valor de todos los bytes del bloque de memoria origen al bloque destino, pero también realiza llamadas a otras funciones mágicas como _LStrAsg, _WStrAsg, _VarCopy, _IntfCopy, y _DynArrayAsg (me permito un comentario al estilo de Ian Marteens: ¿acaso pretenden con esos nombres ahorrarnos memoria en el disco duro? ) para copiar adecuadamente los campos que llevan contadores de referencia o que, por su naturaleza, necesitan un manejo especial. Gracias a esta función, es relativamente fácil para el programador clonar superficialmente una estructura Record, una simple sentencia de asignación basta.

Entonces, la siguiente pregunta que me hice fue «¿dónde utiliza el compilador la función _CopyObject?»...Llevo cinco años preguntándome lo mismo. La intriga resulta de analizar tres cosas:

1. En el código fuente de todas las unidades nativas de Delphi no hay una sola referencia a la función _CopyObject.
2. Aparentemente, no hay ninguna sentencia Object Pascal que se pueda considerar traducible como una llamada a esta función («Objeto1 := Objeto2;» no vale, es una simple copia de puntero; «Objeto1^ := Objeto2^;» tampoco, ya que el compilador no permite esa sintaxis con variables objeto).
3. Curiosamente, _CopyObject llama a la función _CopyRecord, pero sólo una vez. Lo cual significa que sólo clona los campos del objeto que hayan sido directamente declarados por una clase. Para clonarlo por completo, habría que llamar a _CopyRecord por cada una de las clases involucradas en la jerarquía del objeto a copiar.

Menciono lo anterior en base a las pruebas que realicé en aquel entonces. Ante esto, veo tres posibles respuestas a la intriga:

1. Que la función _CopyObject es utilizada para asignaciones de variables objeto object (clases estilo Turbo Pascal), no class.
2. Que la función _CopyObject es utilizada desde afuera, por algún lenguaje compatible con Delphi, como C++ Builder.
3. Que la función _CopyObject sea una asignatura pendiente, algo dejado a medias en la unidad System.pas.

Habría que hacer algunas pruebas e indagaciones adicionales para develar el misterio.

Admito que no sé mucho de lenguaje ensamblador, por lo que expongo aquí una copia de la función _CopyObject de Delphi 7 (que me parece no ha variado desde entonces). Si alguno de los lectores es ducho en el tema y tiene la gentileza de explicarnos cómo entiende este código fuente, más de un programador “paranoico” le estaremos agradecidos.

Código Delphi [-]
 
procedure       _CopyObject{ dest, source: Pointer;
  vmtPtrOffs: Longint; typeInfo: Pointer };
asm
        { ->    EAX pointer to dest             }
        {       EDX pointer to source           }
        {       ECX offset of vmt in object     }
        {       [ESP+4] pointer to typeInfo     }

        ADD     ECX,EAX                         { pointer to dest vmt }
        PUSH    dword ptr [ECX]                 { save dest vmt }
        PUSH    ECX
        MOV     ECX,[ESP+4+4+4]
        CALL    _CopyRecord
        POP     ECX
        POP     dword ptr [ECX]                 { restore dest vmt }
        RET     4

end;

(el código de _CopyRecord y otras funciones mágicas puede ser visto en el archivo System.pas de muchas versiones de Delphi).

¿Logré clonar objetos en aquella aventura? Si. ¿Cómo lo hice? Basándome en el código de la rara función _CopyObject, creando una nueva función que encapsulara y llamara iterativamente a la función _CopyRecord por cada clase existente en la jerarquía del objeto a clonar.

Aquí el código de mi función:

Código Delphi [-]
  Function CopiaObje (Const ObjeOrig, ObjeDest :TObject) :Boolean;
  Var
    { Clase Auxiliar y Clase del Objeto Destino }
    ClasAuxi, ClasObjeDest :TClass;
  Begin
    { Si alguno de los dos objetos dados no es válido }
    If Not BooleanCopiBuff (SonPuntVali ([ObjeOrig, ObjeDest]),
                            Result) Then
      Exit;

    { Guardar la Clase de ObjeDest.  NOTA: Los objetos dados pueden ser
      de diferente clase, pero deben tener estructuras equivalentes.    }
    ClasObjeDest := ObjeDest.ClassType;


    { Realizar copias que incrementen y decrementen, los contadores de
      referencia de los campos de tipo cadena, variante e interfaz     }

    ClasAuxi := TObject;

    Repeat
      { Asignarle a ClasAuxi la siguiente clase hija que Desciende
        hacia la clase del Objeto Origen                           }
      DesceClas (ClasAuxi, ObjeOrig.ClassType);

      { Si la clase actual cuenta con Información de Inicialización }
      If InforInicClas (ClasAuxi) <> Nil Then
        { Usarla para el copiado incrementador/decrementador de
          contadores de referencia de campos                    }
        _CopyRecord (ObjeDest, ObjeOrig, InforInicClas (ClasAuxi));
    Until ClasAuxi = ObjeOrig.ClassType;


    {******************************************************************}
    { Asegurar que sea Copiado todo el bloque de memoria de la         }
    { instancia Origen, ya que _CopyRecord pudo no haberse ejecutado o }
    { pudo haber dejado bytes finales sin copiar.  NOTA: La causa de   }
    { que _CopyRecord deje bytes finales sin copiar, es que la         }
    { estructura devuelta por InforInicClas, no es exactamente igual a }
    { la estructura devuelta por la función TypeInfo, cuando es        }
    { aplicada a un tipo registro.                                     }
    {******************************************************************}
    CopiaBuff (Pointer (ObjeOrig)^, Pointer (ObjeDest)^,
               TamaoObje (ObjeOrig));

    { Restaurar la Clase del Objeto Destino }
    PClass (ObjeDest)^ := ClasObjeDest;
  End;

Varias de las funciones de biblioteca referenciadas pueden ser sustituidas sin muchos problemas por llamadas a funciones nativas (pido disculpas por el robótico estilo que empleaba hasta hace pocos años ), y así obtener una función clonadora de objetos.

Pero quien ya se haya puesto a trabajar en esto se topará con una interrogante final: «¿Cómo hago para llamar a la función _CopyRecord?». Ahí estuvo el principal obstáculo. _CopyRecord es una función interna de System.pas, no puede ser referenciada explícitamente en el código fuente de otra unidad. Lo primero que se me ocurrió fue crear una nueva unidad y poner ahí una copia de la función _CopyRecord, pero de inmediato descarté ese camino, ya que _CopyRecord llama directa e indirectamente a un buen número de otras funciones internas. Así que la solución más práctica que encontré fue clonar (hablando de) la unidad System.pas, creando mi propia "System2.pas” con una función _CopyRecord perfectamente declarada para poder ser utilizada desde afuera.

Código Delphi [-]
Unit System2;

Interface

  Procedure _CopyRecord (Dest, Source, TypeInfo :Pointer);

Para crear un objeto clon, un ejemplo podría ser:

Código Delphi [-]
Begin
  { Crear una instancia en blanco de la misma clase que ObjetoOrigen }
  Clon := ObjetoOrigen.ClassType.Create;

  Try
    { Habiendo creado un nuevo objeto en blanco, copiarle los campos de
      la instancia a la que apunta ObjetoOrigen }
    CopiaObje (ObjetoOrigen, Clon);
  Except  { Si se eleva una excepción }
    Clon.Free;  { Liberar la instancia creada }
    Raise;  { Reelevar la excepción }
  End;

De esta forma sí es viable clonar objetos en Delphi no .NET, y, como se puede ver, el precio resulta incómodo. Desaconsejo esta chapucera práctica de robarle el código privado a System.pas porque uno nunca sabe cuándo Borland decidirá hacer un cambio que teóricamente no debería afectar al programador. Además, se presentan muchos problemas cuando se trata de clonar objetos que tienen reglas de negocio relacionadas con la asignación de valores a sus propiedades (la clonación las pasa por alto), cuando los objetos guardan referencias a otros objetos, y, peor aún, cuando esas referencias son simples identificadores (handles) a objetos de alguna API (como es el caso de la mayoría de los componentes visuales). La clonación puede resultar desde inestable hasta caótica.

Quizá por eso Borland decidió suspender o reservarse la función _CopyObject. O quizá pensaron «Debemos estudiar mejor este asunto, tal vez en el futuro nos pongamos de acuerdo los fabricantes para establecer un estándar de clonación controlada. No le pongamos clonación a Delphi todavía, esperemos mejores tiempos, veamos primero cómo les resulta a otros...».

Java se aventuró a incluir esta característica y al parecer Microsoft, tarde, aunque nada perezoso, reinventó el hilo negro llamándolo interfaz ICloneable de .NET.

Sólo me resta decir que sugiero manejar con reserva primitivos métodos de clonación como el que expuse. Personalmente, la experiencia vivida me resultó más un ejercicio de aprendizaje que una solución a problemas importantes. Tal vez xEsk debería hablarnos de cuál es el objetivo concreto que persigue para proponerle mejores alternativas.

Ah, y aún tengo la esperanza de que alguien me explique la función _CopyObject.

Un abrazo clonado.

Al González.

Última edición por Al González fecha: 14-08-2006 a las 00:59:13.
Responder Con Cita