Click here to Skip to main content
15,903,854 members
Articles / Programming Languages / C

C³ Cubism Extension for the C Programming Language

Rate me:
Please Sign up or sign in to vote.
3.05/5 (4 votes)
19 Sep 2023CPOL8 min read 8K   53   6  
To make programs more understandable and familiar to humans
Based on the Art of the Cubism, this is the desire to decompose the system into its simplest components. This approach makes it possible to accurately describe small modules - cubes, from which a system is automatically assembled that exceeds the complexity of the initial representations.

Introduction

It may seem that this project repeats the principles of object-oriented programming for the C language, but does it in a more meaningful way. Difficult moments are transferred to the field of automation. Thus, program texts are generated automatically, based on a description made by a programmer. I know that the C language has developed in many languages like C++ or C#. I decided to name the new improvement C3, this is C to the cubic power.

Background

One day, a group of young artists asked an experienced craftsman - "How to paint pictures?". He answered that you need to analyze well what you want to draw, decompose it into the simplest geometric shapes, and then put it all together again. Of course, he meant something completely different. But the artists understood his answer in their own way. Cubism is an Art movement of the early 20th century.

I invite you to dive into the amazing world of cubes as applied to programming. You will touch amazing things. I promise you an experience comparable to visiting an exhibition of paintings.

Using the Code

The architecture consists of independent layers that can be used by humans.

First Layer

The first layer is a three-dimensional system of data structures, functions for processing them, and the newest is data transfer routes.

A structure is used for a minimal set of data used together. Transfer routes is special modules responsible for data transfer in a strictly defined direction and over a specific environment.

A pointer to a structure is passed to the function, then all data is available in the function. When I feel that some data is missing, then they need to be added to the structure. Otherwise, if the data is not used, then it makes sense to think about moving it to another structure.

I will explain the use of structure and functions with a simple example of the chars module for a string.

C++
struct chars
{
    char *buf;
    size_t size;
    union
    {
        unsigned int flags;
        struct
        {
            unsigned int is_init:1;
            unsigned int is_alloc:1;
        };
    };
};

int chars_init(struct chars *p, size_t size)
{
    int is_set = 0;

    if (p)
    {
        p->buf = malloc(size * sizeof(char));
        if (p->buf)
        {
            p->size = size;
            p->is_init = 1;
            p->is_alloc = 1;
            is_set = 1;
        }
    }
 
    return is_set;
}

I used anonymous structure and union, so it's better to use the C89 compiler flag, or if you want to use another standard, then you need to add names for the structure.

The name of the function begins with the name of the structure, so that it is clear which structure the function is intended for. The return value is usually nonzero on success and vice versa. It can be checked in the condition.

C++
chars *p = chars_create(1);
if (chars_init(p, 1 KB))
{
    printf("Success");
}

Function chars_create(size_t count) creates one object or array of objects and returns a pointer to the first object in the allocated memory. Function chars_init(T *p) can accept additional arguments for initialization of the members of structure. In this case, this is the size of the buffer.

Here T is the name of the structure. For example, chars_init(T *p) will be replaced with  chars_init(chars *p) by preprocessor.

There is another function chars_free(T *p) to release resources. I'm lazy to call this function every time. In certain cases, this function is called automatically.

C++
int chars_free(struct chars *p)
{
    if (p)
    {
        if (p->buf && p->is_alloc)
        {
            free(p->buf);
            p->buf = NULL;
            p->is_alloc = 0;
        }
    }
    return 1;
}

I'm trying to make the simplest structs, one struct and set of functions per file. When I made several of these files, I had to repeat the create , init and free functions in each one. Then I thought, how can I make the creation of these functions automatic?

Second Layer

The second layer is a modular system of the simplest modules - cubes, which actively uses the C preprocessor.

This is the same chars module in implementation according to the second layer.

C++
/* chars.h */

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

#define MODULE_NAME chars
#define MODULE_STRUCT chars_struct.h
#define MODULE_INIT chars_init.h
#define MODULE_INIT_VAR chars_init_var.h
#define MODULE_FREE chars_free.h

#include "module.h"

/* chars_struct.h */

VAR_POINTER(char,buf)
VAR(size_t,size)
VAR(size_t,rpos)
VAR(size_t,wpos)
VAR(float,factor)
FLAGS(
FLAG(init)
FLAG(alloc)
)

/* chars_init.h */

if (!size)
{
    size = CHARS_DEFAULT_BUFFER_SIZE;
}
p->buf = malloc(size * sizeof(char));
if (p->buf)
{
    p->size = size;
    p->is_init = 1;
    p->is_alloc = 1;
}
else
{
    p->is_alloc = 0;
}
p->factor = CHARS_DEFAULT_GROWTH_FACTOR;

/* chars_init_var.h */

PARAMETER(size_t,size)

/* chars_free.h */

if (p->buf)
{
    free(p->buf);
    p->buf = NULL;
}

The different parts of the module are contained in separate files. Sometimes, this can be inconvenient. So I wrote a program that makes these files for me. Now I don't have to do all those many files. And it is enough to make one file like .mod.h , and run the mod_to_h program in order to make header files like .h .

The mod_to_h program's arguments are the name of the .mod.h file and the destination folder.

mod_to_h file.mod.h include

The mod_to_h program reads file.mod.h, and creates files file.h and others and places them in the include folder.

Third Layer

The third layer is a program - analyzer, for creating modules based on a description made by a programmer.

This is implementation of chars module for third layer in chars.mod.h file:

C++
#module chars

#struct
char *buf;
size_t size;
init;
alloc;

#init (size_t size)
if (!size)
{
    size = CHARS_DEFAULT_BUFFER_SIZE;
}
p->buf = malloc(size * sizeof(char));
if (p->buf)
{
    p->size = size;
    p->is_init = 1;
    p->is_alloc = 1;
}
else
{
    p->is_alloc = 0;
}

#free
if (p->buf)
{
    free(p->buf);
    p->buf = NULL;
}

#include "module.h"

Here are the definition keywords for the preprocessor #module , #struct , #init , #free. These elements are optional, they just add the necessary lines to the construction functions. The definitions are in the file module.h. The file module.txt contains template text for a new module.

During compilation, the program mod_to_h is called, it creates the necessary С language constructs in headers files with extension .h according to the given module description.

To find all .mod.h files and turn them into headers files in the include folder, you can use the following console commands.

For Windows users:

FOR /R %i IN (*.mod.h) DO mod_to_h %i include

For use within scripts, Visual Studio pre-build events use doubling of the percent symbol:

FOR /R %%i IN (*.mod.h) DO mod_to_h %%i include

Warning! The mod_to_h program overwrites headers files in the destination folder, so use this program carefully.

Linux users can use make to compile their test programs because the Makefile contains lines to automatically generate header files.

Various modifiers for automatic actions can be used. This is modifiers: create, init, free. Modifiers can be specified before the type of a variable.

C++
#include "chars.h"

#module chars_test

#struct
create chars *s1;
init chars *s2;
free chars *s3;
chars *s4;

Here, the variable s1 is created, the variable s2 is created and initialized with default value, the variable s3 will be freed when the function free() is called, as well as for s1 and s2 . While the variable s4 is just a pointer to chars type and I should call construction functions manually.

The file function.txt contains template text for a new function.

C++
FUNCTION_INLINE int FUNC(name)(T *p)
{
    int is_set = 0;

    if (p)
    {
        /* Write code here */
    }

    return is_set;
}

Here, I will replace the name with the name of the function. Then the full function name will be module_name _ function_name. For example, chars_read_pchar - is a chars module and function name read_pchar. Here, pchar is a type of argument, it may be file, etc. Inside a module, I can call a function like FUNC(read_pchar), while from other parts of a program like chars_read_pchar .

Functions for different types of arguments are best placed in separate files. And then connect using include. This allows me to create universal functions for working with a specific data type and connect them to different modules.

For example:

C++
#include STR(T_NAME(utils.h))       /* chars_utils.h */
#include STR(T_NAME(char.h))        /* chars_char.h */
#include STR(T_NAME(pchar.h))       /* chars_pchar.h */
#include STR(T_NAME(mark.h))        /* chars_mark.h */

connects functions for working with pchar, char, and utilities to the chars module.

Of course, I can connect these functions to another module, such as file, as long as it has the appropriate members in the structure.

For example, I will create a program for working with strings of chars type:

C++
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Include chars module */
#include "chars.h"

int main(int argc, const char *argv[])
{
    /* Define variable s with chars type */
    chars *s = NULL;

    /* Create chars */
    s = chars_create(1);

    /* Initialize chars */
    if (chars_init(s, 10 KB))
    {
        /* Copy string "Hello" to s */
        chars_read_pchar(s, "Hello", 0, CHARS_FLAG_RESET);

        /* Copy string ", World !" to s next to "Hello" */
        chars_read_pchar(s, ", World !", 0, 0);

        chars_print(s, CHARS_FLAG_PRINT_NEWLINE);
        chars_print_info(s, CHARS_FLAG_PRINT_NEWLINE);
    }

    return 0;
}

The chars_read_pchar function reads the string Hello to the variable s.

Fourth Layer

Of course, it would be more convenient to use an abbreviated notation, like:

C++
s = "Hello"

or something like:

C++
s << "Hello"

or even:

C++
s = "Hello" + ", World !"

I came up with rules such that the << operator means read, and the >> operator means write, in relation to the variable on the left.
Then the function chars_read_pchar should read from pchar and copy to chars .

I decided to replace the write functions with read and vice versa in order to be closer to natural language. This has already been corrected in the article.

Compilation

For Windows users:

You can use Visual Studio C/C++ compiler or Digital Mars C/C++ compiler (DMC).

In the mod_to_h folder there is a project for Visual Studio Express 2005 to build the mod_to_h program.

To use the Digital Mars C/C++ compiler, you can run the console command:

dmc -Iinclude -Icommon -Imodule -Ichars -Iparse -Iutopia test\mod_to_h.c

I tested this command on Windows XP.

Linux users can use the command:

make test/mod_to_h.c

In this case, the line in the Makefile should be commented out:

#include $(OBJECTS_H)

After compiling mod_to_h, this line can be uncommented:

include $(OBJECTS_H)

It is recommended to move the destination folder

HEADERS_DIR=$(HOME)/tmp

to memory /tmpfs in order to frequently overwrite files during compilation.

Known Limitations

It is currently not possible to use double pointer types or addresses or arrays as members of a structure, or as arguments of construction functions. I just don't use them, maybe in the future, I'll add them.

Empty structures are not allowed in C89, so in modules where there is no data in the structure, but only collections of library functions with no module structure type arguments (T *p) , the use of #struct is not recommended, although some compilers allow empty structures, this solution will not be cross-platform. Use #struct only when there is data in the structure.

To Do

I'll be doing this in the fourth layer, which is the line processor, which makes possible additions to the C language that make it easier to work with different types of data, such as strings.

The fifth layer is the transformation of a human-made natural language description into C.

I will do this in future articles.

Points of Interest

I have been creating this technology for many years. As a result, I came to a technology similar to OOP. Yet there are significant differences. It is fully compiled here, so the high speed of the program is ensured.

Also, part of the program is created automatically, which simplifies the task for the programmer. I hope to make programming easier.

History

  • 31st July, 2023: Initial version

The description of three layers of technology is made.

  • 19th September, 2023: Update source code and article

Add compilation instruction.

Add project file for Visual Studio to source code.

Fix errors in source code: Empty struct, cross-platform support.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Systems Engineer
Estonia Estonia
Engineer of Automated Data Processing and Control Systems. Graduated from Novosibirsk State Technical University (NSTU), Department of Automation and Computers.
I worked in television and developed broadcast automation systems.
I write programs in C/C++, Java, PHP, JavaScript, HTML, XML, CSS, MySQL.
I am interested in software architecture. Architecture is art. I use the style of modernism, in particular, it is cubism. Means the desire to parse the problem into the simplest modules. This solution allows you to assemble complex systems that surpass the complexity of the initial design concepts. I am the author of this programming technique.

Comments and Discussions

 
-- There are no messages in this forum --