Categoría: C++

Formulas de validación

Validación de datosUn problema al que nos enfrentamos habitualmente cuando tenemos que obtener datos de un usuario es el de detectar información falsa o con errores.

No siempre, pero cuando se trata de cierto tipo de datos es posible aplicar fórmulas para verificar la veracidad de esos datos.

En esta entrada veremos algunas de esas fórmulas, y algunos ejemplos para implementarlas en C++.

Número DNI o NIF

Al menos en España, desde hace bastantes años, cada ciudadano tiene un DNI (Documento Nacional de Identidad), en el que figuran algunos datos sobre él. Cuando se obtiene por primera vez, se le asigna un número de ocho cifras, que es único para cada persona.

Posteriormente, con fines fiscales, se creó el NIF (Número de Identificación Fiscal). En el caso de personas físicas, el NIF es el mismo número del DNI al que se añadió una letra como control. Esa letra se obtiene aplicando una fórmula al número del DNI, de modo que si se cambia cualquiera de los dígitos, o la letra, es posible detectar el error.

No se trata de un algoritmo demasiado sofisticado, como veremos a continuación.

Algoritmo

Básicamente se compone de dos pasos:

  1. Se calcula el módulo, o resto de la división del número del DNI entre el valor 23.
  2. La letra de control se obtiene usando como índice el valor actual, en la siguiente tabla de 23 letras:
0 1 2 3 4 5 6 7 8 9 10 11
T R W A G M Y F P D X B
12 13 14 15 16 17 18 19 20 21 22
N J Z S Q V H L C K E

Se excluyen en esta tabla las letras: ‘I’, ‘Ñ’, ‘O’, ‘U’.

La ‘I’ y la ‘O’ se descartan porque es fácil confundirlas con otros caracteres, como ‘1’, ‘l’ ó ‘0’. La ‘Ñ’, porque se puede confundir con la ‘N’, y la u, seguramente, porque sobraba una letra.

Ejemplo de validación de NIF:

const int nifok = 0;
const int noesnif = 1;
const int faltaletra = 2;
const int nifincorrecto = 3;
const int niferroneo = 4;

int VerificarNIF(char *dni) {
    char letra[] = "TRWAGMYFPDXBNJZSQVHLCKET";
    unsigned int i;
    char l;
    int retval = nifok;

    if(strlen(dni) == 0) {
        retval = noesnif;
    } else
    if(isdigit(dni[strlen(dni)-1])) {
        retval = faltaletra;
    } else {
        for(i = 0; i < strlen(dni)-1; i++) {
            if(!isdigit(dni[i])) {
                retval = nifincorrecto;
            }
        }
    }
    if(!retval) {
        i = atoi(dni);
        l = dni[strlen(dni)-1];
        if(letra[i % 23] != toupper(l)) {
            retval = niferroneo;
        }
    }
    return retval;
}

Tarjetas de crédito

Para verificar los números de las tarjetas de crédito se usa el algoritmo de Luhn.

En este caso, el último dígito también es un dígito de control, que se puede calcular a partir de los dígitos restantes.

Hay más niveles de verificación, aunque nos limitaremos a este.

Por ejemplo, los seis primeros dígitos, conocidos como BIN, identifican el tipo de tarjeta, el primer dígito, y la entidad que emite la tarjeta, los cinco restantes.

Por ejemplo las tarjetas VISA empiezan siempre por ‘4’, las Mastercard por ‘5’, etc.

El algoritmo de Luhn es también muy simple. Hay que tener en cuenta que estos algoritmos de verificación están pensados para que sean sencillos y sirven para detectar pequeños errores.

  1. Tomando los dígitos de derecha a izquierda, incluyendo el dígito de control, se duplican los de las posiciones pares y si el resultado es mayor de 9 se resta 9.
  2. Se suman todos los dígitos así obtenidos.
  3. Si el resultado es divisible entre 10, es decir, el módulo 10 es cero, el número es correcto.

Por ejemplo:

5245 6585 6635 5458

1285 3575 3665 1418

8+(2*5 mod 9)+4+(2*5 mod 9)+5+(2*3 mod 9)+6+(2*6 mod 9)+5+(2*8 mod 9)+5+(2*6 mod 9)+5+(2*4 mod 9)+2+(2*5 mod 9) =

8+1+4+1+5+6+6+3+5+7+5+3+5+8+2+1 =

70

Es decir, el número es correcto.

bool VerificarTarjeta(const char *numero) {
    char *n;
    int digito;
    bool par = false;
    int res = 0;

    n = new char[strlen(numero)+1];
    strcpy(n, numero);
    do {
        digito = n[strlen(n)-1]-'0';
        n[strlen(n)-1] = 0;
        res += digito;
        if(par) {
            res += digito;
            if(digito > 4) res -= 9;
        }
        par = !par;

    } while(strlen(n));
    delete[] n;
    return !(res % 10);
}

ISBN

El ISBN (International Standard Book Number) es un código numérico que se usa para identificar libros internacionalmente. Hay dos variedades, una de diez dígitos, y posteriormente se creó una segunda, de trece dígitos.

En ambos casos el último dígito se usa como dígito de control. Pero, cada una de ellas usa un algoritmo diferente para calcularlo.

ISBN de diez dígitos

En este caso, el dígito de control se calcula de la siguiente forma:

  1. Se toman nueve dígitos de izquierda a derecha, multiplicando cada uno por la posición que ocupa, es decir, el primero por uno, el segundo por dos, etc. Y se suma cada resultado.
  2. Al valor resultante se le aplica el módulo con once. Esto da un valor entre 0 y 10.
  3. El dígito de control es ese valor, si está entre 0 y 9, ó X si es 10.

Por ejemplo, para un ISBN de diez dígitos:

84-9736-467-8

8+4*2+9*3+7*4+3*5+6*6+4*7+6*8+7*9 = 261

261 % 11 = 8

ISBN de trece dígitos

Se usa el mismo algoritmo que para códigos de barras EAN (European Article Number), que son los códigos de barras de toda la vida, que aparecen en casi todos los productos comerciales.ean13

Algoritmo:

  1. Se toman los doce caracteres de izquierda a derecha, multiplicando los impares por uno y los pares por tres, y se suma cada resultado.
  2. El dígito de control se calcula restando de diez el módulo del resultado con diez. Es decir, el valor que habría que sumar al resultado para que sea divisible entre diez

Por ejemplo, para este ISBN:

978-84-253-4025-3

(9+8+4+5+4+2)+3*(7+8+2+3+0+5) = 32+3*25 = 32+75 = 107

10-107%10 = 10-7 = 3

bool ISBN10(const char *numero) {
    int res = 0;
    char control;

    if(strlen(numero) != 10) return false;
    for(int i = 0; i < 9; i++) {
        res += (numero[i]-'0') * (i+1);
    }
    control = res % 11;
    if(control == 10) control = 'X'; else control += '0';

    return control==numero[9];
}

bool ISBN13(const char *numero) {
    int res = 0;

    if(strlen(numero) != 13) return false;
    for(int i = 0; i < 13; i++) {
        if(i%2) res += 3*(numero[i]-'0');
        else res += numero[i]-'0';
    }

    return !(res % 10);
}

Códigos de cuenta de cliente e IBAN

Los códigos de cuenta de cliente (CCC) son los números de cuenta bancaria, que actualmente se están sustituyendo por el IBAN (International Bank Account Number)

El IBAN se obtiene fácilmente a partir del CCC. Basta con añadir cuatro caracteres delante del CCC, los dos primeros son dos letras que identifican el país, y los otros dos, son dos dígitos de control.

Verificación del CCC

Pero empecemos por el CCC, ya que también tienen un formato con dígitos de control.

El formato tiene veinte dígitos con la estructura siguiente:

 EEEE OOOO CC NNNNNNNNNN

EEEE: 4 dígitos que identifican la entidad bancaria
OOOO: 4 dígitos que identifican la oficina
C Un dígito de control Entidad/Oficina
C Un dígito de control Número de cuenta
NNNNNNNNNN: 10 dígitos de número de cuenta

El primer dígito de control se obtiene de los ocho primeros dígitos, y el segundo de los diez últimos.

El algoritmo es el mismo en los dos casos, aunque para los ocho primeros dígitos añadiremos dos ceros al principio, para que ambos conjuntos tengan diez dígitos cada uno.

  1. Tomando los dígitos uno a uno, de izquierda a derecha, multiplicamos cada uno por los siguientes valores, respectivamente: 1,2,4,8,5,10,9,7,3,6.
  2. Sumamos cada uno de los productos.
  3. Calculamos el módulo once del valor obtenido.
  4. Restamos ese valor de 11.
  5. Si el módulo está entre 0 y 9, ese es el dígito de control, si es 10, tomaremos el valor 1, y si es 11, tomaremos el valor 0.

Los valores del paso 1 se calculan como 2n % 11, desde n=0 a n=9.

Por ejemplo:

1234 5678 06 1234567890

0*1+0*2+1*4+2*8+3*5+4*10+5*9+6*7+7*3+8*6 = 231

231 % 11 = 0

11-0 = 11

DC1 = 0

1*1+2*2+3*4+4*8+5*5+6*10+7*9+8*7+9*3+0*6 = 280

280 % 11 = 5

11-5 = 6

DC2 = 6

bool VerificarCCC(char *ccc) {
    // EEEE OOOO CC NNNNNNNNNN
    // 4 dígitos Entidad
    // 4 dígitos Oficina
    // Dígito de control Entidad/Oficina
    // Dígito de control Número de cuenta
    // 10 dígios Número de cuenta
    int peso[10] = {1,2,4,8,5,10,9,7,3,6};
    unsigned int i;
    int j, DC1, DC2;
    char ccc_l[21];

    if(strlen(ccc) != 20) {
        return false;
    }

    for(i = 0; i < strlen(ccc)-1; i++) {
        if(!isdigit(ccc[i])) {
            return false;
        }
    }
    strcpy(ccc_l, ccc);

    for(DC1 = i = 0; i < 8; i++) {
        DC1 += peso[i+2] * (ccc[i]-'0');
    }
    DC1 = 11-(DC1%11);
    if(DC1 == 10) DC1 = 1;
    if(DC1 == 11) DC1 = 0;

    for(DC2 = i = 0; i < 10; i++) {
        DC2 += peso[i] * (ccc[i+10]-'0');
    }
    DC2 = 11-(DC2%11);
    if(DC2 == 10) DC2 = 1;
    if(DC2 == 11) DC2 = 0;

    if(ccc[8]-'0' != DC1 || ccc[9]-'0' != DC2) {
        return false;
    }
    return true;
}

Verificación del IBAN

Para formar el IBAN se usan veinticuatro caracteres. Los dos primeros son dos letras que identifican el país, en el caso de España, «ES». Los dos siguientes son un número de control. A continuación se añaden los veinte dígitos del CCC.

Para verificar el IBAN se usa el siguiente algoritmo:

  1. Los cuatro nuevos caracteres se eliminan y se añaden al final de número
  2. Las letras se sustituyen por un valor numérico de dos dígitos cada uno. Ese valor numérico es 10 para la ‘A’, 11 para la ‘B’, etc.
  3. Se calcula el módulo del número resultante con 97.
  4. Si es 1, el código IBAN es correcto.

Por ejemplo:

ES68 1234 5678 06 1234567890

12345678061234567890ES68

E: 14, S:28

12345678061234567890142868 % 97 = 1

Tenemos que resolver un pequeño problema adicional: C++ no dispone de enteros lo suficientemente grandes como para manejar números de 26 cifras, y el valor 97 se escogió deliberadamente por ser el mayor número primo menor de 100, de modo que…

Afortunadamente disponemos de un algoritmo para calcular restos de divisiones cuando el dividendo es un número grande y el divisor es relativamente pequeño, al menos lo suficiente para poder manejarlo con un int de C++. Se trata del algoritmo Big Mod, que vimos en una entrada anterior.

En el caso de los códigos IBAN, la verificación es doble, ya que los veinte dígitos finales deben verificar el algoritmo para CCC, y el código completo, el de IBAN:

bool VerificarIBAN(const char *iban) {
    char *iban2;
    int resto;

    if(!VerificarCCC(&iban[4])) return false;

    iban2 = new char[27];
    strcpy(iban2, &iban[4]);
    sprintf(iban2, "%s%02d%02d%c%c", 
        &iban[4], 10+iban[0]-'A', 10+iban[1]-'A', iban[2], iban[3]);
    resto = moduloNG(iban2, 97);
    delete[] iban2;
    return resto == 1;
}

int moduloNG(const char *numero, int d) {
    int i = 0, n = 0;

    while(numero[i]) n = (n * 10 + numero[i++] - '0') % d;
    return n;
}

Enlaces de interés:

NIF: Wikipedia

Código de cuenta cliente: Luciano.com

Código IBAN: CienciaExplora.com

Tarjetas: Wikipedia, eHow Español

Algoritmo Big Mod

numerosPongamos que tenemos que calcular el módulo o resto de la división de un número de muchas cifras entre otro más pequeño, lo suficiente para que pueda almacenarse en un int, o en un long int.

No es algo que no nos vayamos a encontrar nunca. De hecho, la razón de esta entrada es que he necesitado hacer estas operaciones, y pronto veremos un ejemplo en una entrada posterior.

En principio no parece fácil, pero las matemáticas vienen en nuestra ayuda. Concretamente la teoría de números.

El algoritmo que vamos a ver, como dice el título se llama Big Mod.

Este algoritmo se basa en estas dos propiedades:

(a*b*c) mod m =((a mod m)*(b mod m)*(c mod m)) mod m (1)
(a+b+c) mod m =((a mod m)+(b mod m)+(c mod m)) mod m (2)

Primero veamos qué significa exactamente calcular el módulo de una división.

dividendo  = cociente * divisor + resto

Hay que tener en cuenta que, a la hora de calcular el resto, nos podemos despreocupar del valor del cociente. Esto nos puede ayudar, porque el resto es el mismo si restamos del dividendo el divisor un número entero de veces:

dividendo – n * divisor = (cociente-n) * divisor + resto

Por otra parte, si descomponemos el dividendo en dos factores, a y b:

a*b = cociente * divisor + resto

(a-n*divisor)*(b-m*divisor) = (cociente-n-m) * divisor + resto

Es decir, el resto es el mismo si restamos de cualquiera de los factores un múltiplo del divisor.

Lo mismo sirve para sumas, si descomponemos el dividendo en dos sumandos c y d:

c+d = cociente * divisor + resto

(c-n*divisor)+(d-m*divisor) = (cociente-n-m) * divisor + resto

Esto es, el resto es el mismo si restamos de cualquiera de los sumandos un múltiplo del divisor.

¿Cómo nos ayuda esto a resolver nuestro problema?

Veamos un ejemplo:

5657866541284565 % 67 =

(565*1013+7866541284565) % 67

Ahora, podemos restar del primer número, el 565 el valor 67, tantas veces como queramos. Lo haremos el máximo, es decir, nos quedaremos con el resto de 565/67, o lo que es lo mismo, 565 % 67 = 29.

(29*1013+7866541284565) % 67

O:

297866541284565 % 67

Como se puede ver, hemos disminuido en una unidad los dígitos del número grande.

Podemos aplicar esta técnica recursivamente, o mejor, iterativamente.

Algoritmo

  1. Partimos de n igual a cero.
  2. Tomamos un dígito de la izquierda del número grande y lo añadimos a la derecha de n.
  3. Calculamos el módulo n % d, y lo guardamos en n.
  4. Repetimos desde 2, hasta terminar los dígitos del número grande.
  5. n contiene el valor del módulo.

5657866541284565 % 67

5 % 67 = 5

56 % 67 = 56

565 % 67 = 29

297 % 67 = 29

298 % 67 = 30

306 % 67 = 38

etc.

Código C++

Una implementación sencilla es esta:

int moduloNG(const char *numero, int d) {
    int i = 0, n = 0;

    while(numero[i]) {
        n = n * 10 + numero[i] - '0';
        n = n % d;
        i++;
    }
    return n;
}

Otra, más compacta:

int moduloNG(const char *numero, int d) {
    int i = 0, n = 0;

    while(numero[i]) n = (n * 10 + numero[i++] - '0') % d;
    return n;
}

Conectar y desconectar unidades de red

Unidad de redAlgunas veces nos encontramos con la necesidad de que nuestra aplicación acceda a archivos almacenados en un ordenador diferente, aunque conectado a la misma red.

En esta entrada veremos cómo conectarse y cómo desconectarse a una unidad de red, usando el API de Windows. Mientras exista esa conexión, podremos acceder a ficheros en la unidad remota del mismo modo que a cualquier fichero local.

Básicamente usaremos dos funciones del API de Windows: WNetAddConnection2 y WNetCancelConnection2, para crear y eliminar una conexión, respectivamente.

Estas funciones están en la librería de Windows «Mpr«, por lo tanto, tendremos que incluirla en la fase de enlazado de nuestro programa.

Para el programa de ejemplo usaremos una carpeta en una unidad de disco local, por ejemplo, C:\temp. Aunque si estamos en una red podremos conectar a cualquier recurso compartido de otros ordenadores, siempre que nuestro usuario tenga los permisos adecuados.

Antes de poder crear una unidad de red deberemos compartir ese recurso en la red, para ello usaremos el explorador de archivos de Windows, y accederemos a las propiedades de la carpeta que queramos compartir. Vamos a la pestaña de «Compartir», y pulsamos el botón de «Compartir». En el siguiente cuadro de diálogo nos permitirá elegir con qué usuarios queremos compartir el recurso, y una vez elegidos, nos mostrará los datos relativos a ese recurso. En nuestro ejemplo algo como:

temp (\\<etiqueta>)
\\<etiqueta>\temp

Donde <etiqueta> es el nombre de nuestro ordenador.

Este nombre se puede sustituir por la IP, en nuestro ejemplo»127.0.0.1″, o por el nombre que tenga asignado en el DNS local, por ejemplo «localhost».

En general, la etiqueta corresponderá al nombre de un equipo conectado a la red, y el nombre de la unidad o carpeta compartida será diferente.

Las funciones para conectarse y desconectarse a una unidad remota tendrán esta forma, más o menos:

bool ConectarRecurso()
{
    NETRESOURCE nr;
    DWORD ret;
    const char *letra = "Z:";
    const char *recurso = "\\\\localhost\\temp";
    const char *usuario = "usuario";
    const char *password = "contraseña";
    bool conexionRemota;

    nr.dwType = RESOURCETYPE_ANY;
    nr.lpLocalName = (LPSTR)letra;
    nr.lpRemoteName = (LPSTR)recurso;
    nr.lpProvider = NULL;
    ret = WNetAddConnection2(&nr, password, usuario, 0);

    if(ret !=NO_ERROR)
        switch(ret) {
            case ERROR_ACCESS_DENIED: 
               std::printf("Access to the network resource was denied."); 
               break;
            case ERROR_ALREADY_ASSIGNED: 
               std::printf("The local device specified by lpLocalName is already connected to a network resource."); 
               break;
            case ERROR_BAD_DEV_TYPE: 
               std::printf("The type of local device and the type of network resource do not match."); 
               break;
            case ERROR_BAD_DEVICE: 
               std::printf("The value specified by lpLocalName is invalid."); 
               break;
            case ERROR_BAD_NET_NAME: 
               std::printf("The value specified by lpRemoteName is not acceptable to any network resource provider. The resource name is invalid, or the named resource cannot be located."); 
               break;
            case ERROR_BAD_PROFILE: 
               std::printf("The user profile is in an incorrect format."); 
               break;
            case ERROR_BAD_PROVIDER: 
               std::printf("The value specified by lpProvider does not match any provider."); 
               break;
            case ERROR_BAD_USERNAME: 
               std::printf("The specified user name is not valid."); 
               break;
            case ERROR_BUSY: 
               std::printf("The router or provider is busy, possibly initializing. The caller should retry."); 
               break;
            case ERROR_CANCELLED: 
               std::printf("The attempt to make the connection was cancelled by the user through a dialog box from one of the network resource providers, or by a called resource."); 
               break;
            case ERROR_CANNOT_OPEN_PROFILE: 
               std::printf("The system is unable to open the user profile to process persistent connections."); 
               break;
            case ERROR_DEVICE_ALREADY_REMEMBERED: 
               std::printf("An entry for the device specified in lpLocalName is already in the user profile."); 
               break;
            case ERROR_EXTENDED_ERROR: 
               std::printf("A network-specific error occured. Call the WNetGetLastError function to get a description of the error."); 
               break;
            case ERROR_INVALID_ADDRESS: 
               std::printf("An attempt was made to access an invalid address.");
               break;
            case ERROR_INVALID_PARAMETER: 
               std::printf("A parameter is incorrect.");
               break;
            case ERROR_INVALID_PASSWORD: 
               std::printf("The specified password is invalid."); 
               break;
            case ERROR_LOGON_FAILURE: 
               std::printf("A logon failure because of an unknown user name or a bad password."); 
               break;
            case ERROR_NO_NET_OR_BAD_PATH: 
               std::printf("A network component has not started, or the specified name could not be handled."); 
               break;
            case ERROR_NO_NETWORK: 
               std::printf("There is no network present."); 
               break;
            default: 
               std::printf("Error %d, desconocido", ret); 
               break;
        }
    conexionRemota = ((ret == NO_ERROR) || (ret == ERROR_ALREADY_ASSIGNED));
    return conexionRemota;
}

void DesconectarRecurso()
{
    const char *letra = "Z:";

    WNetCancelConnection2(letra, 0, TRUE);
}

Habría que sustituir «usuario» y «contraseña» por los valores adecuados en cada caso.

Se puede descargar un ejemplo completo desde aquí.

Recibir correo POP3

CorreoYa vimos en una entrada anterior cómo podemos enviar mensajes de correo desde una aplicación Windows. En esta entrada veremos cómo podemos recibir correo usando el protocolo POP3.

Explicaré algo de teoría (no mucha), pero no te preocupes, verás que leer correos será realmente sencillo, otra tema muy diferente será procesar su contenido.

Antes que nada, necesitaremos varias cosas para poder recibir correo:

  • Lo primero, por supuesto, es una cuenta de correo POP3. Puede servir, por ejemplo, una de Gmail. (Ver notas al final para ver una explicación de cómo se activa ese protocolo en Gmail).
  • Conocer el protocolo POP3, ya que lo usaremos directamente en nuestro programa.
  • Debido a que hay que tener ciertas medidas de seguridad, será necesario conseguir una librería para soporte del protocolo SSL.

El protocolo POP3

Se trata de un protocolo realmente simple. Si te interesa conocer más detalles, consulta esta entrada de la Wikipedia.

Lo que nos interesa ahora son las órdenes o comandos disponibles en ese protocolo. Como son pocas, las veremos todas.

  • USER <nombre> Identificación de usuario (Solo se realiza una vez).
  • PASS <password> Envía la clave del servidor.
  • STAT Da el número de mensajes no borrados en el buzón y su longitud total.
  • RETR <número> Solicita el envío del mensaje especificando el número (no se borra del buzón).
  • LIST Muestra todos los mensajes no borrados con su longitud.
  • NOOP Permite mantener la conexión abierta en caso de inactividad.
  • TOP <número> <líneas> Muestra la cabecera y el número de líneas requerido del mensaje especificando el número.
  • DELE <número> Borra el mensaje especificando el número.
  • RSET Recupera los mensajes borrados (en la conexión actual).
  • UIDL <número> Devuelve una cadena identificatoria del mensaje persistente a través de las sesiones. Si no se especifica <número> se devuelve una lista con los números de mensajes y su cadena identificatoria de los mensajes no borrados.
  • QUIT Salir.

Una sesión POP3 sigue este esquema:

  1. Establecer una conexión con el servidor, se puede usar telnet, o sockets. Nosotros usaremos una librería, de modo que esta parte es transparente en nuestro caso.
  2. Identificarse, usando el comando USER.
  3. Enviar la contraseña, usando el comando PASS.
  4. Ahora, los comandos a utilizar dependerán de lo que queramos hacer. Si sólo nos interesa saber si hay correo pendiente de leer, podemos usar el comando STAT, que nos devuelve el número total de mensajes y su tamaño en conjunto. Si queremos recuperar los mensajes, tendremos que usar el comando LIST para obtener una lista de los mensajes disponibles, con su número y tamaño, y el comando RETR para recuperar cada uno de ellos. Opcionalmente podemos borrarlos después de descargarlos, usando DELE.
  5. Cerrar la sesión, usando el comando QUIT.

Establecer una conexión segura SSL

El protocolo POP3 no es seguro. Aunque haya que identificarse, las claves se transmiten por la red en forma de texto, por lo que es posible que sean interceptadas.

Para evitar esto, lo normal es usar POP3 sobre SSL, puedes informarte sobre ello en Wikipedia.

Afortunadamente para nosotros, no tenemos que preocuparnos demasiado por esta capa, ya que hay librerías en Internet que permiten conectarse a servidores usando SSL.

En concreto, usaremos OpenSSL, cuya página oficial es http://www.openssl.org.

Y más concretamente, para Windows, usaremos el paquete que se puede descargar desde http://gnuwin32.sourceforge.net/packages/openssl.htm, en esa página descargaremos el primer fichero, «Complete package, except sources», que es un ejecutable que contiene la documentación, bibliotecas dinámicas, estáticas y ficheros de cabecera.

Como con otras librerías, copiaremos cada fichero en el lugar correspondiente:

  • Los ficheros de cabecera, que están en la carpeta «openssl», dentro de la carpeta «include», los copiaremos a la carpeta «include» de nuestro compilador. Copiaremos la carpeta completa «openssl», no sólo los ficheros de cabecera.
  • Los ficheros de biblioteca estática, con la extensión «.a», que están en la carpeta «lib», los copiaremos a la carpeta «lib» de nuestro compilador.
  • Los ficheros de biblioteca de enlace dinámico (DLL), que están en la carpeta «bin», los copiaremos a la carpeta de trabajo de nuestra aplicación, o en un lugar donde sean localizables por el sistema.

Funciones de inicio

Las librerías de OpenSSL disponen de muchas funciones, pero la mayor parte de ellas no las vamos a necesitar para obtener mensajes. Entre ellas usaremos las siguientes:

SSL_library_init()
Debe ser invocada antes que tenga lugar cualquier otra acción. Registra algunos de los algoritmos necesarios para usar SSL.
SSL_load_error_strings()
Registra los mensajes de error.
ERR_load_BIO_strings()
Registra los mensajes de la librería BIO.
OpenSSL_add_all_algorithms()
Registra y crea una tabla interna de algoritmos SSL.
SSL_CTX
Objeto creado como un marco para establecer conexiones TLS/SSL. Varias opciones, como certificados, algoritmos, etc, se pueden asignar mediante este objeto.
SSL_CTX_new()
Crea un objeto SSL_CTX, el valor de retorno es un puntero a un objeto de este tipo.
SSLv23_client_method()
El valor de retorno de esta función se usa como argumento de la anterior.
BIO
Objeto para establecer conexiones.
BIO_new_ssl_connect()
Establece una conexión y devuelve un puntero a un objeto BIO. Como argumento se usa un objeto de tipo SSL_CTX.
SSL
Objeto para conexión segura.
BIO_get_ssl()
recupera el puntero SSL de BIO, que puede ser entonces manipulado usando SSL estándar. Como primer argumento se debe especificar un objeto BIO, y como segundo un objeto SSL.
SSL_set_mode()
Asigna el modo a un SSL. El primer argumento es un puntero a un objeto SSL, el segundo el modo.

Todas estas funciones son sólo para preparar la conexión. No te preocupes si no está claro cómo funcionan en conjunto (ni por separado), el bloque de código para iniciar SSL queda como sigue:

    SSL *tunelSSL;
    BIO *conexionBIO;
    SSL_CTX *datosConexion;

    /* Inicializacion */
    SSL_library_init();
    SSL_load_error_strings();
    ERR_load_BIO_strings();
    OpenSSL_add_all_algorithms();
    datosConexion=SSL_CTX_new(SSLv23_client_method());
    conexionBIO = BIO_new_ssl_connect(datosConexion);
    BIO_get_ssl(conexionBIO, &tunelSSL);
    SSL_set_mode(tunelSSL, SSL_MODE_AUTO_RETRY);

Funciones de cierre

De forma simétrica, cuando terminemos la tarea, deberemos cerrarlo todo. Para ello usaremos las siguientes funciones:

BIO_reset()
Retorna el objeto BIO, cuyo puntero se pasa como argumento, a su estado inicial.
BIO_free_all()
Libera toda la cadena BIO, de nuevo pasamos un puntero al objeto BIO como argumento.
SSL_CTX_free()
Libera el objeto SSL_CTX cuyo puntero se pasa como argumento.

El proceso de cierre queda así:

    BIO_reset(conexionBIO);
    BIO_free_all(conexionBIO);
    SSL_CTX_free(datosConexion);

Funciones de conexión, lectura y escritura

Entre la inicialización y el cierre es donde haremos nuestra tarea, usando el protocolo POP3.

Primero, indicamos con qué servidor queremos establecer una conexión, usando la función BIO_set_conn_hostname. El primer argumento es un puntero a un objeto BIO, que hemos obtenido en la inicialización. El segundo es una cadena con el nombre y puerto del servidor. En este ejemplo, el de Gmail.

    BIO_set_conn_hostname(conexionBIO,"pop.gmail.com:995");

Para establecer la conexión se usa la función BIO_do_connect, indicando como argumento un puntero a un objeto BIO (el mismo objeto). Si el valor de retorno es menor o igual a cero, es que no se ha podido establecer la conexión.

    if(BIO_do_connect(conexionBIO) <= 0) cout << "ERROR: No es posible crear la conexion." << endl;

En este punto estamos conectados al servidor, y recibiremos un mensaje desde él. Para recuperarlo usamos la función BIO_read, indicando en el primer parámetro el puntero al objeto BIO, en el segundo la dirección del buffer de entrada, y en el tercero, el tamaño máximo de bytes a leer. El valor de retorno es el número de bytes leídos.

    int nbytes=0;
    char BufferDeSalida[512];
    char BufferDeEntrada[1025];

    nbytes=BIO_read(conexionBIO, BufferDeEntrada, 1024);
    BufferDeEntrada[nbytes] = 0;
    cout << BufferDeEntrada << endl;

Recibiremos un mensaje de bienvenida, cuyo texto depende del servidor. Aunque siempre empezará por «+OK». En el caso de Gmail tiene esta forma:

+OK Gpop ready for requests from nnn.nnn.nnn.nnn <id>

Básicamente indica que está esperando a que nos identifiquemos. Para eso, lo primero es enviar nuestro nombre de usuario, usando el protocolo POP3, es decir, con el comando «USER». Usaremos la función BIO_write, indicando como primer argumento el puntero a la estructura BIO, como segundo la dirección de la cadena con el comando, terminada en ‘\n’, y como tercero, la longitud de la cadena. El servidor responderá con otra cadena de reconocimiento o de error, dependiendo de si el usuario es reconocido o no.

    strcpy(BufferDeSalida,"USER usuario@gmail.com\n"); // Cambia la cadena según  tu caso
    BIO_write(conexionBIO, BufferDeSalida, strlen(BufferDeSalida));

    nbytes=BIO_read(conexionBIO, BufferDeEntrada,sizeof(BufferDeEntrada));
    BufferDeEntrada[nbytes] = 0;
    cout << BufferDeEntrada << endl;

La respuesta será de este tipo, más o menos:

+OK send PASS

Ahora hay que enviar la contraseña, usando el comando «PASS».

    strcpy(BufferDeSalida,"PASS contraseña\n");
    BIO_write(conexionBIO, BufferDeSalida, strlen(BufferDeSalida));

    nbytes=BIO_read(conexionBIO, BufferDeEntrada, 1024);
    BufferDeEntrada[nbytes] = 0;
    cout << BufferDeEntrada << endl;

En este caso, la respuesta, si el usuario y contraseña son correctos, será de este tipo:

+OK Welcome.

En caso de no ser reconocido, será un mensaje de error:

-ERR [AUTH] Username and password not accepted.

Es el momento de empezar a comunicarse con el servidor de correo para recuperar información.

Podemos, por ejemplo, obtener el número de mensajes pendientes de leer, usando el comando «STAT». Como respuesta obtendremos una cadena con el formato:

+OK <n> <tamaño>

Donde <n> es el número de mensajes y <tamaño> el número de bytes que ocupan todos los mensajes en total.

Podemos usar el comando «LIST» para obtener una lista de los mensajes, cada uno con su tamaño. Como respuesta obtendremos un mensaje de varias líneas, de este tipo:

+OK 5 messages:
1 31052
2 7460
3 12834
4 13749
5 45141
.

Observa que cuando la respuesta tiene más de una línea, la última contiene un punto.

Hay que tener cuidado cuando las repuestas pueden ser muy largas. Es el caso del comando «LIST» y de «RETR». Las lecturas de respuestas largas pueden requerir llamar a la función BIO_read varias veces, porque el buffer de entrada no será lo bastante grande para leer la respuesta completa.

Otro problema es que no podemos conocer con antelación el tamaño de la respuesta que tenemos que leer. En el caso del comando «LIST», sabemos el número de líneas, pero no el tamaño de cada una. En el caso del comando «RETR», sabemos el tamaño del mensaje, pero no podemos tener una confianza ciega en ese dato.

Por ejemplo, me he encontrado con que algunos mensajes de Gmail son mucho más largos que lo que se indica en la respuesta al comando «LIST».

Hay dos posibles soluciones que se pueden aplicar a este problema:

  1. Crear un buffer de lectura tan grande que garantice que tengamos espacio suficiente.
  2. Agrandar el buffer cada vez que se detecte que una lectura producirá un desbordamiento.

En el caso de lecturas de respuestas a la orden «LIST», podemos asumir un tamaño de buffer que permita almacenar 14 caracteres por línea. El número de líneas lo podemos conocer mediante una orden «STAT». Podemos considerar que 14 caracteres por línea es más que suficiente, considerando 4 para el número de mensaje, un espacio separador, 7 para el tamaño del mensaje y dos para el CRLF final.

En el caso de los mensajes, si la información del servidor no es fiable, no hay forma de predecir el tamaño. Por lo tanto optaremos por redimensionar el buffer cada vez que se nos quede pequeño.

Para averiguar cuando se nos acaba el buffer usaremos un bucle en el que primero intentaremos leer tantos caracteres como quepan en él, y después averiguaremos si quedan caracteres pendientes de leer, usando la función BIO_ctrl_pending. Si quedan caracteres por leer, agrandaremos el buffer en el tamaño suficiente para que quepan los caracteres pendientes.

La condición de salida será que en la última lectura, los últimos caracteres sean un punto seguido de CRLF:

    do {
        leido = BIO_read(cBIO, &buffer[nbytes], tambuffer-nbytes);
        nbytes += leido;
        if(BIO_ctrl_pending(cBIO) > 0) {
            // Reasignar buffer.
            Reubicar(tambuffer+BIO_ctrl_pending(cBIO)+1);
            continue;
        }
    } while(buffer[nbytes-3] != '.' || buffer[nbytes-2] != '\r' || buffer[nbytes-1] != '\n');

Procesar mensajes

Cada mensaje se puede dividir en dos partes, las cabeceras y el cuerpo. La separación es una simple línea en blanco, es decir, una línea con un CRLF.

La cabecera contiene información sobre el remitente y el destinatario del mensaje, los servidores por los que ha pasado, fecha de creación, asunto, etc. Los datos no son fijos, ni en el orden ni en la cantidad ni en tipo. Puede haber más o menos, algunos pueden no estar presentes siempre. Los más habituales, que suelen aparecer casi siempre son:

  • Delivered-To: dirección de correo de entrega.
  • Received: que puede y suele aparecer varias veces. Permite conocer el camino que ha seguido el mensaje desde su origen hasta su destino. Cada entrada contiene información sobre el servidor, la hora a la que se procesó el mensaje.
  • Return-Path: dirección de respuesta por defecto, en caso de que el mensaje no pudiera ser enviado. Es la dirección de «rebote». Los servidores pueden procesar esos rebotes para saber si un usuario no ha recibido el mensaje, o si la dirección no existe, o si tiene algún error.
  • From: dirección de correo del remitente, a menudo contiene también un nombre, en el formato nombre <direccion de correo>.
  • Message-ID: un identificador único del mensaje. Estos identificadores se usan para identificar el mensaje, y para establecer jerarquías de mensajes, para determinar qué mensajes responden a cuales.
  • To: dirección del destinatario original del mensaje. No tiene por qué coincidir con el valor de Delivered-To, por ejemplo, si el destinatario es una lista de correo, o está dirigido a varios destinatarios.
  • Date: fecha de creación del mensaje.
  • Importance: prioridad del mensaje, generalmente definida por el remitente.
  • Precedence: indica el tipo de mensaje, puede ser bulk (basura), junk (masivo), list (lista) o un tipo definido por alguna aplicación. Está previsto para que determinados clientes de correo respondan de diferente modo, dependiendo del tipo de mensaje.
  • Reply-To: dirección de correo que se usará para responder al mensaje.
  • Sender: dirección de correo del que ha enviado el mensaje. Puede no coincidir con el campo From, por ejemplo, si se trata de una lista de correo, el que ha escrito el mensaje lo envía a la lista, pero es la lista la que distribuye los mensajes al resto de los miembros, por eso aparecerá la dirección de la lista como «Sender».
  • In-Reply-To: contiene el identificador del mensaje del que el actual es respuesta.
  • References: contiene una lista de identificadores de mensaje que establece una cadena de respuestas.
  • MIME-Version: versión de la codificación MIME usada para codificar el mensaje, normalmente 1.0.
  • Subject: texto del asunto del mensaje.
  • Errors-To: dirección de correo para notificar errores.
  • Content-Type: tipo del contenido del mensaje, dependiendo del formato: texto, html o ambos, de si tiene adjuntos, etc. Este campo es más complicado, y lo veremos en próximas entradas del blog.

Como los nombres pueden estar codificados en diferentes formatos, a menudo en los textos en «From», «Subject», etc. aparece también la información del conjunto de caracteres en el que está codificada la cadena. Por ejemplo: =?iso-8859-1?x?texto?=

Aparecen tres campos, el primero entre =? y ? es el nombre del conjunto de caracteres. El segundo, entre ? y ?, el número de caracteres que contiene la cadena codificada. El tercero, entre ? y ?= la propia cadena.

En próximas entradas del blog veremos más sobre cómo decodificar el formato MIME de los mensajes. De momento nos conformaremos con procesar las cabeceras.

Aplicaciones

Las aplicaciones de leer mensajes desde nuestro programas son muchas. Por ejemplo, podemos crear un programa que nos avise cuando tenemos correo nuevo, o crear un filtro que borre del servidor determinados mensajes. Podríamos crear nuestro propio programa de correo electrónico, o algo más simple, como un programa que sea capaz de procesar órdenes recibidas por correo. Programas para crear listas de correo, etc.

En esta entrada voy a añadir dos programas de ejemplo.

Leer mensajes en consola

El primero se limita a leer mensajes desde un buzón y mostrar una lista de asuntos, remitentes y fechas. Los detalles de la cuenta están codificados en el programa fuente, de modo que habrá que compilarlo para cada caso. De todos modos, es relativamente simple leer esos valores desde una base de datos o desde un fichero de texto. Por supuesto, un programa seguro requeriría encriptar las contraseñas, pero eso está fuera del objetivo de esta entrada.

Descargar desde aquí.

Notificación de correo nuevo

Aprovechando una entrada anterior de este blog, insertar iconos en el área de notificación, haremos un programa que verifique una dirección de correo cada cinco minutos, y muestre un icono en el área de notificación cuando haya correo nuevo.

Descargar desde aquí.

Nota: Para activar el protocolo POP3 en Gmail, hay que ir al menú de configuración y buscar la pestaña «Reenvío y correo POP/IMAP». El punto uno permite habilitar POP3 para los nuevos mensajes que se reciban a partir de ahora, o para todos los mensajes almacenados en la cuenta.

Codificación base64

Base 64Base 64 es un sistema de numeración, como el decimal, el binario o el hexadecimal, pero que usa 64 símbolos.

En la práctica no se usa tanto para codificar números, sino sobre todo para codificar datos binarios. El motivo es que usa 64 símbolos imprimibles: 62 de los símbolos consisten en el alfabeto en mayúsculas y en minúsculas y los dígitos 0 a 9. Para los otros dos se usan distintos caracteres, dependiendo de la versión particular del código. Los más habituales son el ‘+’ y el ‘/’.

Esto hace que cualquier valor codificado en base 64 sea imprimible, lo que a su vez permite que se puedan visualizar con un editor de textos, o eludir los problemas que suelen aparecer con caracteres especiales en transmisiones de datos o manejo de ficheros.

Se suele usar base 64 para codificar los ficheros binarios que se envían adjuntos con mensajes de correo, por ejemplo. Pero se puede usar en otras muchas cosas.

En mi caso, lo he usado para transferir datos binarios usando una línea serie, ya que si lo hacía en binario surgían algunos problemas con algunos valores que tienen un significado especial para el sistema operativo.

Esto en cuanto a ventajas. Entre los inconvenientes cabe mencionar que se necesitan más bytes en la versión codificada en base 64 que en la binaria. La relación es de 4/3, es decir, por cada tres bytes binarios se necesitan cuatro en base 64. Esto es así porque sólo se usan 64 valores de cada byte, es decir, 6 bits de los 8 disponibles, 8/6 equivale a 4/3.

Empaquetado de bits

La idea es tomar los 24 bits correspondientes a tres bytes a codificar y repartirlos en cuatro bytes, de modo que sólo se usen seis de los ocho bits de cada uno. Eso nos da cuatro valores entre 0 y 63 que usaremos como índice en un array de caracteres que contiene los siguientes valores:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

Así, al valor 0 le corresponde el carácter ‘A’, al 1 el carácter ‘B’, etc.

Conversión base 64La forma de repartir esos bits es simple:

  • Se toman los seis bits de mayor peso del primer byte de origen y se copian a los seis bits de menor peso del primer byte de destino.
  • Se toman los dos bits de menor peso del primer byte de origen y los cuatro de mayor peso del segundo y se copian al segundo byte de destino.
  • Se toman los cuatro bits de menor peso del segundo byte de origen y los dos de mayor peso del tercero y se copian al tercer byte de destino.
  • Se toman los seis bits del tercer byte de origen y se copian al cuarto byte de destino.

Ahora los cuatro bytes de destino contienen valores entre 0 y 63. Esos valores se toman como índice para obtener el carácter correspondiente en la codificación base 64.

A la hora de codificar en base 64 hay que tener en cuenta un detalle extra. La longitud de los datos de entrada no será siempre un múltiplo exacto de tres. Cuando no lo sea, en la última iteración de conversión pueden quedar uno o dos bytes. Si es uno, la conversión requiere sólo dos bytes, y si es dos requerirá tres.

Sin embargo, la longitud de los datos de salida siempre será un múltiplo de cuatro, de modo que si en la última iteración sólo queda un byte por convertir se calculan los dos bytes de salida correspondientes y se añaden dos caracteres especiales, que no forman parte de los caracteres permitidos en base 64. El carácter que se usa es el ‘=’. Si en la última iteración quedan dos bytes, se calculan los tres bytes de salida correspondientes y se añade un carácter de relleno.

Esto asegura que será posible no sólo hacer la conversión inversa, sino que además la longitud será la correcta.

Texto E s t
ASCII 69 115 116
Binario 0 1 0 0 0 1 0 1 0 1 1 1 0 0 1 1 0 1 1 1 0 1 0 0
Índice 17 23 13 52
Base 64 R X N 0

En caso de que sobren dos caracteres, la conversión queda así:

Texto E s
ASCII 69 115
Binario 0 1 0 0 0 1 0 1 0 1 1 1 0 0 1 1 0 0 0 0 0 0 0 0
Índice 17 23 12
Base 64 R X M =

Y en caso de que sobre uno:

Texto E
ASCII 69
Binario 0 1 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Índice 17 16
Base 64 R Q = =

Codificación en C++

Este es un ejemplo de funciones en C++ para convertir a y desde Base 64:


int CodificarB64(unsigned char* entrada, char*salida , int l) {
    char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    int i=0, j=0;
    unsigned char A, B, C;

    // Bloques de tres bytes:
    entrada[l]=0;
    if(l%3==1) entrada[l+1]=0;
    if(l%3==2) entrada[l+2]=0;
    for(i = 0; i < l; i+=3){
        A = entrada[i];
        B = entrada[i+1];
        C = entrada[i+2];
        salida[j++] = b64[A>>2];
        salida[j++] = b64[((A<<4)+(B>>4)) & 0x3f];
        if(i+1 < l) salida[j++] = b64[((B<<2)+(C>>6)) & 0x3f]; else salida[j++] = '=';
        if(i+2 < l) salida[j++] = b64[C & 0x3f]; else salida[j++] = '=';
    }
    salida[j] = 0;
    return j;
}

char Indice(const char c) {
    if(isdigit(c)) return c-'0'+52;
    if(isupper(c)) return c-'A';
    if(islower(c)) return c-'a'+26;
    if(c=='+') return 62;
    if(c=='/') return 63;
    return 0;
}

int DecodificarB64(char* entrada, unsigned char* salida) {
    int i=0, j=0;
    unsigned char A, B, C, D;

    // Bloques de tres bytes:
    while(entrada[i]) {
        A = Indice(entrada[i++]);
        B = Indice(entrada[i++]);
        C = Indice(entrada[i++]);
        D = Indice(entrada[i++]);

        salida[j++] = (unsigned char)((A << 2) + (B >> 4));
        salida[j++] = (unsigned char)((B << 4) + (C >> 2));
        salida[j++] = (unsigned char)((C << 6) + D);
    }
    return j;
}

Validar datos en C++

Validación de datosEn aplicaciones de consola C++, uno de los puntos más problemáticos es la lectura de datos por parte del usuario.

Las funciones de entrada C y los métodos del stream cin resultan poco potentes a la hora de hacer lecturas discriminadas de número enteros, o en coma flotante, fechas o cadenas que se ajuste a un formato determinado.

Después de hacer algunos programas, todos nos damos cuenta de que es mala idea usar el operador >> de cin para leer un número. Si el usuario introduce una cadena no numérica, el valor leído es cero, pero la cadena permanece en el buffer de entrada del teclado, y se intenta leer en sucesivas lecturas.

El resultado es que no se leen los datos que se pretenden leer, y generalmente el programa termina por entrar en un bucle infinito o, en el mejor de los datos, da un resultado incorrecto.

Cuando leemos cadenas también existe peligro, concretamente, de overbuffering, es decir, de sobrepasar el espacio de memoria correspondiente a la cadena a leer.

Esto es más cierto cuando leemos cadenas C, en forma de array de caracteres terminadas en nulo. Si usamos objetos de tipo string este peligro no existe.

Con esto en mente, parece claro que es mejor leer siempre cadenas. En el caso de querer capturar valores numéricos, se debe validar la cadena leída. Si la validación tiene éxito se convierte el valor leído a número, y si no, se repite el proceso.

Generalizando, para validar cualquier formato de dato, el proceso es el siguiente:

  • Leer una cadena.
  • Verificar si el formato es correcto.
    • Si es correcto: retornar el valor leído.
    • Si no es correcto: volver al principio.

Leer una cadena

Parece una tarea sencilla, ¿no?

Tal vez no tanto…

Veamos. Si usamos el operador >> de cin para leer un objeto de la clase string se pueden presentar algunos problemas. Por ejemplo, el operador >> deja de leer cuando encuentra un espacio, y además, no lo retira del buffer.

Si intentamos leer un número, y el usuario escribe «varias palabras y un numero 34», se leerá «varias» en la cadena, y el resto quedará en el buffer para siguientes lecturas.

Si usamos el método getline, pronto descubriremos que no sirve para leer objetos de la clase string, sino sólo cadenas terminadas en nulo.

Existe una función getline que evita todos estos inconvenientes: lee objetos string y lee espacios. En el primer argumento debemos indicar el objeto cin, y en el segundo, la cadena a leer:

    string cad;
    getline(cin, cad);

De este modo nos aseguramos de que cad contiene una cadena con todo lo introducido por el usuario, y podemos pasar al siguiente paso.

Verificar el formato correcto

Por supuesto, esta tarea es diferente dependiendo del formato del dato a leer.

Si se trata de un entero, y no somos demasiado exigentes con el formato, podemos usar un stream de cadena para verificar la entrada del usuario.

    int i;
    stringstream mystream(cad);
    if(mystream >> i) cout << "entero leido" << endl;
    else cout << "error" << endl;

En este ejemplo usamos un stringstream creado a partir de la cadena leída, y después intentamos leer un entero a partir de ella. Si al comienzo de la cadena hay un entero, su valor pasa a i, el caso contrario indicará un error.

Digo que «si no somos demasiado exigentes», porque hay muchas cadenas que darán como resultado un entero, aunque no contengan sólo un puntero, por ejemplo «123.3», «123,4» o «123abc».

Si se trata de un número en coma flotante, nos sirve el mismo método, pero usando una variable float o double para la lectura. Este método interpretará correctamente entradas en notación científica, como «1e3», o «1.3e2».

Insistir hasta tener un resultado válido

El último paso es repetir la lectura si el valor leído no es válido.

Para ello pondremos todo el código anterior dentro de un bucle. Por ejemplo, para leer un entero:

    int i;
    string cad;

    while(true) {
        cout << "Introduce un entero: ";
        getline(cin, cad);
        stringstream mystream(cad);
        if(mystream >> i) break;
        cout << "error" << endl;
    }
    cout << "Valor: " << i << endl;

Podemos introducir todo este código en una función, si tenemos que leer varios números, en la que se acepte como entrada una cadena, y como valor de retorno un entero.

Verificación general

El método anterior es suficiente en los casos generales de lectura de números, pero a menudo tenemos que verificar otros tipos de datos, como fechas, o cadenas que se ajusten a un formato concreto.

Estas verificaciones generalmente requieren verificar cada carácter de la cadena capturada para comprobar si se ajusta a un patrón determinado. Por ejemplo, una fecha debe constar de dos dígitos, seguidos de un carácter separador, otros dos dígitos, otro carácter separador y finalmente otros cuatro dígitos. Además los dos primeros dígitos deben estar en el intervalo entre 1 y el número de días correspondiente al mes y año, los segundos entre 1 y 12, ya que se refieren a un mes, y los últimos cuatro, deben estar en el rango de fechas válidas para nuestra aplicación. Esto será variable, dependiendo de cada caso.

Otras lecturas deben ajustarse a otros patrones, a veces más simples y otras más complejos.

Si disponemos de soporte para expresiones regulares, la primera parte de la verificación puede ser más sencilla, y en muchos casos no necesitaremos más.

C++11 tiene soporte para expresiones regulares, dentro de las bibliotecas estándar, concretamente, en <regex>.

En cualquier caso, existen bibliotecas que implementan expresiones regulares para C, por ejemplo, pcre.

Sin embargo, la mayor parte de las veces no es necesario usar esta herramienta, y podemos simplemente verificar si la cadena se ajusta a determinadas reglas o no.

Como ejemplo, veamos una función de validación para capturar valores enteros dentro de un intervalo.

int Intervalo(string msg, int v1, int v2) {
    int i;
    string cad;
    bool valido = false;

    do {
        cout << msg;
        getline(cin, cad);
        stringstream mystream(cad);
        if(mystream >> i) {
            if(i >= v1 && i <= v2) valido = true;
        }
        if(!valido) cout << "error" << endl;
    } while(!valido);
    return i;
}
...
    i = Intervalo("Introduce un entero entre 100 y 200: ", 100, 200);
    cout << "Valor: " << i << endl;

Fuentes: cplusplus.com

Acentos y eñes en programas de consola de Windows

Letra Ñ Cuando creamos programas para consola de Windows hay algo que resulta muy frustrante: las eñes, cedillas y acentos no se muestran como debieran, y en su lugar se ven unos caracteres raros que dificultan la lectura de los textos y resultan muy antiestéticos.

Ante este problema se nos ocurren algunas soluciones, más o menos ingeniosas:

La primera, prescindir de esos caracteres: eliminamos los acentos y evitamos palabras con ‘ñ’. Esto no siempre es posible, y como solución, sinceramente, resulta un intento bastante mediocre.

La segunda consiste en editar el código fuente desde un editor de consola. Haciendo esto, el editor usa el mismo conjunto de caracteres que el fichero ejecutable, y la salida del programa coincide con lo que hemos escrito antes. El resultado es mejor, pero resulta bastante incómodo usar varios editores para cada proyecto.

La tercera es incluso más incómoda. Consiste en escribir las cadenas en una consola, y a continuación cortar y pegar el texto en el editor. Y lo peor, a veces este truco ni siquiera funciona.

Pero, ¿por qué pasa esto?

Tal vez nos resultaría más sencillo solucionar el problema si supiéramos exactamente por qué se produce.

La raíz del problema es que Windows usa un código de caracteres (o código de página) diferente para el entorno GUI y para la consola. Para configuraciones de Windows en español, en GUI se usa el código 1252 y para la consola se usa el código 850.

Si usamos un editor de textos GUI para escribir el código de un programa para consola, se usarán códigos de página diferentes en la edición y en la ejecución, y el resultado será el galimatías al que ya estamos acostumbrados.

Soluciones más interesantes

Conociendo el origen del problema es más sencillo encontrar una solución, o al menos, debería serlo.

Una primera idea sería configurar el editor para que use el código de página 850. Desgraciadamente esto no es siempre posible. En las opciones del editor de Code::Blocks, por ejemplo, no está disponible el código de página 850.

Eso nos deja la solución final, que consiste en usar un par de funciones del API de Windows.

Primero, usaremos la función SetConsoleOutputCP. Que, como su propio nombre indica, sirve para asignar un código de página a la salida de consola. Usaremos el valor 1252, que es el código de página que queremos usar, ya que es el que hemos usado para escribir el código fuente.

#include <iostream>
#include <windows.h>

using namespace std;

int main()
{
    SetConsoleOutputCP(1252);
    cout << "áéíóúñÑçÇüÜ" << endl;
    return 0;
}

Segundo, debemos asegurarnos de que nuestra consola usa fuentes Unicode, de otro modo, el código de página asignado será ignorado y no habremos conseguido nada. En la página de Microsoft hay un artículo titulado «SetConsoleOutputCP sólo es efectivo con fuentes Unicode«:

Hacer esto es sencillo, basta seguir estos pasos:

  1. Abrir una consola Windows.
  2. Desplegar el menú del sistema (en el icono de la parte superior izquierda de la ventana) y seleccionar «Predeterminados».
  3. Activar la pestaña «Fuentes».
  4. Seleccionar una fuente TrueType, si está disponible, en lugar de «Fuentes de mapa de bits». (Yo tengo «Lucida console».)

De este modo, la configuración será la usada para todas las consolas a partir de este momento.

Si sólo están disponibles fuentes de mapas de bits, mucho me temo que esta solución no es viable.

Efectos colaterales

Si nuestro programa de consola tiene que leer cadenas nos encontraremos con otro problema inesperado. Las cadenas que introduzcamos por teclado, si contienen acentos o eñes se verán correctamente, pero si posteriormente las volvemos a mostrar en pantalla veremos que vuelven a aparecer caracteres extraños.

El motivo es simple, el código de página para consola de entrada y salida no tiene por qué ser el mismo. La función SetConsoleOutputCP sólo cambia el código de página para la salida, pero el de entrada sigue siendo el 850. Así, cuando introducimos una cadena, esta se codifica incorrectamente.

La función para cambiar el código de página de entrada no es, como podría esperarse, SetConsoleInputCP. Esa función no existe (a saber por qué), la función correcta es SetConsoleCP.

#include <iostream>
#include <windows.h>

using namespace std;

int main()
{
    char cad[256];

    SetConsoleOutputCP(1252);
    SetConsoleCP(1252);
    cout << "áéíóúñÑçÇçüÜ" << endl;
    cout << "Introduce una cadena con acentos y eñes: ";
    cin >> cad;
    cout << "La cadena es: " << cad;
    return 0;
}

No es una solución ideal, lo sé, pero puede solucionar muchos problemas cuando programamos para consola.

Otra ventaja es que las cadenas leídas de este modo se codifican internamente usando el código de página de Windows GUI, de modo que es una forma de que cualquier dato leído de este modo se visualice correctamente en una aplicación GUI.

Impedir que una aplicación se ejecute dos veces

Una direccion¿Cómo podemos evitar que una aplicación se ejecute más de una vez de forma simultánea?

En Windows existe un método sencillo: usar un mutex.

El nombre mutex es una abreviatura de un algoritmo de exclusión mutua. Es un dispositivo que se usa normalmente para evitar concurrencia, es decir para controlar el acceso de determinadas zonas de código que tienen acceso a recursos compartidos.

En nuestro caso, el recurso al que tenemos que limitar el acceso es el ordenador. Como se trata de un recurso que puede ser compartido, sólo limitaremos el acceso a nuestro programa, es decir, cuando nuestro programa esté accediendo al sistema operativo impedirá que otras instancias del mismo programa puedan acceder a él.

Un mutex es ideal para este propósito, ya que sólo puede estar en posesión de un proceso a la vez. Si un proceso tiene un manipulador de un mutex, no es posible que otro pueda obtenerlo.

De todos modos, para el propósito de este artículo no usaremos el mutex de ese modo, sencillamente intentaremos crear un mutex con un nombre determinado. Si el mutex no existía previamente, continuamos con la ejecución de la aplicación, si existía, abandonamos la ejecución.

Para obtener un manipulador para un mutex usaremos la función del API de Windows CreateMutex. Los dos primeros parámetros no nos importan demasiado. El primero es un descriptor de seguridad, que para este propósito nos basta con el descriptor por defecto. El segundo sirve para obtener la propiedad del mutex cuando es creado o no. Para nuestro caso es indiferente, ya que sólo verificaremos si el mutex existe o no. El tercero es el nombre del mutex. Usaremos un nombre único para nuestra aplicación.

Hay que tener presente que lo que impedirá que nuestra aplicación se ejecute es la presencia del mutex, de modo que este mecanismo no sólo impide que dos instancias del mismo programa se ejecuten a la vez, también impedirá la ejecución de cualquier otra aplicación que use el mismo nombre para el mutex.

// Prueba de mutex
// Mayo 2013 Con Clase
// Salvador Pozo
#include <windows.h>
#include <iostream>

using namespace std;

int main()
{
    bool EnEjecucion;

    //mutex =
    CreateMutex(NULL, FALSE, "PROGRAMA_EN_EJECUCION");
    EnEjecucion = (GetLastError() == ERROR_ALREADY_EXISTS || GetLastError() == ERROR_ACCESS_DENIED);

    if(EnEjecucion) {
        MessageBox(NULL, "El programa ya está en ejecución", "Probar mutex", MB_OK);
        return 0;
    }
    // Resto del programa aquí...
    cin.get();
    return 0;
}

Alternativamente a  usar mutex podemos hacer lo mismo con un fichero de disco. Si no existe, lo creamos al iniciar la aplicación y lo borramos al terminar. Si existe, cerramos la aplicación.

El problema de este método es que si la aplicación termina de forma imprevista el fichero no se borra, y no se podrá ejecutar la aplicación hasta que borremos el fichero de forma manual.

La idea es siempre crear un objeto global desde el punto de vista del sistema operativo, que sólo exista mientras la aplicación esté en ejecución. La ventaja del mutex es que se destruye automáticamente cuando la aplicación termina, independientemente del modo en que termine, aunque sea a causa de un error.

Envío de email desde C++

CorreoDe nuevo, como necesidad de unos de los programas que tuve que escribir hace un tiempo, surgió el problema de enviar mensajes de correo electrónico desde un programa en C++.

De nuevo, me puse a buscar por Internet, con resultados decepcionantes, en la mayoría de los casos.

La opción más simple es usar MAPI (Messaging Application Programming Interface), pero esto tiene como consecuencia directa que se usa la aplicación de correo instalada en Windows como pasarela. Esto provoca algunos inconvenientes, al menos para la aplicación que estaba desarrollando.

  • En mi caso, la aplicación de correo era Outlook. Para evitar el envío de correos de forma indiscriminada, lo cual está bien, Outlook muestra un cuadro de diálogo cada vez que se intente enviar un mensaje desde fuera de él. Esto era un inconveniente en mi caso, ya que la aplicación debía funcionar automáticamente, de modo desatendido, es decir, sin la presencia de un usuario.
  • Los mensajes enviados de este modo permanecen en la carpeta de mensajes enviados. Esto es un problema menor, o incluso, ni siquiera es un problema.
  • Los mensajes se envían usando el servidor de correo por defecto para la cuenta de Outlook.

En mi caso concreto, el mayor inconveniente era el primero, pero no hay que descartar los otros problemas y otros que se me hayan podido pasar por alto.

Lo que buscaba era una forma de enviar correos independiente de las aplicaciones instaladas en el PC, y que funcionase incluso sin una aplicación de correo instalada.

La solución que más me gustó fue usar la librería Blat, (BLAT.DLL). Como siempre, por simplicidad, ya que requiere poca instalación (sólo una DLL), y es fácil de utilizar, al menos relativamente fácil.

Tengo que aclarar que la aplicación de la que hablo no se dedica a hacer spam, aunque pueda parecer lo contrario. Es más, espero que nadie use esta guía con esa finalidad. En la página de Blat advierten sobre el mismo tema. Por supuesto, no me hago responsable del uso que cada uno haga de esta guía, pero si la usas para actividades de legalidad dudosa, al menos no me menciones como fuente.

Descargar Blat

Existe una página dedicada al proyecto Blat: www.blat.net desde la que se puede descargar la última versión, ya sea para 32 ó 64 bits. En el momento que escribo esta entrada, la última versión es la 3.11, y usaré la versión de 32 bits.

Para descargar los ficheros necesarios basta con pulsar la opción «Download», que nos lleva a la página del proyecto en SourceForge, y donde podremos elegir la versión que mejor nos parezca. Para los ejemplos usaremos la versión completa, y no el código fuente.

El fichero zip que descargaremos contiene dos carpetas: docs y full.

La primera contiene información sobre la librería: historial de versiones, créditos, términos de licencia y sintaxis.

La segunda es la que más nos interesa, y contiene los siguientes ficheros:

  • blat.exe: programa para línea de comandos que permite enviar mensajes de correo.
  • blat.dll: librería de enlace dinámico, para usar blat desde nuestros programas en C/C++.
  • blatdll.h: fichero de cabecera para incluir en nuestros proyectos.
  • blat.lib: librería estática para poder enlazar con la dinámica.

El primer inconveniente, si el compilador que usamos es Mingw/GCC, es que la librería estática no nos sirve. Puedes consultar la entrada de cómo convertir librerías LIB a formato A para evitar este problema.

Después de obtener el fichero «libblat.a», lo copiaremos a la carpeta «lib» de Mingw. También tenemos que hacer que el fichero blatdll.dll sea accesible, bien haciendo que esté en la misma carpeta que nuestro programa ejecutable, o bien copiándolo en una carpeta cuya ruta esté incluida en el PATH.

Una vez hecho esto, ya podremos usar la DLL en nuestro programa.

Sintaxis

Para nosotros, la única función de la librería que nos interesa es Send, que sólo tiene un parámetro, en forma de cadena de caracteres terminada en nulo.

Esta sintaxis es engañosamente simple, ya que la sintaxis de la cadena que usaremos como argumento es bastante complicada.

La cadena consta varios parámetros, cada uno de los cuales está formado por dos cadenas. La primera, que siempre empieza con un ‘-‘ indica el nombre del parámetro, y la segunda el valor.

Hay muchos parámetros, pero no voy a exponerlos todos aquí, sólo aquellos imprescindibles para enviar mensajes con ficheros adjuntos.

Relativos al servidor de correo

Esta librería no funciona como un servidor de correo, sino que usa una cuenta existente en un servidor de correo electrónico en Internet. Por lo tanto, necesitamos tener acceso a una cuenta de correo, sus parámetros y contraseña. Puede ser un servidor SMPT o NNTP.

-server <addr>  : especifica el servidor SMTP a usar (opcionalmente, addr:port)
-serverSMTP <addr> : lo mismo que -server
-serverNNTP <addr> : especifica el servidor NNTP a usar (opcionalmente, addr:port)
-serverPOP3 <addr> : especifica el servidor POP3 a usar (opcionalmente, addr:port) cuando se requiera un acceso POP3 antes de enviar un email
-serverIMAP <addr> : especifica el servidor IMAP a usar (opcionalmente, addr:port) cuando se requiera un acceso IMAP antes de enviar un email
-f <sender>     : ignora la dirección de remite por defecto (debe ser conocida para el servidor)
-i <addr>       : una dirección de origen (‘From:’), no necesariamente conocida por el servidor
-port <port>    : puerto a usar por el servidor SMTP, por defecto para SMTP (25)
-portSMTP <port>: lo mismo que -port
-portNNTP <port>: puerto a usar por el servidor NNTP, por defecto para NNTP (119)
-portPOP3 <port>: puerto a usar por el servidor POP3, por defecto para POP3 (110)
-portIMAP <port>: puerto a usar por el servidor IMAP, por defecto para IMAP (110)
-u <username>   : nombre de usuario para AUTH LOGIN (usar con -pw) o para AUTH GSSAPI con -k
-pw <password>  : contraseña para AUTH LOGIN (usar con -u)
-pu <username>  : nombre de usuario para POP3 LOGIN (usar con -ppw)
-ppw <password> : contraseña para POP3 LOGIN (usar con -pu)
-iu <username>  : nombre de usuario para IMAP LOGIN (usar con -ppw)
-ipw <password> : contraseña para IMAP LOGIN (usar con -pu)

-k              : Use UNKNOWN mutual authentication and AUTH GSSAPI
-kc             : Use UNKNOWN client-only authentication and AUTH GSSAPI
-service <name> : Set GSSAPI service name (use with -k), default «smtp@server»
-level <lev>    : Set GSSAPI protection level to <lev>, which should be one of: None, Integrity, or Privacy (default GSSAPI level is Privacy)
-nomd5          : Do NOT use CRAM-MD5 authentication.  Use this in cases where the server’s CRAM-MD5 is broken, such as Network Solutions.

Direcciones de destino

-to <recipient> : recipient list (also -t) (comma separated)

-cc <recipient> : carbon copy recipient list (also -c) (comma separated)

-bcc <recipient>: blind carbon copy recipient list (also -b)

-ur             : set To: header to Undisclosed Recipients if not using the
-to and -cc options

-subject <subj> : subject line, surround with quotes to include spaces(also -s)
-ss             : suppress subject line if not defined

-body <text>    : message body, surround with quotes («) to include spaces

Ficheros adjuntos

-attach <file>  : attach binary file(s) to message (filenames comma separated)
-attacht <file> : attach text file(s) to message (filenames comma separated)
-attachi <file> : attach text file(s) as INLINE (filenames comma separated)
-embed <file>   : embed file(s) in HTML.  Object tag in HTML must specify
content-id using cid: tag.  eg: <img src=»cid:image.jpg»>
-af <file>      : file containing list of binary file(s) to attach (comma
separated)
-atf <file>     : file containing list of text file(s) to attach (comma
separated)
-aef <file>     : file containing list of embed file(s) to attach (comma
separated)
-base64         : send binary files using base64 (binary MIME)
-uuencode       : send binary files UUEncoded
-enriched       : send an enriched text message (Content-Type=text/enriched)
-unicode        : message body is in 16- or 32-bit Unicode format
-html           : send an HTML message (Content-Type=text/html)
-alttext <text> : plain text for use as alternate text
-alttextf <file>: plain text file for use as alternate text
-mime           : MIME Quoted-Printable Content-Transfer-Encoding
-8bitmime       : ask for 8bit data support when sending MIME
-multipart <size>
: send multipart messages, breaking attachments on <size>
KB boundaries, where <size> is per 1000 bytes
-nomps          : do not allow multipart messages
-contentType <string>
: use <string> in the ContentType header for attachments that
do not have a registered content type for the extension
For example: -contenttype «text/calendar»

Parámetros menos frecuentes

-organization <organization>
: Organization field (also -o and -org)
-ua             : include User-Agent header line instead of X-Mailer
-x <X-Header: detail>
: custom ‘X-‘ header.  eg: -x «X-INFO: Blat is Great!»
-noh            : prevent X-Mailer/User-Agent header from showing Blat homepage
-noh2           : prevent X-Mailer header entirely
-d              : request disposition notification
-r              : request return receipt
-charset <cs>   : user defined charset.  The default is iso-8859-1
-a1 <header>    : add custom header line at the end of the regular headers
-a2 <header>    : same as -a1, for a second custom header line
-dsn <nsfd>     : use Delivery Status Notifications (RFC 3461)
n = never, s = successful, f = failure, d = delayed
can be used together, however N takes precedence
-hdrencb        : use base64 for encoding headers, if necessary
-hdrencq        : use quoted-printable for encoding headers, if necessary
-priority <pr>  : set message priority 0 for low, 1 for high
-sensitivity <s>: set message sensitivity 0 for personal, 1 for private,
2 for company-confidential

Ejemplo C++

Veamos un ejemplo sencillo que envía un mensaje de texto con dos adjuntos:

#include <iostream>
#include <windows.h>
#include <cstdio>
#include "blatdll.h"

using namespace std;

int main()
{
    char cad[128];
    ULONG err;
    char nombre[2][256];
    char remiteCorreo[64];
    char servidorCorreo[64];
    char usuarioCorreo[64];
    char passwordCorreo[64];
    char asuntomsg[256];
    char textomsg[1024];
    char cmdline[4096];
    char szTo[64];
    char szCc[64];
    char szCo[64];

    strcpy(remiteCorreo, "<usuario@servidor>");
    strcpy(servidorCorreo, "<servidor.de.correo>");
    strcpy(usuarioCorreo, "<usuariodecorrreo>");
    strcpy(passwordCorreo, "<contraseña>");
    sprintf(nombre[0], ".\\main.cpp");  // Ruta de acceso del 1er fichero
    sprintf(nombre[1], ".\\blatdll.h"); // Ruta de acceso del 2º fichero

    sprintf(asuntomsg, "mensaje enviado usando Blat");
    sprintf(textomsg, "Cuerpo del mensaje.\nRealizando prueba.\n-- \nConClase\n");

    sprintf(cmdline, "-body \"%s\" -q -f %s -server %s -u %s -pw %s -attach %s,%s -base64 -subject \"%s\"",
            textomsg, remiteCorreo, servidorCorreo, usuarioCorreo, passwordCorreo, nombre[0], nombre[1], asuntomsg);
    cout << cmdline << endl;

    // Añadir direcciones To:
    strcpy(szTo, " -to <direccion_destino>");
    strcat(cmdline, szTo);

    strcpy(szCc, " -cc <direccion_copia>");
    strcat(cmdline, szCc);

    strcpy(szCo, " -bcc <direccion_copia_oculta>");
    strcat(cmdline, szCo);

    err = Send(cmdline);
    switch(err) {
         case 0: 
             sprintf(cad, "Mensaje enviado"); 
             break;
         case 1: 
             sprintf(cad, "Error de conexión o dirección de destino erronea."); 
             break;
         case 2: 
             sprintf(cad, "Conexión denegada o dirección de remite erronea."); 
             break;
         default: 
             sprintf(cad, "Error %ld", err); 
             break;
    }
    cout << cad << endl;
    if(err) return -1;

    return 0;
}

Bastará con sustituir los textos entre <> por lo adecuados en tu caso.

Crear librerías .a a partir de librerías .lib

LibrosCon cierta frecuencia encontramos bibliotecas (o librerías) de funciones que nos pueden resultar útiles en nuestros programas. En este blog hablaremos de algunas de ellas.

Casi siempre las encontraremos en formato de de bibliotecas de enlace dinámico (DLL), pero incluso en esos casos, para usarlas en nuestros programas C/C++ usaremos una biblioteca estática para acceder a ellas.

Un problema frecuente, cuando se trata de librerías de dominio público y cuando las queremos usar en Windows junto con MinGW/GCC es que la librería estática se distribuye con la extensión «.lib», y MingW/GCC usan el formato de Unix, con la extensión «.a».

No son formatos compatibles, y no basta con renombrar el fichero, ya lo intenté…

Afortunadamente, junto con el paquete de MinGW se distribuyen algunas herramientas que permiten hacer una conversión de «.lib» a «.a».

Crear una carpeta de trabajo

Con el fin de no contaminar carpetas de aplicaciones, o si no tenemos definido el PATH para la carpeta bin de MinGW, lo más sencillo es crear una carpeta de trabajo en la que haremos las conversiones que necesitemos. Puedes usar una carpeta temporal, o crear la que mejor te parezca.

En esa carpeta copiaremos los siguientes ficheros, que encontraremos en la carpeta MinGW/bin, cuya ruta completa dependerá de la instalación que hayas hecho del compilador.

  • reimp.exe
  • dlltool.exe
  • as.exe

El fichero «reimp.exe» no siempre viene con MinGW, de hecho, casi nunca viene. Forma parte de las utilidades de MinGW, y se encuentra en un paquete que se debe obtener por separado. Puedes buscar la última versión aquí.

Además, copiaremos los ficheros necesarios para hacer la conversión, y que generalmente estarán incluidos en el paquete de la biblioteca que hemos descargado:

  • <biblioteca>.lib
  • <biblioteca>.dll

Obtener el fichero .DEF

El primer paso consiste en obtener el fichero <biblioteca>.def. A veces el fichero forma parte del paquete, si es así, ¡enhorabuena!

Si no, no te preocupes, es fácil de obtener, basta con ejecutar esta orden desde una ventana de línea de comandos:

reimp -d <biblioteca>.lib

A veces hay que eliminar los caracteres ‘_’ que aparecen como prefijos en el fichero .def obtenido. Se puede hacer con un editor normal, ya que el fichero .def sólo contiene texto.

Una segunda alternativa, válida en caso de nos disponer del fichero .lib, es conseguir la utilidad «pexports«, que también forma parte de las utilidades de MinGW.

Para obtener el fichero .def, esta utilidad usa el fichero .dll, con una línea de comandos de este tipo:

pexports fichero.dll > fichero.def

En este caso podemos tener algunos problemas, que aunque fáciles de resolver, pueden darnos algunos dolores de cabeza.

Las funciones de una DLL pueden usar dos tipos de llamadas: stdcall y cdecl. Si se usa stdcall, el nombre de la función en el fichero .def debe tener un sufijo @’n’, donde n es un número entero.

Por desgracia, la utilidad pexports no es capaz de generar esos sufijos, de modo que tendremos que añadirlos manualmente, editando el fichero .def.

La cuestión es, cómo saber qué funciones usan stdcall, y cual es el valor de n en cada una de ellas.

Hasta donde he podido averiguar, la única forma es compilar el programa y ver qué mensajes de error nos da en la fase de enlazado.

Por ejemplo, si obtenemos un mensaje del tipo:

undefined reference to `<funcion>@4'

En este caso sabremos que la función <funcion> usa stdcall, y que debemos añadir el sufijo @4 en el fichero .def. Por supuesto, en ese caso, tendremos que volver a generar el fichero de librería estática .a.

Obtener el fichero .a

A partir del fichero .def y del fichero .dll podemos obtener el fichero de librería estática usando este comando:

dlltool -d <biblioteca>.def -D <biblioteca>.dll -k -l lib<biblioteca>.a -S ./as.exe

Es importante añadir el prefijo «lib» al fichero de biblioteca estática obtenido, ya que MinGW no lo encontrará si no tiene el formato lib<nombre>.a

Cada fichero en su lugar

Ahora sólo queda copiar cada fichero en su sitio.

El fichero con extensión «.a» debe copiarse a la carpeta «lib» de MinGW, de modo que el enlazador pueda localizarlo. Otra alternativa es copiarlo en otra ruta, pero en ese caso hay que añadir esa ruta a cada proyecto que use esta biblioteca.

El fichero con extensión «.h», que formará parte del paquete, debe copiarse a la carpeta «include» de MinGW, o a la carpeta del proyecto. Eso depende de tu propio criterio, y en última instancia, de con qué frecuencia uses estas bibliotecas.

El fichero «.dll» debe distribuirse junto a la aplicación. De nuevo, si se trata de una biblioteca que se usa con cierta frecuencia, puedes copiarla a una carpeta que esté en el PATH del sistema, o bien copiarla en la misma carpeta que el fichero ejecutable de la aplicación.

Cuando se cree un proyecto que use esta DLL hay que incluir el parámetro «-<biblioteca>» entre las opciones del enlazador.

Opciones

En este ejemplo se han añadido las bibliotecas «blat» y «sqlite». El enlazador buscará los ficheros de biblioteca estática «libblat.a» y «libsqlite.a».