jueves, 21 de noviembre de 2013

Paso de parámetros AnsiString y UnicodeString en DLLs

Una de mis formas preferidas de implementar funcionalidades nuevas en aplicaciones legadas (Versiones de Delphi < 2009) es el uso de los DLLs,  aunque en general prefiero no usar los Dlls para evitar problemas de versiones, es la única manera de hacerlo en algunos casos.
El paso de parámetros no es una tarea sencilla entre la aplicación y el DLL, de hecho es el mayor foco de errores después del manejo de las multitareas entre los desarrolladores principiantes en este tema.
Hoy hablaremos de la forma de enviar parámetros de tipo String entre la aplicación y una DLL, pero agregaremos un nivel mayor de complejidad al realizarlo también entre versiones diferentes de Delphi que ya han redefinido el String por una versión Multibyte  denominada UnicodeString.
Primero haremos una breve descripción de los tipos de String para que puedan entender la complejidad que esto representa aunque no ahondaremos en el tema, dado que no es el eje del presente artículo.
A partir de Delphi 2009 se realizó un cambio importante en el lenguaje, que a la fecha es la razón principal por la cual muchos desarrolladores no han migrado sus aplicaciones a las versiones recientes de Delphi.  La definición del String como variable pasó de ser un AnsiString a un UnicodeString.
En UnicodeString  cada carácter está representado por dos bytes, mientras que en un AnsiString cada carácter está representado por un byte. Veamos un ejemplo con el siguiente código

 Var
   St : String;
Begin
  St := 'Hola';

 
En Versiones anteriores a Delphi 2009 la representación en memoria de la variable St ocupa 5 bytes, uno por cada letra y un cero al final de la siguiente forma :

H
o
l
a
0

En las versiones recientes de Delphi >= 2009 físicamente ocupa 10 bytes y se representa así

$ 0
H
$ 0
o
$ 0
l
$ 0
a
$ 0
$ 0
Ch1
Ch2
Ch3
Ch4
Fin

  
No es un aspecto para preocuparse, dado que Delphi en versiones recientes maneja automáticamente las conversiones entre Ansi y unicode cuando se requiere, y en cualquiera de los casos la función Length(St) retornará el mismo valor que corresponden a la cantidad de caracteres y no a los bytes necesarios para su almacenamiento, es decir esto solo afecta el almacenamiento y las funciones que acceden al String en forma de bytes, pero no las funciones de alto nivel de tratamiento de Strings.

Así que mientras que se trabaje dentro del entorno Delphi no hay ningún inconveniente en el manejo y asignación de los diferentes tipos de String,  pero en el momento que se requiere hacer interfaces con otros programas como los Componentes COM de Microsoft o los DLL si es importante conocer las estructuras internas de las variables que se pasan como parámetros.
Para mayor información sobre los Unicode String pueden acceder al siguiente link del maestro Marco Cantú http://edn.embarcadero.com/article/38980

 

Paso de parámetros String a un DLL

Es fácil pasar parámetros simples desde una aplicación hacia una DLL, dado que su estructura es simple y pueden pasarse en los registros del procesador, es decir que "caben" en las variables del procesador y en ninguna forma tienen que apuntar a posiciones de memoria adicionales.
Sin embargo para el paso de parámetros más complejos que requieren posiciones de memoria adicionales, como el caso de los String que pueden medir varios miles de bytes, es necesario utilizar métodos adicionales de paso de dicha información.
La solución más simple es el uso de un administrador de memoria, el cual se encarga del manejo de esta situación, Delphi trae por defecto una librería denominada BORLNDMM.DLL y se incluye tanto dentro de la aplicación como del DLL una unidad denominada ShareMem, también existen otras librerías para el mismo fin en el mercado que pueden hacer mejor esta misma tarea. 
Debe tener en cuenta que si incluye la unidad ShareMem en los uses del DLL será obligatorio distribuir junto con la aplicación la librería BORLNDMM.DLL.
En caso de no utilizar estas librerías es necesario tener algunos cuidados, en primer lugar no es posible pasar el parámetros String, excepto los ShortString como parámetros, así que todos los Strings se pasarán como array de bytes o como PChar.

library Project23;

uses
  System.SysUtils,  System.Classes,  Vcl.Dialogs;

{$R *.res} 
   Procedure MuestreMensaje(Const Valor : PChar); stdcall;
   Begin
      ShowMessage(Valor);
   End; 

Exports
  MuestreMensaje; 

begin
end.


El llamado desde la aplicación puede realizarse de forma estática o dinámica, pero cualquiera que sea la forma de llamado de la función el paso de parámetros quedaría algo similar a esto:

Var
  Texto : String;
Begin
   Texto := 'Hola esta es una prueba de llamado a un DLL';
   MuestreMensaje(PChar(Texto));
 

Retorno de Strings en una función de un Dll

Este es un punto delicado en el paso de parámetros, dado que el área de memoria de la aplicación principal es independiente del área de memoria del DLL,  por lo que no necesariamente pueden compartir memoria o punteros de memoria entre los dos módulo, en especial si la carga del DLL se realizó dinámicamente utilizando la función LoadLibrary en lugar de la forma estática.
Debe tener en cuenta que la memoria de la aplicación estará disponible siempre que la aplicación esté activa, lo que no sucede con la memoria del módulo, esta se destruye al liberar la librería con FreeLibrary,  por lo tanto los swings que se creen en el módulo se perderán después del liberado, causando mal funcionamiento de la aplicación en forma aleatoria.
Por eso en el momento de retornar un String debe separarse una porción de memoria del entorno global del sistema y no del área de memoria del módulo, así que este modelo de retorno de un String está mal diseñado y puede causar errores aleatorios en la aplicación, en especial cuando se realiza una carga dinámica del módulo.

 

//  ESTE MODELO DE PROGRAMA ESTÁ MÁL DISEÑADO,
//    PRESENTA PROBLMAS DE ASIGNACIÓN DE MEMORIA

library Project24;
uses
  System.SysUtils,  System.Classes,  System.StrUtils;

{$R *.res} 

   Function InvierteString(Const Valor : PChar) : PChar; stdcall;
   Begin
      Result := PChar(ReverseString(Valor)); //Error por asignación de memoria
   End; 

Exports
  InvierteString;

begin
end.


Solución al Problema 

Se debe utilizar la función GetMem para reservar memoria suficiente para almacenar el String de retorno, copiar en esta porción de memoria el String y retornar un puntero a la posición de memoria, para esto ya he creado la siguiente función: 

const charlen = 1; //1 para AnsiString, 2 para UnicodeString 

Function StrToPChar(Res : String) : PChar;
 Var
    I : Integer;
 Begin
    I := Length(Res)*charlen;
    If I > 0 then
    Begin
       GetMem(Result,I+1);
       Result := StrPCopy(Result, Res);
    End
    Else
    Begin
       GetMem(Result,2*charlen); //string vacio debe tener al menos 2 o 4 bytes.
       Result := StrPCopy(Result,'');
    End;
 End; 

Así la función de retorno tendrá la siguiente forma

   Function InvierteString(Const Valor : PChar) : PChar; stdcall;
   Begin
      Result := StrToPChar(ReverseString(Valor));
   End; 

Mezclando versiones de Delphi

Este método funciona muy bien si tanto la aplicación como el módulo DLL están utilizando ambos el mismo modelo de String, es decir ambos utilizan AnsiString o UnicodeString, pero que sucede cuando se requiere llamar desde Delphi 7 un Dll creado en XE5?
El problema en este modelo es la representación de la variable String en cada una de las versiones, como hemos mencionado para las versiones 2009 y posteriores definir una variable String por defecto es Unicode mientras que para las versiones anteriores se representa como AnsiString.
En una versión anterior a la 2009 la constante charlen debe ser igual a 1, dado que cada carácter ocupa solamente un byte, de esta manera la función GetMem reserva la cantidad de memoria correcta del String, mientras que para versiones 2009 o posteriores será necesario asignar el charlen a 2.

Para el llamado de la función veremos dos casos, el primer caso sería llamar una DLL desarrollada en Delphi XE desde Delphi 7 y luego el caso contrario, hacer un llamado a una DLL desarrollada en XE desde Delphi 7. 

Llamar una DLL desarrollada en Delphi XE5 desde Delphi 7

Aunque el ejercicio se desarrollo en estas versiones lo que se quiere indicar es cómo hacer el llamado de una función creada en un Dll en una versión >= Delphi 2009 desde un programa desarrollado en Delphi < 2009.
Es importante hacer el cambio de tipo de String de Delphi 7 (AnsiString) a un formato de dos bytes por carácter y eso se logra de la siguiente forma:
 

Funcion  LLamaFuncionXE5(Nombre : String) : String;
Var
   P : PWideChar;
begin

   P := PWideChar(WideString(Nombre)); //Se convierte de AnsiString a WideChar
   Result := String(InvierteString(P)); //El typecast aquí es automático
 

Llamar una DLL desarrollada en Delphi 7 desde Delphi XE5

Aunque el ejercicio se desarrollo en estas versiones lo que se quiere indicar es cómo hacer el llamado de una función creada en un Dll en una versión anterior a Delphi 2009 desde un programa desarrollado en Delphi >= 2009.

 

Funcion  LLamaFuncionD7(Nombre : String) : String;
Var
   P : PAnsiChar;
begin 
   P := PAnsiChar(AnsiString(Nombre));//Se convierte de UnicodeString a AnsiChar
   Result := String(InvierteString(P)); //El typecast aquí es automático
 

Llamar una DLL desarrollada en Delphi con compatibilidad de String

Obviamente si tanto el Dll está desarrollado con la misma definición de Delphi String, no debe haber problemas, dado que existe la compatibilidad y no será necesario hacer typecast, la función quedaría de la siguiente forma

Funcion  LLamaFuncion(Nombre : String) : String;
begin
   Result := String(InvierteString(PChar(Nombre)));

 
Esta metodlogía puede ser muy útil para realizar funcionalidades que solo están disponibles en versiones recientes de Delphi especialmente en RadStudio que no es posible desarrollar en versiones anteriores a la 2009. 
Esta información es válida para DLL desarrollados en otros lenguajes como C y también para desarrollar funciones para ser ejecutadas desde otros lenguajes, dado que los DLL en Windows deben ser compatibles entre los diferentes lenguajes de programación.
 

Atte.Mg. Gustavo Enríquez
http://www.hyper-soft.co

1 comentario:

  1. Éste es un tema muy interesante además de muy útil.

    Muy bien explicado (y)

    Saludos

    ResponderEliminar