SDK Multiplataforma en C logo

SDK Multiplataforma en C

Uso de C

❮ Anterior
Siguiente ❯

La mayoría de los lenguajes de programación contienen partes buenas y partes malas. Descubrí que podía ser un mejor programador usando solo las partes buenas y evitando las partes malas. Después de todo, ¿cómo se puede construir algo bueno con partes malas? Douglas Crockford - JavaScript: The Good Parts.


Programar de forma rápida, reducir la probabilidad de error, asegurar la portabilidad y generar binarios optimizados han sido los principales propósitos de NAppGUI desde sus inicios y eso incluye una revisión del propio lenguaje C. Se ha utilizado como base un subconjunto del ANSI-C (C90) con dos características del C99: Los enteros de tamaño fijo <stdint.h> y los comentarios de una línea de C++. Recomendamos que las aplicaciones basadas en este SDK sigan la misma filosofía. Entrando más en detalle, los objetivos perseguidos han sido estos:

  • Máxima portabilidad: Incluso en compiladores ya desfasados como MSVC 8.0 (Visual Studio 2005) o GCC 4.2 (Xcode 3). Las últimas características del lenguaje pueden no estar disponibles en plataformas donde debas portar tu código (piensa en dispositivos embebidos). También aseguras que dicho código será compatible con las futuras versiones de los principales compiladores.
  • Focalizar la atención: En el "que" y no en el "como". Hay veces que volvemos complicado lo sencillo tan solo por justificar el uso de esa nueva característica tan "cool". También es posible que seas un adicto por "estar a la última", lo que te obligará a "modernizar" el código para adaptarlo a una nueva versión del estándar. Céntrate en resolver el problema que tengas delante y, si puedes invertir más tiempo, en bajar la complejidad asintótica de tu solución. NAppGUI se encargará de que tus aplicaciones funcionen allá donde sea necesario.
  • Evitar características poco relevantes: Como el soporte multi-hilo (<threads.h>) de C11. Esto se resuelve con llamadas al sistema. Ver Hebras.
  • Rápida compilación: Determinadas construcciones del C no son más que una especie de "ensamblador portátil", que el compilador puede interpretar y traducir de una manera increíblemente eficiente.
  • Binarios pequeños y rápidos: Derivada de la anterior, el código generado requerirá pocas sentencias en ensamblador y será muy sencillo de optimizar por parte del compilador.

Evidentemente, este no es lugar para aprender C ni es nuestra pretensión. El núcleo del lenguaje es pequeño y fácil de recordar, pero programar bien requiere años de práctica. Lo que haremos aquí es mostrar la mínima expresión del lenguaje que utilizamos diariamente. En definitiva, estos son nuestros estándares.


1. Tipos básicos

  • Vacío: void.
  • Booleano: bool_t. Tipo de 8 bits con solo dos valores posibles TRUE (1) y FALSE (0).
  • Enteros: uint8_t, uint16_t, uint32_t, uint64_t, int8_t, int16_t, int32_t, int64_t. Los enteros de tamaño fijo se introdujeron en C99 mediante <stdint.h>. Consideramos una ventaja saber que nuestras variables tendrán el mismo tamaño en todos los sistemas. Queda prohibido el uso de int, long, short o unsigned, con la única excepción de las funciones de comparación.
  •  
    
    static int i_cmp_cir(const Cir2Dd *cir1, const Cir2Dd *cir2)
    {
        return (cir1->r < cir2->r) ? 1 : -1;
    }
    
    arrst_sort(circles, i_cmp_cir, Cir2Dd);
    
  • Coma flotante: real32_t, real64_t. No se utiliza float ni double por coherencia con los tipos enteros.
  • Carácter: char_t (8 bits). Se utiliza "de facto" la representación UTF8 en todo el SDK, por lo que queda prohibido el acceso aleatorio a los elementos de una cadena, ya que se trata de una codificación de longitud variable. Se deben utilizar las funciones incluídas en Unicode o Strings para manipular arrays de carácteres. No se utilizan (ni se aconseja) los tipos wchar_t, char16_t, char32_t. No obstante, si tuvieses cadenas wide-char deberás convertirlas a UTF8 antes de utilizarlas en cualquier función de NAppGUI.
  • Uso de cadenas UTF8
     
    
    // Error! 
    const char_t *mystr = "Ramón tiene un camión";
    while (mystr[i] != '\0')
    {
        if (mystr[i] == 'ó')
        {
            // Do something
        }
        else
        {
            i += 1;
        }
    }
    
    // Correct!
    const char_t *it = mystr;
    uint32_t cp = unicode_to_u32(it, ekUTF8);
    while (cp != '\0')
    {
        if (cp == 'ó')
        {
            // Do something
        }
        else
        {
            it = unicode_next(it, ekUTF8);
            cp = unicode_to_u32(it, ekUTF8);
        }
    }
    
    // Avoid using wchar_t constants (when possible).
    // wchar_t uses UTF16 encoding
    const wchar_t *mywstr = L"Ramón tiene un camión";
    char_t mystr[512];
    
    unicode_convers((const char_t*)mywstr, mystr, ekUTF16, ekUTF8, sizeof(mystr));
    
    // This is a NAppGUI function (UTF8-Encoding)
    label_text(label, mystr);
    
  • Enumerados: Su principal cometido es manejar la especialización y serán evaluados en exclusiva dentro de un switch. Queda prohibido asignar valores aleatorios a los elementos de un enum, salvo 1 al primero de ellos. Se considera 0 como no inicializado y ENUM_MAX(align_t) como inválido.
  • Definición de tipos enumerados
     
    
    typedef enum _align_t
    {
        ekTOP = 1,
        ekBOTTOM,
        ekLEFT,
        ekRIGHT
    } align_t;
    

2. Estructuras y uniones

Definición de estructuras y uniones
 
typedef struct _layout_t Layout;
typedef union _attr_t Attr;

struct _layout_t
{
    Cell *parent;
    Panel *panel;
    bool_t is_row_major_tab;
    ArrSt(Cell) *cells;
    ArrPt(Cell) *cells_dim[2];
    real32_t dim_margin[2];
    color_t bgcolor;
    color_t skcolor;
};

union _attr_t
{
    struct _bool_
    {
        bool_t def;
    } boolt;

    struct _int_
    {
        int64_t def;
        int64_t min;
        int64_t max;
        int64_t incr;
        String *format;
    } intt;

    struct _real32_
    {
        real32_t def;
        real32_t min;
        real32_t max;
        real32_t prec;
        real32_t incr;
        uint32_t dec;
        String *format;
    } real32t;
};

Por lo general, las definiciones de estructuras no serán públicas y permanecerán ocultas en el *.c. Esto significa que no podrán declararse variables automáticas en el Segmento Stack y solo serán accesibles mediante funciones que acepten objetos dinámicos opacos.

Uso de punteros opacos
 
Layout *layout = layout_create(2, 2);
layout_edit(layout, edit, 0, 0);
layout_label(layout, label, 0, 1);
...
panel_layout(panel, layout);
 
// Layout definition is hidden
// We do not know the content of Layout
Layout layout;  // Compiler error!

Normalmente, todos los objetos dinámicos dispondrán de una función destructora. Si no existiera, es porque dicho objeto solo tiene sentido como parte de otro. Por ejemplo, no existe layout_destroy() ni panel_destroy(), pero sí window_destroy que se encargará de destruir toda la jerarquía de paneles y layouts asociados a la ventana.


3. Control

  • if/else. Siempre abren un bloque {...}, a no ser que TODOS los caminos estén compuestos por una única sentencia. Por lo general, se evita el uso de funciones como argumentos del if/else con la excepción de funciones puras.
  • Uso de if/else
     
    
    if (x == 1)
        i_do_something(j);
    else
        i_do_nothing();
    
    if (x == 1)
    {
        j += 2;
        i_do_something(j);
    }
    else
    {
        i_do_nothing();
    }
    
    if (bmath_sqrtf(sqlen) < 20.5f)
        i_do_something(j);    
    
  • while. Nada que comentar.
  • do/while. No permitido. Utilizar for o while.
  • for. Para bucles infinitos, se utiliza for(;;) en lugar de while(TRUE), ya que evita warnings en algunos compiladores. Dado que hay compiladores basados en ANSI-C, como MSVC++ 8.0, no utilizamos declaraciones de variables dentro del for(), característica que fué incorporada en C99.
  • Uso de for
     
    
    // Infinite loop
    for(;;)
    {
        ...
    }
    
    // Will not work in some compilers (not used)
    for (uint32_t i = 0; i < 1024; ++i)
    {
        ...
    }
    
    // Ok
    uint32_t i = 0;
    ...
    for (i = 0; i < 1024; ++i)
    {
        ...
    }
    
  • switch. Únicamente se utiliza para discriminar entre los valores de un enum. NUNCA se evaluará otro tipo de datos en un switch ni tampoco se discriminará un enum dentro una construcción if/else. El compilador puede optimizar drásticamente el rendimiento de una construcción con estas características.
  • Uso de switch
     
    
    switch(align) {
    case ekTOP:
        ...
        break;
    
    case ekBOTTOM:
        ...
        break;
    
    case ekLEFT:
        ...
        break;
    
    case ekRIGHT:
        ...
        break;
    
    cassert_default();
    }
    

4. Funciones

  • Una función puede no devolver nada (void), un tipo básico o un puntero.
  • Los parámetros de entrada siempre son const aunque sean tipos simples pasados por valor.
  • Para cualquier parámetro de entrada que no sea de tipo básico se pasará por puntero. Nunca una estructura por valor.
  • Para los parámetros de salida se utilizarán siempre punteros. En C no existen las referencias.
  • Parámetros en funciones.
     
    
    uint32_t myfunc(const uint32_t input1, const Layout *input2, V2Df *output1, real32_t *output2);
    
  • Se debe reducir al máximo la cantidad de funciones públicas, que se declararán en el *.h y se definirán en el *.c.
  • Las funciones de apoyo (o privadas) se definirán static, dentro del módulo *.c y no tendrán declaración.
  • Función pública.
     
    
    // layout.h
    void layout_hsize(Layout *layout, const uint32_t col, const real32_t wid);
    
    // layout.c
    void layout_hsize(Layout *layout, const uint32_t col, const real32_t wid)
    {
        i_LineDim *dim = NULL;
        cassert_no_null(layout);
        cassert_msg(wid >= 0.f, "Column 'width' must be positive.");
        dim = arrst_get(layout->lines_dim[0], col, i_LineDim);
        cassert_no_null(dim);
        dim->forced_size = wid;
    }
    
    Función privada. Solo puede llamarse dentro de layout.c.
     
    
    // layout.c
    static Cell *i_get_cell(Layout *lay, const uint32_t c, const uint32_t r)
    {
        register uint32_t position = UINT32_MAX;
        cassert_no_null(lay);
        cassert(c < arrst_size(lay->lines_dim[0], i_LineDim));
        cassert(r < arrst_size(lay->lines_dim[1], i_LineDim));
        position = r * arrst_size(lay->lines_dim[0], i_LineDim) + c;
        return arrst_get(lay->cells, position, Cell);
    }
    

5. Ámbitos

Las variables se declaran al principio de un bloque y no podrán mezclarse con sentencias, a no ser que abramos un nuevo ámbito. Declaraciones mezcladas con sentencias es una característica de C++ añadida al estándar C99, pero no todos los compiladores de C la soportan. Sí que está permitido inicializar una variable llamando a una función.

Ámbitos de variables en C
 
{
    // Ok!
    uint32_t var1 = 5;
    uint32_t var2 = i_get_value(stm);
    uint32_t var3 = i_get_value(stm);

    i_add_values(var1, var2, var3);

    // Error in C90 compilers
    uint32_t var4 = 6;

    // Ok!
    {
        uint32_t var4 = 6;
        ....
    }
}

6. Punteros

Al margen de las ventajas propias del uso de la aritmética de punteros a la hora de implementar ciertos algoritmos, en NAppGUI se utilizan punteros esencialmente en dos situaciones:

Mención aparte merecen los punteros a función muy utilizados en C, pero menos en C++ ya que el lenguaje los oculta dentro de las vtable. No obstante, un puntero a función estratégicamente colocado puede simplificarnos el hecho de añadir funcionalidad especializada a objetos ya existentes, sin tener que adoptar un diseño orientado a objetos mas purista.

Listado 1: Uso de punteros a función.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typedef struct _shape_t Shape;
typedef void(*FPtr_draw)(const Shape*, DCtx *ctx);

struct _shape_t
{
    ArrSt(V2Df) *points;
    Material *material;
    ...
    FPtr_draw func_draw;
};

static void i_draw_conceptual(const Shape *shape, DCtx *ctx)
{
    // Do simple drawing
}

static void i_draw_realistic(const Shape *shape, DCtx *ctx)
{
    // Do complex drawing
}

Shape *shape[N];
Shape *shape[0] = heap_new(Shape);
Shape *shape[1] = heap_new(Shape);
shape[0]->func_draw = i_draw_conceptual;
shape[1]->func_draw = i_draw_realistic;
...

for (i = 0; i < N; ++i)
    shape[i]->func_draw(shape[i], ctx);

7. Preprocesador

En nuestros estándares se hace un uso intensivo del preprocesador, sobre todo para la comprobación de tipos en tiempo de compilación. Esto ayuda a detectar errores en el código antes de ejecutar el programa (análisis estático), al contrario que el RTTI de C++ que lo hace una vez en marcha (análisis dinámico).

Uso del preprocesador para comprobar tipos.
 
#define arrst_destroy(array, func_remove, type)\
    ((void)((array) == (ArrSt(type)**)(array)),\
    FUNC_CHECK_REMOVE(func_remove, type),\
    array_destroy_imp((Array**)(array), (FPtr_remove)func_remove, (const char_t*)(ARRST#type)))

ArrSt(Product) *products = arrst_create(Product);
...
static void i_remove_product(Product *product)
{

}
...

// 'products' and 'i_remove_product' will be checked at compile time
arrst_destroy(&products, i_remove_product, Product);
El tipado dinámico no es necesariamente bueno. Obtiene errores estáticos en tiempo de ejecución, que realmente deberían poder detectarse en tiempo de compilación. Rob Pike.

8. Comentarios

Por lo general se reducirá el uso de comentarios todo lo posible. Se pondrá un comentario al inicio de cada fichero a modo de descripción general. También utilizamos una línea de comentario como separador a la hora de implementar funciones.

stream.c
 
/* Data streams. Manage connection-oriented communication */

#include "stream.h"
#include "stream.inl"
#include "core.inl"
#include "bfile.h"
#include "bmem.h"
...

/*---------------------------------------------------------------------------*/

static void i_func1(void)
{
    // Do something
}

/*---------------------------------------------------------------------------*/

static void i_func2(void)
{
    // Do something
}

Los comentarios tipo C++ vienen muy bien para anular temporalmente líneas de código en tiempo de diseño, o bloques completos si aprovechamos la opción que ofrecen casi todos los editores. Hacerlo con los comentarios tradicionales de C /* ---- */ puede llegar a ser muy tedioso, sobre todo si ya existen comentarios dentro del bloque que queremos descartar.

Otro aspecto que queda totalmente prohibido es la inclusión de bloques de documentación dentro del código fuente, ni siquiera en las propias cabeceras. NAppGUI utiliza ndoc para tareas de documentación, una utilidad que permite crear documentos html/pdf enriquecidos con imágenes, referencias cruzadas, ejemplos, etc y que utiliza sus propios archivos totalmente separados del código. Otra ventaja añadida es la limpieza que presentan las cabeceras *.h de todos los módulos, donde se hace muy sencillo localizar aquello que buscamos.

Los bloques de documentación NO están permitidos.
 
// Forbidden, non used
/*! Gets the area of ​​the polygon.  
    \param pol The polygon. 
    \return The area.  
*/  
real32_t pol2d_areaf(const Pol2Dd *pol);  
Ejemplo de cabecera en NAppGUI.
 
/* 2d convex polygon */

#include "geom2d.hxx"

__EXTERN_C

Pol2Df* pol2d_createf(const V2Df *points, const uint32_t n);

Pol2Df* pol2d_copyf(const Pol2Df *pol);

void pol2d_destroyf(Pol2Df **pol);

void pol2d_transformf(Pol2Df *pol, const T2Df *t2d);

const V2Df *pol2d_pointsf(const Pol2Df *pol);

uint32_t pol2d_nf(const Pol2Df *pol);

real32_t pol2d_areaf(const Pol2Df *pol);

bool_t pol2d_ccwf(const Pol2Df *pol);

bool_t pol2d_convexf(const Pol2Df *pol);

__END_C
Todos los comentarios en NAppGUI se realizan en lenguaje Inglés.

9. Entrada/Salida

La entrada/salida no forma parte del lenguaje C como tal. A medida que el lenguaje se iba extendiendo a mediados de los 70, se fueron agrupando una serie de rutinas útiles en lo que pasó a formar la Librería Estándar de C. NAppGUI encapsula toda su funcionalidad en Sewer, Osbs o Core implementándola generalmente como llamadas al sistema operativo, mucho más directas y eficientes.

Uso de funciones seguras de E/S.
 
// Do not use cstdlib in applications
#include <stdio.h>
FILE *fp = fopen("/tmp/test.txt", "w+");
fprintf(fp, "This is testing for fprintf...\n");
fclose(fp);

// Use NAppGUI functions
#include "stream.h"
Stream *stm = stm_to_file("/tmp/test.txt", NULL);
stm_printf(stm, "This is testing for stm_printf...\n");
stm_close(&stm);
No se recomienda el uso de la Librería Estándar de C. Busca la función equivalente en Sewer, Osbs o Core.
❮ Anterior
Siguiente ❯