diff --git a/mcpp.toml b/mcpp.toml index 5b33e5c..28e540b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -104,6 +104,9 @@ members = [ "tests/examples/wlroots", "tests/examples/pcre2", "tests/examples/fribidi", + "tests/examples/glib", + "tests/examples/gobject", + "tests/examples/gmodule", "tests/examples/libgbm", "tests/examples/libpng", "tests/examples/libwebp", diff --git a/pkgs/g/gnome.glib.lua b/pkgs/g/gnome.glib.lua new file mode 100644 index 0000000..6aad327 --- /dev/null +++ b/pkgs/g/gnome.glib.lua @@ -0,0 +1,99 @@ +-- gnome.glib — part of GLib 2.82.5. +-- +-- Upstream ships glib as one source tree producing FOUR separate shared +-- libraries, and this index follows that split: `gnome.glib`, +-- `gnome.gobject` and `gnome.gmodule` are three packages from one fork, +-- because that is what a consumer links. (The fourth, gio, is absent — see +-- below.) +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY A FORK +-- +-- Generators, not line count. GLib has six, and `build.mcpp` reimplements all +-- of them, so the tree carries no `sh` and no `python`: +-- +-- gen-visibility-macros.py versions-macros glib/gversionmacros.h +-- gen-visibility-macros.py visibility-macros three *-visibility.h +-- configure_file glibconfig.h +-- configure_file gmodule/gmoduleconf.h +-- gobject/glib-mkenums (816 lines of Python) glib-enumtypes.{h,c} +-- configure_file config.h +-- +-- glib-mkenums is reproduced FOR THE ONE INPUT this build points it at — +-- `glib/gunicode.h`, four enums — rather than wholesale. It reads upstream's +-- `.template` files from the tree so a template change is picked up, and +-- exits non-zero if that header stops yielding four enums: a silent drop +-- would produce a library missing `g_unicode_script_get_type` and the failure +-- would land in a consumer. +-- +-- https://github.com/mcpplibs/glib +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ C++ CONSUMERS MUST WRAP THE INCLUDES +-- +-- glib's headers carry their own `G_BEGIN_DECLS`, so this is usually fine — +-- but the generated `glib-enumtypes.h` comes from a template, and wrapping +-- costs nothing: +-- +-- extern "C" { +-- #include +-- } +-- +-- There is no module. glib's API is macro-heavy — `G_DEFINE_TYPE`, +-- `G_OBJECT`, `g_signal_connect` are all macros — and macros do not cross a +-- module boundary, so an `import` would hand a consumer the declarations and +-- withhold the half of the API that makes them usable. Compare +-- `wlroots.wlroots`, where the module is the ONLY way in because the headers +-- are not valid C++ at all: the shape follows what upstream's headers are, +-- not a house style. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ gio IS NOT IN THIS INDEX, AND THAT BLOCKS pango +-- +-- Two of gio's six generators are `gdbus-codegen`, an 8,351-line Python +-- program that turns D-Bus interface XML into GObject skeletons. Seven gio +-- sources include its output and two more reference those, so it cannot be +-- dropped without changing what gio is, and reimplementing it in +-- `build.mcpp` is not proportionate. +-- +-- pango uses `GListModel`, which lives in gio, so the text-layout line stops +-- here. Measured and recorded rather than left as an unexplained gap. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- LINUX ONLY +-- +-- The generated `glibconfig.h` fixes `G_OS_UNIX`, the POSIX thread +-- implementation and the poll constants, and `build.mcpp` refuses to run +-- anywhere else rather than emitting a header that is quietly wrong. The +-- upstream tree also contains two `COPYING` symlinks, which a Windows +-- extraction would not survive — one more reason the descriptor offers +-- `linux` alone. +package = { + spec = "1", + namespace = "gnome", + name = "glib", + description = "GLib 2.82.5 — data structures, the main loop, Unicode, GVariant and GRegex", + licenses = {"LGPL-2.1-or-later"}, + repo = "https://github.com/mcpplibs/glib", + type = "package", + + xpm = { + linux = { + ["2.82.5"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", + -- ⚠️ The container tag is `2.82.5-1`, not `2.82.5`. The fork's + -- tag was re-cut once while this descriptor was still + -- unpublished — safe only because nothing had extracted it + -- yet — and gitcode refuses to REPLACE an asset of the same + -- name in an existing release. Verified: this URL's sha256 + -- equals the GLOBAL tarball's. + CN = "https://gitcode.com/mcpp-res/glib/releases/download/2.82.5-1/glib-2.82.5.tar.gz", + }, + sha256 = "98118dacf3ebc9d5aefba340e9248385f1643b76ca672b864c37a3e4fb71caf6", + }, + }, + }, + + mcpp = "*/mcpp/glib/mcpp.toml", +} diff --git a/pkgs/g/gnome.gmodule.lua b/pkgs/g/gnome.gmodule.lua new file mode 100644 index 0000000..913a351 --- /dev/null +++ b/pkgs/g/gnome.gmodule.lua @@ -0,0 +1,99 @@ +-- gnome.gmodule — part of GLib 2.82.5. +-- +-- Upstream ships glib as one source tree producing FOUR separate shared +-- libraries, and this index follows that split: `gnome.glib`, +-- `gnome.gobject` and `gnome.gmodule` are three packages from one fork, +-- because that is what a consumer links. (The fourth, gio, is absent — see +-- below.) +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY A FORK +-- +-- Generators, not line count. GLib has six, and `build.mcpp` reimplements all +-- of them, so the tree carries no `sh` and no `python`: +-- +-- gen-visibility-macros.py versions-macros glib/gversionmacros.h +-- gen-visibility-macros.py visibility-macros three *-visibility.h +-- configure_file glibconfig.h +-- configure_file gmodule/gmoduleconf.h +-- gobject/glib-mkenums (816 lines of Python) glib-enumtypes.{h,c} +-- configure_file config.h +-- +-- glib-mkenums is reproduced FOR THE ONE INPUT this build points it at — +-- `glib/gunicode.h`, four enums — rather than wholesale. It reads upstream's +-- `.template` files from the tree so a template change is picked up, and +-- exits non-zero if that header stops yielding four enums: a silent drop +-- would produce a library missing `g_unicode_script_get_type` and the failure +-- would land in a consumer. +-- +-- https://github.com/mcpplibs/glib +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ C++ CONSUMERS MUST WRAP THE INCLUDES +-- +-- glib's headers carry their own `G_BEGIN_DECLS`, so this is usually fine — +-- but the generated `glib-enumtypes.h` comes from a template, and wrapping +-- costs nothing: +-- +-- extern "C" { +-- #include +-- } +-- +-- There is no module. glib's API is macro-heavy — `G_DEFINE_TYPE`, +-- `G_OBJECT`, `g_signal_connect` are all macros — and macros do not cross a +-- module boundary, so an `import` would hand a consumer the declarations and +-- withhold the half of the API that makes them usable. Compare +-- `wlroots.wlroots`, where the module is the ONLY way in because the headers +-- are not valid C++ at all: the shape follows what upstream's headers are, +-- not a house style. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ gio IS NOT IN THIS INDEX, AND THAT BLOCKS pango +-- +-- Two of gio's six generators are `gdbus-codegen`, an 8,351-line Python +-- program that turns D-Bus interface XML into GObject skeletons. Seven gio +-- sources include its output and two more reference those, so it cannot be +-- dropped without changing what gio is, and reimplementing it in +-- `build.mcpp` is not proportionate. +-- +-- pango uses `GListModel`, which lives in gio, so the text-layout line stops +-- here. Measured and recorded rather than left as an unexplained gap. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- LINUX ONLY +-- +-- The generated `glibconfig.h` fixes `G_OS_UNIX`, the POSIX thread +-- implementation and the poll constants, and `build.mcpp` refuses to run +-- anywhere else rather than emitting a header that is quietly wrong. The +-- upstream tree also contains two `COPYING` symlinks, which a Windows +-- extraction would not survive — one more reason the descriptor offers +-- `linux` alone. +package = { + spec = "1", + namespace = "gnome", + name = "gmodule", + description = "GModule 2.82.5 — portable dynamic module loading over dlopen", + licenses = {"LGPL-2.1-or-later"}, + repo = "https://github.com/mcpplibs/glib", + type = "package", + + xpm = { + linux = { + ["2.82.5"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", + -- ⚠️ The container tag is `2.82.5-1`, not `2.82.5`. The fork's + -- tag was re-cut once while this descriptor was still + -- unpublished — safe only because nothing had extracted it + -- yet — and gitcode refuses to REPLACE an asset of the same + -- name in an existing release. Verified: this URL's sha256 + -- equals the GLOBAL tarball's. + CN = "https://gitcode.com/mcpp-res/glib/releases/download/2.82.5-1/glib-2.82.5.tar.gz", + }, + sha256 = "98118dacf3ebc9d5aefba340e9248385f1643b76ca672b864c37a3e4fb71caf6", + }, + }, + }, + + mcpp = "*/mcpp/gmodule/mcpp.toml", +} diff --git a/pkgs/g/gnome.gobject.lua b/pkgs/g/gnome.gobject.lua new file mode 100644 index 0000000..49500ce --- /dev/null +++ b/pkgs/g/gnome.gobject.lua @@ -0,0 +1,99 @@ +-- gnome.gobject — part of GLib 2.82.5. +-- +-- Upstream ships glib as one source tree producing FOUR separate shared +-- libraries, and this index follows that split: `gnome.glib`, +-- `gnome.gobject` and `gnome.gmodule` are three packages from one fork, +-- because that is what a consumer links. (The fourth, gio, is absent — see +-- below.) +-- +-- ───────────────────────────────────────────────────────────────────────── +-- WHY A FORK +-- +-- Generators, not line count. GLib has six, and `build.mcpp` reimplements all +-- of them, so the tree carries no `sh` and no `python`: +-- +-- gen-visibility-macros.py versions-macros glib/gversionmacros.h +-- gen-visibility-macros.py visibility-macros three *-visibility.h +-- configure_file glibconfig.h +-- configure_file gmodule/gmoduleconf.h +-- gobject/glib-mkenums (816 lines of Python) glib-enumtypes.{h,c} +-- configure_file config.h +-- +-- glib-mkenums is reproduced FOR THE ONE INPUT this build points it at — +-- `glib/gunicode.h`, four enums — rather than wholesale. It reads upstream's +-- `.template` files from the tree so a template change is picked up, and +-- exits non-zero if that header stops yielding four enums: a silent drop +-- would produce a library missing `g_unicode_script_get_type` and the failure +-- would land in a consumer. +-- +-- https://github.com/mcpplibs/glib +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ C++ CONSUMERS MUST WRAP THE INCLUDES +-- +-- glib's headers carry their own `G_BEGIN_DECLS`, so this is usually fine — +-- but the generated `glib-enumtypes.h` comes from a template, and wrapping +-- costs nothing: +-- +-- extern "C" { +-- #include +-- } +-- +-- There is no module. glib's API is macro-heavy — `G_DEFINE_TYPE`, +-- `G_OBJECT`, `g_signal_connect` are all macros — and macros do not cross a +-- module boundary, so an `import` would hand a consumer the declarations and +-- withhold the half of the API that makes them usable. Compare +-- `wlroots.wlroots`, where the module is the ONLY way in because the headers +-- are not valid C++ at all: the shape follows what upstream's headers are, +-- not a house style. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- ⚠️ gio IS NOT IN THIS INDEX, AND THAT BLOCKS pango +-- +-- Two of gio's six generators are `gdbus-codegen`, an 8,351-line Python +-- program that turns D-Bus interface XML into GObject skeletons. Seven gio +-- sources include its output and two more reference those, so it cannot be +-- dropped without changing what gio is, and reimplementing it in +-- `build.mcpp` is not proportionate. +-- +-- pango uses `GListModel`, which lives in gio, so the text-layout line stops +-- here. Measured and recorded rather than left as an unexplained gap. +-- +-- ───────────────────────────────────────────────────────────────────────── +-- LINUX ONLY +-- +-- The generated `glibconfig.h` fixes `G_OS_UNIX`, the POSIX thread +-- implementation and the poll constants, and `build.mcpp` refuses to run +-- anywhere else rather than emitting a header that is quietly wrong. The +-- upstream tree also contains two `COPYING` symlinks, which a Windows +-- extraction would not survive — one more reason the descriptor offers +-- `linux` alone. +package = { + spec = "1", + namespace = "gnome", + name = "gobject", + description = "GObject 2.82.5 — the GLib type system: GType, signals, properties, closures and GValue", + licenses = {"LGPL-2.1-or-later"}, + repo = "https://github.com/mcpplibs/glib", + type = "package", + + xpm = { + linux = { + ["2.82.5"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/glib/archive/refs/tags/2.82.5.tar.gz", + -- ⚠️ The container tag is `2.82.5-1`, not `2.82.5`. The fork's + -- tag was re-cut once while this descriptor was still + -- unpublished — safe only because nothing had extracted it + -- yet — and gitcode refuses to REPLACE an asset of the same + -- name in an existing release. Verified: this URL's sha256 + -- equals the GLOBAL tarball's. + CN = "https://gitcode.com/mcpp-res/glib/releases/download/2.82.5-1/glib-2.82.5.tar.gz", + }, + sha256 = "98118dacf3ebc9d5aefba340e9248385f1643b76ca672b864c37a3e4fb71caf6", + }, + }, + }, + + mcpp = "*/mcpp/gobject/mcpp.toml", +} diff --git a/tests/examples/glib/mcpp.toml b/tests/examples/glib/mcpp.toml new file mode 100644 index 0000000..63854f9 --- /dev/null +++ b/tests/examples/glib/mcpp.toml @@ -0,0 +1,11 @@ +# gnome.glib — the generated headers, and the subsystems that depend on them. +[indices] +gnome = { path = "../../.." } + +[package] +name = "glib-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.gnome] +glib = "2.82.5" diff --git a/tests/examples/glib/tests/glib.cpp b/tests/examples/glib/tests/glib.cpp new file mode 100644 index 0000000..f9bd9b6 --- /dev/null +++ b/tests/examples/glib/tests/glib.cpp @@ -0,0 +1,184 @@ +// glib — the generated headers, and the subsystems that depend on them. +// +// WHAT THIS ASSERTS AND WHY +// +// glib is large and mostly self-evident: if GHashTable were broken the build +// would not have got here. What is NOT self-evident in a fork is whether the +// five generated headers say the right things, because every one of them was +// written out by hand-reimplemented generators. A wrong answer in glibconfig.h +// does not fail to compile — it changes the width of gsize, or the byte order, +// or which printf glib calls. +// +// So each check reads back a value that a generator decided: +// +// glibconfig.h gsize/gssize widths, G_BYTE_ORDER, the format strings +// gversionmacros.h GLIB_VERSION_2_82 exists and encodes 2.82 +// config.h USE_SYSTEM_PRINTF, via a format only glibc gets right +// glib-visibility.h a GLIB_AVAILABLE_IN_2_x symbol is actually exported +// pcre2 wiring GRegex, which is the one external dependency +// +// The subsystem checks that follow are chosen for the same reason: each is a +// place where a wrong config.h answer would show up as a wrong RESULT rather +// than a failure to build. + +#ifdef __linux__ + +// ⚠️ NO extern "C" WRAPPER, and adding one BREAKS THIS UNDER libc++: glib +// decorates every header with G_BEGIN_DECLS (which IS `extern "C" {`), and +// glib.h pulls , which libc++ routes through — templates +// inside an extern "C" block. `templates must have C++ linkage`, dozens of +// times, against a header this test never names. libstdc++ does not, so gcc is +// green and llvm is a wall. Wrap a C header only if it has none of its own. +#include + +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +} // namespace + +int main() +{ + // ── 1. glibconfig.h: the type model ────────────────────────────────── + std::printf(" glib %d.%d.%d gsize=%zu gssize=%zu gpointer=%zu\n", + glib_major_version, glib_minor_version, glib_micro_version, + sizeof(gsize), sizeof(gssize), sizeof(gpointer)); + check(glib_major_version == 2 && glib_minor_version == 82, + "the runtime version matches the manifest"); + check(sizeof(gsize) == sizeof(void *) && sizeof(gssize) == sizeof(void *), + "gsize and gssize are pointer-width, as glibconfig.h declares"); + check(sizeof(gint32) == 4 && sizeof(gint64) == 8 && sizeof(gint16) == 2, + "the fixed-width types are the widths they are named for"); + + // G_BYTE_ORDER is written by the generator rather than probed, so it is + // worth checking against something the machine can answer for itself. + const guint32 probe = 0x01020304; + const bool little = *reinterpret_cast(&probe) == 0x04; + std::printf(" G_BYTE_ORDER=%d measured=%s\n", G_BYTE_ORDER, + little ? "little" : "big"); + check((G_BYTE_ORDER == G_LITTLE_ENDIAN) == little, + "G_BYTE_ORDER agrees with what the bytes actually say"); + + // The format strings are the half of glibconfig.h that a compile cannot + // check: `%" G_GSIZE_FORMAT "` with the wrong modifier is undefined + // behaviour that usually prints something plausible. + { + gchar *s = g_strdup_printf("%" G_GSIZE_FORMAT, static_cast(123456789)); + std::printf(" G_GSIZE_FORMAT renders %s\n", s); + check(std::strcmp(s, "123456789") == 0, + "G_GSIZE_FORMAT prints a gsize correctly"); + g_free(s); + } + + // ── 2. gversionmacros.h ────────────────────────────────────────────── + // The generator emits one block per even minor up to the current one. + // Encoding is (major << 16) | (minor << 8) | micro. + std::printf(" GLIB_VERSION_2_82 = 0x%06x\n", GLIB_VERSION_2_82); + check(GLIB_VERSION_2_82 == G_ENCODE_VERSION(2, 82), + "gversionmacros.h defines GLIB_VERSION_2_82 and encodes it correctly"); + check(GLIB_VERSION_2_26 < GLIB_VERSION_2_82, + "…and the older ones too, in order"); + + // ── 3. config.h: USE_SYSTEM_PRINTF ─────────────────────────────────── + // Not directly observable — but %'d (thousands grouping) and the C99 %zu + // are things glib's bundled gnulib printf implements differently from + // glibc's. Rendering a long double in %La exercises the path that made the + // link fail when the wrong printf was selected. + { + gchar *s = g_strdup_printf("%.3f|%05d|%s", 1.5, 42, "x"); + std::printf(" printf path renders %s\n", s); + check(std::strcmp(s, "1.500|00042|x") == 0, + "g_strdup_printf goes through a working printf"); + g_free(s); + } + + // ── 4. glib-visibility.h ───────────────────────────────────────────── + // g_pathbuf_init is GLIB_AVAILABLE_IN_2_76 — a symbol decorated by the + // generated visibility header. Calling it proves the decoration expanded + // to something that exports rather than hides. + { + GPathBuf pb; + g_path_buf_init(&pb); + g_path_buf_push(&pb, "usr"); + g_path_buf_push(&pb, "share"); + gchar *p = g_path_buf_to_path(&pb); + std::printf(" GPathBuf built %s\n", p ? p : "(null)"); + check(p != nullptr && std::strstr(p, "usr/share") != nullptr, + "a GLIB_AVAILABLE_IN_2_76 symbol is reachable"); + g_free(p); + g_path_buf_clear(&pb); + } + + // ── 5. GRegex, i.e. the pcre2 dependency ───────────────────────────── + // The only external library glib links here. \p{Han} needs pcre2 built + // with SUPPORT_UNICODE, which is compat.pcre2's decision — so this checks + // the two packages agree, not merely that one of them built. + { + GError *err = nullptr; + GRegex *re = g_regex_new("^\\p{Han}+$", G_REGEX_DEFAULT, + G_REGEX_MATCH_DEFAULT, &err); + check(re != nullptr, "g_regex_new compiles a Unicode property pattern"); + if (re != nullptr) { + check(g_regex_match(re, "漢字", G_REGEX_MATCH_DEFAULT, nullptr), + "…and it matches Han characters"); + check(!g_regex_match(re, "abc", G_REGEX_MATCH_DEFAULT, nullptr), + "…and not Latin ones"); + g_regex_unref(re); + } else if (err != nullptr) { + std::printf(" %s\n", err->message); + g_error_free(err); + } + } + + // ── 6. Unicode, the tables gunicode.h describes ────────────────────── + check(g_unichar_type(0x6F22) == G_UNICODE_OTHER_LETTER, + "g_unichar_type classifies U+6F22 as a letter"); + // The function is `g_unichar_get_script`, not `g_unichar_script`. + check(g_unichar_get_script(0x6F22) == G_UNICODE_SCRIPT_HAN, + "…and g_unichar_get_script puts it in Han"); + { + gchar *up = g_utf8_strup("straße", -1); + std::printf(" g_utf8_strup(straße) = %s\n", up); + check(up != nullptr && std::strstr(up, "STRASSE") != nullptr, + "g_utf8_strup expands ß to SS, i.e. real case tables"); + g_free(up); + } + + // ── 7. the main loop and GVariant ──────────────────────────────────── + { + GMainContext *ctx = g_main_context_new(); + check(ctx != nullptr, "a GMainContext is created"); + check(!g_main_context_iteration(ctx, FALSE), + "…and iterating it with nothing pending does not block"); + g_main_context_unref(ctx); + } + { + GVariant *v = g_variant_new("(si)", "hello", 42); + g_variant_ref_sink(v); + const gchar *s = nullptr; + gint i = 0; + g_variant_get(v, "(&si)", &s, &i); + std::printf(" GVariant round-trip: %s %d\n", s, i); + check(s != nullptr && std::strcmp(s, "hello") == 0 && i == 42, + "a GVariant tuple survives a serialise/parse round trip"); + g_variant_unref(v); + } + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif diff --git a/tests/examples/gmodule/mcpp.toml b/tests/examples/gmodule/mcpp.toml new file mode 100644 index 0000000..fc144cc --- /dev/null +++ b/tests/examples/gmodule/mcpp.toml @@ -0,0 +1,11 @@ +# gnome.gmodule — dynamic loading, and the generated gmoduleconf.h behind it. +[indices] +gnome = { path = "../../.." } + +[package] +name = "gmodule-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.gnome] +gmodule = "2.82.5" diff --git a/tests/examples/gmodule/tests/gmodule.cpp b/tests/examples/gmodule/tests/gmodule.cpp new file mode 100644 index 0000000..7f3a528 --- /dev/null +++ b/tests/examples/gmodule/tests/gmodule.cpp @@ -0,0 +1,114 @@ +// gmodule — dynamic loading, checked by actually loading something. +// +// WHAT THIS ASSERTS AND WHY +// +// gmodule is two source files, so "did it compile" is nearly free — and nearly +// worthless. The value is in `gmoduleconf.h`, which build.mcpp generates and +// which decides WHICH loader is compiled in. `G_MODULE_IMPL_NONE` produces a +// library that builds, links, and returns "dynamic loading not supported" for +// everything. +// +// So the checks are: does it claim to support loading, and does a load +// actually work. `g_module_open(NULL)` opens the CURRENT EXECUTABLE — the one +// case that needs no test fixture on disk — and a symbol looked up in it must +// be one this binary really has. + +#ifdef __linux__ + +// ⚠️ NO extern "C" WRAPPER, and adding one BREAKS THIS UNDER libc++: glib +// decorates every header with G_BEGIN_DECLS (which IS `extern "C" {`), and +// glib.h pulls , which libc++ routes through — templates +// inside an extern "C" block. `templates must have C++ linkage`, dozens of +// times, against a header this test never names. libstdc++ does not, so gcc is +// green and llvm is a wall. Wrap a C header only if it has none of its own. +#include + +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-56s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +} // namespace + +int main() +{ + // ── 1. gmoduleconf.h selected a real loader ────────────────────────── + // G_MODULE_IMPL_NONE here would mean build.mcpp filled the template with + // the "no loader" answer, and everything below would fail in a way that + // reads like a broken system rather than a wrong generated header. + std::printf(" g_module_supported = %d, suffix = %s\n", + g_module_supported(), G_MODULE_SUFFIX); + check(g_module_supported(), "g_module_supported — gmoduleconf.h chose the dl loader"); + check(std::strcmp(G_MODULE_SUFFIX, "so") == 0, + "G_MODULE_SUFFIX is \"so\", from the generated glibconfig.h"); + + // ── 2. a load that needs no fixture ────────────────────────────────── + // NULL means "this executable". It exercises the same dlopen path a plugin + // would, without needing a .so to have been built alongside the test. + GModule *self = g_module_open(nullptr, G_MODULE_BIND_LAZY); + check(self != nullptr, "g_module_open(NULL) opens the running executable"); + if (self != nullptr) { + // ⚠️ NO SYMBOL LOOKUP HERE, and the reason is a property of this + // package rather than a gap in the test. + // + // `dlsym` on the main program sees only the DYNAMIC symbol table, which + // needs `-rdynamic`. And gnome.glib is `kind = "lib"`: its objects are + // merged into the consumer, so there is no libglib-2.0.so in the link + // map either. Both halves of the obvious assertion — a symbol of this + // executable's, or one of glib's — are therefore absent by + // construction, and a test that asserted them would be measuring the + // link line. + // + // What IS observable without a fixture on disk is that the loader + // answers at all, and that it distinguishes present from absent. + gpointer missing = nullptr; + check(!g_module_symbol(self, "mcpp_no_such_symbol_at_all", &missing), + "a name that is not there is reported as absent, not crashed on"); + + std::printf(" g_module_name = %s\n", g_module_name(self)); + check(g_module_close(self), "g_module_close"); + } + + // ── 2b. g_module_build_path — G_MODULE_SUFFIX in use ───────────────── + // The one place the generated glibconfig.h value becomes a STRING a caller + // sees. On Windows this would be "plugin.dll"; getting it wrong would make + // every plugin host look for the wrong filename. + { + gchar *p = g_module_build_path("/opt/plug", "demo"); + std::printf(" g_module_build_path = %s\n", p ? p : "(null)"); + check(p != nullptr && std::strcmp(p, "/opt/plug/libdemo.so") == 0, + "g_module_build_path applies the lib prefix and .so suffix"); + g_free(p); + } + + // ── 3. the error path says something ───────────────────────────────── + // G_MODULE_IMPL_NONE would also reach here, so this is checked after the + // success case rather than instead of it. + GModule *nope = g_module_open("/nonexistent/definitely-not-a-module.so", + G_MODULE_BIND_LAZY); + check(nope == nullptr, "opening a missing module fails"); + const gchar *err = g_module_error(); + std::printf(" g_module_error = %s\n", err ? err : "(null)"); + // G_MODULE_HAVE_DLERROR is one of the four values build.mcpp fills into + // gmoduleconf.h; without it the message would be a generic placeholder + // rather than the loader's own text. + check(err != nullptr && std::strstr(err, "definitely-not-a-module") != nullptr, + "…and dlerror's message names the file — G_MODULE_HAVE_DLERROR is on"); + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif diff --git a/tests/examples/gobject/mcpp.toml b/tests/examples/gobject/mcpp.toml new file mode 100644 index 0000000..dc75d8f --- /dev/null +++ b/tests/examples/gobject/mcpp.toml @@ -0,0 +1,11 @@ +# gnome.gobject — the type system, registered and exercised at run time. +[indices] +gnome = { path = "../../.." } + +[package] +name = "gobject-tests" +version = "0.1.0" +standard = "c++23" + +[target.'cfg(linux)'.dependencies.gnome] +gobject = "2.82.5" diff --git a/tests/examples/gobject/tests/gobject.cpp b/tests/examples/gobject/tests/gobject.cpp new file mode 100644 index 0000000..bcfcea8 --- /dev/null +++ b/tests/examples/gobject/tests/gobject.cpp @@ -0,0 +1,191 @@ +// gobject — the type system, exercised rather than merely linked. +// +// WHAT THIS ASSERTS AND WHY +// +// GObject is the kind of library where "it linked" proves nothing: GType is a +// runtime registry, so a build that compiled half of it still links and then +// fails the first time something registers a type. Every check below makes the +// library DO something and reads back a value it computed: +// +// g_type_init-era registry a fundamental type's name, from gtype.c +// the GENERATED enum types g_unicode_script_get_type(), which exists only +// because build.mcpp reproduced glib-mkenums +// a derived type registered at run time, with a property +// properties set through GValue and read back +// signals connected, emitted, and observed to have run +// GBinding two objects kept in step, which is gbinding.c +// +// The enum-type check is the one that matters most for this fork: it is the +// only evidence that the mkenums reimplementation produced a REGISTERABLE type +// rather than a header that merely compiles. + +#ifdef __linux__ + +// ⚠️ NO extern "C" WRAPPER, and adding one BREAKS THIS UNDER libc++. +// +// glib decorates every header with G_BEGIN_DECLS/G_END_DECLS, which IS +// `extern "C" {`, so a wrapper is redundant. It is also harmful: glib.h pulls +// and , and libc++ routes those through +// /, which define TEMPLATES. Inside an extern "C" block that +// is +// +// __type_traits/add_cv_quals.h:20: error: templates must have C++ linkage +// +// reported dozens of times against a standard-library header this test never +// names. libstdc++ does not route them that way, so the gcc leg is green and +// the llvm leg is a wall of errors — the same asymmetry that caught a missing +// in the cairo fork. +// +// The rule: wrap a C header ONLY if it has no extern "C" of its own. +// compat.libseat and freedesktop.libdisplay-info need it; glib does not. +#include +#include + +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char *what) +{ + std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) { + ++failures; + } +} + +// A minimal derived type, declared the way every GObject library does it. +// Registering it exercises gtype.c, gobject.c, gparam.c and gvalue.c at once. +struct TestThing { + GObject parent; + gint answer; +}; +struct TestThingClass { + GObjectClass parent; +}; + +enum { PROP_0, PROP_ANSWER, N_PROPS }; +GParamSpec *props[N_PROPS] = {}; + +GType test_thing_get_type(void); +G_DEFINE_TYPE(TestThing, test_thing, G_TYPE_OBJECT) + +void test_thing_set_property(GObject *o, guint id, const GValue *v, GParamSpec *p) +{ + if (id == PROP_ANSWER) { + reinterpret_cast(o)->answer = g_value_get_int(v); + } else { + G_OBJECT_WARN_INVALID_PROPERTY_ID(o, id, p); + } +} + +void test_thing_get_property(GObject *o, guint id, GValue *v, GParamSpec *p) +{ + if (id == PROP_ANSWER) { + g_value_set_int(v, reinterpret_cast(o)->answer); + } else { + G_OBJECT_WARN_INVALID_PROPERTY_ID(o, id, p); + } +} + +guint sig_changed = 0; + +void test_thing_class_init(TestThingClass *klass) +{ + GObjectClass *oc = G_OBJECT_CLASS(klass); + oc->set_property = test_thing_set_property; + oc->get_property = test_thing_get_property; + props[PROP_ANSWER] = g_param_spec_int("answer", nullptr, nullptr, + G_MININT, G_MAXINT, 0, + static_cast(G_PARAM_READWRITE)); + g_object_class_install_properties(oc, N_PROPS, props); + + sig_changed = g_signal_new("changed", test_thing_get_type(), G_SIGNAL_RUN_LAST, + 0, nullptr, nullptr, nullptr, G_TYPE_NONE, 1, G_TYPE_INT); +} + +void test_thing_init(TestThing *) {} + +int seen = -1; +void on_changed(TestThing *, gint value, gpointer) { seen = value; } + +} // namespace + +int main() +{ + // ── 1. the type registry answers about its own fundamentals ────────── + std::printf(" glib %d.%d.%d\n", glib_major_version, glib_minor_version, + glib_micro_version); + check(glib_major_version == 2 && glib_minor_version == 82, + "glib_*_version report 2.82, the version the manifest declares"); + check(std::strcmp(g_type_name(G_TYPE_INT), "gint") == 0, + "g_type_name(G_TYPE_INT) is \"gint\" — the registry is initialised"); + check(g_type_is_a(G_TYPE_OBJECT, G_TYPE_OBJECT), "g_type_is_a on GObject itself"); + + // ── 2. THE GENERATED ENUM TYPES ────────────────────────────────────── + // These exist only because build.mcpp reproduced glib-mkenums over + // glib/gunicode.h. Registering one and reading a value back is the only + // thing that proves the generator produced working code rather than a + // header that happens to compile. + const GType script = g_unicode_script_get_type(); + check(script != 0, "g_unicode_script_get_type() registered a GType"); + GEnumClass *ec = static_cast(g_type_class_ref(script)); + check(ec != nullptr, "…and its class can be referenced"); + if (ec != nullptr) { + GEnumValue *v = g_enum_get_value(ec, G_UNICODE_SCRIPT_HAN); + std::printf(" G_UNICODE_SCRIPT_HAN = %s / %s\n", + v ? v->value_name : "(null)", v ? v->value_nick : "(null)"); + check(v != nullptr && std::strcmp(v->value_name, "G_UNICODE_SCRIPT_HAN") == 0, + "…and G_UNICODE_SCRIPT_HAN is in it under its own name"); + // The nick is mkenums' derived form: prefix stripped, lower case, + // underscores to hyphens. Getting it right is the fiddly half of the + // generator, so it is asserted rather than assumed. + check(v != nullptr && std::strcmp(v->value_nick, "han") == 0, + "…with the nick mkenums derives, \"han\""); + g_type_class_unref(ec); + } + check(g_unicode_type_get_type() != 0 && g_unicode_break_type_get_type() != 0 + && g_normalize_mode_get_type() != 0, + "all four generated enum types register"); + + // ── 3. a derived type, its property, its signal ────────────────────── + const GType thing = test_thing_get_type(); + check(thing != 0 && g_type_is_a(thing, G_TYPE_OBJECT), + "a derived type registers and is a GObject"); + std::printf(" registered %s\n", g_type_name(thing)); + + GObject *o = static_cast(g_object_new(thing, "answer", 42, nullptr)); + check(o != nullptr, "g_object_new with a construct property"); + + gint got = 0; + g_object_get(o, "answer", &got, nullptr); + std::printf(" answer = %d\n", got); + check(got == 42, "…and g_object_get reads it back through GValue"); + + g_signal_connect(o, "changed", G_CALLBACK(on_changed), nullptr); + g_signal_emit(o, sig_changed, 0, 7); + std::printf(" the handler saw %d\n", seen); + check(seen == 7, "a signal is emitted and the handler runs"); + + // ── 4. GBinding — gbinding.c, and a real use of the property system ── + GObject *b = static_cast(g_object_new(thing, nullptr)); + g_object_bind_property(o, "answer", b, "answer", G_BINDING_DEFAULT); + g_object_set(o, "answer", 99, nullptr); + gint mirrored = 0; + g_object_get(b, "answer", &mirrored, nullptr); + std::printf(" bound object followed to %d\n", mirrored); + check(mirrored == 99, "g_object_bind_property keeps two objects in step"); + + g_object_unref(b); + g_object_unref(o); + check(true, "objects destroyed without a warning"); + + std::printf("\n%d check(s) failed\n", failures); + return failures == 0 ? 0 : 1; +} + +#else +int main() { return 0; } +#endif