Then - C is a statically typed language, with no static support for objects. on the other hand, it gives you control almost everything you want to do, and it provides a "generic" data type - which is just the void (void) type pointer. When you declare a variable of type void *
- all that the compiler "knows" is that it is a memory address - and your program will be solely responsible for manipulating the data in that memory region.
So you can not have a "generic" object that uses "Structs" pre-defined in C dynamically - you can not pass a struct type as a parameter to a function.
This means that you will have to structure your object types so that they have some fixed fields at the beginning of the data structure that describe the layout of the data in later sessions (including size).
For example, you could define, in "Portuguese" itself, that for your objects, the first two bytes will be a 16-bit integer defining the length of a string, where each byte corresponds to an ASCII character defining a field - type "B - Unisgned char, b - signed char, I 32bit unsigned integer" - "L 64 bit unsigned integer", "Z 16bit size prefixed string".
And then you write functions that treat data with this formatting as described. Note that this is independent of whether you define this object header as a struct itself, or simply use pointer arithmetic, within its functions, to allocate the required memory and manipulate the attributes of your objects dynamically.
For the type of object I described above, we could have this function to create new objects, allocating the required memory at runtime:
#include <stdlib.h>
void *create_object(char *definition) {
short unsigned int size = 2, def_len=0;
void *new_obj=NULL;
for (int i = 0; definition[i]; i++) {
def_len ++;
size += 1;
switch (defintion[i]) {
case 'B': size += 1; break;
case 'I': size += 4; break;
...
}
}
new_obj = malloc(size);
if (!new_obj) {return NULL;}
(short integer *)(new_obj[0]) = def_len;
for(int i = 0; definition[i]; i++) {(char *)(new_obj[i]) = defintion[i]}
return new_obj;
}
(A function to manipulate the fields themselves, within this reserved memory, would have to go through each character of the definition string to know the position of each field, when it was to access a field by its numeric index):
int get_field_offset(void *obj, int field_num, char *type) {
int field_offset = 2 + *((short int *)obj);
for (int i = 0, j = 0; i < field_num; i ++) {
char field_type = (char *)(obj[i + 2]);
if (i >= *((short int *)obj)) {return 0;}
switch (field_type) {
case 'B': field_offset += 1; break;
case 'I': field_offset += 4; break;
...
}
}
type[0] = field_type
return field_offset
}
void set_field_value(void *obj, int field_num, void *value) {
char *type[1]=0;
int offset;
offset = get_field_offset(obj, field_num, type)
if (!offset) return; // field does not exist
switch (type[0]) {
case 'B': *(char *)(obj[offset]) = value;
case 'I': *(int *)(obj[offset]) = value;
}
}
void * get_field_value(void *obj, int field_num, char *type) {
int offset;
offset = get_field_offset(obj, field_num, type)
if (!offset) return NULL;
// Return the address of the exact field, and its type indication on "type"
return &(obj[offset]);
}
So realize that you can manipulate different data structures that change at runtime, and you do not even need to use the C struct keyword. You can even use an object definition that comes from an input of data - whether the user is typing or reading from a text file.
void *coordenadas = create_object("ff")
set_field_value(coordenadas, 0, 23.0);
set_field_value(coordenadas, 1, 45.23);
...
(For that, just put f
as float
or even double
in the switch cases above) - and you can save latitude and longitude on these objects.
This is a "very crude" form - and it would be hard work to accommodate variable length data types in there. But you could fine-tune how much you wanted, for example, by adding a field to count how many references there are to the object (so, whenever a code snippet no longer needs an object, one of the reference counter decreases - if that counter reaches zero , the object can be deallocated immediately, releasing the memory). Another interesting sophistication is to include a table of strings that would allow, for example, to give textual names to the fields. Of course the C code is getting proportionately more complex.
Various object systems, or generic data protocols, are written in pure C, and all of them have to depart from more or less of these principles (det er fixed fields at the beginning of the data that determine the layout of the whole object) - the "gobject" framework, for example, Google's "protobuf", Cap'n'Proto
and the Python programming language itself - of which all objects have a memory representation that can be used from the C language done well in those terms. (In general, these initial fields that define the layout of an object are not visible if you access the object from Python code, but are there if you access objects from C). The definition of the objects in Python has to be included in any extension in C that goes to manipulate Python objects, for example, and to handle generic objects, it uses the types (typedefs) defined in the file object.h - see this file, near line 112.