jueves, diciembre 28, 2017

Cannot find entry point (DLL name) in the DLL with Visual FoxPro

Este error puede ocurrir por dos motivos, hasta donde sé, actualmente.

El motivo oficial está explicado en la MSDN:
Cannot find entry point (DLL name) in the DLL with Visual FoxPro

Básicamente dice cuando se define una una función DLL se debe respetar estrictamente la capitalización de los nombres (mayúsculas/minúsculas) y los tipos de los parámetros (long/string/etc, y si son por referencia "@" o por valor)

Voy a usar el ejemplo con la función GetUserName que usa la MSDN.

Declaración original en C (siempre buscar la original):

   BOOL GetUserName(

      LPTSTR  lpBuffer,// address of name buffer
      LPDWORD  nSize // address of size of name buffer
     );
   Parameters


Declaración en Visual FoxPro:

   DECLARE GetUserName in Win32API ;
      String @mybuffer, Integer @mybuffersize


Si la función se declara de la siguiente forma, generará el error del artículo:

   DECLARE GetuserName in Win32API ;
      String @mybuffer, Integer @mybuffersize



Ahora bien, hay otro motivo que puede causar este error, y que me ha costado unas cuántas horas encontrar. Es un poco enrevesado para reproducir, pero en resumen puede pasar cuando se tiene una DLL (ej: MiLibreria.dll) en el System32 para que se puede acceder públicamente desde cualquier directorio, y una copia de la misma DLL en otro directorio dentro del ámbito del PATH de Visual FoxPro, que se active con posterioridad.

Ejemplo del caso de uso:
- Se declara una función DLL (ej: DECLARE long MiFuncion in MiLibreria.dll long p1, string @p2 )
- Se usa la función (y funciona bien)
- Se activa un PATH donde hay una copia de la misma (ej: SET PATH TO \proj\dlls ADDITIVE)
- Se intenta usar nuevamente la función => Aquí es donde puede fallar!


Algo importante a saber es que, a diferencia de los componentes tradicionales de Visual FoxPro, como forms, clases y programas, cuando se declara una función de DLL la librería se busca automáticamente en el System32 (o SysWow64), por lo cual no sirve hacer la típica prueba de ? FILE('MiLibreria.dll') para saber si VFP encuentra la librería, porque FILE() no busca en el directorio System32 (o SysWow64) a menos que explícitamente se haga un SET PATH con dicho directorio.

Como digo, esto "puede" pasar, aunque no tiene por qué hacerlo, pero lo importante es tener en cuenta este caso para que no les pase lo mismo.


Hasta la próxima! :D

domingo, noviembre 12, 2017

Plastic SCM: Índice de videos de YouTube con algunas operaciones comunes

El presente post es para tener un índice con los videos que fui haciendo sobre el uso de Plastic SCM con Visual FoxPro 9.


PlasticSCM: Como cambiar el tipo de archivo a texto



PlasticSCM: Cómo cancelar un merge en proceso


PlasticSCM: Resolución manual de conflictos en FoxPro 9



PlasticSCM: Deshacer un changeset con merge sustractivo en FoxPro 9

FoxPro 9: Deshacer un merge de dos ramas en PlasticSCM



FoxPro 9: Merge en PlasticSCM de dos ramas con un directorio duplicado

Modificación en FoxPro 9 y checkin en PlasticSCM


Merge de 2 ramas y resolución de conflictos (directorio repetido)



PlasticSCM: Merge con Visual FoxPro 9

PlasticSCM: Merge con Visual FoxPro 9 - video 2





Hasta la próxima! :D


lunes, marzo 27, 2017

VFP: Algunas formas de usar los arrays

Los arrays son una colección de elementos muy útil y versátil en cualquier lenguaje, que a diferencia de los cursores o tablas permiten también almacenar objetos o referencias a los mismos.

En Visual FoxPro el uso de los arrays conviene mantererlo controlado en cuanto a tamaño, ya que por encima de algunos cientos de elementos, las búsquedas sobre un array comienzan a ponerse lentas. Por eso, para grandes tamaños siempre conviene usar cursores o tablas.



Definición y dimensionamiento


Hay dos tipos de arrays: en forma de variable y en forma de propiedad, ambos usan la misma sintaxis, pero solo se diferencian en la forma de declararse o en dónde se declaran.

Por ejemplo, como variable un array se puede definir de estas formas:

LOCAL laLista(1), laTabla(1,1)  && Se definen y dimensionan en un paso

Y luego se pueden redimensionar así:

DIMENSION laLista(10), laTabla(4,2)

Para una clase, usando el cuadro de diálogo de agregar propiedad (ej.en un form: menú form/New property), lo mismo de antes se define como laLista[1] y luego otra propiedad nueva como laTabla[4,2]

Luego, para redimensionar las propiedades se hace lo mismo, pero indicando el nombre del objeto:

DIMENSION thisform.laLista(10), thisform.laTabla(4,2)

Hay una sintaxis adicional cuando se requiere agregar un array que no sea posible poner en diseño, que es usando AddProperty:

AddProperty( thisform, "laLista[1]" )



Asignación de valores


Para asignar valores, se hace como cualquier variable o propiedad:

laLista(1) = 100
laLista(2) = CREATEOBJECT("custom")
laLista(3) = "Texto"

thisform.laLista(1) = 100
...



Redimensionamiento


Para redimensionar un array se usa el mismo comando que para definirlo, DIMENSION. Solo hay que tener en cuenta algunos casos:

- Para agrandar un array bidimensional con datos, solo se pueden agregar filas, no columnas, ya que si se agregan columnas (técnicamente posible) se distribuirán los datos existentes para usar las nuevas columnas, lo que hará que ya no estén en sus ubicaciones originales. En este caso conviene copiar de un array a otro nuevo con las columnas que se requieran.

- Para achicar un array, conviene hacerlo desde abajo hacia arriba, o sea, desde el índice mayor al menor

Para ejemplificar la eliminación de elementos, algunos habrán intentado hacerlo así:

lnFilas =  ALEN(laLista)
FOR I = 1 TO lnFilas
   ADEL(laLista, I) 
   lnFilas = lnFilas - 1
   DIMENSION laLista(lnFilas)
ENDFOR

El código anterior tiene el problema de que al llegar a la mitad del array, ocurrirá un error porque se estará intentando borrar (ADEL) un elemento cuyo índice supera la cantidad de elementos, y es por eso que esto debe hacerse en orden inverso:

lnFilas =  ALEN(laLista)
FOR I = lnFilas TO 1 STEP -1
   ADEL(laLista, I) 
   lnFilas = lnFilas - 1
   DIMENSION laLista(lnFilas)
ENDFOR

Si bien el algoritmo anterior no tiene sentido para cuando se quieren borrar todos los elementos, porque es mucho más rápido hacer un DIMENSION laLista(1) y luego laLista = "", es la forma correcta de borrar elementos intermedios.



Caso especial de uso


Normalmente los arrays se usan para listas de valores conocidos, como nombres de ciudades, paises y selecciones en general, pero hay un caso especial que requiere una forma distinta de manipular el array, que es cuando no se sabe con antelación cuántos elementos tendrá el array, pero es necesario tenerlo predefinido con un elemento.

El problema se presenta cuando se quiere agregar un elemento nuevo, ya que normalmente se usa ALEN() para saber cuantos tiene y agregar uno nuevo, pero ALEN() devolverá siempre como mínimo 1 y es posible que ese elemento tenga valor, o no, y no siempre es posible usar EMPTY() para controlar si hay un dato, porque un 0 o una cadena vacía podría ser el dato...

¿Cómo controlar esa situación? Para eso se puede usar un contador de elementos:

lnFilasLista = 0
DIMENSION laLista(1)

Y luego, cuando se va a agregar un nuevo elemento, simplemente se incrementa y redimensiona a la par:

lnFilasLista  = lnFilasLista + 1
DIMENSION laLista(lnFilasLista)
laLista(lnFilasLista) = "nuevo valor"


La siguiente es una técnica que define una API para interactuar con el array anterior, donde se encapsulan las operaciones de dimensionar y agregar valores. En este caso lo defino como una clase instanciable, pero los métodos bien podrían ponerse directamente en un form u otra clase.

Este es un ejemplo de uso:

loArrayMgr = createobject("cl_ArrayMgr") 
loArrayMgr.Add("un valor")
loArrayMgr.Add("otro valor")
loArrayMgr.Add("un valor más")
? loArrayMgr.Length
3
loArrayMgr.Delete(2)
? loArrayMgr.Datos(2)
"Un valor más"
? loArrayMgr.Length
2

Y la clase podría definirse así en un PRG o visualmente en la clase que se requiera:

DEFINE CLASS cl_ArrayMgr AS Custom
   DIMENSIO aDatos[1]
   nDatos = 0 

   FUNCTION Length
      RETURN THIS.nDatos
   ENDFUNC

   PROCEDURE Add(tuValor)
      THIS.nDatos = THIS.nDatos + 1
      DIMENSION THIS.aDatos(THIS.nDatos)
      THIS.aDatos(THIS.nDatos) = tuValor
   ENDPROC

   PROCEDURE Delete(tnElemento)
      IF THIS.nDatos > 1 AND tnElemento <= THIS.nDatos
         ADEL(THIS.aDatos, tnElemento)
         THIS.nDatos = THIS.nDatos - 1
         DIMENSION THIS.aDatos(THIS.nDatos)
      ENDIF
   ENDPROC
ENDDEFINE


Sobre este ejemplo se pueden hacer variaciones y mejoras, pero creo que se entiende la idea de cómo manipular un array en las condiciones comentadas.


Hasta la próxima! :D

martes, febrero 28, 2017

Los peligros de SET COLLATE

Se me ha presentado una situación que no veo descripta claramente en la documentación de Visual FoxPro y que hasta ahora creía que se aplicaba solamente a los índices de las tablas.

Como es sabido, el comando SET COLLATE (que por defecto es "MACHINE") permite indicar el algoritmo de comparación a usar para los índices y funciones relacionadas con ellos (búsquedas, ordenamientos, etc), de forma que si se definen unos datos así:

CREATE CURSOR test (palabra C(30))
INSERT INTO test VALUES ("Comparación")
INSERT INTO test VALUES ("Comparación 2")
INSERT INTO test VALUES ("Comparación 3")


Con SET EXACT OFF y SET COLLATE TO "MACHINE" las comparaciones son exactas a nivel de mayúsculas/minúsculas (capitalización), pero teniendo en cuenta cómo comienzan las cadenas:

LIST all for palabra = "Comparación"

Record#  PALABRA                      
      1  Comparación                  
      2  Comparación 2                
      3  Comparación 3                


Lo siguiente no retorna resultados:

LIST all for palabra = "COMPARACIÓN"

Record#  PALABRA                      

Cambiando a SET COLLATE TO "GENERAL", las comparaciones cambian y ya no distinguen entre minúsculas/mayúsculas:

LIST all for palabra = "Comparación"

Record#  PALABRA                      
      1  Comparación                  
      2  Comparación 2                
      3  Comparación 3                


LIST all for palabra = "COMPARACIÓN"

Record#  PALABRA                      
      1  Comparación                  
      2  Comparación 2                
      3  Comparación 3                



Hasta aquí, todo esto es de conocimiento público.

Algo importante a tener en cuenta es que el COLLATE también afecta a las comparaciones lógicas que no tengan que ver con tablas ni datos de las mismas, lo que abarca cualquier tipo de comparación ASCII, o sea, de caracteres y cadenas.

Por ejemplo, cuando comparamos caracteres, internamente lo que se compara son los valores ASCII, de modo que A < Z porque el ASCII de A es 65 y el de Z es 90 y la comparación que realmente se hace es 65 < 90:

? "A" < "Z" && En ASCII: 65 < 90
.T.

? "a" < "z" && En ASCII: 97 < 122.T.

? BETWEEN("F", "a", "Z") && En ASCII: 70 está entre 97 y 90
.F.

? "a" < "Z" && En ASCII: 97 < 90
.F.

Lo anterior es cierto cuando el COLLATE es "MACHINE", pero cuando es "GENERAL" las cosas cambian:

? "A" < "Z" && En ASCII: 65 < 90
.T.


? "a" < "z" && En ASCII: 97 < 122
.T.

? BETWEEN("F", "a", "Z") && En ASCII: 70 está entre 97 y 90
.T.

? "a" < "Z" && En ASCII: 97 < 90
.T.

Estos dos últimos casos son los que pueden resultar más confusos, ya que va contra toda lógica... a menos que se sepa cuál es la lógica a aplicar, y es que cuando se evalúan las opciones con COLLATE "GENERAL", lo que realmente se hace es comparar todo en la misma capitalización (que supongo que es en mayúsculas, aunque no puedo confirmarlo).

Bajo esta lógica, si normalizamos primero la capitalización de la última comparación, realmente sería lo mismo que hacerla así:

? "A" < "Z" && En ASCII: 65 < 90
.T.


? BETWEEN("F", "A", "Z") && En ASCII: 70 está entre 65 y 90
.T.

Y lo mismo se aplica a las demás funciones de comparación de cadenas.

Este último caso es el que se me presentó recientemente, y realmente puede resultar algo perturbador ver algo como esto sin lógica aparente:


Lo que se ve en la imagen es el depurador abierto con una aplicación antigua en ejecución (parte superior) y la ventana de comandos en la parte inferior. Como se puede observar, la aplicación hace una comparación que en definitiva hace esto:

IF BETWEEN(caracter, "a", "Z")
   lnCuantos = lnCuantos + 1
ELSE
   EXIT
ENDIF

A primera vista, esa comparación no tiene sentido porque jamás un valor puede estar "entre 97 y 90" (comparando en ASCII) y la ventana de comandos lo confirma, pero resulta que la aplicación tiene sesión de datos privada y COLLATE "GENERAL", por lo que realmente está comparando todo con la misma capitalización de mayúsculas/minúsculas, y por eso funciona.



Resumen:


Este es uno de esos casos donde se vuelve a comprobar la importancia de usar buenas prácticas de programación. En este caso algunas formas correctas de escribir esta condición sería:

IF BETWEEN(carac, "A", "z") && carac entre 65 y 122

o bien:

IF BETWEEN(UPPER(carac), "A", "Z") && carac entre 65 y 90


Y de esa forma independizarse del valor del SET COLLATE.

Espero que esto les evite algunos problemas,

Hasta la próxima! :D