¡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:
_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;
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
ClasAuxi, ClasObjeDest :TClass;
Begin
If Not BooleanCopiBuff (SonPuntVali ([ObjeOrig, ObjeDest]),
Result) Then
Exit;
ClasObjeDest := ObjeDest.ClassType;
ClasAuxi := TObject;
Repeat
DesceClas (ClasAuxi, ObjeOrig.ClassType);
If InforInicClas (ClasAuxi) <> Nil Then
_CopyRecord (ObjeDest, ObjeOrig, InforInicClas (ClasAuxi));
Until ClasAuxi = ObjeOrig.ClassType;
CopiaBuff (Pointer (ObjeOrig)^, Pointer (ObjeDest)^,
TamaoObje (ObjeOrig));
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
Clon := ObjetoOrigen.ClassType.Create;
Try
CopiaObje (ObjetoOrigen, Clon);
Except
Clon.Free;
Raise;
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.