Cross-platform C SDK logo

Cross-platform C SDK

Data binding

❮ Back
Next ❯
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_tdbind (...)
dbindst_tdbind_enum (...)
dbindst_tdbind_binary (...)
dbindst_tdbind_alias (...)
dbindst_tdbind_unreg (...)
type*dbind_create (...)
type*dbind_copy (...)
voiddbind_init (...)
voiddbind_remove (...)
voiddbind_destroy (...)
voiddbind_destopt (...)
intdbind_cmp (...)
bool_tdbind_equ (...)
type*dbind_read (...)
voiddbind_write (...)
voiddbind_default (...)
voiddbind_range (...)
voiddbind_precision (...)
voiddbind_increment (...)
voiddbind_suffix (...)

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.
  • Schema showing a data structure, its table in DBind, and the operations that can be performed automatically.
    Figure 1: Automation of operations on data with DBind.

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):

Listing 1: Data model based on the Product structure.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
typedef enum _type_t
{
    ekCPU,
    ekGPU,
    ekHDD,
    ekSCD
} type_t;

typedef struct _product_t
{
    type_t type;
    String *code;
    String *desc;
    Image *image;
    real32_t price;
} Product;

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.

Listing 2: Registering the data model in (Listing 1).
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);
Scheme showing a data structure and its equivalent table.
Figure 2: Internal tables created by DBind when registering the data model.

1.1. Type aliases

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:

Listing 3: Misuse of unregistered types.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
typedef uint32_t color_t;
typedef struct _product_t
{
    type_t type;
    String *code;
    String *desc;
    Image *image;
    real32_t price;

    color_t color;
} Product;

// ekDBIND_TYPE_UNKNOWN
dbind(Product, color_t, color);

To support equivalent types declared using the C typedef, we will only have to add them as 'alias' in DBind (Listing 4):

Listing 4: Declaring a typedef via alias in DBind.
1
2
3
4
5
6
typedef uint32_t color_t;
...
dbind_alias(uint32_t, color_t);
...
// ekDBIND_OK 'color_t' is a known type
dbind(Product, color_t, color);

2. Creating 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.

Listing 5: Automatic construction and destruction.
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

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().

Listing 6: Automatic initialization and release.
 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

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).

Listing 7: Automatic object copy.
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).

Listing 8: Editing objects managed with DBind.
 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).

Listing 9: Objects of type 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

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:

Listing 10: Using images (binary object) with DBind.
 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):

Listing 11: Registro de nuestro tipo binario.
 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) .

Listing 12: Using arrays with DBind.
 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.

Table 1: Default values.
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.

Listing 13: Set default values.
 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.).

Listing 14: Range and precision of the 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.

Listing 15: Object compare with DBind.
 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 and 0 if they are equal.
  • For text strings, it will perform a character-by-character alphabetical comparison, returning -1, 1 upon finding the first mismatch, or 0 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 return 0 only if all fields are equal.

4. Serialization with DBind

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).

Listing 16: Object serialization with DBind.
1
2
3
ArrPt(Product) *products = dbind_read(stream, ArrPt(Product));
...
dbind_write(stream, products, ArrPt(Product));
Access to dbind tables to serialize objects.
Figure 3: Reading/Writing objects using DBind.

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.

Listing 17: Export to JSON with DBind.
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));
Listing 18: Import from JSON with DBind.
1
2
3
4
5
Stream *stream = http_dget("http://mywebservice.com/dproducts.php", NULL, NULL);
ArrSt(Product) *products = json_read(stream, NULL, ArrSt(Product));
// 'products' is now a DBind-known object
...
dbind_destroy(&products, ArrSt(Product));
Listing 19: JSON generated from 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",
    ...
  }
    ...
}
Connections between the fields of an object and a JSON script.
Figure 4: Data Binding in JSON script parsing.

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.

Connections between a graphical user interface and the data it manipulates.
Figure 5: Automatic data synchronization and graphical interface.
❮ Back
Next ❯

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

See Object initialization.


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

See Object initialization.


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 NULL upon destruction.

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

See Serialization with DBind.


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

See Serialization with DBind.


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. .05f in real32_t values).

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.

❮ Back
Next ❯