Add support for org.gtk.GDBus.Since annotation

And use this for a) documentation purposes; and b) to preserve C ABI
when an interface is extended. See

 https://bugzilla.gnome.org/show_bug.cgi?id=647577#c5

for more details. Also add test cases for this.

Signed-off-by: David Zeuthen <davidz@redhat.com>
This commit is contained in:
David Zeuthen 2011-04-12 16:17:28 -04:00
parent 76d3653721
commit 34a28f2f06
9 changed files with 193 additions and 13 deletions

View File

@ -215,6 +215,28 @@ gdbus-codegen --c-namespace MyApp \
</listitem>
</varlistentry>
<varlistentry>
<term><literal>org.gtk.GDBus.Since</literal></term>
<listitem>
<para>
Can be used on any <literal>&lt;interface&gt;</literal>,
<literal>&lt;method&gt;</literal>,
<literal>&lt;signal&gt;</literal> and
<literal>&lt;property&gt;</literal> element to specify the
version (any free-form string but compared using a
version-aware sort function) the element appeared in.
</para>
<para>
When generating C code, this field is used to ensure
function pointer order for preserving ABI/API.
</para>
<para>
When generating Docbook XML, the value of this tag appears
in the documentation.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><literal>org.gtk.GDBus.C.ForceGVariant</literal></term>
<listitem>

View File

@ -2,6 +2,7 @@
import sys
import argparse
import distutils.version
import config
import utils
@ -201,27 +202,60 @@ class CodeGenerator:
self.h.write('struct _%sIface\n'%(i.camel_name))
self.h.write('{\n')
self.h.write(' GTypeInterface parent_iface;\n')
function_pointers = {}
if len(i.methods) > 0:
self.h.write('\n')
self.h.write(' /* GObject signal class handlers for incoming D-Bus method calls: */\n')
for m in i.methods:
self.h.write(' gboolean (*handle_%s) (\n'
' %s *object,\n'
' GDBusMethodInvocation *invocation'%(m.name_lower, i.camel_name))
key = (m.since, '_method_%s'%m.name_lower)
value = ' gboolean (*handle_%s) (\n'%(m.name_lower)
value += ' %s *object,\n'%(i.camel_name)
value += ' GDBusMethodInvocation *invocation'%()
for a in m.in_args:
self.h.write(',\n %s%s'%(a.ctype_in, a.name))
self.h.write(');\n')
self.h.write('\n')
value += ',\n %s%s'%(a.ctype_in, a.name)
value += ');\n\n'
function_pointers[key] = value
if len(i.signals) > 0:
self.h.write('\n')
self.h.write(' /* GObject signal class handlers for received D-Bus signals: */\n')
for s in i.signals:
self.h.write(' void (*%s) (\n'
' %s *object'%(s.name_lower, i.camel_name))
key = (s.since, '_signal_%s'%s.name_lower)
value = ' void (*%s) (\n'%(s.name_lower)
value += ' %s *object'%(i.camel_name)
for a in s.args:
self.h.write(',\n %s%s'%(a.ctype_in, a.name))
self.h.write(');\n')
self.h.write('\n')
value += ',\n %s%s'%(a.ctype_in, a.name)
value += ');\n\n'
function_pointers[key] = value
# Sort according to @since tag, then name.. this ensures
# that the function pointers don't change order assuming
# judicious use of @since
#
# Also use a proper version comparison function so e.g.
# 10.0 comes after 2.0.
#
# See https://bugzilla.gnome.org/show_bug.cgi?id=647577#c5
# for discussion
# I'm sure this could be a lot more elegant if I was
# more fluent in python...
def my_version_cmp(a, b):
if len(a[0]) > 0 and len(b[0]) > 0:
va = distutils.version.LooseVersion(a[0])
vb = distutils.version.LooseVersion(b[0])
ret = va.__cmp__(vb)
else:
ret = cmp(a[0], b[0])
if ret != 0:
return ret
return cmp(a[1], b[1])
keys = function_pointers.keys()
if len(keys) > 0:
keys.sort(cmp=my_version_cmp)
for key in keys:
self.h.write('%s'%function_pointers[key])
self.h.write('};\n')
self.h.write('\n')
self.h.write('GType %s_get_gtype (void) G_GNUC_CONST;\n'%(i.name_lower))

View File

@ -163,6 +163,8 @@ class DocbookCodeGenerator:
self.out.write(' <listitem><para>%s</para></listitem>\n'%(self.expand(a.doc_string)))
self.out.write('</varlistentry>\n'%())
self.out.write('</variablelist>\n')
if len(m.since) > 0:
self.out.write('<para role="since">Since %s</para>\n'%(m.since))
self.out.write('</refsect2>\n')
def print_signal(self, i, s):
@ -180,6 +182,8 @@ class DocbookCodeGenerator:
self.out.write(' <listitem><para>%s</para></listitem>\n'%(self.expand(a.doc_string)))
self.out.write('</varlistentry>\n'%())
self.out.write('</variablelist>\n')
if len(s.since) > 0:
self.out.write('<para role="since">Since %s</para>\n'%(s.since))
self.out.write('</refsect2>\n')
def print_property(self, i, p):
@ -190,6 +194,8 @@ class DocbookCodeGenerator:
self.print_property_prototype(i, p, in_synopsis=False)
self.out.write('</programlisting>\n')
self.out.write('<para>%s</para>\n'%(self.expand(p.doc_string)))
if len(p.since) > 0:
self.out.write('<para role="since">Since %s</para>\n'%(p.since))
self.out.write('</refsect2>\n')
def expand(self, s):
@ -255,6 +261,8 @@ class DocbookCodeGenerator:
self.out.write('<refsect1 role="desc" id="gdbus-interface-%s">\n'%(utils.dots_to_hyphens(i.name)))
self.out.write(' <title role="desc.title">Description</title>\n'%())
self.out.write(' <para>%s</para>\n'%(self.expand(i.doc_string)))
if len(i.since) > 0:
self.out.write(' <para role="since">Since %s</para>\n'%(i.since))
self.out.write('</refsect1>\n'%())
if len(i.methods) > 0:

View File

@ -14,10 +14,13 @@ class Arg:
self.signature = signature
self.annotations = []
self.doc_string = ''
self.since = ''
def post_process(self, interface_prefix, c_namespace, arg_number):
if len(self.doc_string) == 0:
self.doc_string = utils.lookup_docs(self.annotations)
if len(self.since) == 0:
self.since = utils.lookup_since(self.annotations)
if self.name == None:
self.name = 'unnamed_arg%d'%arg_number
@ -158,10 +161,13 @@ class Method:
self.out_args = []
self.annotations = []
self.doc_string = ''
self.since = ''
def post_process(self, interface_prefix, c_namespace):
if len(self.doc_string) == 0:
self.doc_string = utils.lookup_docs(self.annotations)
if len(self.since) == 0:
self.since = utils.lookup_since(self.annotations)
name = self.name
overridden_name = utils.lookup_annotation(self.annotations, 'org.gtk.GDBus.Name')
@ -186,10 +192,13 @@ class Signal:
self.args = []
self.annotations = []
self.doc_string = ''
self.since = ''
def post_process(self, interface_prefix, c_namespace):
if len(self.doc_string) == 0:
self.doc_string = utils.lookup_docs(self.annotations)
if len(self.since) == 0:
self.since = utils.lookup_since(self.annotations)
name = self.name
overridden_name = utils.lookup_annotation(self.annotations, 'org.gtk.GDBus.Name')
@ -224,10 +233,13 @@ class Property:
else:
raise RuntimeError('Invalid access type %s'%self.access)
self.doc_string = ''
self.since = ''
def post_process(self, interface_prefix, c_namespace):
if len(self.doc_string) == 0:
self.doc_string = utils.lookup_docs(self.annotations)
if len(self.since) == 0:
self.since = utils.lookup_since(self.annotations)
name = self.name
overridden_name = utils.lookup_annotation(self.annotations, 'org.gtk.GDBus.Name')
@ -250,12 +262,15 @@ class Interface:
self.annotations = []
self.doc_string = ''
self.doc_string_brief = ''
self.since = ''
def post_process(self, interface_prefix, c_namespace):
if len(self.doc_string) == 0:
self.doc_string = utils.lookup_docs(self.annotations)
if len(self.doc_string_brief) == 0:
self.doc_string_brief = utils.lookup_brief_docs(self.annotations)
if len(self.since) == 0:
self.since = utils.lookup_since(self.annotations)
overridden_name = utils.lookup_annotation(self.annotations, 'org.gtk.GDBus.Name')
if overridden_name:

View File

@ -133,6 +133,8 @@ class DBusXMLParser:
if self.doc_comment_params.has_key('short_description'):
short_description = self.doc_comment_params['short_description']
self._cur_object.doc_string_brief = short_description
if self.doc_comment_params.has_key('since'):
self._cur_object.since = self.doc_comment_params['since']
elif self.state == DBusXMLParser.STATE_INTERFACE:
if name == DBusXMLParser.STATE_METHOD:
@ -161,6 +163,8 @@ class DBusXMLParser:
# assign docs, if any
if attrs.has_key('name') and self.doc_comment_last_symbol == attrs['name']:
self._cur_object.doc_string = self.doc_comment_body
if self.doc_comment_params.has_key('since'):
self._cur_object.since = self.doc_comment_params['since']
elif self.state == DBusXMLParser.STATE_METHOD:
if name == DBusXMLParser.STATE_ARG:
@ -191,6 +195,8 @@ class DBusXMLParser:
doc_string = self.doc_comment_params[attrs['name']]
if doc_string != None:
self._cur_object.doc_string = doc_string
if self.doc_comment_params.has_key('since'):
self._cur_object.since = self.doc_comment_params['since']
elif self.state == DBusXMLParser.STATE_SIGNAL:
if name == DBusXMLParser.STATE_ARG:
@ -215,6 +221,8 @@ class DBusXMLParser:
doc_string = self.doc_comment_params[attrs['name']]
if doc_string != None:
self._cur_object.doc_string = doc_string
if self.doc_comment_params.has_key('since'):
self._cur_object.since = self.doc_comment_params['since']
elif self.state == DBusXMLParser.STATE_PROPERTY:
if name == DBusXMLParser.STATE_ANNOTATION:

View File

@ -48,6 +48,13 @@ def lookup_docs(annotations):
else:
return s
def lookup_since(annotations):
s = lookup_annotation(annotations, 'org.gtk.GDBus.Since')
if s == None:
return ''
else:
return s
def lookup_brief_docs(annotations):
s = lookup_annotation(annotations, 'org.gtk.GDBus.DocString.Short')
if s == None:

View File

@ -1,11 +1,13 @@
<node>
<!-- org.gtk.GDBus.Example.ObjectManager.Animal:
@short_description: Example docs generated by gdbus-codegen
@since: 2.30
This D-Bus interface is used to describe a simple animal.
-->
<interface name="org.gtk.GDBus.Example.ObjectManager.Animal">
<!-- Mood: The mood of the animal.
@since: 2.30
Known values for this property include
<literal>Happy</literal> and <literal>Sad</literal>. Use the
@ -23,6 +25,7 @@
Poke:
@make_sad: Whether to make the animal sad.
@make_happy: Whether to make the animal happy.
@since: 2.30
Method used to changing the mood of the animal. See also the
#org.gtk.GDBus.Example.ObjectManager.Animal:Mood property.
@ -35,6 +38,7 @@
<!--
Jumped:
@height: Height, in meters, that the animal jumped.
@since: 2.30
Emitted when the animal decides to jump.
-->

View File

@ -2144,6 +2144,27 @@ gpointer name_forcing_4 = foo_rocket123_get_speed_xyz;
/* ---------------------------------------------------------------------------------------------------- */
/* See https://bugzilla.gnome.org/show_bug.cgi?id=647577#c5 for details */
#define CHECK_FIELD(name, v1, v2) g_assert_cmpint (G_STRUCT_OFFSET (FooChangingInterface##v1##Iface, name), ==, G_STRUCT_OFFSET (FooChangingInterface##v2##Iface, name));
static void
test_interface_stability (void)
{
CHECK_FIELD(handle_foo_method, V1, V2);
CHECK_FIELD(handle_bar_method, V1, V2);
CHECK_FIELD(handle_baz_method, V1, V2);
CHECK_FIELD(foo_signal, V1, V2);
CHECK_FIELD(bar_signal, V1, V2);
CHECK_FIELD(baz_signal, V1, V2);
CHECK_FIELD(handle_new_method_in2, V2, V10);
CHECK_FIELD(new_signal_in2, V2, V10);
}
#undef CHECK_FIELD
/* ---------------------------------------------------------------------------------------------------- */
int
main (int argc,
char *argv[])
@ -2167,6 +2188,7 @@ main (int argc,
usleep (500 * 1000);
g_test_add_func ("/gdbus/codegen/annotations", test_annotations);
g_test_add_func ("/gdbus/codegen/interface_stability", test_interface_stability);
g_test_add_func ("/gdbus/codegen/object-manager", test_object_manager);
ret = g_test_run();

View File

@ -338,4 +338,64 @@
<property name="FancyProperty" type="s" access="read"/>
</interface>
<interface name="ChangingInterfaceV1">
<method name="FooMethod"/>
<method name="BarMethod"/>
<method name="BazMethod"/>
<signal name="FooSignal"/>
<signal name="BarSignal"/>
<signal name="BazSignal"/>
</interface>
<interface name="ChangingInterfaceV2">
<!--
NewSignalIn2:
@since: 2.0
-->
<signal name="NewSignalIn2"/>
<!--
NewMethodIn2:
@since: 2.0
-->
<method name="NewMethodIn2"/>
<!-- reverse order -->
<signal name="BazSignal"/>
<signal name="BarSignal"/>
<signal name="FooSignal"/>
<method name="BazMethod"/>
<method name="BarMethod"/>
<method name="FooMethod"/>
</interface>
<interface name="ChangingInterfaceV10">
<!--
AddedSignalIn10:
@since: 10.0
-->
<signal name="AddedSignalIn10"/>
<method name="AddedMethodIn10">
<annotation name="org.gtk.GDBus.Since" value="10.0"/>
</method>
<!--
NewSignalIn2:
@since: 2.0
-->
<signal name="NewSignalIn2"/>
<!--
NewMethodIn2:
@since: 2.0
-->
<method name="NewMethodIn2"/>
<!-- reverse order -->
<signal name="BazSignal"/>
<signal name="BarSignal"/>
<signal name="FooSignal"/>
<method name="BazMethod"/>
<method name="BarMethod"/>
<method name="FooMethod"/>
</interface>
</node>