From ff23397701a1ec3073450238a6c3d3f2419aa65e Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Thu, 29 Aug 2019 16:18:55 +0200 Subject: [PATCH] GtkBuilder: Add support for precompiling builder xml --- gtk/gtkbuilderparser.c | 17 +- gtk/gtkbuilderprecompile.c | 563 +++++++++++++++++++++++++++++++++++++ gtk/gtkbuilderprivate.h | 9 + gtk/meson.build | 1 + 4 files changed, 585 insertions(+), 5 deletions(-) create mode 100644 gtk/gtkbuilderprecompile.c diff --git a/gtk/gtkbuilderparser.c b/gtk/gtkbuilderparser.c index ac5becec94..40629104ae 100644 --- a/gtk/gtkbuilderparser.c +++ b/gtk/gtkbuilderparser.c @@ -177,11 +177,18 @@ gtk_buildable_parse_context_parse (GtkBuildableParseContext *context, { gboolean res; - context->ctx = g_markup_parse_context_new (context->internal_callbacks, - G_MARKUP_TREAT_CDATA_AS_TEXT, - context, NULL); - res = g_markup_parse_context_parse (context->ctx, text, text_len, error); - g_markup_parse_context_free (context->ctx); + if (_gtk_buildable_parser_is_precompiled (text, text_len)) + { + res = _gtk_buildable_parser_replay_precompiled (context, text, text_len, error); + } + else + { + context->ctx = g_markup_parse_context_new (context->internal_callbacks, + G_MARKUP_TREAT_CDATA_AS_TEXT, + context, NULL); + res = g_markup_parse_context_parse (context->ctx, text, text_len, error); + g_markup_parse_context_free (context->ctx); + } return res; } diff --git a/gtk/gtkbuilderprecompile.c b/gtk/gtkbuilderprecompile.c new file mode 100644 index 0000000000..198806e6fc --- /dev/null +++ b/gtk/gtkbuilderprecompile.c @@ -0,0 +1,563 @@ +/* gtkbuilderparser.c + * Copyright (C) 2019 Red Hat, + * Alexander Larsson + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#include "config.h" + +#include +#include "gtkbuilderprivate.h" +#include "gtkbuilder.h" +#include "gtkbuildable.h" + +/***************************************** Record a GMarkup parser call ***************************/ + +typedef enum +{ + RECORD_TYPE_ELEMENT, + RECORD_TYPE_END_ELEMENT, + RECORD_TYPE_TEXT, +} RecordTreeType; + +typedef struct RecordDataTree RecordDataTree; + +/* All strings are owned by the string table */ +struct RecordDataTree { + RecordDataTree *parent; + RecordTreeType type; + int n_attributes; + const char *data; + const char **attributes; + const char **values; + GList *children; +}; + +typedef struct { + char *string; + int count; + int offset; +} RecordDataString; + +static RecordDataTree * +record_data_tree_new (RecordDataTree *parent, RecordTreeType type, const char *data) +{ + RecordDataTree *tree = g_slice_new0 (RecordDataTree); + + tree->parent = parent; + tree->type = type; + tree->data = data; + + if (parent) + parent->children = g_list_prepend (parent->children, tree); + + return tree; +} + +static void +record_data_tree_free (RecordDataTree *tree) +{ + g_list_free_full (tree->children, (GDestroyNotify)record_data_tree_free); + g_free (tree->attributes); + g_free (tree->values); + g_slice_free (RecordDataTree, tree); +} + +static void +record_data_string_free (RecordDataString *s) +{ + g_free (s->string); + g_slice_free (RecordDataString, s); +} + +static const char * +record_data_string_lookup (GHashTable *strings, const char *str, gssize len) +{ + char *copy = NULL; + RecordDataString *s; + + if (len >= 0) + { + /* Ensure str is zero terminated */ + copy = g_strndup (str, len); + str = copy; + } + + s = g_hash_table_lookup (strings, str); + if (s) + { + g_free (copy); + s->count++; + return s->string; + } + + s = g_slice_new (RecordDataString); + s->string = copy ? copy : g_strdup (str); + s->count = 1; + + g_hash_table_insert (strings, s->string, s); + return s->string; +} + +typedef struct { + GHashTable *strings; + RecordDataTree *root; + RecordDataTree *current; +} RecordData; + +static void +record_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **names, + const gchar **values, + gpointer user_data, + GError **error) +{ + gsize n_attrs = g_strv_length ((char **)names); + RecordData *data = user_data; + RecordDataTree *child; + int i; + + child = record_data_tree_new (data->current, RECORD_TYPE_ELEMENT, + record_data_string_lookup (data->strings, element_name, -1)); + data->current = child; + + child->n_attributes = n_attrs; + child->attributes = g_new (const char *, n_attrs); + child->values = g_new (const char *, n_attrs); + + for (i = 0; i < n_attrs; i++) + { + child->attributes[i] = record_data_string_lookup (data->strings, names[i], -1); + child->values[i] = record_data_string_lookup (data->strings, values[i], -1); + } +} + +static void +record_end_element (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + RecordData *data = user_data; + + data->current = data->current->parent; +} + +static void +record_text (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + RecordData *data = user_data; + + record_data_tree_new (data->current, RECORD_TYPE_TEXT, + record_data_string_lookup (data->strings, text, text_len)); +} + +static const GMarkupParser record_parser = +{ + record_start_element, + record_end_element, + record_text, + NULL, // passthrough, not stored + NULL, // error, fails immediately +}; + +static gint +compare_string (gconstpointer _a, + gconstpointer _b) +{ + const RecordDataString *a = _a; + const RecordDataString *b = _b; + + return b->count - a->count; +} + +static void +marshal_uint32 (GString *str, + guint32 v) +{ + /* + We encode in a variable length format similar to + utf8: + + v size byte 1 byte 2 byte 3 byte 4 byte 5 + 7 bit: 0xxxxxxx + 14 bit: 10xxxxxx xxxxxxxx + 21 bit: 110xxxxx xxxxxxxx xxxxxxxx + 28 bit: 1110xxxx xxxxxxxx xxxxxxxx xxxxxxxx + 32 bit: 11110000 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx + */ + + if (v < 128) + { + g_string_append_c (str, (guchar)v); + } + else if (v < (1<<14)) + { + g_string_append_c (str, (guchar)(v >> 8) | 0x80); + g_string_append_c (str, (guchar)(v & 0xff)); + } + else if (v < (1<<21)) + { + g_string_append_c (str, (guchar)(v >> 16) | 0xc0); + g_string_append_c (str, (guchar)((v >> 8) & 0xff)); + g_string_append_c (str, (guchar)(v & 0xff)); + } + else if (v < (1<<28)) + { + g_string_append_c (str, (guchar)(v >> 24) | 0xe0); + g_string_append_c (str, (guchar)((v >> 16) & 0xff)); + g_string_append_c (str, (guchar)((v >> 8) & 0xff)); + g_string_append_c (str, (guchar)(v & 0xff)); + } + else + { + g_string_append_c (str, 0xf0); + g_string_append_c (str, (guchar)((v >> 24) & 0xff)); + g_string_append_c (str, (guchar)((v >> 16) & 0xff)); + g_string_append_c (str, (guchar)((v >> 8) & 0xff)); + g_string_append_c (str, (guchar)(v & 0xff)); + } +} + +static void +marshal_string (GString *marshaled, + GHashTable *strings, + const char *string) +{ + RecordDataString *s; + + s = g_hash_table_lookup (strings, string); + g_assert (s != NULL); + + marshal_uint32 (marshaled, s->offset); +} + +static void +marshal_tree (GString *marshaled, + GHashTable *strings, + RecordDataTree *tree) +{ + GList *l; + int i; + + /* Special case the root */ + if (tree->parent == NULL) + { + for (l = g_list_last (tree->children); l != NULL; l = l->prev) + marshal_tree (marshaled, strings, l->data); + return; + } + + switch (tree->type) + { + case RECORD_TYPE_ELEMENT: + marshal_uint32 (marshaled, RECORD_TYPE_ELEMENT); + marshal_string (marshaled, strings, tree->data); + marshal_uint32 (marshaled, tree->n_attributes); + for (i = 0; i < tree->n_attributes; i++) + { + marshal_string (marshaled, strings, tree->attributes[i]); + marshal_string (marshaled, strings, tree->values[i]); + } + for (l = g_list_last (tree->children); l != NULL; l = l->prev) + marshal_tree (marshaled, strings, l->data); + + marshal_uint32 (marshaled, RECORD_TYPE_END_ELEMENT); + break; + case RECORD_TYPE_TEXT: + marshal_uint32 (marshaled, RECORD_TYPE_TEXT); + marshal_string (marshaled, strings, tree->data); + break; + case RECORD_TYPE_END_ELEMENT: + default: + g_assert_not_reached (); + } +} + +/** + * _gtk_buildable_parser_precompile: + * @text: chunk of text to parse + * @text_len: length of @text in bytes + * + * Converts the xml format typically used by GtkBuilder to a + * binary form that is more efficient to parse. This is a custom + * format that is only supported by GtkBuilder. + * + * returns: A #GByte with the precompiled data + **/ +GBytes * +_gtk_buildable_parser_precompile (const gchar *text, + gssize text_len, + GError **error) +{ + GMarkupParseContext *ctx; + RecordData data = { 0 }; + GList *string_table, *l; + GString *marshaled; + int offset; + + data.strings = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)record_data_string_free); + data.root = record_data_tree_new (NULL, RECORD_TYPE_ELEMENT, NULL); + data.current = data.root; + + ctx = g_markup_parse_context_new (&record_parser, G_MARKUP_TREAT_CDATA_AS_TEXT, + &data, NULL); + + if (!g_markup_parse_context_parse (ctx, text, text_len, error)) + { + record_data_tree_free (data.root); + g_hash_table_destroy (data.strings); + g_markup_parse_context_free (ctx); + return NULL; + } + + g_markup_parse_context_free (ctx); + + string_table = g_hash_table_get_values (data.strings); + + string_table = g_list_sort (string_table, compare_string); + + offset = 0; + for (l = string_table; l != NULL; l = l->next) + { + RecordDataString *s = l->data; + s->offset = offset; + offset += strlen (s->string) + 1; + } + + marshaled = g_string_new (""); + /* Magic marker */ + g_string_append_len (marshaled, "GBU\0", 4); + marshal_uint32 (marshaled, offset); + + for (l = string_table; l != NULL; l = l->next) + { + RecordDataString *s = l->data; + g_string_append_len (marshaled, s->string, strlen (s->string) + 1); + } + + g_list_free (string_table); + + marshal_tree (marshaled, data.strings, data.root); + + record_data_tree_free (data.root); + g_hash_table_destroy (data.strings); + + return g_string_free_to_bytes (marshaled); +} + +/***************************************** Replay GMarkup parser callbacks ***************************/ + +static guint32 +demarshal_uint32 (const char **tree) +{ + const guchar *p = (const guchar *)*tree; + guchar c = *p; + /* see marshal_uint32 for format */ + + if (c < 128) /* 7 bit */ + { + *tree += 1; + return c; + } + else if ((c & 0xc0) == 0x80) /* 14 bit */ + { + *tree += 2; + return (c & 0x3f) << 8 | p[1]; + } + else if ((c & 0xe0) == 0xc0) /* 21 bit */ + { + *tree += 3; + return (c & 0x1f) << 16 | p[1] << 8 | p[2]; + } + else if ((c & 0xf0) == 0xe0) /* 28 bit */ + { + *tree += 4; + return (c & 0xf) << 24 | p[1] << 16 | p[2] << 8 | p[3]; + } + else + { + *tree += 5; + return p[1] << 24 | p[2] << 16 | p[3] << 8 | p[4]; + } +} + +static const char * +demarshal_string (const char **tree, const char *strings) +{ + guint32 offset = demarshal_uint32 (tree); + + return strings + offset; +} + +static void +propagate_error (GtkBuildableParseContext *context, + GError **dest, + GError *src) +{ + (*context->internal_callbacks->error) (NULL, src, context); + g_propagate_error (dest, src); +} + +static gboolean +replay_start_element (GtkBuildableParseContext *context, + const char **tree, + const char *strings, + GError **error) +{ + const char *element_name; + guint32 i, n_attrs; + const gchar **attr_names; + const gchar **attr_values; + GError *tmp_error = NULL; + + element_name = demarshal_string (tree, strings); + n_attrs = demarshal_uint32 (tree); + + attr_names = g_newa (const gchar *, n_attrs + 1); + attr_values = g_newa (const gchar *, n_attrs + 1); + for (i = 0; i < n_attrs; i++) + { + attr_names[i] = demarshal_string (tree, strings); + attr_values[i] = demarshal_string (tree, strings); + } + attr_names[i] = NULL; + attr_values[i] = NULL; + + (* context->internal_callbacks->start_element) (NULL, + element_name, + attr_names, + attr_values, + context, + &tmp_error); + + if (tmp_error) + { + propagate_error (context, error, tmp_error); + return FALSE; + } + + return TRUE; +} + +static gboolean +replay_end_element (GtkBuildableParseContext *context, + const char **tree, + const char *strings, + GError **error) +{ + GError *tmp_error = NULL; + + (* context->internal_callbacks->end_element) (NULL, + gtk_buildable_parse_context_get_element (context), + context, + &tmp_error); + if (tmp_error) + { + propagate_error (context, error, tmp_error); + return FALSE; + } + + return TRUE; +} + +static gboolean +replay_text (GtkBuildableParseContext *context, + const char **tree, + const char *strings, + GError **error) +{ + const char *text; + GError *tmp_error = NULL; + + text = demarshal_string (tree, strings); + + (*context->internal_callbacks->text) (NULL, + text, + strlen (text), + context, + &tmp_error); + + if (tmp_error) + { + propagate_error (context, error, tmp_error); + return FALSE; + } + + return TRUE; +} + +gboolean +_gtk_buildable_parser_is_precompiled (const gchar *data, + gssize data_len) +{ + return + data_len > 4 && + data[0] == 'G' && + data[1] == 'B' && + data[2] == 'U' && + data[3] == 0; +} + +gboolean +_gtk_buildable_parser_replay_precompiled (GtkBuildableParseContext *context, + const gchar *data, + gssize data_len, + GError **error) +{ + const char *data_end = data + data_len; + guint32 type, len; + const char *strings; + const char *tree; + + data = data + 4; /* Skip header */ + + len = demarshal_uint32 (&data); + + strings = data; + data = data + len; + tree = data; + + while (tree < data_end) + { + gboolean res; + type = demarshal_uint32 (&tree); + + switch (type) + { + case RECORD_TYPE_ELEMENT: + res = replay_start_element (context, &tree, strings, error); + break; + case RECORD_TYPE_END_ELEMENT: + res = replay_end_element (context, &tree, strings, error); + break; + case RECORD_TYPE_TEXT: + res = replay_text (context, &tree, strings, error); + break; + default: + g_assert_not_reached (); + } + + if (!res) + return FALSE; + } + + return TRUE; +} diff --git a/gtk/gtkbuilderprivate.h b/gtk/gtkbuilderprivate.h index a33c6c8659..0846efa378 100644 --- a/gtk/gtkbuilderprivate.h +++ b/gtk/gtkbuilderprivate.h @@ -147,6 +147,15 @@ typedef struct { typedef GType (*GTypeGetFunc) (void); /* Things only GtkBuilder should use */ +GBytes * _gtk_buildable_parser_precompile (const gchar *text, + gssize text_len, + GError **error); +gboolean _gtk_buildable_parser_is_precompiled (const gchar *data, + gssize data_len); +gboolean _gtk_buildable_parser_replay_precompiled (GtkBuildableParseContext *context, + const gchar *data, + gssize data_len, + GError **error); void _gtk_builder_parser_parse_buffer (GtkBuilder *builder, const gchar *filename, const gchar *buffer, diff --git a/gtk/meson.build b/gtk/meson.build index cdf186af91..f0474cef00 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -28,6 +28,7 @@ gtk_private_sources = files([ 'gtkbookmarksmanager.c', 'gtkbuilder-menus.c', 'gtkbuilderparser.c', + 'gtkbuilderprecompile.c', 'gtkcellareaboxcontext.c', 'gtkcoloreditor.c', 'gtkcolorplane.c',