Using Python function as a parameter to a C function through PyGObject

I have a C library that I’m building with gobject-introspection in mind. I have a function that takes a function prototype as a parameter:

/**
 * YggRxFunc:
 * @addr: (transfer full): destination address of the data to be transmitted.
 * @id: (transfer full): a UUID.
 * @response_to: (transfer full) (nullable): a UUID the data is in response to
 *               or %NULL.
 * @metadata: (transfer full) (nullable) (element-type utf8 utf8): a #GHashTable
 *            containing key-value pairs associated with the data or %NULL.
 * @data: (transfer full): the data.
 * @user_data: (transfer none) (closure): The owning #YggWorker instance.
 *
 * Signature for callback function used in ygg_worker_set_rx_func(). It is
 * invoked each time the worker receives data from the dispatcher.
 */
typedef void (* YggRxFunc) (gchar      *addr,
                            gchar      *id,
                            gchar      *response_to,
                            GHashTable *metadata,
                            GBytes     *data,
                            gpointer    user_data);

/**
 * ygg_worker_set_rx_func:
 * @worker: A #YggWorker instance.
 * @rx: (scope call): A #YggRxFunc callback.
 *
 * Stores a pointer to a handler function that is invoked whenever data is
 * received by the worker.
 *
 * Returns: %TRUE if setting the function handler succeeded.
 */
gboolean
ygg_worker_set_rx_func (YggWorker *self,
                        YggRxFunc  rx)
{
  YggWorkerPrivate *priv = ygg_worker_get_instance_private (self);
  priv->rx = rx;
  return TRUE;
}

This all works as expected when using this library in a C program, but when I use it through GObject Introspection in Python, the rx callback never gets invoked.

from gi.repository import Ygg, GLib

def rx(addr, id, response_to, meta_data, data, user_data):
    print("in rx")

worker = Ygg.Worker(directive="test", remote_content=False, features=None)
worker.set_rx_func(rx)
worker.connect()

loop = GLib.MainLoop()
loop.run()

When I trigger the object (via the appropriate D-Bus method), I expect the rx callback to get invoked and print “in rx” to the console, but nothing happens. The various g_debug calls I have within the C library print, but as soon as the control flow should enter the rx function it just stops.

Am I doing something wrong with the annotations? Is there another way to properly label a Python function as a callback to a GObject Introspection library?

This is not a call scope function, if you’re not calling it immediately.

You need to add a user data argument (a “closure”) and a function pointer to call when the data is freed—e.g.

/**
  * ygg_worker_set_rx_func:
  * @self: ...
  * @func: (scope notified) (closure user_data): ...
  * @user_data: ...
  * @notify: ...
  *
  * ...
  */
gboolean
ygg_worker_set_rx_func (YggWorker *self,
                        YggRxFunc func,
                        gpointer user_data,
                        GDestroyNotify notify)

Additionally, do not ever use bare GHashTable as arguments or return values.

Please, read how to write bindable API and the addendum I wrote in February 2023.

Is there a different dictionary-type that is recommended instead?

Yes: the one you write, embedding a hash table inside it, and that you can give proper ownership and type information.

For instance, you could have a proper YggMetadata type:

typedef struct _YggMetadata YggMetadata;

const char *
ygg_metadata_get (const YggMetadata *self,
                  const char *key);

const char *
ygg_metadata_get_or_default (const YggMetadata *self,
                             const char *key,
                             const char *or_default);

// etc.

GHashTable does not have type information on its contents, and does not have ownership information on the data it contains either: the data can be copied, or can be stored as is. When you get a GHashTable you don’t know who is responsible for freeing its data when the hash table is gone, since the getter function is all without transfer information.

Do not mistake the fact that there’s an annotation and some barebones support in the introspection information and some bindings with the ability to properly describe what a GHashTable is, or contains. The same applies to GArray, GPtrArray, and even to the linked list types. All of the untyped data containers in GLib are meant to be used internally, not at the API boundary.

1 Like

In the definition of this function, I would hold a reference to func to be called later. Is this user_data to be held as well, and passed into func when I invoke it later? When is the appropriate time to then call notify? After func has finished executing?

Yes.

When you’re clearing the priv->rx field, e.g. in the dispose() or finalize() virtual function of your YggWorker object instance; or if somebody is setting a new “rx” function, for instance:

gboolean
ygg_worker_set_rx_func (YggWorker *self,
                        YggRxFunc func,
                        gpointer user_data,
                        GDestroyNotify notify)
{
  YggWorkerPrivate *priv = ygg_worker_get_instance_private (self);

  if (priv->rx_func_data_notify != NULL)
     priv->rx_func_data_notify (priv->rx_func_data);

  priv->rx_func = func;
  priv->rx_func_data = user_data;
  priv->rx_func_data_notify = notify;

  return TRUE;
}

That depends on whether the function is going to be called once, or multiple times. If it’s a one-off, then you can unset it at the end, yes.

Thank you. This is making more sense. I’ll give this a try and see if I can clear up the API some more.

In your blog post, you mention:

Your callbacks should always be in the form of a simple callable with a data argument:

typedef void (* SomeCallback) (SomeObject *obj,
                               gpointer data);

Should we avoid having callbacks that accept multiple arguments, instead favoring an approach where we pack the arguments into a data structure?

What I meant was that the user_data argument must always be present; additional arguments are allowed.

Ah okay, perfect. Thank you! I was able to get the callbacks working correctly by adding the user_data and notify parameters too.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.