Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
2 : : *
3 : : * Copyright © 2018 Endless Mobile, Inc.
4 : : *
5 : : * This library is free software; you can redistribute it and/or
6 : : * modify it under the terms of the GNU Lesser General Public
7 : : * License as published by the Free Software Foundation; either
8 : : * version 2.1 of the License, or (at your option) any later version.
9 : : *
10 : : * This library is distributed in the hope that it will be useful,
11 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 : : * Lesser General Public License for more details.
14 : : *
15 : : * You should have received a copy of the GNU Lesser General Public
16 : : * License along with this library; if not, write to the Free Software
17 : : * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 : : *
19 : : * Authors:
20 : : * - Philip Withnall <withnall@endlessm.com>
21 : : */
22 : :
23 : : #include "config.h"
24 : :
25 : : #include <glib.h>
26 : : #include <glib-object.h>
27 : : #include <gobject/gvaluecollector.h>
28 : : #include <libmogwai-schedule/tests/signal-logger.h>
29 : :
30 : :
31 : : /**
32 : : * MwsSignalLogger:
33 : : * @log: the logged signal emissions
34 : : * @closures: the set of currently connected signal handler closures
35 : : *
36 : : * An object which allows signal emissions from zero or more #GObjects to be
37 : : * logged easily, without needing to write specific callback functions for any
38 : : * of them.
39 : : *
40 : : * Since: 0.1.0
41 : : */
42 : : struct _MwsSignalLogger
43 : : {
44 : : GPtrArray *log; /* (element-type MwsSignalLoggerEmission) (owned) */
45 : : GPtrArray *closures; /* (element-type MwsLoggedClosure) (owned) */
46 : : };
47 : :
48 : : /**
49 : : * MwsLoggedClosure:
50 : : * @closure: parent #GClosure
51 : : * @logger: the #MwsSignalLogger this belongs to
52 : : * @obj: pointer to the object instance this closure is connected to; no ref is
53 : : * held, and the object may be finalised before the closure, so this should
54 : : * only be used as an opaque pointer; add a #GWeakRef if the object needs to
55 : : * be accessed in future
56 : : * @obj_type_name: a copy of `G_OBJECT_TYPE_NAME (obj)` for use when @obj may
57 : : * be invalid
58 : : * @signal_name: name of the signal this closure is connected to, including
59 : : * detail (if applicable)
60 : : * @signal_id: ID of the signal connection, or 0 if this closure has been
61 : : * disconnected
62 : : *
63 : : * A closure representing a connection from @logger to the given @signal_name
64 : : * on @obj.
65 : : *
66 : : * The closure will be kept alive until the @logger is destroyed, though it will
67 : : * be invalidated and disconnected earlier if @obj is finalised.
68 : : *
69 : : * Since: 0.1.0
70 : : */
71 : : typedef struct
72 : : {
73 : : GClosure closure;
74 : : MwsSignalLogger *logger; /* (not owned) */
75 : : gpointer obj; /* (not owned) */
76 : : gchar *obj_type_name; /* (owned) */
77 : : gchar *signal_name; /* (owned) */
78 : : gulong signal_id; /* 0 when disconnected */
79 : : } MwsLoggedClosure;
80 : :
81 : : /**
82 : : * MwsSignalLoggerEmission:
83 : : * @closure: the closure this emission was captured by
84 : : * @param_values: array of parameter values, not including the object instance
85 : : * @n_param_values: number of elements in @param_values
86 : : *
87 : : * The details of a particular signal emission, including its parameter values.
88 : : *
89 : : * @param_values does not include the object instance.
90 : : *
91 : : * Since: 0.1.0
92 : : */
93 : : struct _MwsSignalLoggerEmission
94 : : {
95 : : MwsLoggedClosure *closure; /* (owned) */
96 : : GValue *param_values; /* (array length=n_param_values) */
97 : : gsize n_param_values;
98 : : };
99 : :
100 : : /**
101 : : * mws_signal_logger_emission_free:
102 : : * @emission: (transfer full): a #MwsSignalLoggerEmission
103 : : *
104 : : * Free a #MwsSignalLoggerEmission.
105 : : *
106 : : * Since: 0.1.0
107 : : */
108 : : void
109 : 192 : mws_signal_logger_emission_free (MwsSignalLoggerEmission *emission)
110 : : {
111 [ + + ]: 514 : for (gsize i = 0; i < emission->n_param_values; i++)
112 : 322 : g_value_unset (&emission->param_values[i]);
113 : 192 : g_free (emission->param_values);
114 : :
115 : 192 : g_closure_unref ((GClosure *) emission->closure);
116 : 192 : g_free (emission);
117 : 192 : }
118 : :
119 : : /* Version of G_VALUE_LCOPY() that allows %NULL return locations. */
120 : : #define VALUE_LCOPY(value, var_args, __error) \
121 : : G_STMT_START { \
122 : : const GValue *_value = (value); \
123 : : GType _value_type = G_VALUE_TYPE (_value); \
124 : : GTypeValueTable *_vtable = g_type_value_table_peek (_value_type); \
125 : : const gchar *_lcopy_format = _vtable->lcopy_format; \
126 : : GTypeCValue _cvalues[G_VALUE_COLLECT_FORMAT_MAX_LENGTH] = { { 0, }, }; \
127 : : guint _n_values = 0; \
128 : : \
129 : : while (*_lcopy_format != '\0') \
130 : : { \
131 : : g_assert (*_lcopy_format == G_VALUE_COLLECT_POINTER); \
132 : : _cvalues[_n_values++].v_pointer = va_arg ((var_args), gpointer); \
133 : : _lcopy_format++; \
134 : : } \
135 : : \
136 : : if (_n_values == 2 && !!_cvalues[0].v_pointer != !!_cvalues[1].v_pointer) \
137 : : *(__error) = g_strdup_printf ("all return locations need the same nullability"); \
138 : : else if (_cvalues[0].v_pointer != NULL) \
139 : : *(__error) = _vtable->lcopy_value (_value, _n_values, _cvalues, 0); \
140 : : } G_STMT_END
141 : :
142 : : /**
143 : : * mws_signal_logger_emission_get_params:
144 : : * @self: a #MwsSignalLoggerEmission
145 : : * @...: return locations for the signal parameters
146 : : *
147 : : * Get the parameters emitted in this signal emission. They are returned in the
148 : : * return locations provided as varargs. These locations must have the right
149 : : * type for the parameters of the signal which was emitted.
150 : : *
151 : : * To ignore a particular parameter, pass %NULL as the one (or more) return
152 : : * locations for that parameter.
153 : : *
154 : : * Since: 0.1.0
155 : : */
156 : : void
157 : 192 : mws_signal_logger_emission_get_params (MwsSignalLoggerEmission *self,
158 : : ...)
159 : : {
160 : : va_list ap;
161 : :
162 : 192 : va_start (ap, self);
163 : :
164 [ + + ]: 514 : for (gsize i = 0; i < self->n_param_values; i++)
165 : : {
166 : 322 : g_autofree gchar *error_message = NULL;
167 [ - + + + : 644 : VALUE_LCOPY (&self->param_values[i], ap, &error_message);
- + - - +
+ ]
168 : :
169 : : /* Error messages are not fatal, as they typically indicate that the user
170 : : * has passed in %NULL rather than a valid return pointer. We can recover
171 : : * from that. */
172 [ - + ]: 322 : if (error_message != NULL)
173 : 0 : g_debug ("Error copying GValue %" G_GSIZE_FORMAT " from emission of %s::%s from %p: %s",
174 : : i, self->closure->obj_type_name, self->closure->signal_name,
175 : : self->closure->obj, error_message);
176 : : }
177 : :
178 : 192 : va_end (ap);
179 : 192 : }
180 : :
181 : : static void
182 : 192 : mws_logged_closure_marshal (GClosure *closure,
183 : : GValue *return_value,
184 : : guint n_param_values,
185 : : const GValue *param_values,
186 : : gpointer invocation_hint,
187 : : gpointer marshal_data)
188 : : {
189 : 192 : MwsLoggedClosure *self = (MwsLoggedClosure *) closure;
190 : :
191 : : /* Log the @param_values. Ignore the @return_value, and the first of
192 : : * @param_values (which is the object instance). */
193 [ - + ]: 192 : g_assert (n_param_values >= 1);
194 : :
195 : 384 : g_autoptr(MwsSignalLoggerEmission) emission = g_new0 (MwsSignalLoggerEmission, 1);
196 : 192 : emission->closure = (MwsLoggedClosure *) g_closure_ref ((GClosure *) self);
197 : 192 : emission->n_param_values = n_param_values - 1;
198 : 192 : emission->param_values = g_new0 (GValue, emission->n_param_values);
199 : :
200 [ + + ]: 514 : for (gsize i = 0; i < emission->n_param_values; i++)
201 : : {
202 : 322 : g_value_init (&emission->param_values[i], G_VALUE_TYPE (¶m_values[i + 1]));
203 : 322 : g_value_copy (¶m_values[i + 1], &emission->param_values[i]);
204 : : }
205 : :
206 : 192 : g_ptr_array_add (self->logger->log, g_steal_pointer (&emission));
207 : 192 : }
208 : :
209 : : static void
210 : 147 : mws_logged_closure_invalidate (gpointer user_data,
211 : : GClosure *closure)
212 : : {
213 : 147 : MwsLoggedClosure *self = (MwsLoggedClosure *) closure;
214 : :
215 : 147 : self->signal_id = 0;
216 : 147 : }
217 : :
218 : : static void
219 : 147 : mws_logged_closure_finalize (gpointer user_data,
220 : : GClosure *closure)
221 : : {
222 : 147 : MwsLoggedClosure *self = (MwsLoggedClosure *) closure;
223 : :
224 : : /* Deliberately don’t g_ptr_array_remove() the closure from the
225 : : * self->logger->closures list, since finalize() can only be called when the
226 : : * final reference to the closure is dropped, and self->logger->closures holds
227 : : * a reference, so we must be being finalised from there (or that GPtrArray
228 : : * has already been finalised). */
229 : :
230 : 147 : g_free (self->obj_type_name);
231 : 147 : g_free (self->signal_name);
232 : :
233 [ - + ]: 147 : g_assert (self->signal_id == 0);
234 : 147 : }
235 : :
236 : : /**
237 : : * mws_logged_closure_new:
238 : : * @logger: (transfer none): logger to connect the closure to
239 : : * @obj: (not nullable) (transfer none): #GObject to connect the closure to
240 : : * @signal_name: (not nullable): signal name to connect the closure to
241 : : *
242 : : * Create a new #MwsLoggedClosure for @logger, @obj and @signal_name. @obj must
243 : : * be a valid object instance at this point (it may later be finalised before
244 : : * the closure).
245 : : *
246 : : * This does not connect the closure to @signal_name on @obj. Use
247 : : * mws_signal_logger_connect() for that.
248 : : *
249 : : * Returns: (transfer full): a new closure
250 : : * Since: 0.1.0
251 : : */
252 : : static GClosure *
253 : 147 : mws_logged_closure_new (MwsSignalLogger *logger,
254 : : GObject *obj,
255 : : const gchar *signal_name)
256 : : {
257 : 294 : g_autoptr(GClosure) closure = g_closure_new_simple (sizeof (MwsLoggedClosure), NULL);
258 : :
259 : 147 : MwsLoggedClosure *self = (MwsLoggedClosure *) closure;
260 : 147 : self->logger = logger;
261 : 147 : self->obj = obj;
262 : 147 : self->obj_type_name = g_strdup (G_OBJECT_TYPE_NAME (obj));
263 : 147 : self->signal_name = g_strdup (signal_name);
264 : 147 : self->signal_id = 0;
265 : :
266 : 147 : g_closure_add_invalidate_notifier (closure, NULL, (GClosureNotify) mws_logged_closure_invalidate);
267 : 147 : g_closure_add_finalize_notifier (closure, NULL, (GClosureNotify) mws_logged_closure_finalize);
268 : 147 : g_closure_set_marshal (closure, mws_logged_closure_marshal);
269 : :
270 : 147 : g_ptr_array_add (logger->closures, g_closure_ref (closure));
271 : :
272 : 147 : return g_steal_pointer (&closure);
273 : : }
274 : :
275 : : /**
276 : : * mws_signal_logger_new:
277 : : *
278 : : * Create a new #MwsSignalLogger. Add signals to it to log using
279 : : * mws_signal_logger_connect().
280 : : *
281 : : * Returns: (transfer full): a new #MwsSignalLogger
282 : : * Since: 0.1.0
283 : : */
284 : : MwsSignalLogger *
285 : 39 : mws_signal_logger_new (void)
286 : : {
287 : 78 : g_autoptr(MwsSignalLogger) logger = g_new0 (MwsSignalLogger, 1);
288 : :
289 : 39 : logger->log = g_ptr_array_new_with_free_func ((GDestroyNotify) mws_signal_logger_emission_free);
290 : 39 : logger->closures = g_ptr_array_new_with_free_func ((GDestroyNotify) g_closure_unref);
291 : :
292 : 39 : return g_steal_pointer (&logger);
293 : : }
294 : :
295 : : /**
296 : : * mws_signal_logger_free:
297 : : * @self: (transfer full): a #MwsSignalLogger
298 : : *
299 : : * Free a #MwsSignalLogger. This will disconnect all its closures from the
300 : : * signals they are connected to.
301 : : *
302 : : * This function may be called when there are signal emissions left in the
303 : : * logged stack, but typically you will want to call
304 : : * mws_signal_logger_assert_no_emissions() first.
305 : : *
306 : : * Since: 0.1.0
307 : : */
308 : : void
309 : 39 : mws_signal_logger_free (MwsSignalLogger *self)
310 : : {
311 [ - + ]: 39 : g_return_if_fail (self != NULL);
312 : :
313 : : /* Disconnect all the closures, since we don’t care about logging any more. */
314 [ + + ]: 186 : for (gsize i = 0; i < self->closures->len; i++)
315 : : {
316 : 147 : GClosure *closure = g_ptr_array_index (self->closures, i);
317 : :
318 : 147 : g_closure_invalidate (closure);
319 : : }
320 : :
321 : 39 : g_ptr_array_unref (self->closures);
322 : 39 : g_ptr_array_unref (self->log);
323 : :
324 : 39 : g_free (self);
325 : : }
326 : :
327 : : /**
328 : : * mws_signal_logger_connect:
329 : : * @self: a #MwsSignalLogger
330 : : * @obj: (type GObject): a #GObject to connect to
331 : : * @signal_name: the signal on @obj to connect to
332 : : *
333 : : * A convenience wrapper around g_signal_connect() which connects the
334 : : * #MwsSignalLogger to the given @signal_name on @obj so that emissions of it
335 : : * will be logged.
336 : : *
337 : : * The closure will be disconnected (and the returned signal connection ID
338 : : * invalidated) when:
339 : : *
340 : : * * @obj is finalised
341 : : * * The closure is freed or removed
342 : : * * The signal logger is freed
343 : : *
344 : : * This does not keep a strong reference to @obj.
345 : : *
346 : : * Returns: signal connection ID, as returned from g_signal_connect()
347 : : * Since: 0.1.0
348 : : */
349 : : gulong
350 : 147 : mws_signal_logger_connect (MwsSignalLogger *self,
351 : : gpointer obj,
352 : : const gchar *signal_name)
353 : : {
354 [ - + ]: 147 : g_return_val_if_fail (self != NULL, 0);
355 [ - + ]: 147 : g_return_val_if_fail (G_IS_OBJECT (obj), 0);
356 [ - + ]: 147 : g_return_val_if_fail (signal_name != NULL, 0);
357 : :
358 : 147 : g_autoptr(GClosure) closure = mws_logged_closure_new (self, obj, signal_name);
359 : 147 : MwsLoggedClosure *c = (MwsLoggedClosure *) closure;
360 : 147 : c->signal_id = g_signal_connect_closure (obj, signal_name, g_closure_ref (closure), FALSE);
361 : 147 : return c->signal_id;
362 : : }
363 : :
364 : : /**
365 : : * mws_signal_logger_get_n_emissions:
366 : : * @self: a #MwsSignalLogger
367 : : *
368 : : * Get the number of signal emissions which have been logged (and not popped)
369 : : * since the logger was initialised.
370 : : *
371 : : * Returns: number of signal emissions
372 : : * Since: 0.1.0
373 : : */
374 : : gsize
375 : 182 : mws_signal_logger_get_n_emissions (MwsSignalLogger *self)
376 : : {
377 [ - + ]: 182 : g_return_val_if_fail (self != NULL, 0);
378 : :
379 : 182 : return self->log->len;
380 : : }
381 : :
382 : : /**
383 : : * mws_signal_logged_pop_emission:
384 : : * @self: a #MwsSignalLogger
385 : : * @out_obj: (out) (transfer none) (optional) (not nullable): return location
386 : : * for the object instance which emitted the signal
387 : : * @out_obj_type_name: (out) (transfer full) (optional) (not nullable): return
388 : : * location for the name of the type of @out_obj
389 : : * @out_signal_name: (out) (transfer full) (optional) (not nullable): return
390 : : * location for the name of the emitted signal
391 : : * @out_emission: (out) (transfer full) (optional) (not nullable): return
392 : : * location for the signal emission closure containing emission parameters
393 : : *
394 : : * Pop the oldest signal emission off the stack of logged emissions, and return
395 : : * its object, signal name and parameters in the given return locations. All
396 : : * return locations are optional: if they are all %NULL, this function just
397 : : * performs a pop.
398 : : *
399 : : * If there are no signal emissions on the logged stack, %FALSE is returned.
400 : : *
401 : : * @out_obj does not return a reference to the object instance, as it may have
402 : : * been finalised since the signal emission was logged. It should be treated as
403 : : * an opaque pointer. The type name of the object is given as
404 : : * @out_obj_type_name, which is guaranteed to be valid.
405 : : *
406 : : * Returns: %TRUE if an emission was popped and returned, %FALSE otherwise
407 : : * Since: 0.1.0
408 : : */
409 : : gboolean
410 : 192 : mws_signal_logger_pop_emission (MwsSignalLogger *self,
411 : : gpointer *out_obj,
412 : : gchar **out_obj_type_name,
413 : : gchar **out_signal_name,
414 : : MwsSignalLoggerEmission **out_emission)
415 : : {
416 [ - + ]: 192 : g_return_val_if_fail (self != NULL, FALSE);
417 : :
418 [ - + ]: 192 : if (self->log->len == 0)
419 : : {
420 [ # # ]: 0 : if (out_obj != NULL)
421 : 0 : *out_obj = NULL;
422 [ # # ]: 0 : if (out_obj_type_name != NULL)
423 : 0 : *out_obj_type_name = NULL;
424 [ # # ]: 0 : if (out_signal_name != NULL)
425 : 0 : *out_signal_name = NULL;
426 [ # # ]: 0 : if (out_emission != NULL)
427 : 0 : *out_emission = NULL;
428 : :
429 : 0 : return FALSE;
430 : : }
431 : :
432 : : /* FIXME: Could do with g_ptr_array_steal() here.
433 : : * https://bugzilla.gnome.org/show_bug.cgi?id=795376 */
434 : 192 : g_ptr_array_set_free_func (self->log, NULL);
435 : 192 : g_autoptr(MwsSignalLoggerEmission) emission = g_steal_pointer (&self->log->pdata[0]);
436 : 192 : g_ptr_array_remove_index (self->log, 0);
437 : 192 : g_ptr_array_set_free_func (self->log, (GDestroyNotify) mws_signal_logger_emission_free);
438 : :
439 [ + - ]: 192 : if (out_obj != NULL)
440 : 192 : *out_obj = emission->closure->obj;
441 [ + - ]: 192 : if (out_obj_type_name != NULL)
442 : 192 : *out_obj_type_name = g_strdup (emission->closure->obj_type_name);
443 [ + - ]: 192 : if (out_signal_name != NULL)
444 : 192 : *out_signal_name = g_strdup (emission->closure->signal_name);
445 [ + - ]: 192 : if (out_emission != NULL)
446 : 192 : *out_emission = g_steal_pointer (&emission);
447 : :
448 : 192 : return TRUE;
449 : : }
450 : :
451 : : /**
452 : : * mws_signal_logger_format_emission:
453 : : * @obj: a #GObject instance which emitted a signal
454 : : * @obj_type_name: a copy of `G_OBJECT_TYPE_NAME (obj)` for use when @obj may
455 : : * be invalid
456 : : * @signal_name: name of the emitted signal
457 : : * @emission: details of the signal emission
458 : : *
459 : : * Format a signal emission in a human readable form, typically for logging it
460 : : * to some debug output.
461 : : *
462 : : * The returned string does not have a trailing newline character (`\n`).
463 : : *
464 : : * @obj may have been finalised, and is just treated as an opaque pointer.
465 : : *
466 : : * Returns: (transfer full): human readable string detailing the signal emission
467 : : * Since: 0.1.0
468 : : */
469 : : gchar *
470 : 0 : mws_signal_logger_format_emission (gpointer obj,
471 : : const gchar *obj_type_name,
472 : : const gchar *signal_name,
473 : : const MwsSignalLoggerEmission *emission)
474 : : {
475 [ # # ]: 0 : g_return_val_if_fail (obj != NULL, NULL); /* deliberately not a G_IS_OBJECT() check */
476 [ # # ]: 0 : g_return_val_if_fail (signal_name != NULL, NULL);
477 [ # # ]: 0 : g_return_val_if_fail (emission != NULL, NULL);
478 : :
479 : 0 : g_autoptr(GString) str = g_string_new ("");
480 : 0 : g_string_append_printf (str, "%s::%s from %p (",
481 : : obj_type_name, signal_name, obj);
482 : :
483 [ # # ]: 0 : for (gsize i = 0; i < emission->n_param_values; i++)
484 : : {
485 [ # # ]: 0 : if (i > 0)
486 : 0 : g_string_append (str, ", ");
487 : :
488 : 0 : g_auto(GValue) str_value = G_VALUE_INIT;
489 : 0 : g_value_init (&str_value, G_TYPE_STRING);
490 : :
491 [ # # ]: 0 : if (g_value_transform (&emission->param_values[i], &str_value))
492 : 0 : g_string_append (str, g_value_get_string (&str_value));
493 : : else
494 : 0 : g_string_append_printf (str, "GValue of type %s",
495 : 0 : G_VALUE_TYPE_NAME (&emission->param_values[i]));
496 : : }
497 : :
498 [ # # ]: 0 : if (emission->n_param_values == 0)
499 : 0 : g_string_append (str, "no arguments");
500 : 0 : g_string_append (str, ")");
501 : :
502 : 0 : return g_string_free (g_steal_pointer (&str), FALSE);
503 : : }
504 : :
505 : : /**
506 : : * mws_signal_logger_format_emissions:
507 : : * @self: a #MwsSignalLogger
508 : : *
509 : : * Format all the signal emissions on the logging stack in the #MwsSignalLogger,
510 : : * in a human readable format, one per line. The returned string does not end
511 : : * in a newline character (`\n`). Each signal emission is formatted using
512 : : * mws_signal_logger_format_emission().
513 : : *
514 : : * Returns: (transfer full): human readable list of all the signal emissions
515 : : * currently in the logger, or an empty string if the logger is empty
516 : : * Since: 0.1.0
517 : : */
518 : : gchar *
519 : 0 : mws_signal_logger_format_emissions (MwsSignalLogger *self)
520 : : {
521 [ # # ]: 0 : g_return_val_if_fail (self != NULL, NULL);
522 : :
523 : : /* Work out the width of the counter we need to number the emissions. */
524 : 0 : guint width = 1;
525 : 0 : gsize n_emissions = self->log->len;
526 [ # # ]: 0 : while (n_emissions >= 10)
527 : : {
528 : 0 : n_emissions /= 10;
529 : 0 : width++;
530 : : }
531 : :
532 : : /* Format each emission and list them. */
533 : 0 : g_autoptr(GString) str = g_string_new ("");
534 : :
535 [ # # ]: 0 : for (gsize i = 0; i < self->log->len; i++)
536 : : {
537 : 0 : const MwsSignalLoggerEmission *emission = g_ptr_array_index (self->log, i);
538 : :
539 [ # # ]: 0 : if (i > 0)
540 : 0 : g_string_append (str, "\n");
541 : :
542 : 0 : g_autofree gchar *emission_str = mws_signal_logger_format_emission (emission->closure->obj,
543 : 0 : emission->closure->obj_type_name,
544 : 0 : emission->closure->signal_name,
545 : : emission);
546 : 0 : g_string_append_printf (str, " %*" G_GSIZE_FORMAT ". %s", (int) width, i + 1, emission_str);
547 : : }
548 : :
549 : 0 : return g_string_free (g_steal_pointer (&str), FALSE);
550 : : }
|