Gobject serialisation
From Buzztard
The goal is to lessen the maintenance work. When now adding fields to songs classes, one also needs to add code to load/save methods in the BtSongIONative io modules to make these fields persistent. We also need this for cut/copy/paste to serialize parts of out document and load partial fragments.
Idea 1 is to add metadata fields as qdata to allow serialisation/deserialisation of GObjects. Advantage here is that is less intrusive for objects and allows for different serialisation formats.
Idea 2 is to add a Serializeable interface to objects. The interface has serialize and desezialize methods (aka: load_myself, save_myself). Advantage here is that it is easy to implement as objects know what need to be serialized.
Contents |
[edit] Introduction
[edit] simple cases
These examples can be done without any extensions.
[edit] simple example case
code:
class object1 { gulong numeric_property=5; gchar text_property="test"; }
xml:
<object1 numeric_property="5" text_property="test"/>
[edit] containment example case
code:
class object2 { gchar id; } class object1 { gulong numeric_property=5; gchar text_property="test"; object2 { id="1"; } object2 { id="2"; } }
xml:
<object1 numeric_property="5" text_property="test"> <object2 id="1"/> <object3 id="1"/> </object1>
[edit] extended cases
These examples need special metadata.
[edit] volatile example
code:
class object1 { gulong numeric_property=5; gchar text_property="test"; glong runtime_value=10; /* GSerialize::Flags=Volatile */ }
xml:
<object1 numeric_property="5" text_property="test"/>
[edit] collection example
code:
class object2 { gchar id; } class object1 { gulong numeric_property=5; gchar text_property="test"; GList object_list; /* GSerialize::CollectionName="children" */ }
xml:
<object1 numeric_property="5" text_property="test"> <children> <object2 id="1"/> <object3 id="1"/> </children> </object1>
[edit] inheritance example
code:
class object1 { gchar id="1"; } class object2::object1 { gulong val=5; }
xml:
<object2 val"5" id="1"/>
[edit] implementation ideas
[edit] GParamSpec flags
A class registers its properties with the GType system via GParamsSpecs. These GParamSpecs are GObjects themself. Thus one can do
pspec = g_param_spec_glong("runtime_value", "runtime value prop", "a long interger to control ...", 0, 100, 0, G_PARAM_READWRITE); g_object_set_qdata(pspec, GPersistenceFlagsQuark, G_PERSISTENCE_FLAGS_VOLATILE); g_object_class_install_property(gobject_class, MY_OBJECT_NAME, pspec);
Next we need a GXMLPersistence (or also GMySQLPersistence if you want one).
GXMLPersistence xml_persistence; xml_persistence = g_xml_persistence_new("mydata.xml"); g_xml_persistence_serialize(xml_persistence,G_OBJECT(my_root)); g_object_unref(xml_persistence);
The de-serializer will read in the data and uses
// read node obj_type=g_type_from_name(node_name); g_object_new(obj_type,"par0",val0,...,NULL);
The beauty of the approach is that is requires less work than writing
my_object_save_myself(object,..) my_object_load_myself(object,..)
functions and it is format agnostic.
[edit] problems
[edit] hidden collection types
What if a collection type (list, hashmap, array) is private and the object has methods like:
my_object_add_item(obj,item); my_object_del_item(obj,item);
Serialisation code can not access the collection to store items and the deserialisation code cannot add items back.
Exposing the collection as a read-only member does not help in cases when the add_function does something else.
[edit] gst-controller
Controllable GObject properties have not just one value, but a value that depends on time. This does not cause problems for buzztard, as we store the time dependency in our patterns and build the controller queue at runtime.
[edit] Interface
In this idea all serializable objects would implement two methods:
new_node=bt_persistence_save(object,node,selection); object=bt_persistence_load(type,object,node,location,...);
where node is an XmlNode* (see ideas for improvements).
selection and location are abstract basetypes and need to be derived locally for each class. They are used for copy/paste operations (serializing a partial object, and deserializing into a existing object).
Normally bt_persistence_load() will be called with a type and NULL for object. In that case it creates a new object.
[edit] Dealing with inheritance
Saving is the easy case, as the full object is available. When loading we need to have the full data that is needed for object construction, before we can create the object. Now g_object_new() is a varargs function, which means we have to have all args statically. The g_object_new_valist need va_list and to my knowledge we can't dynamically build a va_list. That leaves g_object_newv(). For that we would need some helpers:
- we get parameters as va_list in _load()
- the helper needs to step through the va_list, look-up the GParamSpecs on the object class and build a GSList of GParameter entries.
- next we need to step thru the class hierarchy, as long as the parent, implements the persistence-iface we parse the parameters and add them to the GSList (need bt_persistence_make_param()).
- finally we build a array and point to the list entries and free the list
- we can create the object (need bt_persistence_object_new)
- we can configure an existing object (need bt_persistence_object_set_properties)
- both could use a common helper to build the GParameter array from GSList and va_list
The helpers can be in bt_persistence.c. Besides we would require more functions on the interface:
new_node=bt_persistence_save(object,node,selection); success=bt_persistence_save_head(object,node,selection); success=bt_persistence_save_body(object,new_node,selection); object=bt_persistence_load(type,object,node,location,...); list=bt_persistence_load_head(type,node,location); success=bt_persistence_load_body(object,node,location);
where
xmlNodePtr bt_xxx_persistence_save(object,node,selection) { xmlNodePtr new_node; new_node=xmlNewChild(node,NULL,XML_CHAR_PTR("object-name"),NULL); bt_xxx_persistence_save_head(object,new_node,selection); bt_xxx_persistence_save_body(object,new_node,selection); return(new_node); } BtPersistence *bt_xxx_persistence_load(type,object,node,location,...) { list=bt_xxx_persistence_load_head(type,object,node,location); if(!object) { object=bt_persistence_object_new(type,list,var_args); } else { bt_persistence_object_set_properties(object,list,var_args); } bt_xxx_persistence_load_body(object,node,location); return(object); }
and
// chain up last when saving gboolean bt_persistence_save_head(object,new_node,selection) { gboolean result=FALSE; BtPersistenceInterface *parent_iface; // format own properties xmlNewProp(node,XML_CHAR_PTR("property-name"),XML_CHAR_PTR(property_val_str)); result=TRUE; // chain up if(parent_iface=g_type_interface_peek_parent(BT_PERSISTENCE_GET_INTERFACE(object))) { result=parent_iface->save_head(object,new_node,selection); } return(result); } gboolean bt_persistence_save_body(object,new_node,selection) { gboolean result=FALSE; BtPersistenceInterface *parent_iface; // save own body data result=bt_persistence_save(BT_PERSISTENCE(self->priv->child),node,NULL); // chain up if(parent_iface=g_type_interface_peek_parent(BT_PERSISTENCE_GET_INTERFACE(object))) { result=parent_iface->save_body(object,new_node,selection); } return(result); } // chain up first when loading GSList *bt_persistence_load_head(type,node,location) { GSList *list=NULL; GObjectClass *klass; BtPersistenceInterface *iface, *parent_iface; // chain up (FIXME: add helper) klass=g_type_class_ref (type); iface=g_type_interface_peek (klass, BT_TYPE_PERSISTENCE); if(parent_iface=g_type_interface_peek_parent(iface)) { list=iface->load_head(type,node,location); } g_type_class_unref(klass); // parse properties and add to list (FIXME: more helpers) xmlChar * const property_value_str=xmlGetProp(node,XML_CHAR_PTR("property-name")); GParameter param=bt_persistence_make_param(type,"property-name",property_value_str); list=g_slist_prepend(list,param); xmlFree(property_value_str); return(list); } gboolean bt_persistence_load_body(object,node,location) { gboolean result=FALSE; BtPersistenceInterface *parent_iface; // chain up if(parent_iface=g_type_interface_peek_parent(BT_PERSISTENCE_GET_INTERFACE(object))) { result=parent_iface->load_body(object,node,location); } // load own body data self->priv->child=bt_persistence_load(BT_TYPE_xxx,NULL,node,NULL,"parent",self,NULL); if(!self->priv->child) return=FALSE; return(result); }
All this is only needed to support inheritance. In buzztard this is mainly the case for machines, so its not that urgent (it can be worked around with some code duplication).
[edit] Failure during construction
We need a way to mark instances as invalid. Instances for some classes cannot always be constructed in a meaningful way. Examples are BtMachine (plugin is missing), BtWave (sample is missing), BtWire (cannot be linked). The problem is that g_object_new() is not allowed to return(NULL) (see this ticket for glib).
We need to construct the object, but store information about construction failure. One idea is to use GError
GError *err=NULL; object=g_object_new(BT_TYPE_XXX,"param1",param1,....,"construction-error",&err,NULL); if(err!=NULL) { /* Report error to user, and free error */ fprintf (stderr, "Unable to create object: %s\n", err->message); g_error_free (err); g_object_unref(object);object=NULL; }
In the objects itself, it is importnat to only use the GError during construction, that is in _constructor() or _constructed(). Therefore we also need to make it a G_PARAM_CONSTRUCT_ONLY.
static GObject *bt_xxx_constructor(...) { BtXxx *self=BT_XXX(...) /* chain up */ GError **err; g_object_get(self,"construction-error",&err,NULL); if(*err==NULL) { // everything fine so far, do construction ... if(!by_xxx_init_things(self)) { g_set_error (err, BT_XXX_ERROR, /* error domain */ BT_XXX_ERROR_CONSTRUCT, /* error code */ "Failed to init things."); /* error message format string */ goto Error: } } Error: return(G_OBJECT(self)); } static GObject *bt_xxx_constructed(...) { BtXxx *self=BT_XXX(object); GError **err; /* chain up */ g_object_get(self,"construction-error",&err,NULL); if(*err==NULL) { // everything fine so far, do post-construction ... if(!by_xxx_init_things(self)) { g_set_error (err, BT_XXX_ERROR, /* error domain */ BT_XXX_ERROR_CONSTRUCT, /* error code */ "Failed to init things."); /* error message format string */ return; } } }
If we don't want to introduce a new base-class we need add a "construction-error" parameter to each of our base-objects (macros for _class_init, _set_property and _get_property?).
[edit] Extra Ideas
Instead of passing XmlNode* we could pass a PersitenceReader/Writer object that obstracts the format. Then all the xml handling is isolated and we can change the implementation. For that we need to do some reasearch on what operations we need to do on the objects.
[edit] links
Links to discussion and other approaches
- http://bugzilla.gnome.org/show_bug.cgi?id=157734
- http://gwyddion.net/documentation/cvs/libgwyddion/libgwyddion-gwyserializable.php
- http://gnome.org/~robsta/gopersist/docs/reference/
Links to OO-2-XML mappings:



