Ir al contenido principal

Modularidad, abstracción y múltiples instancias en C para Embedded Software

Está claro que la modularidad, el encapsulamiento y la abstracción fomentan la producción de software reutilizable, escalable, flexible, transportable y sumamente legible. Generando sistemas de software óptimos respecto del uso de memoria de datos y programa, menos complejos, fáciles de mantener y escalar. El presente artículo aborda estos principios y los aplica a un ejemplo concreto muy tradicional en los embedded systems, específicamente la estructura de datos queue, proveyendo diversas técnicas de programación no sólo para la construcción de este tipo de estructuras, sino también para la construcción de todo tipo de módulos de software genéricos, flexibles y escalables, desde la definición de su interfaz hasta su implementación en lenguaje C. Lo que implica generalizar, parametrizar e instanciar una solución general a un problema específico.

Adicionalmente, el artículo describe la implementación de varios patrones para instanciar la entidad que encapsula los atributos del módulo y así lograr que estos manipulen múltiples entidades. También explora la aplicación de punteros opacos, desmitificando su uso en la programación.

Las técnicas mostradas en el artículo se basan en los principios del diseño orientado a objetos (OOP) aplicados en C, lo que demuestra que estos, no son propios de un lenguaje o herramienta particular, sino más bien, son una manera disciplinada de organizar, diseñar y codificar software.

La misión del artículo es contribuir con técnicas y métodos de programación, para aumentar la productividad y calidad del desarrollo de embedded software.

Introducción


Supongamos que un determinado sistema procesa comandos recibidos asincrónicamente desde un único canal de comunicaciones. Los cuales se constituyen por cadenas de caracteres ASCII. En este caso particular, la entidad que los interpreta y procesa, se ejecuta de manera independiente, en contexto y tiempo, a la recepción de los mismos. Por tal motivo, el sistema utiliza una estructura de datos cola [1,5,8], ya que la misma permite almacenar en forma ordenada una cierta cantidad de elementos, en este caso caracteres ASCII del tipo char, para luego, en otro contexto y tiempo, ser extraídos y procesados acordemente, desacoplando así, la recepción del procesamiento de los datos. Particularmente, en esta estructura, los elementos se extraen en orden cronológico, desde el más antiguo al más reciente, esto determina que la cola sea una estructura de datos First-Input First-Output (FIFO), en la cual el primer elemento agregado será el primero en extraerse. Generalmente, se implementa como un buffer circular [8] o una lista enlazada [8].

El sistema en cuestión, implementa la cola de caracteres como un simple y estático buffer circular, también conocido como ring buffer [5,8], cuya estructura se muestra en la figura F1.


F1 - Estructura del ring buffer

Tanto las operaciones como los atributos de la cola de caracteres se concentran (encapsulan) en un único par de archivos, donde el archivo chqueue.h hace visibles las variables y operaciones públicas, mientras que el archivo de implementación chqueue.c mantiene el cuerpo de las funciones públicas y privadas, al igual que sus variables. El archivo de inclusión debe mantenerse limpio y claro, ya que este establece la interfaz que provee al resto del sistema para que pueda utilizarlo. De esta manera, se mantiene la modularidad y la simpleza del sistema de software. Por lo general, el nombre de las operaciones públicas de un módulo con funciones específicas, contiene un prefijo, que se utiliza para identificar claramente a qué módulo pertenece y además evitar que se solapen nombres entre módulos del sistema. Así, el archivo chqueue.h sería algo así como muestra el listado L1:

#ifndef __CHQUEUE_H__
#define __CHQUEUE_H__

...

int ChQueue_init(void);
int ChQueue_insert(const char *pData);
int ChQueue_remove(char *pData);
int ChQueue_isEmpty(void);
int ChQueue_checkQty(unsigned numA, unsigned denA,
                     unsigned numB, unsigned denB);

#endif
L1 - Archivo de inclusión, chqueue.h

El listado L2 muestra parte del archivo de implementación asociado. En caso que estas funciones se ejecuten concurrentemente, debe protegerse su código crítico (o sección crítica) [1,5,9]. En la función ChQueue_init() y a modo ilustrativo, se "encierra" el código crítico entre las macros __ENTER_CRITICAL() y __EXIT_CRITICAL(), estas garantizan el acceso exclusivo a la sección crítica. Por razones de legibilidad, los listados de código del presente artículo omiten estas protecciones, sin embargo, un sistema real si debe incluirlas. Generalmente, estos mecanismos son dependientes de la plataforma en uso.

#include "chqueue.h"

#define NUM_CHARRAY               128
#define LIMIT_NUM_CHARRAY         (NUM_CHARRAY * 4 / 5)
#define GET_LIMIT(num_, den_)     \
            (NumElem)((long)NUM_CHARRAY * (num_) / (den_))

static char chArray[NUM_CHARRAY];
static char *pIn, *pOut, *pTail;
static int qty;

int
ChQueue_init(void)
{ 
  __ENTER_CRITICAL();

  pIn = pOut = chArray;
  pTail = (uint8_t *)(chArray + sizeof(char) * NUM_CHARRAY);
  qty = 0;

  __EXIT_CRITICAL();
  return 0;
}

int 
ChQueue_insert(const char *pData)
{
  if (qty >= NUM_CHARRAY)
    return -CHQUEUE_FULL;

  if (pData == NULL)
    return -CHQUEUE_BAD_PTR;

  *pIn++ = (char*)*pData;

  if (pIn == pTail)
    pIn = chArray;

  ++qty;
  return CHQUEUE_SUCCESS;
}

int 
ChQueue_remove(char *pData)
{
  if (qty == 0)
    return -CHQUEUE_EMPTY;

  if (pData == NULL)
    return -CHQUEUE_BAD_PTR;  

  *pData = *pOut++;

  if (pOut == me->pTail) 
    pOut = chArray;

  --qty;
  return QUEUE_SUCCESS; 
}

int 
ChQueue_isEmpty(void)
{
  return qty == 0;
}

int 
ChQueue_checkQty(unsigned numA, unsigned denA,
                 unsigned numB, unsigned denB)
{
  int upperLimit, lowerLimit;
  
  upperLimit = GET_LIMIT(numA, denA); 
  lowerLimit = GET_LIMIT(numB, denB);
  return (qty > upperLimit) ? 1 : ((qty < lowerLimit) ? -1 : 0);
}
L2 - Parte del archivo de implementación, chqueue.c

Donde:

      (9) Almacena los elementos de la cola, en este caso, caracteres ASCII. Sobre este se aplica la estructura de cola.
      (10) El puntero pIn indica la siguiente posición en donde insertar un nuevo elemento en la cola, operación ChQueue_insert(), el puntero pOut indica la siguiente posición desde donde extraer el elemento más antiguo de la cola, operación ChQueue_remove(), y el puntero pTail mantiene la posición final del buffer, evitando que la misma se calcule durante la ejecución de las operaciones.
      (11) Para determinar si las operaciones de inserción y extracción de elementos son factibles, se utiliza el contador qty, que mantiene el número de elementos en la cola, con lo cual, una inserción es posible si hay al menos un lugar disponible, de forma similar, una extracción es posible si hay al menos un elemento en el cola. 
      (29) La palabra const (calificador) en la declaración del puntero pData, asegura que la función no cambie su valor [9].

Ahora consideremos la siguiente situación, en la cual, luego de un buen tiempo de implementada chque, el sistema requiere una nueva cola, esta vez para almacenar patrones de datos de 48-bits, en otras palabras, elementos del tipo typedef unsigned char BitPatt[NUM_BYTE_PAT], donde NUM_BYTE_PAT define la cantidad de bytes del patrón, en este caso 6. Dado que el sistema ya cuenta con una implementación probada y funcionando de una cola (chque), la solución, en principio, es tan simple como copiar y adaptar esta última, cambiando nombres, tipos de datos y funciones, creando así la cola bpqueue. Esta manera de proceder, podríamos definirla como el método de la "fuerza bruta", que si bien, es simple y directo, trae consigo una serie de desventajas, ya que no sólo duplica el código de programa, desperdiciando memoria, sino también complejiza su mantenimiento. Afortunadamente, existen otros métodos, que no sólo evitan estas cuestiones sino que además proveen beneficios adicionales. La descripción de estos es el objetivo del presente artículo.

 El listado L3 muestra el archivo de inclusión bpqueue.h, que define la funcionalidad de la cola bpqueue.

#ifndef __BITPATTQUEUE_H__
#define __BITPATTQUEUE_H__

#define NUM_BYTE_PAT    6
typedef unsigned char BitPatt[NUM_BYTE_PAT];
...

void BitPattQueue_init(void);
int BitPattQueue_insert(const BitPatt *pbp);
int BitPattQueue_remove(BitPatt *pbp);
int BitPattQueue_isEmpty(void);

#endif
L3 - Archivo de inclusión, bpqueue.h

La implementación de bpqueue, siguiendo el principio de la "fuerza bruta", no se ejemplifica, queda como tarea al lector.

Generalización


Para evitar los inconvenientes anteriores, básicamente necesitamos un único módulo de software capaz de administrar múltiples colas, que pueda adaptarse a las necesidades funcionales de cada cola en particular. Como veremos, esto implica generalizar, parametrizar, instanciar y finalmente especializar. Esto produce sistemas de software altamente escalables y flexibles.

Para aumentar nuestra productividad, partiremos del módulo de cola chque, el cual se encuentra probado y en pleno funcionamiento. Luego, analizaremos las características y funcionalidades de las colas chque y bpqueue, las compararemos y encontraremos sus diferencias,  las cuales definirán los parámetros del nuevo módulo de colas, de aquí en adelante queue. Luego diseñaremos e implementaremos este módulo, generalizando chque, de modo tal que pueda manipular múltiples colas, de acuerdo con los parámetros establecidos para cada una de ellas, sin la necesidad de repetir código de programa. La figura F2 explicita las diferencias en atributos y operaciones entre chque y bpqueue, definiendo los parámetros de queue.


F2 - Diferencias entre chque y bpqueue

Generalizado el módulo chque, y para lograr mayor flexibilidad, concentramos los datos o atributos  de cada cola en una única entidad, la estructura Queue, como muestra el listado L4. Los atributos de Queue no sólo permiten la administración de una cola en particular, sino también mantienen las características de la misma, es decir, los valores de los parámetros que admite queue. Esto implica que exista una instancia de Queue por cada cola que se requiera utilizar. Además, y como consecuencia del uso de Queue, el módulo queue puede manipular múltiples colas sin mayores problemas. La concentración de los atributos en un único tipo de datos, como Queue, tiene muchos beneficios, aún cuando exista una única instancia en el programa.

typedef int NumElem;

typedef struct Queue Queue;
struct Queue
{
    uint8_t elemSize;
    NumElem nElem;
    NumElem qty;
    uint8_t *pArray;
    uint8_t *pIn;
    uint8_t *pOut;
    uint8_t *pTail;
};
L4 - Generalización mediante estructura Queue

Donde:

      (1) Indica el tipo de los atributos relacionados con la cantidad de elementos. El uso de typedef para definir tipos es, en términos generales, una buena práctica, que facilita el mantenimiento y la actualización del software, entre otras cosas.
      (3-12) Define el tipo Queue. De la figura F2, sus atributos nElem y elemSize, son los parámetros que se ajustan de acuerdo con la cola en particular, mientras que el resto, son básicos para el funcionamiento del módulo queue.

En este contexto, una instancia es simplemente un objeto de tipo Queue ubicado en memoria. De la misma manera que una variable específica x (de tipo int) es una instancia de su tipo (int), donde el mismo conjunto de operadores (como los aritméticos) se aplican sobre todas las variables de ese tipo, sin embargo, los valores específicos de una instancia difieren de otras, por ejemplo y.

Luego, queue debe proveer al menos las operaciones básicas para manipular una cola o instancia de Queue, como Queue_insert() y Queue_remove(). Estas son suficientes para lograr la funcionalidad que requiere bpqueue, no obstante, chque requiere una función adicional,  para verificar si la cantidad de elementos en cola está en un rango específico, determinado por las llamadas "marcas de agua", de acuerdo con la función ChQueue_checkQty(). Para lo cual queue agrega la función Queue_getNum(). De esta manera, la funcionalidad de ambas colas, chque y bpqueue, queda en función de la provista por queue.

Identificando las instancias


Dado que queue administra múltiples colas o instancias de Queue, para asegurar que sus funciones accedan a los datos de la instancia correcta, necesitamos indicarle de qué instancia se trata. Si bien existen diversas maneras de resolver dicha situación, sólo exploraremos la identificación de instancias por referencia y por descriptores. Esto está directamente ligado con el tipo de interfaz que presente queue.

Por referencia


La manera más tradicional de indicarle de qué instancia se trata, consiste en pasar a cada función (operación) de queue un puntero a la instancia en cuestión, generalmente de nombre me. Esto le permite a queue manejar múltiples instancias Queue y asegurar que sus funciones operan con las copias correctas de los datos. Así, el listado L5 muestra parte de las definiciones públicas de queue, ubicadas posiblemente en el archivo de inclusión queue.h. En este caso, y por cuestión meramente estilísticas, el nombre de las operaciones se forma por <prefijo>_<operación>(), donde el prefijo provien del nombre del tipo Queue. Del mismo modo, el tipo Queue, se utiliza para definir los nombres de los archivos de interfaz e implementación, queue.h y queue.c, respectivamente, es decir, se intenta mantener una coherencia en la manera de codificar software, de modo tal que sea más legible y fácil de mantener.

#ifndef __QUEUE_H__
#define __QUEUE_H__

typedef int NumElem;

typedef struct Queue Queue;
struct Queue
{
  uint8_t elemSize;
  NumElem nElem;
  NumElem qty;
  uint8_t *pArray;
  uint8_t *pIn;
  uint8_t *pOut;
  uint8_t *pTail;
};

int Queue_insert(Queue *const me, const void *pElem);
int Queue_remove(Queue *const me, void *pElem);
int Queue_is_full(Queue *const me);
int Queue_isEmpty(Queue *const me);
NumElem Queue_getNum(Queue *const me);

#endif
L5 - Definiciones públicas de queue por referencia

La palabra const luego del * en la declaración del puntero me, asegura que las operaciones no puedan cambiar su valor, o sea, la instancia sobre la que se intenta operar.

Si bien el método presentado se emplea asiduamente en lenguaje C, manifiesta un inconveniente: la definición de la estructura Queue queda innecesariamente visible al sistema, y por lo tanto los datos asociados con las instancias Queue se vuelven vulnerables, propensos a errores no intencionales del programa e inclusive puede que se pierda el encapsulamiento y la abstracción propuesta, en especial el ocultamiento de la implementación. Para evitar estos cuestiones, los miembros de Queue no deberían accederse directamente, sino a través de las operaciones provistas por queue.h, de esta manera, si bien Queue se mantiene visible, el acceso a los datos es privado y exclusivo a queue. Sin embargo, como veremos en breve, existen algunas técnicas más robustas y fiables para solucionar esta cuestión, de modo tal de no poner en riesgo la abstracción y el encapsulamiento, evitando que estas queden libradas a las buenas prácticas del programador.

Definida las operaciones básicas de queue, podemos codificar y presentar en el listado L6 parte del archivo de implementación de chqueue, escrito en función de los servicios provistos por queue. Lo notable aquí es que la interfaz definida en chqueue.h no requiere cambio alguno, lo que significa que esta se mantiene aislada de la implementación, y por lo tanto el software que utiliza las operaciones de chqueue no sufre cambio alguno.

#include "queue.h"
#include "chqueue.h"

#define NUM_CHARRAY             128
#define LIMIT_NUM_CHARRAY       ((NUM_CHARRAY * 4) / 5)
#define GET_LIMIT(num_, den_)   \
            (NumElem)((long)NUM_CHARRAY * (num_) / (den_))

static char chArray[NUM_CHARRAY];
static Queue chque;
...

int
ChQueue_insert(const unsigned char *pCh)
{
  int status;

  if ((status = Queue_insert(&chque, pCh)) < 0)
    return status;
  return 0;
}

int
ChQueue_remove(char *pCh)
{
  int status;

  if ((status = Queue_remove(&chque, pCh)) < 0)
    return status;
  return 0;
}

int
ChQueue_isEmpty(void)
{
  NumElem qty;

  qty = Queue_getNum(&chque);
  return qty;
}

NumElem
ChQueue_checkQty(unsigned numA, unsigned denA,
                 unsigned numB, unsigned denB)
{
  NumElem qty, upperLimit, lowerLimit;
  
  upperLimit = GET_LIMIT(numA, denA); 
  lowerLimit = GET_LIMIT(numB, denB);
  qty = Queue_getNum(&chque);
  return (qty > upperLimit) ? 1 : ((qty < lowerLimit) ? -1 : 0);
}
L6 - Operaciones de chqueue por referencia

En L6 el objeto chqueue se asigna estáticamente en memoria.

Por descriptores


Otra manera de identificar las instancias es por medio de números enteros no negativos, también conocidos como descriptores, conceptualmente similar a los descriptores de archivos [10] en el OS Linux. Estos números los suministra el módulo en cuestión, en este caso queue, el cual utiliza internamente el descriptor para encontrar la referencia a la memoria asignada. De esta forma, no se necesita conocer la definición de la estructura Queue y por lo tanto, esta última permanece privada al módulo queue y oculta al resto del sistema. Por "privada" se entiende que el acceso directo a los miembros de la estructura Queue es exclusivo de queue y por "oculta" que el resto del sistema accede a estos únicamente a través las operaciones públicas provistas por queue. Esta es la diferencia más notable con respecto al método por referencia, donde la abstracción y el encapsulamiento son más débiles. Por estas razones, este método produce programas más limpios, seguros, fáciles de crear, y mantener. Sus desventajas quedan para análisis del lector.

El listado L7, muestra las definiciones públicas de queue utilizando la interfaz por descriptores, ubicadas en el archivo de inclusión queue.h. Donde la definición de la estructura Queue ya no es visible, en su lugar, se incluye el tipo QD_T para los descriptores.

#ifndef __QUEUE_H__
#define __QUEUE_H__

typedef int NumElem;
typedef int QD_T; /* Queue descriptor */

int Queue_insert(QD_T qd, const void *pElem);
int Queue_remove(QD_T qd, void *pElem);
int Queue_is_full(QD_T qd);
int Queue_isEmpty(QD_T qd);
NumElem Queue_getQty(QD_T qd);

#endif
L7 - Definiciones públicas de queue por descriptores

El listado L8 muestra una posible implementación de alguna de las operaciones de chqueue, utilizando queue en su versión con interfaz por descriptores.

#include "queue.h"
#include "chqueue.h"

#define NUM_CHARRAY             128
#define LIMIT_NUM_CHARRAY       (NUM_CHARRAY * 4 / 5)
#define GET_LIMIT(num_, den_)   \
            (NumElem)((long)NUM_CHARRAY * (num_) / (den_))

static char chArray[NUM_CHARRAY];
static QD_T chque;
...

int
ChQueue_insert(const unsigned char *pCh)
{
  int status;

  if ((status = Queue_insert(chque, pCh)) < 0)
    return status;
  return 0;
}

int
ChQueue_remove(char *pCh)
{
  int status;

  if ((status = Queue_remove(chque, pCh)) < 0)
    return status;
  return 0;
}

int
ChQueue_isEmpty(void)
{
  NumElem qty;

  qty = Queue_getNum(chque);
  return qty;
}

NumElem
ChQueue_checkQty(unsigned numA, unsigned denA,
                 unsigned numB, unsigned denB)
{
  NumElem qty, upperLimit, lowerLimit;
  
  upperLimit = GET_LIMIT(numA, denA); 
  lowerLimit = GET_LIMIT(numB, denB);
  qty = Queue_getNum(chque);
  return (qty > upperLimit) ? 1 : ((qty < lowerLimit) ? -1 : 0);
}
L8 - Operaciones de chqueue por descriptores

Los listados L7 y L8 muestran dos implementaciones diferentes de chqueue, sin embargo, proveen los mismos servicios y mantienen la misma interfaz al resto del sistema, respetando uno de nuestros objetivos. Otra de las cuestiones importantes a resaltar es que: (1) si bien chqueue se implementa en función de los servicios de queue, ambas implementaciones se mantienen desacopladas, y (2) no es necesario implementar las operaciones de queue para resolver las funciones de chqueue.
   

Creando instancias


Una instancia de Queue es un objeto de tipo Queue asignado en memoria, el cual representa en tiempo de ejecución una cola determinada, como chqueue o bpqueue. Es necesario aclarar, que por definición, los objetos existen en tiempo de ejecución y en un momento determinado, lo que significa que pueden existir y dejar de hacerlo, a criterio del sistema. Al instanciar, la memoria asignada se reserva durante la existencia del objeto, desde su creación hasta su destrucción. Existen diversas maneras de implementar la asignación de memoria, el presente artículo explora los patrones de asignación de memoria: dinámico, estático y pool de objetos [1]. Por "pool" entendemos una colección o lista de objetos disponibles y preasignados en memoria. Otro de los métodos muy utilizados en embedded, es el patrón de bloques de tamaño fijo, el cual no describe el presente artículo, sin embargo las referencias [1,5] muestran implementaciones prácticas del mismo. 

Si bien la aplicación de un patrón u otro, se determina según las características particulares del sistema, es común encontrar que un mismo embedded system de complejidad media o superior, utilice uno o varios de los patrones listados.
    

Patrón de asignación dinámica de memoria


El hecho que un objeto exista durante un momento determinado, implica un instante de creación y otro de destrucción. Por destrucción, entendemos que el objeto en cuestión ya no se utiliza en el sistema y en consecuencia, es una buena oportunidad para que el sistema recupere la memoria que le asignó, de modo tal que, en principio, el sistema pueda re-utilizarla. En general, esta mecánica produce sistemas óptimos respecto de la utilización de memoria.

La manera más tradicional y simple de asignar y liberar memoria es mediante la asignación dinámica de memoria [1]. Si bien es un método de uso frecuente, es como una maldición para los embedded systems, incluyendo aquellos de tiempo-real, fundamentalmente porque presenta dos problemas: (1) la temporización no determinística durante la asignación y liberación de memoria [1], y (2) la fragmentación de memoria [1]. Ambos, pero en especial el segundo, puede provocar una falla fatal en el sistema. Justamente, por dichas razones, el uso de este método debe justificarse claramente, caso contrario, es preferible aplicar un método alternativo, como los detallados en este artículo.

Funciones especiales


Si durante la ejecución, el sistema requiere liberar memoria asignada a una instancia, necesitamos dotar a queue con funciones "especiales". Específicamente, un constructor que crea la instancia de Queue, un inicializador (opcional) que inicializa la instancia y sus atributos, y un destructor que libera la memoria asignada a la instancia. Por lo general, estas funciones adoptan nombres tradicionales, que aplicados al caso en cuestión podrían ser Queue_construct(), Queue_ctor(), Queue_create() o Queue_alloc() para el constructor, Queue_destroy()Queue_dtor(), Queue_close() o Queue_free() para el destructor, y Queue_init() para  el inicializador. Obviamente, si el sistema no necesita liberar la memoria asignada a las instancias, el constructor podría omitirse. 

Al listado L5 se agregan las operaciones "especiales" de queue.

#ifndef __QUEUE_H__
#define __QUEUE_H__

enum
{
  QUEUE_SUCCESS,
  QUEUE_BAD, QUEUE_NOT_ALLOCATED, QUEUE_FULL,
  QUEUE_NO_ROOM, QUEUE_BAD_PTR, QUEUE_EMPTY
};

typedef int NumElem;

typedef struct Queue Queue;
struct Queue
{
  uint8_t elemSize;
  NumElem nElem;
  NumElem qty;
  uint8_t *pArray;
  uint8_t *pIn;
  uint8_t *pOut;
  uint8_t *pTail;
};

int Queue_insert(Queue *const me, const void *pElem);
int Queue_remove(Queue *const me, void *pElem);
int Queue_is_full(Queue *const me);
int Queue_isEmpty(Queue *const me);
NumElem Queue_getNum(Queue *const me);

Queue *Queue_construct(void);
void Queue_destroy(Queue *const me);
int Queue_init(Queue *const me, void *pArray,
               uint8_t elemSize,
               NumElem nElem);

#endif 
L9 - Archivo de inclusión, queue.h para memoria dinámica

Asociado a L9, las funciones "especiales" podrían implementarse como muestra el listado L10, cuya interfaz se basa en la identificación de instancias por referencia. Por lo general, en lenguaje C, el uso de asignación dinámica de memoria, utiliza funciones como malloc() y free() de la biblioteca stdlib.h.

Queue *
Queue_construct(void)
{
  Queue *me = (Queue *)malloc(sizeof(Queue));
  return me;
}

void
Queue_destroy(Queue *const me)
{
  free(me);
}

int
Queue_init(Queue *const me, void *pArray,
           uint8_t elemSize,
           NumElem nElem)
{
  if (me != NULL)
  {
    me->pIn = me->pOut = me->pArray = (uint8_t *)pArray;
    me->pTail = (uint8_t *)((uint8_t *)pArray + elemSize * nElem);
    me->elemSize = elemSize;
    me->nElem = nElem;
    me->qty = (NumElem)0;
    return QUEUE_SUCCESS;
  }
  return -QUEUE_NOT_ALLOCATED;
}
L10 - Operaciones especiales con memoria dinámica

Generalmente, crear una instancia implica invocar al constructor y luego al inicializador, no obstante, y si el sistema lo permite, el constructor también podría inicializar los atributos de la instancia, como muestra el listado L11, para lo cual Queue_construct() agrega los parámetros de la operación  Queue_init().

Queue *Queue_construct(void *pArray, uint8_t elemSize, 
                                            NumElem nElem)
{
  Queue *me = (Queue *)malloc(sizeof(Queue)));
  
  if (me != NULL)
    Queue_init(me, pArray, elemSize, nElem);
  return me;
}
L11 - Constructor e inicializador con memoria dinámica

Siguiendo la idea de L11, y si el sistema no requiere re-inicializar la instancia previamente creada, la función Queue_init() puede omitirse, y por lo tanto el constructor podría implementarse como muestra L12.

Queue *Queue_construct(void *pArray, uint8_t elemSize, 
                                            NumElem nElem)
{
  Queue *me = (Queue *)malloc(sizeof(Queue)));
  
  if (me != NULL)
  {
    me->pIn = me->pOut = me->pArray = (uint8_t *)pArray;
    me->pTail = (uint8_t *)((uint8_t *)pArray + elemSize * nElem);
    me->elemSize = elemSize;
    me->nElem = nElem;
    me->qty = (NumElem)0;
  }
  return me;
}
L12 - Simplificación constructor e inicializador con memoria dinámica

El listado L13 muestra a modo ilustrativo, parte de las operaciones básicas asociadas. Nuevamente, en estas funciones, por razones de legibilidad, se omite el uso del mecanismo apropiado y necesario para proteger las secciones de código críticas.

int
Queue_insert(Queue *const me, const void *pElem)
{
  NumElem qty;

  if (me == NULL)
    return -QUEUE_NOT_ALLOCATED;

  qty = me->qty;

  if (qty >= me->num_elems)
    return -QUEUE_FULL;

  if (pElem == NULL)
    return -QUEUE_BAD_PTR;

  memcpy(me->pIn, pElem, me->elemSize);

  if ((me->pIn += me->elemSize) >= me->pTail)
    me->pIn = me->pArray;

  ++me->qty;
  return QUEUE_SUCCESS;
}


int
Queue_remove(Queue *const me, void *pElem)
{
  NumElem qty;

  if (me == NULL)
    return -QUEUE_NOT_ALLOCATED;

  qty = me->qty;

  if (qty == (NumElem)0)
    return -QUEUE_EMPTY;

  if (pElem == NULL)
    return -QUEUE_BAD_PTR;

  memcpy(pElem, me->pOut, me->elemSize);

  if ((me->pOut += me->elemSize) >= me->pTail)
    me->pOut = me->pArray;
  --me->qty;
  return QUEUE_SUCCESS;
}

int
Queue_is_full(Queue *const me)
{
  NumElem qty;

  if (me == NULL)
    return -QUEUE_NOT_ALLOCATED;

  qty = me->qty;
  return qty == me->nElem;
}

int
Queue_isEmpty(Queue *const me)
{
  NumElem qty;

  if (me == NULL)
    return -QUEUE_NOT_ALLOCATED;

  qty = me->qty;
  return qty == (NumElem)0;
}

NumElem
Queue_getNum(Queue *const me)
{
  NumElem qty;

  if (me == NULL)
    return -QUEUE_NOT_ALLOCATED;

  qty = me->qty;
  return qty;
}
L13 - Parte de las operaciones básica con memoria dinámica

Ahora estamos en condiciones de implementar el inicializador ChQueue_init() en base al constructor de queue.

#include "queue.h"
#include "chqueue.h"

#define NUM_CHARRAY             128
#define LIMIT_NUM_CHARRAY       (NUM_CHARRAY * 4 / 5)
#define GET_LIMIT(num_, den_)   \
            (NumElem)((long)NUM_CHARRAY * (num_) / (den_))

static char chArray[NUM_CHARRAY];
static Queue *chque;
...

int
ChQueue_init(void)
{
  chque = Queue_construct(chArray, sizeof(char), NUM_CHARRAY);
  return chque == NULL;
}

...
L14 - Inicializador de chqueue con asignación dinámica

Donde:

      (12) La variable global chqueue es un puntero del tipo Queue, que representa la instancia en cuestión, manteniendo la dirección de la memoria asignada a dicha instancia, provista por el constructor de queue. Este puntero lo utilizan las restantes operaciones de chqueue, como argumento me, de forma similar al listado L6.
    

Patrón de asignación estática de memoria


Para resolver los principales inconvenientes de la asignación dinámica, como el no-determinismo y la fragmentación, este patrón adopta un enfoque muy simple: no permitirla. Específicamente, asignando todas las instancias u objetos en startup, es decir, durante el arranque del sistema. Por lo general, este mecanismo es útil cuando el mapa de memoria (todos los objetos o instancias posibles) puede asignarse para el peor de los casos durante la ejecución del sistema. Esto significa que: (1) el peor de los casos se conoce y se comprende perfectamente, y (2) hay memoria suficiente para estos.
Los sistemas que adoptan este patrón no tienen demasiada diferencia entre el peor caso y el caso promedio de carga de memoria, es decir, existe poca variación para todos los perfiles de ejecución.

Al aplicar este patrón surge de manera inmediata la siguiente cuestión: la asignación estática de memoria, puede provocar que se necesite más memoria que la requerida si se utilizara la asignación dinámica. Por lo tanto, el sistema debe ser relativamente inmune al costo de la memoria.

Por otro lado, su aplicación tiene algunas consecuencias:
  1. Dado que la creación de todos los objetos (instancias) ocurre durante el arranque (startup), la ejecución del sistema luego de la inicialización es, por lo general, más rápida que cuando se utiliza la asignación dinámica, y en ciertos casos mucho más rápida. 
  2. La ejecución durante el funcionamiento del sistema es más predecible, debido a la eliminación de una de las principales fuentes del no determinismo (asignación dinámica de memoria). Por otro lado, dado que no se libera memoria, ya no existe el efecto de la fragmentación.

    Funciones especiales


    De manera similar al inicializador de chqueue para su versión con memoria dinámica, se muestra en L15 una posible implementación para su versión con asignación de memoria estática. El resto de las operaciones se mantiene de forma similar al listado L13.

    #include "queue.h"
    #include "chqueue.h"
    
    #define NUM_CHARRAY             128
    #define LIMIT_NUM_CHARRAY       (NUM_CHARRAY * 4 / 5)
    #define GET_LIMIT(num_, den_)   \
                (NumElem)((long)NUM_CHARRAY * (num_) / (den_))
    
    static char chArray[NUM_CHARRAY];
    static Queue chque;
    ...
    
    int
    ChQueue_init(void)
    {
      int status;
    
      status = Queue_init(&chque, chArray, sizeof(char), NUM_CHARRAY);
      return status < 0;
    }
    
    ...
    
    L15 - Inicializador de chqueue con asignación estática

    Donde:

          (11) La asignación de memoria para la instancia de Queue, chqueue, se efectúa en startup.
          (20) La memoria asignada al objeto chqueue, es externa al módulo queue, a diferencia de la implementación realizada en la sección anterior, en donde la memoria era proporcionada por queue, con lo cual el constructor, en principio, ya no es necesario, en su lugar se utiliza la función Queue_init().

    Como bien sugiere el listado L15, el constructor no es estrictamente necesario, aunque podría utilizarse, sólo para mantener cierta coherencia con los métodos descriptos en las secciones anteriores. En cuyo caso, el constructor inicializaría únicamente la instancia correspondiente según indique me, algo así como muestra el listado L16. Sin embargo, esto es una cuestión estilística irrelevante.

    int
    Queue_construct(Queue *const me, void *pArray,
                    uint8_t elemSize,
                    NumElem nElem)
    {
      if (me != NULL)
      {
        me->pIn = me->pOut = me->pArray = (uint8_t *)pArray;
        me->pTail = (uint8_t *)((uint8_t *)pArray + elemSize * nElem);
        me->elemSize = elemSize;
        me->nElem = nElem;
        me->qty = (NumElem)0;
        return QUEUE_SUCCESS;
      }
      return -QUEUE_NOT_ALLOCATED;
    }
    L16 - Constructor para asignación estática

    Por otro lado, si el sistema únicamente inicializa la instancia de Queue durante el arranque, el uso de la función ChQueue_init() no es necesaria y por lo tanto tampoco la función Queue_init(). En cuyo caso podríamos utilizar una macro como la mostrada en L17, QUEUE_CREATE(). La cual crea e inicializa la instancia durante el arranque, tal como muestra el listado L18. Aún así, y con cierto cuidado, la instancia podría re-inicializarse por medio de Queue_init(). Así, la inicialización es parte del trabajo del compilador.

    #define QUEUE_CREATE(me_, arr_, es_, ne_)
                  Queue me_ = {(es_), (ne_), (NumElem)0, \
                               (arr_), (arr_), (arr_), \
                               ((arr_)+(es_)*(ne_)}
    
    L17 - Inicializador estático en startup

    #include "queue.h"
    #include "chqueue.h"
    
    #define NUM_CHARRAY             128
    #define LIMIT_NUM_CHARRAY       (NUM_CHARRAY * 4 / 5)
    #define GET_LIMIT(num_, den_)   \
                (NumElem)((long)NUM_CHARRAY * (num_) / (den_))
    
    static char chArray[NUM_CHARRAY];
    static QUEUE_CREATE(chque, chArray, sizeof(char), NUM_CHARRAY);
    ...
    
    int
    ChQueue_insert(const char *pCh)
    {
      ...
    }
    
    L18 - Uso de inicializador estático en startup

    El patrón descripto puede aplicarse aún en sistemas que fueron concebidos utilizando el patrón de asignación dinámica y que por algún motivo, como el no determinismo y la fragmentación de memoria, requieren prescindir de su utilización, pero, en lo posible, manteniendo acotando el impacto del cambio, es decir, no pueden aplicarse las técnicas relacionadas con la creación e inicialización de instancias en startup mostradas recientemente. Obviamente, esto es posible, siempre y cuando, el sistema cumpla con las premisas de aplicabilidad descriptas: sistemas simples con cargas de memoria altamente predecibles y estables. En cuyo caso, puede practicarse una solución de compromiso o poco ortodoxa, en la cual se utiliza el constructor tradicional de la asignación dinámica, tal como fue concebido, pero se elimina el uso del destructor en todo el sistema, y por lo tanto, se evita el problema de la fragmentación y el no-determinismo causado por la liberación de memoria. No obstante, permanece el no-determinismo provocado en la creación de una instancia.

    Patrón de asignación de objetos desde un pool


    El patrón de asignación estática funciona correctamente, en sistemas que son de naturaleza estática. Si este no fuera el caso, por ejemplo, en funcionamiento se necesitan grupos de objetos con diversos propósitos en momentos diferentes, es decir, existe un cierto grado de dinamismo, ya no es adecuada la asignación estática. En su lugar, puede aplicarse el patrón de asignación de objetos desde un pool, el cual crea en startup diferentes colecciones estáticas de objetos del mismo tipo. En cuyo caso, si un cliente requiere un objeto de un tipo determinado, lo solicita de la colección correspondiente, cuando ya no lo precisa, lo libera, quedando nuevamente disponible para su re-utilización.

    Un claro ejemplo para la aplicación de este patrón, podría ser un sistema que requiere hasta 64 buffers de datos de 1024 bytes cada uno, pero no dispone de la memoria suficiente para asignarlos estáticamente en startup, ni puede tolerar la fragmentación de memoria provocada por la asignación dinámica, o mejor dicho, la probable falla debido a la fragmentación. Para resolver dicha situación, se realiza un análisis preciso del funcionamiento del sistema, del cual se determina que el peor caso de carga de memoria corresponde al uso simultáneo de hasta 8 de estos objetos, typedef uint8_t BUF_T[1024], que si pueden asignarse en memoria, con lo cual es realmente provechoso y adecuado utilizar este patrón, creando una colección de 8 objetos de tipo BUF_T, y así evitar tanto el uso excesivo de memoria como los inconvenientes de la asignación dinámica.

    Si bien este patrón, no soluciona las necesidades del uso de memoria dinámica, permite lidiar con sistemas más complejos que con el patrón de asignación estática, en lo referido al uso óptimo de memoria.

    Funciones especiales


    Al igual que el patrón de asignación dinámica, su aplicación requiere las funciones "especiales", para la creación y destrucción de instancias. Donde la creación implica solicitar un objeto a la colección correspondiente, mientras que la destrucción implica liberar o "devolver" el objeto a la colección.

    Las funciones especiales podrían adoptar una forma similar a la mostrada en el listado L19, cuya interfaz se basa en la identificación de instancias por referencia. En esta definición, el constructor se encarga de inicializar la instancia creada. Debido a la abstracción de la interfaz, de L19 es imposible determinar el patrón de asignación de memoria que utiliza el módulo queue.

    Queue *Queue_construct(void *pArray, uint8_t elemSize, 
                                                NumElem nElem);
    void Queue_destroy(Queue *conststroy(Queue *const me);
    int Queue_init(Queue *const me, void *pArray, 
                                       uint8_t elemSize, 
                                       NumElem nElem);
    
    L19 - Definición de operaciones especiales con pool y *me

    De forma similar al listado L7, también podría utilizarse la interfaz basada en descriptores, como muestra el listado L20.

    typedef int QD_T; /* Queue descriptor */
    
    QD_T Queue_construct(void *pArray, uint8_t elemSize, 
                         NumElem nElem);
    void Queue_destroy(QD_T qd);
    int Queue_init(QD_T qd, void *pArray, uint8_t elemSize, 
                   NumElem nElem);
    
    L20 - Definición de operaciones especiales con pool y descriptores

    El listado L21 muestra una posible implementación de las funciones especiales de este patrón, la cual se basa en la interfaz por descriptores, ya que por referencia se ejemplificó para la asignación dinámica.

    #include "queue.h"
    
    ...
    static Queue queues[MAX_QUEUES];
    
    
    static
    Queue *
    Queue_verify(QD_T qd, int *pStatus)
    {
      Queue *q;
    
      *pStatus = QUEUE_SUCCESS;
      if (qd < 0 || qd >= MAX_QUEUES)
      {
        *pStatus = -QUEUE_BAD;
        return NULL;
      }
    
      q = & queues[qd];
      if (q->pArray == NULL)
      {
        *pStatus = -QUEUE_NOT_ASSIGNED;
        return NULL;
      }
      return q;
    }
    
    
    QD_T
    Queue_construct(void *pArray, uint8_t elemSize,
                    NumElem nElem)
    {
      Queue *q;
    
      for (q = queues; q < queues + MAX_QUEUES; ++q)
        if (q->pArray == NULL)
        {
          q->pIn = q->pOut = q->pArray = (uint8_t *)pArray;
          q->pTail = (uint8_t *)((uint8_t *)pArray + elemSize * nElem);
          q->elemSize = elemSize;
          q->nElem = nElem;
          q->qty = (NumElem)0;
          return q - queues;
        }
        return -QUEUE_NO_ROOM;
    }
    
    void
    Queue_destroy(QD_T qd)
    {
      Queue *q;
      int status;
    
      if ((q = Queue_verify(qd, &status)) == NULL)
        return status;
    
      q->pArray = NULL;
      return QUEUE_SUCCESS;
    }
    
    int
    Queue_init(QD_T qd, void *pArray, uint8_t elemSize,
               NumElem nElem)
    {
      Queue *q;
      int status;
    
      if ((q = Queue_verify(qd, &status)) == NULL)
        return status;
    
      me->pIn = me->pOut = me->pArray = (uint8_t *)pArray;
      me->elemSize = elemSize;
      me->nElem = nElem;
      me->qty = (NumElem)0;
    
      return QUEUE_SUCCESS;
    }
    
    L21 - Implementación de operaciones especiales con pool y descriptores

    Donde:

          (4) La colección de objetos del tipo Queue se implementa de la manera más simple, por medio del arreglo queues. Cuya dimensión, indicada por MAX_QUEUES, podría ubicarse en queue.c o en  queue.h, dependiendo de la visibilidad que se considere oportuna. Si bien, la implementación del pool como un arreglo estático es simple, presenta algunas desventajas. Las cuales podrían salvarse utilizando, por ejemplo, listas enlazadas.
          (31) El constructor instancia e inicializa el objeto de tipo Queue. Su objetivo es buscar un objeto libre desde la colección, si lo encuentra devuelve un entero positivo que lo representa, el cual será utilizado por el resto de las operaciones como descriptor. Caso contrario, devuelve un número negativo informando que no hay espacio disponible. Un objeto libre, corresponde con el miembro pArray no asignado, de valor NULL. En esta implementación, el descriptor es el índice del array queues que corresponde con el objeto en cuestión.
          (9) Para aumentar la robustez del módulo, se utiliza la función privada Queue_verify(). La cual se invoca internamente por las operaciones de queue. Su propósito consiste en determinar la validez del descriptor proporcionado en la llamada a la operación, si este es válido y está asignado, devuelve la referencia a la memoria de la instancia. Caso contrario, devuelve un número negativo que indica el error producido. Esta función puede simplificarse o inclusive omitirse.
          (50) La destrucción de una instancia, consiste en liberar la memoria reservada a dicho objeto, en este caso, liberar un objeto de la colección, asignando NULL al campo pArray.

    El listado L22 muestra una posible implementación del inicializador de chque, ChQueue_init(), para su versión con asignación desde un pool e interfaz por descriptores. El resto de las operaciones se mantiene de forma similar al listado L21.

    #include "queue.h"
    #include "chqueue.h"
    
    #define NUM_CHARRAY             128
    #define LIMIT_NUM_CHARRAY       (NUM_CHARRAY * 4 / 5)
    #define GET_LIMIT(num_, den_)   \
                (NumElem)((long)NUM_CHARRAY * (num_) / (den_))
    
    static char chArray[NUM_CHARRAY];
    static QD_T chque;
    ...
    
    int
    ChQueue_init(void)
    { 
      chque = Queue_construct(chArray, sizeof(char), NUM_CHARRAY);
      return chque < 0;
    }
    
    ...
    
    L22 - Inicializador de chque con asignación desde un pool y descriptores


    Aún más abstracción utilizando punteros opacos


    Cuando se utiliza la interfaz por referencia, ver sección "Indetificando instancias por referencia" listado L5, la definición de la estructura Queue queda innecesariamente visible al sistema, y como consecuencia los datos asociados con las instancias Queue se vuelven vulnerables, propensos a errores no intencionales del programa e inclusive pone en riesgo el encapsulamiento y la abstracción propuesta, fundamentalmente el ocultamiento de la implementación. Para evitar estos potenciales problemas, los miembros de Queue deben accederse únicamente a través de las operaciones provistas por queue, de esta manera el acceso a los datos es privado y exclusivo. Aún así, existen otras técnicas más robustas y fiables, que intentan evitar estos inconvenientes, por ejemplo la interfaz por descriptores, presentada en las secciones previas, y la técnica basada en los llamados "punteros opacos" [2,3]. Un puntero opaco es uno tal que su tipo no está definido. El uso de estas técnicas hace más fuerte el encapsulamiento y la abstracción, evitando que estas queden libradas a las buenas prácticas del programador.

    En lenguaje C, la declaración:
    struct Queue;

    Es una declaración de tipo incompleto, que declara Queue como una etiqueta de estructura sin definir sus miembros, también se la conoce como especificador de tipo elaborado. Siendo Queue un tipo incompleto, el compilador desconoce cuanta memoria asignar a cada objeto Queue. Por lo tanto, este no permitirá declarar objetos como tal. Sin embargo, si permite declarar objetos del tipo "puntero a Queue" debido a que el tamaño de un puntero es independiente de lo que apunta, su tipo. La declaración anterior, declara Queue como una etiqueta, no como un tipo. En C, las etiquetas también se aplican a las uniones y enumerados. Por lo tanto, una declaración como:
    Queue *p;

    es un error, porque Queue no es un tipo. Para ello se utiliza typedef, que si declara un tipo, por ejemplo:
    typedef struct Queue Queue;

    Una cuestión a considerar, es que no es necesario escribir esta declaración luego de la definición del tipo incompleto, ya que en realidad este la incorpora. 

    Si Queue aún no ha sido declarado, entonces esta declaración define Queue como una etiqueta que designa un tipo incompleto y como un nombre de tipo que designa el mismo tipo incompleto. Luego, podemos completar el tipo incompleto especificando sus miembros en una definición como:


    struct Queue
    {
      uint8_t elemSize;
      NumElem nElem;
      NumElem  qty;
      uint8_t *pArray;
      uint8_t *pIn;
      uint8_t *pOut;
      uint8_t *pTail;
    };
    

    A partir de entonces podemos declarar tanto objetos Queue como punteros a Queue. En este contexto, un puntero de tipo incompleto es un puntero opaco. Por lo general, estos se emplean en la construcción de bibliotecas, ya que aíslan la interfaz de la biblioteca de su implementación.

    El presente artículo ejemplifica la práctica de esta técnica mediante dos métodos diferente. El primero de ellos, consiste en ocultar por completo la definición de Queue, mientras que el segundo, oculta Queue en forma parcial, de forma tal que la definición de Queue se constituya por dos partes, una privada y otra pública.


    Método:  definición privada


    El método oculta por completo la declaración de Queue, por lo tanto, los atributos de esta última son privados, es decir, no son visibles por el resto del sistema, tal como muestra L23.

    #ifndef __QUEUE_H__
    #define __QUEUE_H__
    
    #include <stdint.h>
    
    typedef struct Queue Queue;
    ...
    
    Queue *Queue_construct(void *pArray, uint8_t elemSize, 
                           NumElem nElem);
    int Queue_destroy(Queue *const me);
    ...
    
    #endif
    
    L23 - Puntero opaco - Método definición privada en queue.h

    Donde:

          (6) La interfaz de queue se mantiene de acuerdo con su definición basada en referencias, en lo que respecta a definiciones de tipos, constantes y operaciones. Respecto a la definición de tipos, se muestra una diferencia superlativa, el tipo Queue ya no define la estructura Queue, y por lo tanto, sus atributos no son públicos. No obstante y como se explicó anteriormente, el tipo Queue existe y es visible en la interfaz, por lo tanto los prototipos de funciones se mantienen de acuerdo con su versión tradicional, por ejemplo el listado L9.
          (11) De esta manera, el resto del sistema accede a los atributos de Queue, sólo a través de las operaciones públicas. Esto implica, que una acción realizada fuera de queue como chqueue->qty no es válida. Como consecuencia, hemos reforzado el encapsulamiento y la abstracción de queue.

    Una posible implementación del archivo queue.c, se muestra en el listado L23. Una particularidad del uso de los punteros opacos radica en que estos complican la práctica de la asignación estática externa, tal como se presentó en secciones anteriores, ya que un puntero opaco no especifica completamente su tipo y por lo tanto, no puede determinarse su tamaño, es decir, la sentencia static Queue chque, no es válida. Por lo tanto, es necesario que la asignación de memoria sea responsabilidad del módulo queue. En L24 se utiliza el patrón de asignación desde un pool.
    #include "queue.h"
    
    struct Queue
    {
      uint8_t elemSize;
      NumElem nElem;
      NumElem  qty;
      uint8_t *pArray;
      uint8_t *pIn;
      uint8_t *pOut;
      uint8_t *pTail;
    };
    
    static Queue queues[MAX_QUEUES];
    
    static
    Queue *
    Queue_verify(Queue *const me, int *pStatus)
    {
      *pStatus = QUEUE_SUCCESS;
    
      if (me == NULL || me->pArray == NULL)
      {
        *pStatus = -QUEUE_BAD;
        return NULL;
      }
      return me;
    }
    
    Queue *
    Queue_construct(void *pArray, uint8_t elemSize, NumElem nElem)
    {
      Queue *q;
    
      for (q = queues; q < queues + MAX_QUEUES; ++q)
        if (q->pArray == NULL)
        {
          q->pIn = q->pOut = q->pArray = (uint8_t *)pArray;
          q->pTail = (uint8_t *)((uint8_t *)pArray + elemSize * nElem);
          q->elemSize = elemSize;
          q->nElem = nElem;
          q->qty = 0;
          return q;
        }
    
      return NULL;
    }
    
    int
    Queue_destroy(Queue *const me)
    {
      int status;
    
      if (Queue_verify(me, &status) == NULL)
        return -QUEUE_BAD;
    
      me->pArray = NULL;  
      return QUEUE_SUCCESS;
    }
    ...
    
    L24 - Puntero opaco - Método definición privada en queue.c

    Donde:

          (3) El archivo de implementación queue.c define los atributos de Queue. De esta manera, el acceso a los miembros es privado y exclusivo de este archivo.
          (15) El asignación de memoria para las instancias de Queue se implementa mediante un pool, el cual es una colección de objetos del tipo Queue, arreglo queues. El hecho de declararlo con el identificador static nos asegura que este se inicializa a cero (NULL) en startup. La constante MAX_QUEUES define el tamaño del pool.
          (34) En este caso, se utiliza la asignación de memoria mediante un pool, donde el constructor busca un objeto libre de la colección, si lo encuentra devuelve su dirección, la cual será utilizada por el resto de las operaciones como parámetro me. Caso contrario devuelve NULL.
          (34,54) La implementación de las operaciones de queue es muy similar a las mostradas en las secciones anteriores.
          (20) La función privada Queue_verify() provee mayor robustez y fiabilidad al módulo queue. Básicamente y a modo ilustrativo, su misión consiste en determinar la validez de la referencia al objeto del pool. Su uso es opcional.

    El listado L25 muestra un ejemplo de uso de esta técnica, en el cual se implementa el inicializador de chque, ChQueue_init(). Tal como muestra L25, el uso de esta técnica no tiene impacto alguno respecto a las implementaciones tradicionales, ya que el truco se concentra exclusivamente en la declaración de tipo Queue, en el archivo queue.h. Este es una de los motivos que demuestra el poder de los punteros opacos.

    static Queue *chque;
    
    int
    ChQueue_init(void)
    {
      chque = Queue_construct(chArray, sizeof(char), NUM_CHARRAY);
      return chque == NULL;
    }
    ...
    
    L25 - Puntero opaco - Uso del método definición privada en chque

    Donde:

          (1,6) El uso de una cola sólo requiere mantener un puntero de tipo Queue, cuya dirección fue provista por el constructor, al igual que los métodos tradicionales.


    Método: definición pública y privada


    Si por algún motivo, utilizando el método anterior, necesitamos publicar parte de los atributos de la estructura privada Queue. Una de las posible solución es utilizar las bondades del anidamiento de estructuras que provee el lenguaje C, la cual está relacionada con la herencia simple de clases implementada en C [7].

    Concretamente, definiremos el tipo Queue como una estructura que contiene los campos que necesitamos publicar. Esta se publicará en el archivo de inclusión, queue.h.

    typedef struct
    {
      char name[QUEUE_NAME_SIZE];
    } Queue;
    

    Luego, mediante el anidamiento de estructuras, la vincularemos con la estructura que contiene los atributos privados de una cola, para continuar con la idea de concentrar todo lo relacionado con colas en una única entidad. La definición de esta estructura es privada al archivo de implementación queue.c. Su declaración es similar al método anterior.

    typedef struct PrivQueue PrivQueue;
    struct PrivQueue
    {
      Queue base;
      uint8_t elemSize;
      int nElem;
      int qty;
      uint8_t *pArray;
      uint8_t *pIn;
      uint8_t *pOut;
      uint8_t *pTail;
    }
    

    Donde PrivQueue es la estructura privada que desciende de Queue, y en consecuencia hereda sus atributos. A su vez, PrivQueue extiende los atributos de su clase base (Queue), para incorporar las características esenciales de una cola, manteniéndolos ocultos al resto del sistema.

    De esta manera, flexibilizamos el método anterior, ya que por un lado tenemos el control de publicar los atributos que se requieran, y por otro mantenemos el mismo grado de abstracción, ocultando los detalles de la implementación. El listado L26 aplica el método descripto.

    #include <stdint.h>
    
    #define QUEUE_NAME_SIZE    8
    
    typedef struct
    {
      char name[QUEUE_NAME_SIZE];
    } Queue
    ...
    
    
    Queue *Queue_construct(void *pArray, uint8_t elemSize, 
                           NumElem nElem);
    int Queue_destroy(Queue *const me);
    ...
    
    L26 - Puntero opaco - Método definición pública y privada, queue.h

    Donde:

          (5-8) El archivo de inclusión queue.h, define la estructura que concentra los atributos visibles de una cola, en este caso, el campo name, que mantiene el nombre de la cola en cuestión. Este campo es meramente ilustrativo.
          (12,14) Observemos que la interfaz no presente cambios respecto del método anterior, ya que el tipo Queue existe, definiendo en este caso, los atributos visibles.

    Siguiendo la idea básica de la implementación propuesta para el método anterior, el listado L27 muestra parte del archivo queue.c.
     
    #include "queue.h"
    
    ...
    #define Q_PRIV_CAST(me_)    ((PrivQueue*)(me_))
    #define Q_PUB_CAST(me_)     ((Queue*)(me_))
    
    typedef struct PrivQueue PrivQueue;
    struct PrivQueue
    {
      Queue base;
      uint8_t elemSize;
      uint8_t elemSize;
      NumElem nElem;
      NumElem  qty;
      uint8_t *pArray;
      uint8_t *pIn;
      uint8_t *pOut;
      uint8_t *pTail;
    };
    
    static PrivQueue queues[MAX_QUEUES];
    
    static
    Queue *
    Queue_verify(Queue *const me, int *pStatus)
    {
      *pStatus = QUEUE_SUCCESS;
    
      if (me == NULL || Q_PRIV_CAST(me)->pArray == NULL)
      {
        *pStatus = -QUEUE_BAD;
        return NULL;
      }
      return me;
    }
    
    Queue *
    Queue_construct(void *pArray, uint8_t elemSize, NumElem nElem)
    {
      PrivQueue *q;
    
      for (q = queues; q < queues + MAX_QUEUES; ++q)
        if (q->pArray == NULL)
        {
          q->pIn = q->pOut = q->pArray = (uint8_t *)pArray;
          q->pTail = (uint8_t *)((uint8_t *)pArray + elemSize * nElem);
          q->elemSize = elemSize;
          q->nElem = nElem;
          q->qty = 0;
          return Q_PUB_CAST(q);
        }
    
      return NULL;
    }
    
    int
    Queue_destroy(Queue *const me)
    {
      int status;
    
      if (Queue_verify(me, &status) == NULL)
        return -QUEUE_BAD;
    
      Q_PRIV_CAST(me)->pArray = NULL;
      return QUEUE_SUCCESS;
    }
    ...
    
    L27 - Puntero opaco - Método definición pública y privada, queue.c

    Donde:

          (8) El tipo PrivQueue define una estructura que concentra o agrupa, tanto los atributos privados como los públicos. Los atributos públicos los define el tipo Queue, publico en la interfaz de queue, mientras que los atributos privados, al igual que el método anterior, se mantienen ocultos al resto del sistema.
          (10) La estructura de tipo PrivQueue tiene como primer miembro la estructura Queue, para que PrivQueue sea una estructura derivada de Queue, siendo esta última la estructura base o padre [7].
          (7) Esta declaración define el tipo PrivQueue. En este método su aplicación es estilística, a diferencia del método anterior, ya que podríamos omitirla, declarando el tipo PrivQueue directamente en la definición de la estructura, es decir, typedef struct{ Queue base; .... } PrivQueue. Obviamente, sin necesidad de etiquetar la estructura.
          (5) La conversión de tipo propuesta en la macro permite tratar de forma segura, un puntero de tipo PrivQueue (estructura o clase derivada) como uno de tipo Queue (estructura o clase base). Siendo esto último legal, transportable y garantizado por el standard de C, ya que el anidamiento de estructuras propuesto siempre alinea el miembro base al comienzo de cada instancia de la estructura, de acuerdo con ISO/IEC 9899:TC2 [6,7], es decir, no hay rellenos (padding) al comienzo de una estructura. En términos de OOP, la conversión de tipo de una clase derivada a una clase base más general se denomina upcasting.
          (4) La macro propone la conversión de tipo de una clase base a una clase derivada, lo que en términos de OOP se denomina downcasting. Aunque no es una práctica 100% segura, el ejemplo en cuestión la utiliza con cierta precaución.
          (22) Al igual que el método anterior, la asignación de memoria se efectúa como un pool de objetos, en este caso de tipo PrivQueue.
          (41) El constructor busca un objeto libre desde el pool de tipo PrivQueue, si lo encuentra, devuelve su dirección, de lo contrario devuelve NULL.
          (53) Dado que la asignación se realiza sobre objetos de tipo PrivQueue, y el prototipo del constructor devuelve un puntero a Queue, se realiza la conversión de tipo de la clase derivada (PrivQueue) a su clase base (Queue), upcasting
          (68) La implementación de las operaciones es similares a las del método anterior, la diferencia radica en que desde queue, se manipulan objetos de tipo PrivQueue pero el parámetro que indica la instancia en cuestión, es de tipo Queue, por lo tanto, se requiere aplicar un downcating, para acceder a los atributos de PrivQueue. En este caso, la conversión es segura, ya que la asignación de memoria proviene del constructor, y este referencia objetos de tipo PrivQueue. En caso que se instancie directamente un tipo Queue, sin utilizar el constructor, el downcast ya no es seguro, produciendo una falla catastrófica. Existen algunas técnicas para aumentar la seguridad en esta operación, su investigación queda para el lector.

    El uso de queue no tiene cambio alguno respecto del método anterior, ver listado L28. Sin embargo, el resto del sistema puede acceder a los atributos visibles definidos por Queue, por ejemplo al campo name. Nuevamente, chque es un puntero opaco, ya que su tipo no esta totalmente definido.

    static Queue *chque;
    
    int
    ChQueue_init(void)
    {
      chque = Queue_construct(chArray, sizeof(char), NUM_CHARRAY);
      strcpy(chque->name, "chque");
      return chque == NULL;
    }
    ...
    L28 - Puntero opaco - Uso del método definición pública y privada en chque

    Conclusiones


    El cuadro de la figura F3 resume las técnicas y métodos detallados en el presente artículo. Si bien, existen otras maneras de lograr el encapsulamiento y la abstracción de un módulo de software escrito en C, los temas expuestos en el artículo son básicos y elementales para construir nuevas y diferentes técnicas. El cuadro sigue el proceso de desarrollo propuesto por el artículo, donde primero se trabaja en el análisis y diseño de las funcionalidades requeridas, la cual define la interfaz, luego en la generalización y parametrización, y después se determina e implementa el patrón para instanciar. Por su parte, el uso de punteros opacos está relacionado con la interfaz.

    F3 -Resumen de técnicas empleadas

    La aplicación de estas técnicas siempre se determina por los requerimientos y características propias del sistema. No obstante, es común encontrar que un mismo embedded system de complejidad media o superior, utilice uno o varios de los patrones detallados. Inclusive una combinación de estos. Sin dudas, estos son alternativas válidas para evitar, la tradicional y no tan deseada, asignación dinámica de memoria.

    Ya sea en los ejemplos como en las prácticas mostradas, siempre se destaca el hecho de mantener la interfaz y ocultar tras esta, la implementación, logrando así un mayor poder de encapsulamiento y abstracción. En especial se desmitifica la técnica o "truco" de los llamados punteros opacos.

    Cada una de las técnicas mostradas se basa en los principios del diseño orientado a objetos, los cuales aplicados a la programación, no son propios de un lenguaje o herramienta particular, sino más bien, son una manera disciplinada de organizar, diseñar y codificar software. Estos son: el encapsulamiento, la herencia y el polimorfismo. Como se demostró en el artículo, su aplicación, en sistemas de software, incluyendo los embedded systems codificados en C, tiene grandes beneficios, ya que fomenta la producción de software modular, reutilizable, escalable, flexible, transportable y sumamente legible.

    El artículo, demuestra que la modularidad está relacionada con el encapsulamiento, las interfaces con la abstracción, y estas con el ocultamiento de la implementación.

    Por otro lado, el artículo entero es cuidadoso con el estilo de la codificación, ya que no es algo menor en sistemas que se conciben y se mantienen por diferentes desarrolladores, en cuyo caso, es necesario mantener una coherencia en los nombre de funciones, variables, archivos, etc. De aquí la razón de los manuales de codificación y estilo.

    Referencias


    [1] Bruce Douglass, “Real-Time Design Patterns: Robust Scalable Architecture for Real-Time
    Systems", September 27, 2002. 
    [2] Dan Saks, "Abstract Types Using C", Embedded.com, October 14, 2003.
    [3] Dan Saks, "Tag vs Type Names", Embedded.com, October 1, 2002.
    [4] Chris McKillop, "Opaque Pointers", QNX.
    [5] Framework RKH, “RKH Sourceforge download site,”, http://sourceforge.net/projects/rkh-reactivesys/
    [6] ISO/IEC International Standard, "ISO/IEC 9899:TC2: Programming languages - C", May 6, 2005
    [7] Leandro Francucci, "Principios de OOP aplicados en C para Embedded Systems", Embedded-Exploited, February, 2014.
    [8] Mark Weiss,"Data Structures and Algorithm Analysis in C", October 7, 1994.
    [9] Kernighan & Ritchie, "C Programming Language (2nd Edition)", April 1, 1988.
    [10] Alessandro Rubini, "Linux Device Drivers, 3rd Edition", O'Reilly, February, 2005.

      Comentarios