Data binding
This page has been automatically translated using the Google Translate API services. We are working on improving texts. Thank you for your understanding and patience.
Functions
dbindst_t | dbind (...) |
dbindst_t | dbind_enum (...) |
dbindst_t | dbind_binary (...) |
dbindst_t | dbind_alias (...) |
dbindst_t | dbind_unreg (...) |
type* | dbind_create (...) |
type* | dbind_copy (...) |
void | dbind_init (...) |
void | dbind_remove (...) |
void | dbind_destroy (...) |
void | dbind_destopt (...) |
int | dbind_cmp (...) |
bool_t | dbind_equ (...) |
type* | dbind_read (...) |
void | dbind_write (...) |
void | dbind_default (...) |
void | dbind_range (...) |
void | dbind_precision (...) |
void | dbind_increment (...) |
void | dbind_suffix (...) |
- 1. Register data types
- 2. Creating objects
- 2.1. Object initialization
- 2.2. Object copy
- 2.3. Editing objects
- 2.4. Basic types
- 2.5. Nested objects
- 2.6. Binary objects
- 2.7. Using arrays
- 2.8. Default values
- 2.9. Numeric ranges
- 3. Object compare with DBind
- 4. Serialization with DBind
- 5. Import and export to JSON
- 6. Synchronization with graphical interfaces
In high-level languages, such as .NET or Javascript, data binding is a technique that allows establishing an automatic connection between the data of an application and its user interface elements. The NAppGUI DBind module implements and extends this concept in C language, since it makes it possible to automate certain tasks on the structures and objects of our application (Figure 1). Thanks to this we will avoid generating redundant code that is problematic to maintain, providing a general interface for:
- Creation, destruction and copying of objects.
- Comparison of objects.
- Serialization: Reading and writing in streams.
- Import/export in different formats, such as JSON.
- Synchronization with user interfaces.
1. Register data types
- Use dbind to register structures.
- Use dbind_enum to register enumerations.
The first step to use data binding is to register in DBind the user-defined types (struct
and enum
). The basic types are known in advance, since they are added automatically when starting the program. We start from our simple data model (Listing 1):
Product
structure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
We will add it to DBind when starting the application (Listing 2). This will create a sort of "database" that will house the name, type and offset of the fields of each structure (Figure 2). Thanks to this information it will be possible to manipulate objects completely automatically and without the need to create additional code by the programmer.
1 2 3 4 5 6 7 8 9 |
dbind_enum(type_t, ekCPU, ""); dbind_enum(type_t, ekGPU, ""); dbind_enum(type_t, ekHDD, ""); dbind_enum(type_t, ekSCD, ""); dbind(Product, type_t, type); dbind(Product, String*, code); dbind(Product, String*, desc); dbind(Product, Image*, image); dbind(Product, real32_t, price); |
1.1. Type aliases
- Use dbind_alias to register alias (
typedef
).
dbind()
uses the type name of each field in the structure to locate it within its internal record. Using unregistered types will result in a ekDBIND_TYPE_UNKNOWN
error. For example, in (Listing 3), DBind does not know that the type color_t
is actually a uint32_t
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
To support equivalent types declared using the C typedef
, we will only have to add them as 'alias' in DBind (Listing 4):
typedef
via alias in DBind.
1 2 3 4 5 6 |
2. Creating objects
- Use dbind_create to create objects.
- Use dbind_destroy to destroy objects.
One of the first uses of DBind is the creation, initialization, copying and destruction of objects without having to explicitly program constructors and destructors. This operation can become cumbersome when there are nested objects or containers as part of the main object. In (Listing 5) we have a simple example of constructing and destroying an object of type Product
without having explicitly defined functions for it. When registered, DBind knows how to reserve memory and initialize each field according to Default values.
1 2 3 4 5 |
Product *prod = dbind_create(Product); // 'prod' correctly initialized by default ... dbind_destroy(&prod, Product); // 'prod' correctly destroyed including all its fields |
2.1. Object initialization
- Use dbind_init to initialize objects.
- Use dbind_remove to free objects.
dbind_create()
and dbind_destroy()
act on the Heap Segment, that is, they allocate and free the dynamic memory necessary for the object itself. But sometimes it is possible that objects reside in an automatically managed memory space, either because they are housed in the Stack Segment or in a container like ArrSt
or SetSt
. In these cases we will use initializers and releasers that will work on the internal fields of the object without worrying about the memory of the object itself (Listing 6). Obviously, the internal fields of a structure initialized with dbind_init()
can reserve dynamic memory that will be freed by dbind_remove()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Object in stack Product prod1; // Object in container Product *prod2 = arrst_new(arrst, Product); dbind_init(&prod1, Product); dbind_init(prod2, Product); // 'prod1', 'prod2' correctly initialized by default ... dbind_remove(&prod1, Product); dbind_remove(prod2, Product); // ONLY 'prod1', 'prod2' fields destroyed // The object itself memory will be managed automatically // Because lives in stack or container |
2.2. Object copy
- Use dbind_copy to copy objects.
Object duplication is also automated, allowing a "deep" and recursive copy of all fields and nested objects, without the need to define any copy function (Listing 7).
1 2 3 |
Product *nprod = dbind_copy(prod, Product); ... dbind_destroy(&nprod, Product); |
2.3. Editing objects
Once an object of a registered type has been created, it can be edited and manipulated like any C object since, in reality, it is still an instance of a struct
type (Listing 8).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Product *prod1 = dbind_create(Product); Product prod2; dbind_init(&prod2, Product); // 'prod1', 'prod2' are really struct instances ... str_upd(&pr1->desc, "Another desc"); ... pr2.price = 100.23f; ... bstd_printf("Product name: %s with price: %.2f\n", tc(pr2.desc), pr2.price); ... dbind_destroy(&prod1, Product); dbind_remove(&prod2, Product); |
2.4. Basic types
As we already mentioned at the beginning, we only have to register the structures and enumerations of our application. DBind already knows the basic types and strings (String
) in advance, so they will be accepted as field types in struct
:
- Boolean:
bool_t
. - Integers:
uint8_t
,uint16_t
,uint32_t
,uint64_t
,int8_t
,int16_t
,int32_t
,int64_t
. - Real:
real32_t
,real64_t
. - Dynamic text strings:
String
.
Use of unregistered types will be ignored by dbind()
. Use dbind_alias() if you want to use equivalent basic types.
2.5. Nested objects
A registered object can be part of another registered object, using static or dynamic memory reservation (Listing 9). In this case, the nested objects stock1
and stock2
of type Stock
will be initialized with their default values when creating the main object using dbind_create(Product)
.
Stock
nested in Product
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
typedef struct _stock_t { uint32_t min_units; uint32_t max_units; uint32_t cur_units; String *location; bool_t required; } Stock; typedef struct _product_t { ... Stock stock1; // Static alloc Stock *stock2; // Dynamic alloc } Product; // Stock struct to DBind dbind(Stock, uint32_t, min_units); dbind(Stock, uint32_t, max_units); dbind(Stock, uint32_t, cur_units); dbind(Stock, String*, location); dbind(Stock, bool_t, required); // Stock fields in Product dbind(Product, Stock, stock1); dbind(Product, Stock*, stock2); ... Product *prod = dbind_create(Product); // 'stock1', 'stock2' instances correctly initialized bstd_printf("Product locations: %s, %s\n", tc(prod->stock1.location), tc(prod->stock2->location)); dbind_destroy(&prod, Product); |
2.6. Binary objects
- Use dbind_binary to declare binary types.
A binary (or opaque) object is one whose declaration is hidden, that is, we do not have access to (or do not want to register in DBind) its struct
type. These types of objects will be handled as indivisible blocks of bytes, without going into details about the nature or origin of their content. We have a clear example with the type Image, automatically declared by NAppGUI. Thanks to this we can use images within our data model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
typedef struct _product_t { ... Image *image; } Product; dbind(Product, Image*, image); Product *prod = dbind_create(Product); if (prod->image != NULL) { // Exists a default image draw_image(prod->image); } // product->image will be destroyed if exists. dbind_destroy(&prod, Product); |
Si queramos registrar nuestros propios tipos binarios, deberemos proveer a DBind de funciones para copiar, serializar y destruir objetos de dicho tipo (Listing 11):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
typedef _mytype_t MyType; // Definition is hidden static MyType *mytype_copy(const MyType *obj) { // Return a copy of 'obj' } static MyType *mytype_read(Stream *stm) { // Read the object from stream data and return it } static void mytype_write(Stream *stm, const MyType *obj) { // Write the object data into the stream } static void mytype_destroy(MyType **obj) { // Destroy the object here } // Register 'MyType' objects in DBind dbind_binary(MyType, mytype_copy, mytype_read, mytype_write, mytype_destroy); // Now we can use 'MyType' objects with DBind typedef struct _product_t { ... MyType *mytype; } Product; dbind(Product, MyType*, mytype); Product *prod = dbind_create(Product); if (prod->mytype != NULL) { // Exists a default 'MyType' object } // 'prod->mytype' will be destroyed if non-NULL. dbind_destroy(&prod, Product); |
2.7. Using arrays
The containers of type ArrSt
and ArrPt
are also recognized by DBind and, therefore, can be part of the fields in a registered structure (Listing 12) .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
typedef struct _product_t { ... ArrPt(Image) *images; ArrSt(Stock) *stocks; } Product; dbind(Product, ArrPt(Image)*, images); dbind(Product, ArrSt(Stock)*, stocks); // Create an object with inner arrays Product *prod = dbind_create(Product); // Create an array of registered objects ArrSt(Product) *products = dbind_create(ArrSt(Product)); // Will destroy 'images' and 'stocks' arrays and its elements. dbind_destroy(&prod, Product); // Will destroy 'products' array and its elements. dbind_destroy(&products, ArrSt(Product)); |
An important fact, which we should not overlook, is that containers of type ArrSt
can only be used for "open" types, where their definition and, therefore, the memory that the container need to reserve for each item is known. For binary or opaque types (String
, Image
, MyType
, etc.) we must use containers ArrPt
that contain pointers to objects.
2.8. Default values
- Use dbind_default to set the default values of an object's fields.
We have mentioned previously that, when we create a registered object, its fields are initialized with the default values, which we show in defaultval
.
Type | Value |
Booleans | FALSE |
Integers | 0 |
Real | 0.0 |
Enumerated | The minimum value (it does not have to be 0). |
String | Empty string "" , (not NULL ). |
Objects | Default values for each field. |
Objects (pointers) | Memory reservation and default values for each field. |
Binaries | NULL |
Containers | Container is created with 0 elements. |
It is possible to change these values for each field of a (Listing 13) object. In addition to default values for basic types, we can set "default nested objects" or "default containers" for each new instance that is created or initialized with DBind.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// Defaults of basic types dbind_default(Product, type_t, type, ekHDD); dbind_default(Product, real32_t, price, 100.0f); // Defaults of strings // NULL is allowed dbind_default(Product, String*, desc, "Empty-desc"); dbind_default(Product, String*, desc, NULL); // Defaults of binaries // NULL is allowed Image *empty_icon = get_image("empty"); dbind_default(Product, Image*, image, empty_icon); dbind_default(Product, Image*, image, NULL); dbind_destroy(&empty_icon, Image); // Defaults of static nested objects // NULL is NOT allowed Stock *defstock = get_default_stock(); dbind_default(Product, Stock, stock1, defstock); dbind_destroy(&defstock, Stock); // Defaults of dynamic nested objects // NULL is allowed dbind_default(Product, Stock, stock2, defstock); dbind_default(Product, Stock, stock2, NULL); // Defaults of containers // NULL is allowed ArrSt(Stock) *defstocks = get_3_locations_stocks(); dbind_default(Product, ArrSt(Stock)*, stocks, defstocks); dbind_destroy(&defstocks, ArrSt(Stock)); |
2.9. Numeric ranges
- Use dbind_range to set a maximum and minimum on numeric values.
- Use dbind_precision to set the precision to real values.
- Use dbind_increment to set the value of discrete increments.
- Use dbind_suffix to set a suffix that will be added when converting numbers to text.
To conclude with the initialization options, DBind allows us to automatically filter and limit the values related to numeric fields uint32_t
, int8_t
, real64_t
, etc (Listing 14). Internally, it will be responsible for validating the data every time values are read from any data source (GUI, JSON, Streams, etc.).
price
value.
1 2 3 4 |
dbind_range(Product, real32_t, price, .50f, 10000f); dbind_precision(Product, real32_t, price, .01f); dbind_increment(Product, real32_t, price, 5.f); dbind_suffix(Product, real32_t, price, "€"); |
3. Object compare with DBind
Performing a "deep" comparison of objects can involve a lot of work, especially on large objects with nests or containers. DBind provides this function for any registered type (Listing 15). See Comparators and keys.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static int i_cmp(const Product *pr1, const Product *pr2) { return dbind_cmp(pr1, pr2, Product); } ArrPt(Product) *products = create_products(); ... arrpt_sort(product, i_cmp, Product); ... const Product *pr1 = get_product1(); const Product *pr2 = get_product1(); if (dbind_equ(pr1, pr2, Product) == TRUE) { // 'pr1' and 'pr2' are equals } |
The order relationship established by dbind_cmp()
is from lowest to highest, which translates to:
- For numeric types it will return
-1
if the first element is less,1
if the first element is greater and0
if they are equal. - For text strings, it will perform a character-by-character alphabetical comparison, returning
-1
,1
upon finding the first mismatch, or0
if Both chains are totally the same. - For arrays, it will first compare the number of elements in each container, considering "smaller" the one with the fewest elements. If this number matches, an element-by-element comparison will be performed until the first "not equal" is found.
- For nested objects, it will perform a recursive field-by-field comparison in the order they are declared in the
struct
. It will return0
only if all fields are equal.
4. Serialization with DBind
- Use dbind_read to read object from a stream.
- Use dbind_write to write an object to a stream.
Another great advantage that DBind offers is the automatic serialization of registered objects, knowing the detailed composition of each type of data. Therefore, it is possible to access the I/O channels without having to explicitly program write and read functions, as we did in Array serialization (Listing 16) (Figure 3).
1 2 3 |
ArrPt(Product) *products = dbind_read(stream, ArrPt(Product)); ... dbind_write(stream, products, ArrPt(Product)); |
5. Import and export to JSON
DBind provides a private API for external modules to access registry information and take advantage of the full power of data binding. One of these modules is JSON (Figure 4) which allows to export (Listing 17) and import (Listing 18) objects of registered types automatically without no additional effort. In (Listing 19) we see a fragment of the generated JSON file.
1 2 3 4 |
ArrSt(Product) *products = dbind_create(ArrSt(Product)); ... Stream *stream = stm_to_file("data.json", NULL); json_write(stream, products, NULL, ArrSt(Product)); |
1 2 3 4 5 |
ArrSt(Product)
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ [ { "code":"i7-8700K", "desc":"Intel BX80684I78700K 8th Gen Core i7-8700K Processor", "type":0, "price":374.8899999999999863575794734060764312744140625, "image":"\/9j\/4AAQSkZJRgABAQ.... }, { "code":"G3900", ... } ... } |
6. Synchronization with graphical interfaces
And finally, the main use that has traditionally been given to data binding: The possibility of synchronizing the graphical interface with the objects that make up the data model. This paradigm is known as MVVM (Model-View-ViewModel) (Figure 5) and uses the Layout and Cell types to associate struct
instances and fields respectively. More information at GUI Data binding.
dbind ()
Adds a field from a structure to its internal table within DBind.
dbindst_t dbind(type, mtype, name);
type | Type of the structure. |
mtype | Type of the field to register. |
name | Name of the field within the structure. |
Return
Registration result.
dbind_enum ()
Registers a value of type enum
.
dbindst_t dbind_enum(type, value, const char_t *alias);
type | Enum type |
value | Value. |
alias | Alias for the value. |
Return
Registration result.
Remarks
dbind_enum(mode_t, ekIMAGE_ANALISYS, "Image Analisys")
will use the string "Image Analisys" instead of "ekIMAGE_ANALISYS" for those I/O or interface operations that require displaying enumeration literals. For example, to populate the fields of a PopUp linked to a data field.
dbind_binary ()
Registers a binary (opaque) type.
dbindst_t dbind_binary(type, FPtr_copy func_copy, FPtr_read func_read, FPtr_write func_write, FPtr_destroy func_destroy);
type | Object type. |
func_copy | Copy function. |
func_read | Read function. |
func_write | Write function. |
func_destroy | Destruction function. |
Return
Registration result.
Remarks
See Binary objects.
dbind_alias ()
Registers an alias for a data type (typedef
).
dbindst_t dbind_alias(type, alias);
type | Object type. |
alias | Alias name. |
Return
Registration result.
Remarks
See Type aliases.
dbind_unreg ()
Removes a data type from the DBind record.
dbindst_t dbind_unreg(type);
type | Object type. |
Return
Elimination result.
dbind_create ()
Creates an object of registered type, initializing its fields with the default values.
type* dbind_create(type);
type | Object type. |
Return
Newly created object or NULL
if DBind does not recognize the data type.
Remarks
See Creating objects.
dbind_copy ()
Copies an object of registered type.
type* dbind_copy(const type *obj, type);
obj | Object to copy. |
type | Object type. |
Return
Copy of the object or NULL
if DBind does not recognize the data type.
Remarks
See Object copy.
dbind_init ()
Initializes the fields of a registered type object with the default values.
void dbind_init(type *obj, type);
obj | Object whose memory has been reserved, but not initialized. |
type | Object type. |
Remarks
dbind_remove ()
Frees the memory reserved by the fields of an object of registered type, but does not destroy the object itself.
void dbind_remove(type *obj, type);
obj | Object. |
type | Object type. |
Remarks
dbind_destroy ()
Destroys an object of registered type. Memory allocated to fields and sub-objects will also be freed recursively.
void dbind_destroy(type **obj, type);
obj | Object. It will be set to |
type | Object type. |
Remarks
See Creating objects.
dbind_destopt ()
Optional destroyer. Same as dbind_destroy, but accepting that the object is NULL
.
void dbind_destopt(type **obj, type);
obj | Object. |
type | Object type. |
dbind_cmp ()
Compares two objects of registered type.
int dbind_cmp(const type *obj1, const type *obj2, type);
obj1 | First object to compare. |
obj2 | Second object to compare. |
type | Object type. |
Return
-1
, 1
or 0
if obj1
is less than, greater than or equal to obj2
.
Remarks
See Object compare with DBind.
dbind_equ ()
Checks if two objects of registered type are the same.
bool_t dbind_equ(const type *obj1, const type *obj2, type);
obj1 | First object to compare. |
obj2 | Second object to compare. |
type | Object type. |
Return
TRUE
if they are equal.
Remarks
See Object compare with DBind.
dbind_read ()
Creates a registered type object from data read from a stream.
type* dbind_read(Stream *stm, type);
stm | Reading stream. |
type | Type of the object to read. |
Return
Newly created object or NULL
if there has been an error.
Remarks
dbind_write ()
Writes the contents of a registered type object to a write stream.
void dbind_write(Stream *stm, const type *obj, type);
stm | Write stream. |
obj | Object to write. |
type | Type of the object to write. |
Remarks
dbind_default ()
Sets the default value of a field.
void dbind_default(type, mtype, name, mtype value);
type | Struct type. |
mtype | Field type. |
name | Name of the field within the struct. |
value | Default value from now on. |
Remarks
See Default values.
dbind_range ()
Sets the maximum and minimum value in numeric fields.
void dbind_range(type, mtype, name, mtype min, mtype max);
type | Struct type. |
mtype | Field type. |
name | Name of the field within the struct. |
min | Minimum value. |
max | Maximum value. |
Remarks
See Numeric ranges.
dbind_precision ()
Sets the jump between two consecutive real values.
void dbind_precision(type, mtype, name, mtype prec);
type | Struct type. |
mtype | Field type. |
name | Name of the field within the struct. |
prec | Precision (e.g. |
Remarks
See Numeric ranges.
dbind_increment ()
Sets the increment of a numeric value, for example, when clicking an UpDown control.
void dbind_increment(type, mtype, name, mtype incr);
type | Struct type. |
mtype | Field type. |
name | Name of the field within the struct. |
incr | Increment. |
Remarks
See Numeric ranges.
dbind_suffix ()
Sets a suffix that will be added to the numeric value when converting to text.
void dbind_suffix(type, mtype, name, const char_t *suffix);
type | Struct type. |
mtype | Field type. |
name | Name of the field within the struct. |
suffix | Suffix. |
Remarks
See Numeric ranges.