10
\$\begingroup\$

Dynamic Type - [ int, char, bool, ... => dtype ]

Dynamic Type in C, A project that I had been thinking of for a while and made it.

How it works ?

Dtype currently is same as char, when doing dtype var[size]; size bytes are reserved for var as normal char would do. The actual working of dtype is that it stores the type of variable in first 1 byte i.e var[0] = (char)type;, and stores its size info in second & third bytes as unsigned short, i.e * ( unsigned short * ) (var + 1) = size; after that any data to be stored is checked with the size, i.e * (unsigned short * ) (var + 1) and if the size is equal or greater than the data to be stored, the data is then hard-copied from fourth byte onwards, i.e from ( var + 3) and to get data, it simply checks the type and grabs the data, if type mismatch occurs, it produces a warning [ in runtime ].

Advantages over normal types

  • Dynamic types enable you to store any kind of variable type in one variable
  • has reusability, meaning an integer dynamic variable can be reused to store string!
  • saving memory space, kind of - If you need let's say long integer, string with 10 bytes, integer, normally you would make different variables for each of them which causes more use of memory whereas you can just make a dtype of maximum size among all and use it to store anything of that byte size, no need to take more memory.
  • custom types are also supported!

Runtme Speed test ( with -O2 flag, on my device - results may vary on yours. ) :-

No. Normal variables doing same thing Dtype variables doing same thing
1 ~20.945 ms on average [50+ tests] ~21.025 ms on average [50+ tests]
2 ~19.881 ms on average [100+ tests] ~19.997 ms on average [100+ tests]

As seen above not much performance impact has been seen. And considering the advantages, It seems to be good

The code :-

/**
 * @file dtype.h
 * 
 * @brief Contains the definition for `dtype` along with `dtypes` & `dtypes.*` function signatures.
*/

#ifndef DTYPE_H_INCL
#define DTYPE_H_INCL

#include <stdbool.h>

/**
 * @brief the actual dtype which stores the data
 * @example dtype var[size_in_bytes];
*/
typedef char dtype;

/// @brief Current size_type for `dtype`
typedef unsigned short DTYPE_SIZE;

/// @brief Display warning if true
#ifndef DTYPE_WARN_OUT
#define DTYPE_WARN_OUT true
#endif
/// @brief Immediately display errors if true
#ifndef DTYPE_ERROR_OUT
#define DTYPE_ERROR_OUT true
#endif
/// @brief Exit on error if true
#ifndef DTYPE_EXIT_ON_ERR
#define DTYPE_EXIT_ON_ERR true
#endif

/**
 * @enum DTYPES_ERR
 * @brief ErrorCode enums for DTYPES_ERR
*/
enum DTYPES_ERR {
    DTYPE_NO_ERR,
    DTYPE_SIZE_LOW,
    DTYPE_SIZE_HIGH,
    DTYPE_MIN_MEM,
    DTYPE_VALUE_ERR,
    DTYPE_NOT_INIT,
    DTYPE_UNKNOWN_ERR
};

/**
 * @enum DTYPES_TYPES
 * @brief Typecode for identifying current type of `dtype`.
 * @brief DTYPE_TYPE for normal types,
 * @brief DTYPE_UTYPE for unsigned types.
*/
enum DTYPES_TYPES {
    DTYPE_INVALID, // Invalid type, not initialized!
    DTYPE_NONE,
    DTYPE_BOOL,
    DTYPE_SHORT,
    DTYPE_USHORT,
    DTYPE_INT,
    DTYPE_UINT,
    DTYPE_LONG,
    DTYPE_ULONG,
    DTYPE_FLOAT,
    DTYPE_DOUBLE,
    DTYPE_CHAR,
    DTYPE_STR,
    DTYPE_OBJECT, // object type -> currently holds a custom type of variable.
    DTYPE_UNKNOWN // unknown type!
};

/**
 * @struct __dtypes_struct
 * @typedef __dtypes_struct
 * 
 * @brief a wrapper for all dtype functions.
*/
typedef struct __dtypes_struct {

    /**
     * @brief Initializes the variable.
     * @param var The variable to initialize.
     * @param size Size of the variable in bytes
    */
    void (*init)(dtype* var, DTYPE_SIZE size);
    /**
     * @brief Prints the current error even if there is not an error.
     * @returns The ErrorCode of current error
    */
    enum DTYPES_ERR (*get_error)(void);
    /**
     * @brief Returns the usable size of variable..
     * @param var The varible to get size of.
     * @returns The usable size of given variable.
    */
    DTYPE_SIZE (*get_size)(dtype* var);
    /**
     * @brief Returns the current `typecode` of the variable..
     * @param var The variable to get `typecode` of.
     * @returns The typecode of given variable.
    */
    enum DTYPES_TYPES (*get_type)(dtype* var);
    /**
     * @brief Returns the current `type` of variable..
     * @param var The variable to get type from.
     * @returns The `type` of variable in readable string.
    */
    const char* (*get_str_type)(dtype* var);
    /**
     * @brief Set the type of variable..
     * @param var The variable to set the type..
     * @param type the typecode to set.
    */
    void (*set_type)(dtype* var, enum DTYPES_TYPES);

    /**
     * @brief Set the content of variable to none..
     * @param var The variable to set to.
    */
    void (*set_none)(dtype* var);
    /**
     * @brief Set the content of variable to given boolean value..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_bool)(dtype* var, bool value);
    /**
     * @brief Set the content of variable to given short value..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_short)(dtype* var, short value);
    /**
     * @brief Set the content of variable to given unsigned short value..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_ushort)(dtype* var, unsigned short value);
    /**
     * @brief Set the content of variable to given integer value..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_int)(dtype* var, int value);

    /**
     * @brief Set the content of variable to given unsigned integer value..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_uint)(dtype* var, unsigned int value);

    /**
     * @brief Set the content of variable to given long integer value..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_long)(dtype* var, long value);
    /**
     * @brief Set the content of variable to given unsigned long integer value..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_ulong)(dtype* var, unsigned long value);
    /**
     * @brief Set the content of variable to given float value..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_float)(dtype* var, float value);
    /**
     * @brief Set the content of variable to given double value..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_double)(dtype* var, double value);
    /**
     * @brief Set the content of variable to given character value..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_char)(dtype* var, char value);
    /**
     * @brief Set the content of variable to given string..
     * @param var The variable to set to..
     * @param value The value to set to.
    */
    void (*set_str)(dtype* var, char * value);
    /**
     * @brief Copy the contents of the object into the variable..
     * @param var The variable to copy to..
     * @param obj The void pointer of object to copy from..
     * @param size The size of the object.
    */
    void (*set_object)(dtype* var, void * obj, DTYPE_SIZE size);

    /**
     * @brief Get the content of variable as boolean.
     * @param var The variable to get from
    */
    bool (*get_bool)(dtype* var);
    /**
     * @brief Get the content of variable as short.
     * @param var The variable to get from
    */
    short (*get_short)(dtype* var);
    /**
     * @brief Get the content of variable as unsigned short.
     * @param var The variable to get from
    */
    unsigned short (*get_ushort)(dtype* var);
    /**
     * @brief Get the content of variable as integer.
     * @param var The variable to get from
    */
    int (*get_int)(dtype* var);
    /**
     * @brief Get the content of variable as unsigned integer.
     * @param var The variable to get from
    */
    unsigned int (*get_uint)(dtype* var);
    /**
     * @brief Get the content of variable as long integer.
     * @param var The variable to get from
    */
    long (*get_long)(dtype* var);
    /**
     * @brief Get the content of variable as unsigned long integer.
     * @param var The variable to get from
    */
    unsigned long (*get_ulong)(dtype* var);
    /**
     * @brief Get the content of variable as float.
     * @param var The variable to get from
    */
    float (*get_float)(dtype* var);

    /**
     * @brief Get the content of variable as double.
     * @param var The variable to get from
    */
    double (*get_double)(dtype* var);
    /**
     * @brief Get the content of variable as character.
     * @param var The variable to get from
    */
    char (*get_char)(dtype* var);
    /**
     * @brief Get the content of variable as string (char *).
     * @param var The variable to get from
    */
    char * (*get_str)(dtype* var);
    /**
     * @brief Get the content of variable as object [ void pointer ].
     * @param var The variable to get from
    */
    void * (*get_object)(dtype* var);

    /**
     * @brief Get the direct pointer of variable as bool *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to bool.
     * @param var The variable to get pointer from.
    */
    bool* (*in_bool)(dtype* var);
    /**
     * @brief Get the direct pointer of variable as short *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to short.
     * @param var The variable to get pointer from.
    */
    short* (*in_short)(dtype* var);
    /**
     * @brief Get the direct pointer of variable as unsigned short *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to unsigned short.
     * @param var The variable to get pointer from.
    */
    unsigned short* (*in_ushort)(dtype* var);
    /**
     * @brief Get the direct pointer of variable as int *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to integer.
     * @param var The variable to get pointer from.
    */
    int* (*in_int)(dtype* var);
    /**
     * @brief Get the direct pointer of variable as unsigned int *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to unsigned int.
     * @param var The variable to get pointer from.
    */
    unsigned int* (*in_uint)(dtype* var);
    /**
     * @brief Get the direct pointer of variable as long *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to long.
     * @param var The variable to get pointer from.
    */
    long* (*in_long)(dtype* var);
    /**
     * @brief Get the direct pointer of variable as unsigned long *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to unsigned long.
     * @param var The variable to get pointer from.
    */
    unsigned long* (*in_ulong)(dtype* var);
    /**
     * @brief Get the direct pointer of variable as float *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to float.
     * @param var The variable to get pointer from.
    */
    float* (*in_float)(dtype* var);
    /**
     * @brief Get the direct pointer of variable as double *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to double.
     * @param var The variable to get pointer from.
    */
    double* (*in_double)(dtype* var);
    /**
     * @brief Get the direct pointer of variable as char *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to char.
     * @param var The variable to get pointer from.
    */
    char* (*in_char)(dtype* var);
    /**
     * @brief Get the direct pointer of variable as char *
     * @brief to directly set the value / use in functions like scanf().
     * @brief Also sets the current type to string.
     * @param var The variable to get pointer from.
    */
    char* (*in_str)(dtype* var);

    enum DTYPES_TYPES (*print)(dtype* var);

} __dtypes_struct;

__dtypes_struct dtypes;

#endif
/**
 * @file dtype.c
 * 
 * @brief contains the actual code for definition in dtype.h
*/

#include "dtype.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/// @brief Current Errorcode
enum DTYPES_ERR DTYPE_CURRENT_ERRCODE = DTYPE_NO_ERR;
/// @brief The function in which current error occured.
const char* DTYPE_CURRENT_ERRFUNC = "none";
/// @brief Current Error description.
const char* DTYPE_CURRENT_ERRDESC = "none";

// function that prints current error and returns the errcode
enum DTYPES_ERR get_error ( void )
{
    // print out error
    printf ("\n---------- Dtype Error ----------\n");
    printf ("in function %s, errcode: %d :\n\tinfo: %s\n",
        DTYPE_CURRENT_ERRFUNC, DTYPE_CURRENT_ERRCODE, DTYPE_CURRENT_ERRDESC
    );

    // store the current errorcode to later return
    enum DTYPES_ERR ErrCode = DTYPE_CURRENT_ERRCODE;

    // set the current error to null as we have printed it!

    DTYPE_CURRENT_ERRFUNC = "none";
    DTYPE_CURRENT_ERRCODE = DTYPE_NO_ERR;
    DTYPE_CURRENT_ERRDESC = "none";

    // return the errcode
    return ErrCode;
}

// function that sets error as given.
void set_error ( enum DTYPES_ERR errcode, const char* fname, const char* desc )
{
    // set the error
    DTYPE_CURRENT_ERRCODE = errcode;
    DTYPE_CURRENT_ERRFUNC = fname;
    DTYPE_CURRENT_ERRDESC = desc;

    // print the error via get_error () if DTYPE_ERROR_OUT is true
    if ( DTYPE_ERROR_OUT)
    {
        get_error ();
        // exit if DTYPE_EXIT_ON_ERR is true!
        if ( DTYPE_EXIT_ON_ERR ) {
            exit (1);
        }
    }
}

// set_type of the variable as provided.
void set_type ( dtype * var, enum DTYPES_TYPES type )
{
    var[0] = ( char ) type;
}

// set the size of the variable as provided
void set_size ( dtype * var, DTYPE_SIZE size )
{
    *( (DTYPE_SIZE*) (var+1) ) = size;
}

// get_type of the variable
enum DTYPES_TYPES get_type ( dtype * var )
{
    return var[0];
}

// get_type of the the variable but in a readable format, not in typecode.
const char* get_stype ( dtype * var )
{
    // self-explanatory.
    switch ( get_type ( var ) )
    {
        case DTYPE_NONE:
            return "none ( null value )";
        case DTYPE_BOOL:
            return "bool";
        case DTYPE_SHORT:
            return "short";
        case DTYPE_USHORT:
            return "unsigned short";
        case DTYPE_INT:
            return "int";
        case DTYPE_UINT:
            return "unsigned int";
        case DTYPE_LONG:
            return "long";
        case DTYPE_ULONG:
            return "unsigned long";
        case DTYPE_FLOAT:
            return "float";
        case DTYPE_DOUBLE:
            return "double";
        case DTYPE_CHAR:
            return "char";
        case DTYPE_STR:
            return "string ( char *)";
        case DTYPE_OBJECT:
            return "object ( custom )";
        case DTYPE_UNKNOWN:
            return "unknown";
        default:
            return "invalid ( not initialized )";
    }
}

// get usable size of the the variable
DTYPE_SIZE get_size ( dtype * var )
{
    return *( (DTYPE_SIZE*) (var+1));
}

// initialization of the variable
// this basically sets the type to DTYPE_NONE and sets the size as provided
// also has a check that size must be greater than or equal to 4.
void dt_init ( dtype * var, DTYPE_SIZE size )
{
    if ( size < 4) {
        set_error (DTYPE_SIZE_LOW, "init", "Size too low, < 4");
    }
    // store the type
    set_type ( var, DTYPE_NONE);
    // store the size
    set_size ( var, size-DTYPE_MIN_MEM);
}

// clear the data inside the variable
void dt_clear ( dtype * var ) {
    // for old c support!
    DTYPE_SIZE i;
    for ( i = DTYPE_MIN_MEM; i < DTYPE_MIN_MEM + get_size(var); ++i )
    {
        var[i] = '\0';
    }
}

// internal set_object function used for setting the variable's value
bool _dtset_obj ( dtype * var, void *obj, DTYPE_SIZE size )
{
    // check for initialization
    if ( get_type ( var ) == DTYPE_INVALID || get_type ( var ) > DTYPE_UNKNOWN) {
        set_error (
            DTYPE_NOT_INIT, "internal set_object()",
            "trying to set to non-initialized variable!"
        );
        // indicating error
        return true;
    }
    
    // clear previous value before storing
    dt_clear ( var );

    // if the size to store is greater than 
    if ( size > get_size ( var ) )
    {
        // not enough room to store!
        // indicating error!
        return true;
    }
    // for old c support
    int i;
    // hardcopy every byte till given size
    for ( i = 0; i < size; ++i )
    {
        var[i+DTYPE_MIN_MEM] = ( ( char*) obj )[i];
    }
    // no error
    return false;
}

void dtset_none ( dtype * var )
{
    // clear the data
    dt_clear ( var );
    // set type to none
    set_type ( var, DTYPE_NONE);
}

// set function for boolean value
void dtset_bool ( dtype * var, bool value )
{
    bool val = value;
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var, &val, sizeof ( bool ) ) ) {
        set_type ( var, DTYPE_BOOL);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_bool", "the variable size is too low to store bool !");
    return;
}

// set function for short value
void dtset_short ( dtype * var, short value )
{
    short val = value;
    // use internal set obj to set data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var, &val, sizeof ( short ) ) ) {
        set_type ( var, DTYPE_SHORT);
        return;
    }
    //set the error
    set_error (DTYPE_SIZE_HIGH, "set_short", "the variable size is too low to store short !");
    return;
}

// set function for unsigned short value
void dtset_ushort ( dtype * var, unsigned short value )
{
    unsigned short val = value;
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var, &val, sizeof ( unsigned short ) ) ) {
        set_type ( var, DTYPE_USHORT);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_ushort", "the variable size is too low to store unsigned short !");
    return;
}

// set function for int value
void dtset_int ( dtype * var, int value )
{
    int val = value;
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var, &val, sizeof ( int ) ) ) {
        set_type ( var, DTYPE_INT);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_int", "the variable size is too low to store int !");
    return;
}

// set function for unsigned int
void dtset_uint ( dtype * var, unsigned int value )
{
    unsigned int val = value;
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var, &val, sizeof ( unsigned int ) ) ) {
        set_type ( var, DTYPE_UINT);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_uint", "the variable size is too low to store unsigned int !");
    return;
}

// set function for long value
void dtset_long ( dtype * var, long value )
{
    long val = value;
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var, &val, sizeof ( long ) ) ) {
        set_type ( var, DTYPE_LONG);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_long", "the variable size is too low to store long !");
    return;
}

// set function for unsigned long
void dtset_ulong ( dtype * var, unsigned long value )
{
    unsigned long val = value;
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var, &val, sizeof ( unsigned long ) ) ) {
        set_type ( var, DTYPE_ULONG);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_ulong", "the variable size is too low to store unsigned long !");
    return;
}

// set function for float
void dtset_float ( dtype * var, float value )
{
    float val = value;
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var, &val, sizeof ( float ) ) ) {
        set_type ( var, DTYPE_FLOAT);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_float", "the variable size is too low to store float !");
    return;
}

// set function for double
void dtset_double ( dtype * var, double value )
{
    double val = value;
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var, &val, sizeof ( double ) ) ) {
        set_type ( var, DTYPE_DOUBLE);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_double", "the variable size is too low to store double !");
    return;
}

// set function for char
void dtset_char ( dtype * var, char value )
{
    char val = value;
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var, &val, sizeof ( char ) ) ) {
        set_type ( var, DTYPE_CHAR);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_char", "the variable size is too low to store char !");
}

// set function for strings ( char *)
void dtset_str ( dtype * var, char* value )
{
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var,value,strlen ( value ) +1) ) {
        set_type ( var, DTYPE_STR);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_str", "the variable size is too low to store string !");
}


// set function for custom objects / custom type variables
void dtset_obj ( dtype * var, void* obj, DTYPE_SIZE size )
{
    // use internal set_object function to set the dtype data to value
    // if error is not indicated, set the type and return
    if ( !_dtset_obj ( var,obj,size ) ) {
        set_type ( var, DTYPE_OBJECT);
        return;
    }
    // set the error
    set_error (DTYPE_SIZE_HIGH, "set_obj", "the variable size is too low to store the object !");
}

// ------- dtget_type(dtype) : returns the value as type, printing warning if needed ------

// get function to get boolean value
bool dtget_bool ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_BOOL && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `bool` from another type: `%s` ]\n",get_stype ( var ) );
    }
    // return the value as a boolean
    return *( ( bool*) ( var + DTYPE_MIN_MEM) );
}

// get function to get short value
short dtget_short ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_SHORT && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `short` from another type: `%s` ]\n",get_stype ( var ) );
    }
    // return the value as short
    return *( ( short*) ( var + DTYPE_MIN_MEM) );
}

// get function to get unsigned short value
unsigned short dtget_ushort ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_USHORT && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `unsigned short` from another type: `%s` ]\n",get_stype ( var ) );
    }
    // return the value as unsigned short
    return *( ( unsigned short *) ( var + DTYPE_MIN_MEM) );
}

// get function to get int
int dtget_int ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_INT && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `int` from another type: `%s` ]\n",get_stype ( var ) );
    }
    // return the value as int
    return *( ( int*) ( var + DTYPE_MIN_MEM) );
}

// get function to get unsigned int
unsigned int dtget_uint ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_UINT && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `unsigned int` from another type: `%s` ]\n",get_stype ( var ) );
    }
    // return the value as unsigned int
    return *( ( unsigned int*) ( var + DTYPE_MIN_MEM) );
}

// get function to get long
long dtget_long ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_LONG && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `long` from another type: `%s` ]\n", get_stype ( var ) );
    }
    // return the value as long
    return *( ( long*) ( var + DTYPE_MIN_MEM) );
}

// get function to get unsigned long
unsigned long dtget_ulong ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_ULONG && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `unsigned long` from another type: `%s` ]\n", get_stype ( var ) );
    }
    // return the value as unsigned long
    return *( ( unsigned long*) ( var + DTYPE_MIN_MEM) );
}

// get function to get float
float dtget_float ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_FLOAT && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `float` from another type: `%s` ]\n", get_stype ( var ) );
    }
    // return the value as float
    return *( ( float*) ( var + DTYPE_MIN_MEM) );
}

// get function to get double
double dtget_double ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_DOUBLE && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `double` from another type: `%s` ]\n", get_stype ( var ) );
    }
    // return the value as double
    return *( ( double*) ( var + DTYPE_MIN_MEM) );
}

// get function to get char
char dtget_char ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_CHAR && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `char` from another type: `%s` ]\n", get_stype ( var ) );
    }
    // return value as char
    return *( ( var + DTYPE_MIN_MEM) );
}

// get function to get string ( char *)
char * dtget_str ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_STR && DTYPE_WARN_OUT ) {
        printf ("\n[ Dtype warn: getting `string` from another type: `%s` ]\n", get_stype ( var ) );
    }
    // return value as string ( char *)
    return ( ( var + DTYPE_MIN_MEM) );
}

// get function to get void pointer of custom object ( void *)
void* dtget_object ( dtype * var )
{
    // type check, if failed and DTYPE_WARN_OUT is set,
    // print the warning, that we are trying to get it from another type!
    if ( get_type ( var )!=DTYPE_OBJECT && DTYPE_WARN_OUT ) {
        printf (
            "\n[ Dtype warn: getting `object ( custom )` from another type: `%s` ]\n",
            get_stype ( var )
        );
    }
    // return the value as void pointer that can be changed
    // ( void *)
    return ( void*) ( var + DTYPE_MIN_MEM);
}

//function to print the value & return it's type
enum DTYPES_TYPES dt_print ( dtype * var )
{
    switch ( get_type ( var ) )
    {
        // switch cases according to the type
        // print them as required as their types and return it's type!
        case DTYPE_NONE:
            printf ("None"); return DTYPE_NONE;
        case DTYPE_BOOL:
            printf ( dtget_bool ( var ) ? "true" : "false"); return DTYPE_BOOL;
        case DTYPE_SHORT:
            printf ("%i",dtget_short ( var ) ); return DTYPE_SHORT;
        case DTYPE_USHORT:
            printf ("%d",( int ) dtget_ushort ( var ) );
            return DTYPE_USHORT;
        case DTYPE_INT:
            printf ("%d", dtget_int ( var ) ); return DTYPE_INT;
        case DTYPE_UINT:
            printf ("%u", dtget_uint ( var ) ); return DTYPE_UINT;
        case DTYPE_LONG:
            printf ("%ld", dtget_long ( var ) ); return DTYPE_LONG;
        case DTYPE_ULONG:
            printf ("%lu", dtget_ulong ( var ) ); return DTYPE_ULONG;
        case DTYPE_FLOAT:
            printf ("%f", dtget_float ( var ) ); return DTYPE_FLOAT;
        case DTYPE_DOUBLE:
            printf ("%lf", dtget_double ( var ) ); return DTYPE_DOUBLE;
        case DTYPE_CHAR:
            printf ("%c", dtget_char ( var ) ); return DTYPE_CHAR;
        case DTYPE_STR:
            printf ("%s", dtget_str ( var ) ); return DTYPE_STR;
        case DTYPE_OBJECT:
            printf ("( custom object at %p )", (void*) var); return DTYPE_OBJECT;
        case DTYPE_UNKNOWN:
            printf ("unknown value"); return DTYPE_UNKNOWN;
        default:
            printf ("Invalid ( not initialized )"); return DTYPE_INVALID;
    }
}

// -- dtin_type(dtype*) : returns the pointer to the data as type* and sets current type to type --

bool * dtin_bool ( dtype * var )
{
    // set the type & send the pointer as ( bool * )
    set_type ( var, DTYPE_BOOL);
    return ( bool *) ( var + DTYPE_MIN_MEM);
}

short * dtin_short ( dtype * var )
{
    // set the type & send the pointer as ( short * )
    set_type ( var, DTYPE_SHORT);
    return ( short *) ( var + DTYPE_MIN_MEM);
}

unsigned short * dtin_ushort ( dtype * var )
{
    // set the type & send the pointer as ( unsigned short * )
    set_type ( var, DTYPE_USHORT);
    return ( unsigned short *) ( var + DTYPE_MIN_MEM);
}

int * dtin_int ( dtype * var )
{
    // set the type & send the pointer as ( int * )
    set_type ( var, DTYPE_INT);
    return ( int*) ( var + DTYPE_MIN_MEM);
}

unsigned int * dtin_uint ( dtype * var )
{
    // set the type & send the pointer as ( unsigned int * )
    set_type ( var, DTYPE_UINT);
    return ( unsigned int *) ( var + DTYPE_MIN_MEM);
}

long * dtin_long ( dtype * var )
{
    // set the type & send the pointer as ( long * )
    set_type ( var, DTYPE_LONG);
    return ( long *) ( var + DTYPE_MIN_MEM);
}

unsigned long * dtin_ulong ( dtype * var )
{
    // set the type & send the pointer as ( unsigned long * )
    set_type ( var, DTYPE_ULONG);
    return ( unsigned long *) ( var + DTYPE_MIN_MEM);
}

float * dtin_float ( dtype * var )
{
    // set the type & send the pointer as ( float * )
    set_type ( var, DTYPE_FLOAT);
    return ( float *) ( var + DTYPE_MIN_MEM);
}

double * dtin_double ( dtype * var )
{
    // set the type & send the pointer as ( double * )
    set_type ( var, DTYPE_DOUBLE);
    return ( double *) ( var + DTYPE_MIN_MEM);
}

char * dtin_char ( dtype * var )
{
    // set the type & send the pointer as ( char * )
    set_type ( var, DTYPE_CHAR);
    return ( char *) ( var + DTYPE_MIN_MEM);
}

char * dtin_str ( dtype * var )
{
    // set the type & send the pointer as ( char * )
    set_type ( var, DTYPE_STR);
    return ( char *) ( var + DTYPE_MIN_MEM);
}

// pack everything into dtypes

__dtypes_struct dtypes = {
    // basic functions
    dt_init, get_error, get_size, get_type, get_stype, set_type,

    // set functions
    dtset_none, dtset_bool, dtset_short, dtset_ushort, dtset_int, dtset_uint, dtset_long, dtset_ulong,
    dtset_float, dtset_double, dtset_char, dtset_str, dtset_obj,

    // get functions
    dtget_bool, dtget_short, dtget_ushort, dtget_int, dtget_uint, dtget_long, dtget_ulong, dtget_float,
    dtget_double, dtget_char, dtget_str, dtget_object,

    // in functions
    dtin_bool, dtin_short, dtin_ushort, dtin_int, dtin_uint, dtin_long, dtin_ulong, dtin_float,
    dtin_double, dtin_char, dtin_str,

    // printing function
    dt_print
};

Basic usage example :-

#include "dtype.h"
#include <stdio.h>

int main(void)
{
    /*
         Note :- printf("%d", dtypes.print(var)) kind of printing causes the variable to 
                 be printed first, and then display the typecode of variable.
    */

    // dtype var of 20 bytes
    dtype var[20];

    // initialize it with 20 bytes! [ it won't use more than size_of_bytes given in init ]
    dtypes.init(var, 20);

    // Example's of setting value!

    // set_str set's the data of var to the string provided
    dtypes.set_str(var, "Hello World!\n");

    // dtypes.print() prints the content, and returns the typecode of that var!
    // get_str_type returns the type in readable format than typecode
    printf("Typecode: %d, Type: %s\n\n", dtypes.print(var), dtypes.get_str_type(var));

    // store a boolean & print value

    // set_bool set's data of the var to the boolean provided
    dtypes.set_bool(var, 1);
    printf("Value in boolean 1 : ");
    dtypes.print(var); // true

    printf("\n");
    dtypes.set_bool(var, 0); // false
    printf("Value in boolean 0 : ");
    printf("\nTypecode: %d, Type: %s\n\n", dtypes.print(var), dtypes.get_str_type(var));

    // store an int & print value

    // set_int set's the data of var to the int provided
    dtypes.set_int(var, -9856);
    printf("\nTypecode: %d, Type: %s\n\n", dtypes.print(var), dtypes.get_str_type(var));

    // set_uint set's the data of var to unsigned int provided
    // let's try setting it to negative to see if it unsigned or not
    dtypes.set_uint(var, -9856);
    printf("\nTypecode: %d, Type: %s\n\n", dtypes.print(var), dtypes.get_str_type(var));

    // copying from dtypes to other types!
    unsigned int copied = dtypes.get_uint(var);
    printf("copied from dtypes.get_uint value: %u\n", copied);

    // copying from dtypes type mismatch, causes a warning to appear :) but copies it anyway
    // type mismatch here is that we are trying to get integer value but stored value is of type unsigned int
    int trytocopy = dtypes.get_int(var);
    printf("tried to copy int from unsigned int stored in var, warning should be above ^^ ");
    printf("\ntrytocopy value: %d!\n\n", trytocopy);

    // to clear the variable
    dtypes.set_none(var);
    printf("After setting it to none, value: ");
    printf("\nTypecode: %d, Type: %s\n\n", dtypes.print(var), dtypes.get_str_type(var));
    return 0;
}

Basic usage example : with scanf()

#include <stdio.h>
#include "dtype.h"

void scan_err(int scanf_output, dtype * var)
{
    if(!scanf_output) {
        printf("\n--- Invalid input or EOF ---\n");
        // clear variable if input error
        dtypes.set_none(var);
    }
}

int main()
{
    // dtype var of 20 bytes
    dtype var[20];

    // initialization
    dtypes.init(var, 20);

    printf("Enter integer: ");
    // in_int returns an integer pointer to store/access value directly
    scan_err(scanf("%d", dtypes.in_int(var)), var);
    printf("Value: ");
    printf("\nTypecode: %d, Type: %s\n\n", dtypes.print(var), dtypes.get_str_type(var));

    printf("Enter string: ");
    scan_err(scanf("%s", dtypes.in_str(var)), var);
    printf("Value: ");
    printf("\nTypecode: %d, Type: %s\n\n", dtypes.print(var), dtypes.get_str_type(var));

    printf("Enter unsigned long value: ");
    scan_err(scanf("%lu", dtypes.in_ulong(var)), var);
    printf("Value: ");
    printf("\nTypecode: %d, Type: %s\n\n", dtypes.print(var), dtypes.get_str_type(var));

    return 0;
}

I will be more than happy to receive suggestions, feedback, bugs and overall code review for this project.

\$\endgroup\$
10
  • 1
    \$\begingroup\$ "If you need let's say long integer, string with 10 bytes, integer, normally you would make different variables for each of them". Not really. you would use a union in this case. \$\endgroup\$
    – E.M.
    Oct 6, 2022 at 23:54
  • \$\begingroup\$ @E.M. yeah, unions but they don't store the type info, I could well create a struct and include the type with the union, but that would be different for all kind of usage, If you want a new data type (custom), then you need to add it to the union and also remember the data member name \$\endgroup\$
    – Pear
    Oct 7, 2022 at 4:56
  • 2
    \$\begingroup\$ You've just reinvented the discriminated union. They're sometimes useful (which is why std::variant was added to C++), but hardly new. \$\endgroup\$
    – Useless
    Oct 7, 2022 at 10:36
  • 2
    \$\begingroup\$ What does your speed test look like? There are many ways to measure things incorrectly. It is highly suspicious that they take the same time, when using dtype requires you to use function calls to access the variables. \$\endgroup\$
    – G. Sliepen
    Oct 7, 2022 at 11:21
  • \$\begingroup\$ How does interface to standard library function? \$\endgroup\$
    – CWallach
    Oct 7, 2022 at 21:11

3 Answers 3

14
\$\begingroup\$

Consider whether you should

C is not really suited for having dynamic types, and attempts to add such features very often fail in some way. There are other languages that are more suitable, and for example C++'s std::any is a way to get a safe dynamic type.

However, C does have unions, which provide you with most of the "advantages over normal types" you mention, the exception being that it doesn't actually keep track of what type is actually currently stored inside a union variable. But you can add another variable to track that.

Below I'll point out some issues your implementation has.

Safety issues

Your implementation of a dynamic type makes it easy to make mistakes. Consider your own example of how to use it:

dtype var[20];
dtypes.init(var, 20);

You have to ensure you use the same number for both the array declaration and the call to dtypes.init(). It would be much better if you only needed to specify the size once.

But it's also confusing to have to create an array in the first place, of type char even. Now you can pass var to everything that accepts a char *, and the compiler will happily allow it. It would be much nicer if dtype was implemented in such a way that you would write:

dtype var;
dtypes.init(var, 20);

And where dtype would actually be a struct.

Only one custom object type is supported

There's only one DTYPE_OBJECT, and it allows the user to store anything as long as it fits. What if the user has multiple custom types, and wants to store them in your dynamic type, and still be able to check which type exactly was stored in a dtype object?

Unaligned memory access

Not all CPU architectures allow unaligned access. When you store the size of a type in the second and third bytes, it's almost certain that you violate alignment restrictions. While it may work on Intel and AMD architectures, it is actually undefined behavior in C, and you code might crash on other architectures.

This is another strong argument to make dtype a proper struct, as then you can have a uint16_t size member that the compiler will properly align for you. But if you want to keep things the way they are now, use memcpy() to copy the bytes in the dtype to and from a local variable, for example:

DTYPE_SIZE get_size(const dtype *var)
{
   DTYPE_SIZE size;
   memcpy(&size, var + DTYPE_SIZE_LOW, sizeof size);
   return size;
}

Don't worry about the call to memcpy(); the compiler will optimize that away. Alternatively, you can manually split/recombine a number into bytes:

DTYPE_SIZE get_size(const dtype *var)
{
   const uint8_t *size_bytes = (const uint8_t *)(var + DTYPE_SIZE_LOW);
   return size_bytes[0] | (size_bytes[1] << 8);
}

Don't create a struct with member functions in C

Instead of creating a struct __dtypes_struct which allows you to write things like dtypes.init(), just write normal free functions, and prefix them all with dtypes_. This way, you can write dtypes_init(), which is the same number of characters, but avoids the indirection.

Also, if you want to write code like it's C++, then just use C++ instead.

Don't mix error codes with other constants

It's very weird to see constants like DTYPE_SIZE_LOW, DTYPE_SIZE_HIGH and DTYPE_MIN_MEM inside enum DTYPES_ERR. Don't mix unrelated things in a single enum. Either write:

static const int DTYPE_SIZE_LOW = 1;
static const int DTYPE_SIZE_HIGH = 2;
static const int DTYPE_MIN_MEM = 3;

Or put them in a separate (possibly anonymous) enum:

enum {
    DTYPE_SIZE_LOW = 1,
    DTYPE_SIZE_HIGH = 2,
    DTYPE_MIN_MEM = 3,
};

Use const where appropriate

The getters don't need to modify the variable, so they should all take a const pointer to dtype.

\$\endgroup\$
6
  • \$\begingroup\$ I was worried about alignment errors as well. \$\endgroup\$
    – pacmaninbw
    Oct 6, 2022 at 22:09
  • \$\begingroup\$ About "only one custom object is supported", Dtype is designed for storing one type of data at a time. And about the struct, I wanted it not to use heap memory, like malloc() and all. Making a struct would be nice but implementing it without malloc() would greatly increase the minimum memory requirement. And thanks for pointing out unaligned memory access, I really didn't think of that. Anyways, thank you for your review and pointing out the issues, I will try to improve them as much as possible :) \$\endgroup\$
    – Pear
    Oct 7, 2022 at 4:57
  • 8
    \$\begingroup\$ and you code might crash on other architectures. - It's UB and can break even on x86-64, because the optimizer might make assumptions about alignment when doing loop-unroll or vectorization calculations. Why does unaligned access to mmap'ed memory sometimes segfault on AMD64? is one case, and has links to a couple other problem cases digging into GCC breaking code that doesn't respect alignof(T). (@Pear). You can use memcpy or GNU C __attribute__((aligned(1))) to do safe unaligned load/store if you can't avoid it. \$\endgroup\$ Oct 7, 2022 at 6:51
  • 3
    \$\begingroup\$ Unfortunately, when compiling for ISAs that don't have cheap unaligned loads, GCC doesn't always optimize away memcpy, but it is good for keeping the compiler happy when targeting ISAs with HW support for unaligned loads. (including x86 and AArch64, but not for RISC-V: godbolt.org/z/v1xGKcq43. For 32-bit ARM, you need -mcpu=cortex-a15 or something that indicates ARMv7 or whenever that changed, so unaligned loads are guaranteed to work.) \$\endgroup\$ Oct 7, 2022 at 6:59
  • 1
    \$\begingroup\$ Even on AMD/Intel, bad alignment can cause speed issues. \$\endgroup\$ Oct 7, 2022 at 10:35
8
\$\begingroup\$

General Observations

It is apparent that you have done a lot of work, the code is generally readable and maintainable. The code is also mostly portable which is good.

On Linux using gcc this compiles without errors or warnings, but on Windows 10 using Visual Studio 2022 there is a warning message:
1>C:\...\Documents\ProjectsNfwsi\CodeReview\dtype\dtype\dtype.c(343,47): warning C4267: 'function': conversion from 'size_t' to 'DTYPE_SIZE', possible loss of data
The warning message is on this line:

    if (!_dtset_obj(var, value, strlen(value) + 1)) {

The strlen() function returns size_t just like the sizeof() function. A size_t type is an unsigned value and will be at least an unsigned integer, but may larger than an unsigned integer, it is defined by the operating system and the compiler.

I'm very curious about why this is using an array of char to represent values rather than a struct.

Function Length

Generally the functions are small atomic units which follow the Single Responsibility Principle, however, both versions of main() are larger than a single screen in my IDE. It would be easier to maintain and expand the unit tests if each unit test was a single function and main() just called all the unit test function. This might also reduce the number of comments necessary or at least localize the comments.

Function, Variable and Type Names

Symbol names that start with underscore or double underscore are reserved for the compiler and the system in C.

This is a personal preference, but I would use dtype_set_... rather than dtset_..., it is clearer.

Comments

It appears that the comments are for Doxygen, if that is so, there is no need to restate @brief on every line. Here is an example of a multiline comment from the Doxygen manual:

/**
 * A brief history of JavaDoc-style (C-style) comments.
 *
 * This is the typical JavaDoc-style C-style comment. It starts with two
 * asterisks.
 *
 * @param theory Even if there is only one possible unified theory. it is just a
 *               set of rules and equations.
 */
void cstyle( int theory );

Stick with one type of comment, either old style C comments /* ... */ or C++ comments // ...
otherwise the comments can get confusing.

Translating Enums to Strings

The code that translates the enums to strings is not as maintainable because the enums are in the dtype.h header file and the code that translates them is in the dtype.c source file. You may add or remove enums and forget to add the new cases in the switch statement in const char* get_stype(dtype* var). One way around this is to define an array of strings in the header file that has a one to one relationship with the enums, this will also perform better. You can use string compare to convert in the other direction, but that is slow.

** dtype.h**

/**
 * @enum DTYPES_TYPES
 * @brief Typecode for identifying current type of `dtype`.
 * @brief DTYPE_TYPE for normal types,
 * @brief DTYPE_UTYPE for unsigned types.
*/
enum DTYPES_TYPES {
    DTYPE_INVALID, // Invalid type, not initialized!
    DTYPE_NONE,
    DTYPE_BOOL,
    DTYPE_SHORT,
    DTYPE_USHORT,
    DTYPE_INT,
    DTYPE_UINT,
    DTYPE_LONG,
    DTYPE_ULONG,
    DTYPE_FLOAT,
    DTYPE_DOUBLE,
    DTYPE_CHAR,
    DTYPE_STR,
    DTYPE_OBJECT, // object type -> currently holds a custom type of variable.
    DTYPE_UNKNOWN // unknown type!
};

#ifdef DTYPES_C
const char* dtype_names[] =
{
    "invalid ( not initialized )",
    "none ( null value )",
    "bool",
    "short",
    "unisgned short",
    "int",
    "unsigned int",
    "long",
    "unsigned long",
    "float",
    "double",
    "char",
    "string",
    "object",
    "unknown"
};
#endif

** dtype.c **

#define DTYPES_C
#include "dtype.h"

...

// get_type of the the variable but in a readable format, not in typecode.
const char* get_stype(dtype* var)
{
    enum DTYPES_TYPES this_type = get_type(var);

    if (this_type >= DTYPE_INVALID && this_type <= DTYPE_UNKNOWN)
    {
        return dtype_names[this_type];
    }

    return dtype_names[DTYPE_INVALID];
}
\$\endgroup\$
3
  • \$\begingroup\$ About using char * rather than struct, I didn't want it to use heap memory, malloc(), etc. Making a struct and making it not use heap memory to store info is really a pain. Please let me know if there is a way. About function, variable and typenames, yeah that is personal preference, will improve. About translating enums to strings, Thanks for that, hadn't thought it would be that easy. About doxygen comments, I am kinda new to that and definitely improve that \$\endgroup\$
    – Pear
    Oct 7, 2022 at 5:08
  • \$\begingroup\$ For the strlen, you might want to use strncpy with the length as a limit. But that would force zeroing of the full dtype buffer out to its max size, because strncpy has a silly definition (or is designed for a use-case other than length-capped strcpy). And strncpy still wouldn't guarantee zero-termination, so maybe not a good suggestion. \$\endgroup\$ Oct 7, 2022 at 13:42
  • 2
    \$\begingroup\$ Anyway, we know that the max size of a dtype object is USHORT_MAX, so worst case we modulo the length of a huge string and take the first few bytes of it when the buffer had room for more. But now that you point it out, that's actually kinda bad, and easily avoidable. dtype_size is a narrow type to save bytes in the objects, but _dtset_obj doesn't need to take a narrow arg. It can and should take size_t or at least unsigned, so it can check to see if you're trying to copy from a way-too-big string. (@Pear). Maybe I should put this in my own answer. \$\endgroup\$ Oct 7, 2022 at 13:46
5
\$\begingroup\$

You should document the requirement for the dtype var[20] buffer to be at least as aligned as anything you want to keep in it. e.g. alignas(unsigned long long) dtype var[20]. Or alignas(max_align_t) dtype var[20]. (#include <stdalign.h> so you can use alignas instead of just _Alignas.)

With your size taking up the first 2 bytes, that means you need to leave padding if you want alignment for later storage.

Unfortunately you can't bake the alignment into a typedef alignas(16) char dtype, that would require every array element to be 16-byte aligned. GNU C typedef char aligned_char __attribute__((aligned(16))) might sometimes be different, I seem to recall testing what happened when you make an array out of that.

It will often happen to work in practice for your example, at least in the case of the x86-64 System V calling convention. It specifies that any array of size 16 or later (or runtime-variable size) gets 16-byte alignment. Even if it's a local variable inside a function, which seems a bit of an intrusive thing for an ABI to require about something that's not visible across function boundaries. Perhaps that was just documentation of actual GCC behaviour slipping into the ABI doc.

You might think that compiling for an ISA with unaligned loads would make it safe in C to ignore alignof. But that is not the case. It's UB and can break even on x86-64, because the optimizer might make assumptions about alignment when doing loop-unroll or vectorization calculations. See Why does unaligned access to mmap'ed memory sometimes segfault on AMD64? for one case, and links to a couple other articles digging into the fact that code which doesn't respect alignof(T) is broken when compiled by GCC.

You can use ISO C memcpy or GNU C __attribute__((aligned(1))) to do safe unaligned load/store if you don't want to require the user to align the storage, and respect alignments yourself with padding. Probably better to use aligned types, but you could do it this way:

int dtget_int ( const dtype * var )
{
   //... some checking ...
   typedef int unaligned_int __attribute__((aligned(1)));  // GNU C
    return *( (const unaligned_int*) ( var + DTYPE_MIN_MEM) );
}

It would compile fairly efficiently for most ISAs, but not for ones like RISC-V, or ARM without -mcpu= something recent like an ARMv7, where GCC/clang are unwilling to use unaligned loads. https://godbolt.org/z/v1xGKcq43 shows you getting byte loads and shift/OR to merge.

Perhaps a CPP macro for declaring a dtype object would be good, since that can include an alignas. But that wouldn't solve the design problem of keeping the size somewhere and leaving padding.

You could get the compiler to calculate how much padding to leave for alignment by asking it about the layout of a struct dummy { dtype_size sz; T x; } using offsetof(struct dummy, x};. Or much more simply, use max(sizeof(dtype_size), alignof(T)) bytes from the start of the dtype[20] object.


I'm pretty sure using other types in a char buf[20] is at least technically a strict-aliasing violation, so maybe you should be using memcpy to get data in/out. This isn't anonymous memory you got from malloc; it has a static type, which is char. char* can alias anything, but int* isn't allowed to alias char objects or arrays.

See Is strict aliasing one-way? on Stack Overflow, and examples of breakage like GCC AVX _m256i cast to int array leads to wrong values.. (GCC defines __m256i with __attribute__((may_alias)), so for aliasing purposes __m256i* works like char*.) But this code isn't reading the char elements of the buffer, only accessing as the type stored, so actual breakage is much less likely.

C++ explicitly allows using an array-of-unsigned char object as storage for other objects, via placement-new. But C doesn't have placement-new, so if this is allowed, it would rely on a store beginning the lifetime of an object of one type, and a store with a different type ending the lifetime of the original object. Probably best to use unsigned char instead of char as your building block, even in C.

This might be fine in practice since you're only ever reading using the type of the last write. @Supercat has plenty to say about compilers being overly willing to break code that should clearly work, and problems when reusing the same storage as a different type (e.g. after the 2nd write with a different type). This might be one of those cases that breaks; if you were really going to use this in production, that would be something to check on.

I forget the details, maybe @supercat can confirm whether some compilers break code like this even if you follow the ISO C rules for storing an int where there used to be a struct. But like I said, I think it's problematic in strict ISO C just to deref an int* that points at a char[4] in the first place.

Workarounds include memcpy and typedef int unaligned_aliasing_int __attribute__((aligned(1),may_alias));

I'm not 100% sure this is a real problem. But if it is, it might not be avoidable. One way might to to declare the object initially with an array member to make it the full size, and then have the library functions use a version the same struct-of-union with a flexible array member at the end. And rely on the common-initial-sequence rule in C to avoid strict-aliasing problems. I hope.

  struct dtype {
     dtype_size sz;
     unsigned char type;   // or dtype_size
     union { 
       signed char sc; unsigned char uc;
       signed short ss; unsigned short us;
       int i; unsigned u;
       // ...
       char str[ max(sizeof(long long), sizeof(long double)) ];
       // or use a user-supplied size, with this declaration as a CPP macro
       // TODO: is it safe to write past the end of this array?
       // That would be much better than a separate space[] member.
     };
     //char space[];   // flexible array member, no size specified.
     // Probably not safe to access outside str[] and into this, especially if it has a specified size.
     // Hopefully str[] inside the union can work as a flexible array member.
  };

Maybe make that definition a macro that takes a size, and declare dtype with DECLARE_DTYPE(/**/); so the array member is empty. And a specific variable gets declared with DECLARE_DTYPE(10) var; to have char str[10]; in the union. So after expansion, inside a function you'd have struct local_dtype { ... ; union{ ...; char str[32]; }; };. And you'd be passing a pointer to that as an arg to a function declared as taking void* or a pointer to struct dtype { ...; union {...; char str[16]; };};. Probably have to be void* because implicit conversion from &var to a different struct tag won't work, even if they have a compatible initial sequence.

I'm not fully sure this would work with ISO C rules, this is just an idea / suggestion for something to look into. The common initial sequence rule is something you'd have to look into carefully, and flexible array members. It would also I think rule out runtime-variable sizes, which you could do with a VLA using your char array method.


Since you care about efficiency, don't indirect all your function call through non-const function pointers in separate compilation unit.

It's like you're making it as hard as possible for a compiler to inline trivial functions like dtget_int. And for ones it chooses not to inline, forcing a load of a global variable and indirect call through that. More instructions, harder for branch prediction, and the prediction can't be checked as soon (has to wait for a data load, not just decoding an instruction like bl dtget_int).

As others have said, just don't do this, make free functions like a normal C library.

But it's not just a style choice, you're actively hurting the optimizer. Even whole-program or link-time optimization might not be able to inline because you didn't even declare __dtypes_struct dtypes as const.

The compiler has to assume that every / any call to a library function like printf or strchr could change one of those function pointers, so they might not still have the values initialized in dtype.c. And even with const, there's no chance for the compiler to inline without gcc -flto to do link-time optimization so it can see across source files.

I'm pretty skeptical of your benchmarks; perhaps they didn't bottleneck on front-end instruction throughput so a powerful modern CPU was able to chew through those non-inline indirect calls while there was some other bottleneck it was waiting for. And they didn't bottleneck on store/reload latency. Since this would stop a value from getting kept in a register: across non-inline function boundaries the state of memory has to reflect the C abstract machine.

If you used this for most integers in your program, it would be a disaster.


I didn't look at the details of everything else, mostly just wanted to talk in more detail about those two issues, but did spot a few other things.


typedef unsigned short DTYPE_SIZE; seems odd to me; normally type names aren't in all-caps, and it seems very ugly. Unfortunately all names ending with _t are reserved by POSIX, but you could use dtype_size or dtype_size_type. See on SO: C type naming conventions, _t or ALLCAPS - all the answers there recommend against ALL_CAPS type names. C stdio's FILE* is an ancient exception, not a model to follow. (C stdio predates the C language even being fully C so has other clunky things like strings for fopen modes, not constant flags because the C preprocessor didn't exist yet.)


pacmaninbw pointed out that _dtset_obj(var, value, strlen(value) + 1) in dtset_str truncates the size_t return value to dtype_size, which is currently unsigned short.

You might might want to define this function specially, and strncpy with the length as a limit. But that would force zeroing of the full dtype buffer out to its max size, because strncpy has a silly definition (or is designed for a use-case other than length-capped strcpy). And strncpy still wouldn't guarantee zero-termination, so maybe not a good suggestion.

We know that the max size of a dtype object is USHORT_MAX, so very large strings can't go into one in the first place.

But _dtset_obj doesn't need to take a narrow arg. It can and should take size_t or at least unsigned, so it can check to see if you're trying to copy from a way-too-big string.

Instead, the actual behaviour is that it truncates the string length to (usually) 16-bit before checking. So worst case (strlen+1) % 65536 is some small number that fits, and we take first few bytes of the string. Even if that's less than the the size of the dtype object. This is weird and should be avoided, so take a size_t arg.

\$\endgroup\$
14
  • \$\begingroup\$ A char* or unsigned char* is allowed to alias any type in C, so there is no strict-aliasing violation. There might be a memory-alignment violation. \$\endgroup\$
    – Davislor
    Oct 8, 2022 at 4:20
  • \$\begingroup\$ @Davislor: yeah that mitigates things somewhat for working in practice, because array access works as pointer math involving a char*. But I think it's still officially UB to have a char c; and do *(bool*)&c = true;, since the lvalue doesn't match the statically-known type of the object, and it's not a char* deref. Same problem for pointing an int* at char buf[4]; the memory isn't anonymous like from malloc, it has a known type that isn't int. I may be wrong about this, at least in practice, but *(int*)something isn't a char* deref so that argument doesn't help. \$\endgroup\$ Oct 8, 2022 at 4:29
  • \$\begingroup\$ The Standard appears to say that bool from <stdbool.h> (an alias for _Bool) must have a lower conversion rank than char, and sizeof(char) must be 1. I think that makes the *(bool*) example valid, but there might be some room for problems with things like bool bitfields. The buf[4] to int example might be misaligned, although a pointer from malloc() is guaranteed to be aligned for any standard type. \$\endgroup\$
    – Davislor
    Oct 8, 2022 at 5:17
  • \$\begingroup\$ The C17 draft says, “When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.” There’s UB if the pointer is misaligned, or in some other cases such as a possible trap representation, but memcpy() or writing through an aligned pointer seem to be safe. \$\endgroup\$
    – Davislor
    Oct 8, 2022 at 5:23
  • 1
    \$\begingroup\$ This has srifted pretty far off-topic, but it looks to me like an implementation that only allowed casting an object pointer to char* and modifying the bytes of its object representation, but not aliasing an aligned array of char to another type, could claim to conform (although it would break a lot of existing code). I think there were once some obsolete mainframes where char data and float data would be stored in different memory, so that attempting to convert a pointer from the former to the latter would fail. A fat-pointer implementation might also store the actual type and trap. \$\endgroup\$
    – Davislor
    Oct 10, 2022 at 1:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.