Los inconvenientes de mezclar dos mundos


En Muchas ocasiones cuando vemos las letras C++ o C, realmente no nos damos cuento de cuanto diferentes son ambos mundos. El facil acceso a C desde C++ sin duda es una gran ventaja ya que por una parte nos permite acceder a la gran funcionalidad que C ha adquirido en todos estos años y por otro lado nos permite utilizar el gran número de librerías que existen para el lenguaje de programación más común.

En el gran y “tedioso” libro sobre C++ de Bjarne Stroustrup, podemos encontrar las razones de porque cuando pensaba en crear un nuevo lenguaje de programación, pensó en C. Sus principales razones fueron: “C puede compilarse y ejecutarse en casi cualquier dispositivo electrónico”.

Sin embargo como vamos a ver en este post, C y C++ realmente son mundos muy diferentes.

Esto viene a cuento por un problema con el que me he topado recientemente, y que me ha dado algún que otro quebradero de cabeza.

Supongamos que tenemos una librería diseñada para ser usada en C. Dicha librería permite hacer llamadas asíncronas, de tal manera que a la función que realiza el proceso debemos pasarle un puntero a función con la dirección a la que llamara cuando la accion se termine, lo que normalmente se conoce como callback. Pongamos como ejemplo, una funcion de lectura de datos.

lib_read(int handle, int waitTime, int (*read_callback)(void *buffer, int sizeRead) );

El prototipo de la función de callback sería algo como:

int my_callback(void *buffer, int size)
{
/* CODIGO */
}

El funcionamiento normal sería realizar la llamada de la siguiente manera:

read(my_handle, 1000 /*ms*/, my_callback);

Hasta aquí, todo correcto. Cuando el proceso haya leído los datos, se llamara a la función my_callback con los datos listos para ser procesados.

Os preguntareis, ¿Cual es el problema?
Como decía anteriormente, vamos a introducir a C++ en la ecuación.
Digamos que tenemos una clase que se encarga de realizar las lecturas de esos datos que hablabamos anteriormente. El ejemplo serÌa algo como:

class ReadHandler
{
public:
ReadHandler();
vector ProcessData();
private:
char* readData;
int read();
/*Teorica funcion de callback para la libreria de lectura */
int my_callback(void* buffer, int size);

}

El objetivo de esta clase es leer los datos y procesarlos, de tal manera que cuando llamamos a ProcessData nos devolverá un array con los datos ya procesados. Gracias al diseño orientado a Objectos de C++, los datos están encapsulados en su clase. Y si creamos varias instancias de ReadHandler, los datos no se compartirán.

Como hemos visto que la librería anterior se ajusta a nuestras necesidades, por lo que nos planteamos usarla para nuestro programa.
Para ello pensamos que el constructor de la clase puede ser un buen lugar para realizar la llamada asincrona, asi cuando nos llamen a ProcessData, si el callback ya ha rellenado los datos contenidos en readData los podamos procesar y devolver su contenido.

Con todo organizado en nuestra cabeza nos ponemos manos a la obra. Añadiremos la función de callback a la clase, y en el constructor la llamaremos

lib_read(handle, 1000, my_callback);

Compilamos y…. error. Todo parece correcto y aun así falla, ¿por qué?. Bienvenidos al mundo de C🙂

En la función le estamos pasando un puntero a función, y en C++, no es posible pasar de esa manera un puntero a una función miembro de la clase, como my_callback es, por lo que este será nuestro primer error, seguramente el mas sencillo de solucionar, ya que es el compilador quien se queja.

En C++, para crear un puntero a función miembro se debe usar el operando & precediendo al nombre, aún así, el compilador vuelve a fallar, ya que no es posible pasar un puntero a una función miembro no estática.

Esto nos deja un poco fuera de juego, ya que la librería era perfecta para nuestras necesidades y oye, al fin y al cabo C++ puede usar código C. El problema real es que la convención de llamadas a funciones en C y en C++ es diferente, en C++ aunque no se vea a ojos del programador, el primer parámetro de todas las funciones es un puntero a la instancia sobre la cual llamamos la función, lo que se conoce como la convención thiscall.

La siguiente aproximación puede pasar por abandonar temporalmente C++ y ya que la función necesita como callback, una función estática o una función que no sea de C++, pues podríamos usar una función de C.

El problema de esta aproximación es volver al “mundo” de C++, si el callback seria una función fuera de cualquier clase, no tendríamos una referencia a la instancia que llamo originalmente a la función que realiza la lectura asíncrona.

Llegados a este punto, hemos visto la problemática que tiene la situación, y de hecho este problema no tiene una solución elegante, pues se necesita que la librería proporcione un medio para pasar la instancia que le esta llamando.

Una solución bastante poco elegante pero que puede salvar alguna situación limite es usar una variable global para almacenar la instancia que llama a la función, y así poder hacer la llamada de vuelta.
En código sería algo como:

ReadHandler* global;
ReadHandler::ReadHandler()
{
global = this;
lib_read(…., callback_c);
}

int callback_c(…)
{
this->my_callback(…);
}

Sin duda, esta solución esta plagada de problemas, por ejemplo, ¿Qué pasaría si varios hilos crean instancias de ReadHandler?, pues como podemos predecir, habría un conflicto ya que la variable global no tendría un valor valido en cada llamada.

Dado a este problema y ya que es habitual utilizar librerías de C en C++, el diseño de este tipo de llamadas de forma correcta es crítico, y la mayoría de ellas habilita una variable donde se puede enviar la instancia que realiza la llamada. Por ejemplo, en la API de Windows nos encontramos con funciones con este prototipo

HANDLE WINAPI CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId );

Esta función crea un Hilo, llamando a la función en el parametro lpStartAddress, y pasandole como parametro el que le indicamos en lpParameter. El protipo del callback es:

DWORD WINAPI ThreadProc(LPVOID lpParameter );

Usando esta convención, en nuestro ejemplo:

ReadHandler::ReadHandler()
{
CreateThread(….,callback_compatible, (LPVOID) this, …. );
}

DWORD WINAPI callback_compatible(LPVOID lpParameter)
{
ReadHandler* rh = (ReadHandler) lpParameter;
this->my_callback(……);
}

Muchas librerías ya tienen esto en cuenta, y si estas desarrollando una librería deberias tenerlo en cuenta, ya que no es ningun esfuerzo extra y ganaras la compatibilidad con otros lenguajes como c++.


Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: