TVersion

Saludos

Siempre hay un comienzo, encuentro interesante trabajar con esta unidad para levantar de versiones porque da la oportunidad de explorar nuevos elementos de Object Pascal de Delphi. Es particularmente interesante para revisar las extensiones a la definición de Record.

El código mostrado aquí esta en su mayor parte en el archivo VersionUnit.pas el cual define TVersion.

Record

En pascal estándar los record se formulan con un bloque de memoria continuo con un conjunto de campos con los que accedemos a esa memoria según el tipo de cada campo. Esto por ejemplo permite un record de tipo número complejo que nos permita una parte real y una imaginaria, un registro para un vector con un angulo y una magnitud, o una ficha de empleados de nómina. Esto es conocimiento común del programador.

type
// Complejo con parte r real e
// i inmaginaria
TComplex = record
    r, i: Extended;
end;
type
// Vector, puede ser x,y o
// de forma polar magnitud y ángulo
TVector = record
     magnitud, angulo: double;
end;

Lo interesante es que en Delphi se agregaron posibilidades como las de asociar funciones, constructores o acceso a operadores. Más adelante vemos estas características.

Las versiones de los archivos en Ms-windows

Si en windows (con el file manager) busca un archivo tipo dll, scr, exe o algunos otros podrá con clic derecho ver las propiedades.

Un versión como por ejemplo 5.5.0.0, está compuesto por 4 números separados por “.”. Siendo el primero el número de versión mayor, luego el número de versión menor, luego el release (liberación, esto es básicamente cuando se entrega el programa a los usuarios) y por último build (que en el caso de Delphi es es el número de compilación completa del código fuente).

• vMayor 
• vMenor 
• Release 
• Build

NOTA: Normalmente los cambios en el primer número son cambios en la experiencia del usuario, nueva interfaz, nueva base de datos o nuevas características. Los cambios en versión menor por otro lado no implican un cambio del sistema ya conocido por el usuario, sino algún cambio de interfaz con una nueva característica pero manteniendo lo esencial igual. El release con las veces que el programa sale de manos del programador y Build se refiere al proceso de una compilación completa. Al final es algo que depende del criterio de cada desarrollador.

El API de MS-Windows

Aquí requerimos de “GetFileVersionInfoSize” (winApi). Llamar esta rutina supone primero preparar uno bloque de memoria y luego llamar la misma para rellenar el bloque con distintos valores, entre ellos los que deseamos referentes a la versión.

Así obtendremos, por separado cuatro enteros, para manejarlos juntos se suelen registrar como una cadena, por ello considero dos formatos:
… como ejemplo 2.3.12.32
. (B.) como ejemplo 2.3 (B 12.32)

La llamada a “GetFileVersionInfoSize” supone señalar un archivo que será abierto y analizado en busca de la información de versión.
Esta función regresa varios parámetros enteros con los valores decodificados de versión, y una cadena de caracteres que puede resultar conveniente.

Function GetFileVersion(fn: string;
                      var aver: string;
                      var vMayor, vMinor, Release, Build: word): boolean; overload;

var
  newsize : integer;
  buffer : pointer;
  pointertopointer : pointer;
  Filename : Array[0..127] of Char;
  len : Cardinal;
  tozero : Cardinal;
  v1,v2,v3,v4 : word;

begin
  result := false;
  StrPCopy(FileName, fn);
  newsize := GetFileVersionInfoSize(Filename,tozero);
  aver := '';
  if newsize>0 then
   begin
    GetMem(buffer, newsize);
    try
      GetFileVersionInfo(Filename, 0, newsize, buffer);

      VerQueryValue(buffer,'\', pointertopointer, len);

      v1 := TVSFixedFileInfo(pointertopointer^).dwFileVersionMS div $ffff;
      v2 := TVSFixedFileInfo(pointertopointer^).dwFileVersionMS and $FFFF;

      v3 := TVSFixedFileInfo(pointertopointer^).dwFileVersionLS div $ffff;
      v4 := TVSFixedFileInfo(pointertopointer^).dwFileVersionLS and $FFFF;

      aver := Format('%d.%d (B %d.%d)',[v1,v2,v3,v4]);
      vMayor := v1; vMinor:= v2; Release:= v3; Build:=v4;
      result := true;
    finally
      FreeMem(buffer);
    end
   end;
end;


Desde los recursos del programa

En stackoverflow Jiri Krivanek planteó que no siempre se puede llamar “GetFileVersionInfoSize” (ejecutar un archivo no implica que se pueda leer). Una alternativa es leer desde el propio programa los recursos definidos y montados por Delphi. Esta rutina es realmente una opción interesante pero solo sirve para leer la versión del propio archivo ejecutable, no ningún otro. Lo interesante es que siempre tenemos acceso a los recursos propios, así que no debe haber problemas en leerlos.
El código original está disponible en el sitio, aquí el código ha sido reducido porque el original brindaba alguna información extra que no es de interés (nombre de la aplicación por ejemplo).

Function GetAppVersionString: String;
var
  AppVersionString: string;
  verblock:PVSFIXEDFILEINFO;
  versionMS,versionLS:cardinal;
  verlen:cardinal;
  rs:TResourceStream;
  m:TMemoryStream;
begin
  m:=TMemoryStream.Create;
  try
    rs:=TResourceStream.CreateFromID(HInstance,1,RT_VERSION);
    try
      m.CopyFrom(rs,rs.Size);
    finally
      rs.Free;
    end;
    m.Position:=0;
    if VerQueryValue(m.Memory,'\',pointer(verblock),verlen) then
      begin
        VersionMS:=verblock.dwFileVersionMS;
        VersionLS:=verblock.dwFileVersionLS;
        AppVersionString:=
          IntToStr(versionMS shr 16)+'.'+
          IntToStr(versionMS and $FFFF)+'.'+
          IntToStr(VersionLS shr 16)+'.'+
          IntToStr(VersionLS and $FFFF);
      end;
  finally
    result := AppVersionString;
    m.Free;
  end;
end;


Estructura para versión Record

Lo mínimo en todo caso son los cuatro números de versión, esto es:
vMayor, vMinor, Release, Build todos tipo word.

La alternativa sería usar un string, pero queremos tener cierto control aritmético que nos permita comparar si una versión es mayor que otra, con los string esto es algo truculento. Ejemplo
‘1.2.3.4’ > ‘1.2.0.4’ cierto
‘1.2.3.4’ > ‘1.2.10.4’ cierto si hablamos de un string, pero este no es el caso

Ya queda decidido que no será string la estructura. TVersion en primera instancia queda como sigue:

TVersion = record
  vMayor, vMinor, Release, Build: word;
end;


Record, class operator

Las class operator son algo nuevo en Delphi y nos permite asociar el uso de operadores. Podemos definir por este medio como funciona el ‘+’, ‘-’, ‘>’, ‘<=’ y otros con el tipo record que creamos. Como queremos comparar se ha creado GreaterThan para >. LessThan para < además igual (Equal) y diferente (NotEqual).

TVersion = record
public
  vMayor, vMinor, Release, Build: word;
  class operator LessThan( a, b: TVersion): Boolean;
  class operator GreaterThan ( b, a: TVersion): Boolean;

  class operator Equal ( a, b: TVersion): Boolean;
  class operator NotEqual ( a, b: TVersion): Boolean;

  class operator Implicit (OrigVer: string): TVersion;
  class operator Implicit (a: TVersion): string;

end;


Este sería el código que implemente Equal (=)

class operator TVersion.Equal(a, b: TVersion): Boolean;
begin
  result := (a.vMayor = b.vMayor) and
            (a.vMinor = b.vMinor) and
            (a.Release = b.Release) and
            (a.Build   = b.Build);
end;


Aquí caben varias, preguntas, la primera es por qué el código no es result := a = b. La razón primaria es que estamos definiendo precisamente como es la operación a= b, por lo que esto sería teóricamente recursivo. En ese caso quizás lo mejor sea no escribir método Equal, pero no es tan cierto que en records a sea igual que b aunque tenga los mismos valores . De hecho si no definimos el método Equal el compilador protestará un instrucción similar a=b siendo a y b de un mismo tipo record. En ese caso mostrará un mensaje como «[dcc32 Error] xxxxx: E2015 Operator not applicable to this operand type«

Realmente luego de escribir esa rutina el siguiente pensamiento es: “debe haber una mejor forma”. Una forma de comparar más interesante es si se organiza la información dentro de un número grande. Estamos usando 4 word (16 bits). Así que deberíamos poder meterlo en una estructura de 64 bits. Dado que no nos interesa el signo lo adecuando es uint64.
Una forma es usar operadores shl (shift left) en pasos de 16 bits.

Esta sería una forma de meter segmentos de 16 bits en un espacio de 64.

x_uint64 := (a.vMayor shl 48) or 
            (a.vMinor shl 32) or 
            (a.Release shl 16) or 
            (a.Build);


El problema es que no funciona. Shl solo trabaja sobre el tipo integer. El tipo integer en algunos casos tiene 32 bits y en otros 64 (depende del sistema operativo).

Un entero grande son 4 pequeños

En lenguaje “C” existe una estructura de tipo union que permite superponer espacios de memorias de distintos tipos, Podemos superponer un byte y un carácter, y lo que se lea como byte 64 sería como carácter “@”. El equivalente en Delphi existe como “Variant parts” dentro de los records. Tiene una sintaxis algo curiosa. Primero la parte variante, que comienza con case, va al final de la estructura y segundo no hay un end lo cuál al menos para mi es incómodo dado que el case de flujo del programa si tiene un end (y curiosamente es un end sin begin). Esto es sólo un segmento de la declaración (más abajo se muestra completo).

TVersion = packed record
public

//  tenemos otro código que por ahora omitimos
//  la parte variante debe estar al final

  case Boolean of
    true: (Build, Release, vMinor, vMayor : word);
    false:(ver64 : UInt64);
end;


Esto implica que una variable de tipo Tversion cuenta con un campo Build, vMinor y otros como veníamos usando, pero ademas con ver64 de tipo Uint64 que se superpone a los 4 word señalados (Nótese que además lleva un orden inverso).
Ahora la comparación es más simple. Ya no tenemos que comparar vMayor, luego vMenor, Release y Build. Todo se resumen a una línea.

class operator TVersion.LessThan(a, b: TVersion): Boolean;
begin
  result := a.version < b.version;
end;


Se ha reducido tanto que da algo de vergüenza escribir una función tan simple, pero es la ironía de la programación, lo más simple ha resultado lo más difícil.

NOTA: El compilador tiende a organizar las cosas de forma más cómoda y eficiente posible. En un cpu de 64 bits, lo mas cómo puede ser manejar enteros de 64 bits, siendo es el caso si definimos 3 campos word, el compilador pudiese perfectamente alinearlos en espacio de 64 bits aunque solo requiera 16 simplemente porque la instrucciones de cpu son más eficientes, así que el record ocuparía 64×3 y no 16×3. Packed simplemente le señala al compilador que debe respetar el uso de espacio como lo definimos. En este ejemplo 16×3 y la estructura ocuparía 48 bytes en ram

Asignar enteros y leer cadenas

Al final hemos colocado dos implicit que permiten al record aceptar asignaciones como cadena de caracteres, las cuales son convertidas convenientemente y en el segundo caso permite la emisión de un string pero como parte de una expresión

Var
  V1, V2, Vloc : TVersion;
begin
  V1 := '2.0.3.0';
  V2 := '1.9.3.0';
  if V1 < V2 then
    Memo1.Lines.Add(V1 + ' < ' + V2)
    :


Implicit, caso interesante decodificar un string

Convertir una cadena en los cuatro campos de versión en el código original de esta rutina tenía unas 40 lineas donde se hacía un recorrido del string leyendo puntos como separadores. Para esta versión hay dos aproximaciones más simples de programar. La primera es crear un TStringlist, definir delimitador el “.” y extraer los n ítems uno a uno.
Sería algo como esto (aunque falta ver que hacer con caracteres extraños y casos de borde).

class operator TVersion.Implicit(OrigVer: string): TVersion;
var
  i : integer;
  StrLst : TStringList;
begin
  StrLst := TStringList.Create;
  StrLst.Delimiter := '.';
  StrLst.DelimitedText := OrigVer;

  result.vMayor :=  StrToInt(StrLst[0]);
  result.vMinor :=  StrToInt(StrLst[1]);
  result.Release :=  StrToInt(StrLst[2]);
  result.Build :=  StrToInt(StrLst[3]);
  StrLst.Free
end;


El uso del string helper split es más interesante aunque similar.
Split recibe como parámetros un separador que utiliza para dividir el string en segmentos que son dados a un arreglo.

class operator TVersion.Implicit(OrigVer: string): TVersion;
var
  i : integer;
  Splitted: TArray<String>;
begin
  Splitted := OrigVer.Split(['.'],4);

  result.vMayor  :=  StrToInt(Splitted[0]);
  result.vMinor  :=  StrToInt(Splitted[1]);
  result.Release :=  StrToInt(Splitted[2]);
  result.Build   :=  StrToInt(Splitted[3]);
end;


El código anterior esta en su mayor parte en el archivo VersionUnit.pas el cual define TVersion.

Helper

Hemos definido un par de funciones para TVersion, la primera LessThan y por simetría GreaterThan.

Esta expresión

V1.GreaterThan(1,2,3,0)

es equivalente a

V1> ‘1.2.3.0’

El código de la funciones GreaterThan y su contraparte originalmente se basaba en comparar los campos vMayor, vMinor, Release, Build uno a uno. Creamos un constructor para el record. Ciertamente, el record se parece a una clase teniendo contructor y helper, tiene mucho más que estos elementos en común con las clases, pero no es una clase. Ya con el constructor se hace posible crear un record y acceder a sus miembros sin que tengamos una variable identificada.

LessThan ahora se puede escribir así:

function TVersion.LessThan(avMayor, avMinor, aRelease, aBuild: word): boolean;
begin
  result := self.ver64  < TVersion.Create(avMayor, avMinor, aRelease, aBuild).ver64
end;


En la nueva rutina se ha creado un espacio de memoria con los parámetros organizado en la estructura TVersion, esto en la práctica es una variable sin nombre con la que podemos operar. En este caso la comparamos con el contenido record en el que efectivamente nos encontramos.

NOTA: Mi experiencia con los Helper viene de Python, y son un aditamento relativamente reciente en Delphi. Básicamente es tener funciones asociadas a una variable. El uso o ejemplo por excelencia es ToString, si se tiene una variable integer llamada X se tiene X.ToString, si X por otro lado es double, también se tiene X.ToString (que sabemos que está implementado de otra forma), no hay mucho que pensar, en nuestro caso X es Tversion, podemos usar X.LessThan (a propósito crear ToString para este TVersion es relativamente sencillo).

Resumen

En estos momentos tenemos un record de nombre Tversion con un conjunto de campos superpuestos (ver64 superpuesto a vMayor, vMenor, Release y Build). Además contamos con un conjunto de operadores de comparación que nos permite <, >, = y otros que hemos agregado para tener el juego completo. Y por último nuestra estructura acepta que le asignemos un string o lo expresemos como uno según requiramos.
Además un constructor que recibe los cuatro valores de la versión.

Ya con esto cerramos por hoy. TVersion es una unidad mayormente creada con un propósito docente. Yo la utilizo en producción, pero suelo usar con mayor frecuencia TRzVersionInfo de los controles Raize dado que en la mayoría de los casos sólo requiero desplegar información, no la aritmética de versiones, pero en esos casos donde requiero saber si una versión es mayor que otra TVersion es muy cómoda.