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
.
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.
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.
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 struct
s, 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.
#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"
VAR_POINTER(char,buf)
VAR(size_t,size)
VAR(size_t,rpos)
VAR(size_t,wpos)
VAR(float,factor)
FLAGS(
FLAG(init)
FLAG(alloc)
)
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;
PARAMETER(size_t,size)
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:
#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.
#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.
FUNCTION_INLINE int FUNC(name)(T *p)
{
int is_set = 0;
if (p)
{
}
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:
#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 string
s of chars
type:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "chars.h"
int main(int argc, const char *argv[])
{
chars *s = NULL;
s = chars_create(1);
if (chars_init(s, 10 KB))
{
chars_read_pchar(s, "Hello", 0, CHARS_FLAG_RESET);
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:
s = "Hello"
or something like:
s << "Hello"
or even:
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 string
s.
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.