Friday, September 7, 2018

Part 3. Deriving from GObject derived class - Object properties

In the previous post, we introduced a concept about getter and setter methods. Basically, they provide safer access to the class members. However, it is absolutely not necessary to have getters and/or setters for all class member. The decision to have or not to have getters and setters should be driven by the class design logic. Here, we will review two approaches to implement get/set methods. Let's start with an example where public API available to serve our getter and setter purpose. We continue our work on the MyPoint class expanding it. Let's add a public API to access our name member. As a reminder, our public structure should be:

1
2
3
4
5
6
typedef struct {
  gint x;
  gint y;
  gchar *name;
  /* stuff */
} MyPointPrivate;

Which corresponds to the derivable class from the part 2. For public API, we need to declare our methods in the header file and define them in the source file. The declaration in the header file is pretty straightforward and will be:

1
2
void my_point_set_name (MyPoint *self, const gchar *name);
const gchar *my_point_get_name (MyPoint *self);

The definition in the source file should be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void
my_point_set_name (MyPoint *self, 
                   const gchar *name)
{
  g_return_if_fail (self);
  
  MyPointPrivate *priv = my_point_get_instance_private (self);
  
  g_free (priv->name);
  priv->name = g_stdup (name);
}

const gchar*
my_point_get_name (MyPoint *self)
{
  g_return_val_if_fail (self, NULL);
  
  MyPointPrivate *priv = my_point_get_instance_private (self);
  
  return priv->name;
}

Did you notice some extra calls on line #5 and #16? They are common checkers for method parameters. Without those checkers, we may pass a NULL as the self argument and it will cause an error. Since we are trying to dereference it in the function. With these checkers, we know for sure that self is not NULL. GObject provides an alternative and a general approach to accomplish the same task. It is called GObject properties. How it works? We introduce a property for an object and associate that property to a class member. After that, when we manipulate the property we manipulate our class member. Let's implement a property for the class member name for our MyPoint class.

 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
enum {
  PROP_0,
  PROP_NAME,
  N_PROPERTIES
};

static GParamSpec *properties[N_PROPERTIES] = {NULL};

static void
my_point_set_property (GObject      *object,
                       guint         property_id,
                       const GValue *value,
                       GParamSpec   *pspec)
{
  MyPoint *self = MY_POINT(object);
  MyPointPrivate *priv = my_point_get_instance_private (self);

  switch (property_id)
    {
      case PROP_NAME:
        g_free (priv->name);
        priv->name = g_value_dup_string (value);
        break;
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
        break;
    }
}

static void
my_point_get_property (GObject      *object,
                       guint         property_id,
                       GValue       *value,
                       GParamSpec   *pspec)
{
  MyPoint *self = MY_POINT(object);
  MyPointPrivate *priv = my_point_get_instance_private (self);

  switch (property_id)
    {
      case PROP_NAME:
        g_value_set_string (value, priv->name);
        break;
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
        break;
    }
}


static void
my_point_class_init (MyPointClass *klass)
{
  GObjecClass *cobject = G_OBJECT(klass);

  cobject->get_property = my_point_get_property;
  cobject->set_property = my_point_set_property; 

  properties[PROP_NAME] = g_param_spec_string ("name",
                         "Name",
                         "Name of our point",
                         "Default value",
                         G_PARAM_READWRITE));  
  
  g_object_class_install_properties (cobject,N_PROPERTIES,properties);
}

Let's review this code line-by-line. In the beginning, we declare an enumerator that defines all properties. The property with value "0" is reserved for internal GObject use and should not be used in the user-written code. We can even remove it from our declaration and set the next member to have value "1":

1
2
3
4
enum {
  PROP_NAME = 1,
  N_PROPERTIES
};

On the line #7 (previous block) we declare an array to hold our properties and set all values to NULL. In the next two blocks, we define static functions to implement a code for our object to work with our properties. At the beginning of the function's body, we get an object to the private structure and then use the switch statement to work with each property based on passed property ID, which in fact enumerator values defined earlier. Pay attention to the object type we pass to our static methods. It is GValue. Because of that, it is very general and a user only needs to implement how to set/get values to the GValue variable. Exactly what we have in our switch statement. GObject class defines two pointers to functions that we need to set using our static functions: set_property and get_property (lines #56 and #57).  The next step would be setting values to our properties array. We should do this in our ..._class_init function. One-by-one, we need to set values for every element of our array. g_param_spec_... family of functions provides a convenient mechanism to create a GParamSpec instance. The user should use an appropriate function that corresponds to the type of member. In our case it is const gchar* , therefore, we use g_param_spec_string method. Please see a full list of the available methods in the official documentation. In the last step (line #65), we register our array with properties. That's it. Now, if we want to get a value of our name class member we need to use g_object_get() method and to set a new value for name, we should use g_object_set() method:

1
2
3
4
5
gchar *buffer;
MyPoint *point;

g_object_get (point,"name",&buffer,NULL);
g_object_set (point,"name",buffer,NULL);

The code should be self-explained. The first argument for both methods is our MyPoint instance. The second argument is our property name. To get a value we need to pass a reference to an object where we would like to have a copy. To set a new value we need to pass a regular object instance. We can repeat this step and get as many as we need properties at once. Just add more property-value pairs. In the end, we should terminate with NULL.
As you can see, two approaches are very similar for API user and can be used interchangeably as soon as an appropriate code to define properties was implemented. For library development, the generic approach is favorable since it provides a better binding implementation for other languages.

No comments:

Post a Comment