[gjs/ewlsh/glogfield-support] add support for glogfield




commit af9ccf46390b06e77a23326b3087ee4a6a4cb2d1
Author: Evan Welsh <contact evanwelsh com>
Date:   Fri Jul 2 13:45:43 2021 -0700

    add support for glogfield
    
    glogfield is a struct with defined behavior that can't be expressed in GI (a pointer that is a string 
sometimes, raw binary data other times)
    
    let's create a custom object wrapper for it so we can support the full GLib log APIs.

 gi/arg.cpp                        |   6 +-
 gi/glogfield.cpp                  | 230 ++++++++++++++++++++++++++++++++++++++
 gi/glogfield.h                    | 136 ++++++++++++++++++++++
 gi/repo.cpp                       |   7 +-
 gi/value.cpp                      |   5 +-
 gjs/mem-private.h                 |   2 +
 installed-tests/js/meson.build    |   1 +
 installed-tests/js/testConsole.js |  41 +++++++
 meson.build                       |   1 +
 9 files changed, 426 insertions(+), 3 deletions(-)
---
diff --git a/gi/arg.cpp b/gi/arg.cpp
index 2a82b07b..67721c34 100644
--- a/gi/arg.cpp
+++ b/gi/arg.cpp
@@ -34,6 +34,7 @@
 #include "gi/foreign.h"
 #include "gi/fundamental.h"
 #include "gi/gerror.h"
+#include "gi/glogfield.h"
 #include "gi/gtype.h"
 #include "gi/interface.h"
 #include "gi/js-value-inl.h"
@@ -2655,7 +2656,10 @@ gjs_value_from_g_argument (JSContext             *context,
 
                 JSObject *obj;
 
-                if (copy_structs || g_type_is_a(gtype, G_TYPE_VARIANT))
+                if (gjs_struct_info_is_log_field(interface_info))
+                    obj = GjsGLogFieldInstance::new_for_c_struct(
+                        context, interface_info, gjs_arg_get<void*>(arg));
+                else if (copy_structs || g_type_is_a(gtype, G_TYPE_VARIANT))
                     obj = BoxedInstance::new_for_c_struct(
                         context, interface_info, gjs_arg_get<GVariant*>(arg));
                 else
diff --git a/gi/glogfield.cpp b/gi/glogfield.cpp
new file mode 100644
index 00000000..da75cf2b
--- /dev/null
+++ b/gi/glogfield.cpp
@@ -0,0 +1,230 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#include "gi/glogfield.h"
+#include <glib.h>
+#include <js/CallArgs.h>            // for CallArgs, CallArgsBase
+#include <js/Class.h>               // for JSCLASS_HAS_RESERVED_SLOTS, JSCLA...
+#include <js/PropertyDescriptor.h>  // for JSPROP_ENUMERATE, JSPROP_PERMANENT
+#include <js/RootingAPI.h>          // for Rooted, Handle, MutableHandle
+#include <js/TypeDecls.h>
+#include <js/Utility.h>  // for UniqueChars
+#include <js/Value.h>    // for Value
+#include <jsapi.h>       // for JS_DefineProperty
+#include <utility>       // for forward
+#include "gi/repo.h"
+#include "gjs/jsapi-util-args.h"
+#include "gjs/jsapi-util.h"  // for gjs_string_from_utf8, gjs_throw
+#include "gjs/mem-private.h"
+
+bool gjs_struct_info_is_log_field(GIStructInfo* info) {
+    // TODO(ewlsh): There is perhaps a better way to do this.
+    return g_strcmp0(g_base_info_get_namespace(info), "GLib") == 0 &&
+           g_strcmp0(g_base_info_get_name(info), "LogField") == 0;
+}
+
+// clang-format off
+const struct JSClassOps GjsGLogFieldBase::class_ops = {
+    nullptr,  // addProperty
+    nullptr,  // deleteProperty
+    nullptr,  // enumerate
+    nullptr,
+    nullptr,
+    nullptr,  // mayResolve
+    &GjsGLogFieldBase::finalize,
+    nullptr,  // call
+    nullptr,  // hasInstance
+    nullptr,  // construct
+    nullptr,
+};
+
+const struct JSClass GjsGLogFieldBase::klass = {
+    "GObject_GLogField",
+    JSCLASS_HAS_PRIVATE | JSCLASS_FOREGROUND_FINALIZE |
+        JSCLASS_HAS_RESERVED_SLOTS(1),
+    &GjsGLogFieldBase::class_ops
+};
+
+[[nodiscard]] GjsGLogFieldBase* GjsGLogFieldBase::get_copy_source(JSContext* cx,
+                                             JS::Value value) const {
+    if (!value.isObject())
+        return nullptr;
+
+    JS::RootedObject object(cx, &value.toObject());
+    GjsGLogFieldBase* source_priv = GjsGLogFieldBase::for_js(cx, object);
+    if (!source_priv || !g_base_info_equal(info(), source_priv->info()))
+        return nullptr;
+
+    return source_priv;
+}
+
+bool GjsGLogFieldPrototype::key_getter(JSContext* cx, unsigned int argc,
+                                       JS::Value* vp) {
+    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, this_obj, GjsGLogFieldBase,
+                           priv);
+    if (!priv->check_is_instance(cx, "field getter"))
+        return false;
+
+    GLogField* field = priv->to_instance()->ptr();
+    if (!gjs_string_from_utf8(cx, field->key, args.rval()))
+        return false;
+
+    return true;
+}
+
+bool GjsGLogFieldPrototype::value_getter(JSContext* cx, unsigned int argc,
+                                         JS::Value* vp) {
+    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, this_obj, GjsGLogFieldBase,
+                           priv);
+    if (!priv->check_is_instance(cx, "field getter"))
+        return false;
+
+    GLogField* field = priv->to_instance()->ptr();
+    if (field->length != -1) {
+        gjs_throw(cx, "Cannot get non-string value with length %li.", field->length);
+        return false;
+    }
+
+    const char* str = static_cast<const char*>(field->value);
+    return gjs_string_from_utf8(cx, str, args.rval());
+}
+
+GjsGLogFieldPrototype::GjsGLogFieldPrototype(GIStructInfo* info, GType gtype) : GIWrapperPrototype(info, 
gtype) {
+    GJS_INC_COUNTER(logfield_prototype);
+}
+
+bool GjsGLogFieldPrototype::define_class(JSContext* cx,
+                                         JS::HandleObject in_object,
+                                         GIStructInfo* info) {
+    JS::RootedObject prototype(cx), unused_constructor(cx);
+    GType gtype = g_registered_type_info_get_g_type(info);
+    GjsGLogFieldPrototype* priv = GjsGLogFieldPrototype::create_class(
+        cx, in_object, info, gtype, &unused_constructor, &prototype);
+    if (!priv)
+        return false;
+
+    if (!JS_DefineProperty(cx, prototype, "key",
+                           &GjsGLogFieldPrototype::key_getter, nullptr,
+                          JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY) ||
+        !JS_DefineProperty(cx, prototype, "value",
+                           &GjsGLogFieldPrototype::value_getter, nullptr,
+                          JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY)) {
+        return false;
+    }
+
+    return true;
+}
+
+bool GjsGLogFieldPrototype::init(JSContext*) {
+    return true;
+}
+
+GjsGLogFieldInstance::GjsGLogFieldInstance(JSContext* cx, JS::HandleObject obj)
+    : GIWrapperInstance(cx, obj),
+      m_owning_ptr(false) {
+    GJS_INC_COUNTER(logfield_instance);
+}
+
+bool GjsGLogFieldInstance::init_from_c_struct(JSContext*, GLogField* glogfield,
+                                              NoCopy) {
+    // We need to create a JS Boxed which references the original C struct, not
+    // a copy of it. Used for G_SIGNAL_TYPE_STATIC_SCOPE.
+    share_ptr(glogfield);
+    debug_lifecycle("Boxed pointer acquired, memory not owned");
+    return true;
+}
+
+bool GjsGLogFieldInstance::init_from_c_struct(JSContext*,
+                                              GLogField* glogfield) {
+    own_ptr(g_malloc0(sizeof(GLogField)));
+
+    ptr()->key = g_strdup(glogfield->key);
+
+    if (glogfield->length != -1) {
+        ptr()->value = nullptr;
+        ptr()->length = 0;
+    } else if (glogfield->length < 0) {
+        ptr()->value = g_strdup(static_cast<const char*>(glogfield->value));
+        ptr()->length = -1;
+    } else {
+        
+    }
+
+
+    debug_lifecycle("GLogField pointer allocated");
+    return true;
+}
+
+template <typename... Args>
+JSObject* GjsGLogFieldInstance::new_for_c_struct_impl(JSContext* cx,
+                                                 GIStructInfo* info,
+                                                 void* glogfield,
+                                                 Args&&... args) {
+    if (glogfield == nullptr)
+        return nullptr;
+
+    gjs_debug_marshal(GJS_DEBUG_GBOXED,
+                      "Wrapping GLogField struct %s %p with JSObject",
+                      g_base_info_get_name((GIBaseInfo*)info), glogfield);
+
+    JS::RootedObject obj(cx, gjs_new_object_with_generic_prototype(cx, info));
+    if (!obj)
+        return nullptr;
+
+    GjsGLogFieldInstance* priv =
+        GjsGLogFieldInstance::new_for_js_object(cx, obj);
+    if (!priv || !priv->init_from_c_struct(cx, static_cast<GLogField*>(glogfield), 
std::forward<Args>(args)...))
+        return nullptr;
+
+    return obj;
+}
+
+JSObject* GjsGLogFieldInstance::new_for_c_struct(JSContext* cx, GIStructInfo* info,
+                                          void* gboxed) {
+    return new_for_c_struct_impl(cx, info, gboxed);
+}
+
+JSObject* GjsGLogFieldInstance::new_for_c_struct(JSContext* cx, GIStructInfo* info,
+                                          void* gboxed, NoCopy no_copy) {
+    return new_for_c_struct_impl(cx, info, gboxed, no_copy);
+}
+
+// See GIWrapperBase::constructor().
+bool GjsGLogFieldInstance::constructor_impl(JSContext* cx,
+                                            JS::HandleObject,
+                                            const JS::CallArgs& args) {
+    // Short-circuit copy-construction in the case where we can use copy_boxed()
+    // or copy_memory()
+    GjsGLogFieldBase* source_priv;
+
+    if (args.length() == 1 &&
+        (source_priv = get_copy_source(cx, args[0]))) {
+        if (!source_priv->check_is_instance(cx, "construct logfield object"))
+            return false;
+
+        return init_from_c_struct(cx, source_priv->to_instance()->ptr());
+    }
+
+    JS::UniqueChars key, value;
+    if (!gjs_parse_call_args(cx, "GLib.FieldInstance()", args, "ss", "key", &key, "value", &value))
+        return false;
+
+    GLogField log_field {};
+    log_field.key = key.get();
+    log_field.value = value.get();
+    log_field.length = -1;
+
+    return init_from_c_struct(cx, &log_field);
+}
+
+GjsGLogFieldInstance::~GjsGLogFieldInstance() {
+    if (m_owning_ptr) {
+        g_free(m_ptr.release());
+    }
+
+    GJS_DEC_COUNTER(logfield_instance);
+}
+
+GjsGLogFieldPrototype::~GjsGLogFieldPrototype(void) {
+    GJS_DEC_COUNTER(logfield_prototype);
+}
\ No newline at end of file
diff --git a/gi/glogfield.h b/gi/glogfield.h
new file mode 100644
index 00000000..8be864fb
--- /dev/null
+++ b/gi/glogfield.h
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#ifndef GI_GLOGFIELD_H_
+#define GI_GLOGFIELD_H_
+
+#include <glib.h>
+
+#include <girepository.h>     // for GIStructInfo
+#include <glib-object.h>      // for GType
+#include <js/TypeDecls.h>     // for HandleObject
+#include "gi/cwrapper.h"      // for CWrapperPointerOps
+#include "gi/wrapperutils.h"  // for GjsSmartPointer, GIWrapperBase, GIWrapp...
+#include "gjs/macros.h"       // for GJS_JSAPI_RETURN_CONVENTION
+#include "util/log.h"         // for GJS_DEBUG_GBOXED, GjsDebugTopic
+
+namespace JS {
+class CallArgs;
+}
+
+class GjsGLogFieldInstance;
+class GjsGLogFieldPrototype;
+
+bool gjs_struct_info_is_log_field(GIStructInfo* info);
+
+class GjsGLogFieldBase
+    : public GIWrapperBase<GjsGLogFieldBase, GjsGLogFieldPrototype,
+                           GjsGLogFieldInstance> {
+    friend class CWrapperPointerOps<GjsGLogFieldBase>;
+    friend class GIWrapperBase<GjsGLogFieldBase, GjsGLogFieldPrototype,
+                               GjsGLogFieldInstance>;
+
+ protected:
+    explicit GjsGLogFieldBase(GjsGLogFieldPrototype* proto = nullptr)
+        : GIWrapperBase(proto) {}
+
+    static constexpr GjsDebugTopic DEBUG_TOPIC = GJS_DEBUG_GBOXED;
+    static constexpr const char* DEBUG_TAG = "logfield";
+
+    static const struct JSClassOps class_ops;
+    static const struct JSClass klass;
+
+ public:
+    [[nodiscard]] GjsGLogFieldBase* get_copy_source(JSContext* cx,
+                                                    JS::Value value) const;
+};
+
+class GjsGLogFieldPrototype
+    : public GIWrapperPrototype<GjsGLogFieldBase, GjsGLogFieldPrototype,
+                                GjsGLogFieldInstance, GIStructInfo> {
+    friend class GIWrapperPrototype<GjsGLogFieldBase, GjsGLogFieldPrototype,
+                                    GjsGLogFieldInstance, GIStructInfo>;
+    friend class GIWrapperBase<GjsGLogFieldBase, GjsGLogFieldPrototype,
+                               GjsGLogFieldInstance>;
+
+    explicit GjsGLogFieldPrototype(GIStructInfo* info, GType gtype);
+    ~GjsGLogFieldPrototype(void);
+
+    GJS_JSAPI_RETURN_CONVENTION bool init(JSContext* cx);
+
+    static constexpr InfoType::Tag info_type_tag = InfoType::Struct;
+
+    // Accessors
+    bool define_fields(JSContext* cx, JS::HandleObject prototype);
+
+    static bool key_getter(JSContext* cx, unsigned int argc, JS::Value* vp);
+    static bool value_getter(JSContext* cx, unsigned int argc, JS::Value* vp);
+
+ public:
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool define_class(JSContext* cx, JS::HandleObject in_object,
+                             GIStructInfo* info);
+};
+
+class GjsGLogFieldInstance
+    : public GIWrapperInstance<GjsGLogFieldBase, GjsGLogFieldPrototype,
+                               GjsGLogFieldInstance> {
+    friend class GIWrapperInstance<GjsGLogFieldBase, GjsGLogFieldPrototype,
+                                   GjsGLogFieldInstance>;
+    friend class GIWrapperBase<GjsGLogFieldBase, GjsGLogFieldPrototype,
+                               GjsGLogFieldInstance>;
+    friend class GjsGLogFieldBase;  // for field_getter, etc.
+
+    bool m_owning_ptr : 1;  // if set, the JS wrapper owns the C memory referred
+                            // to by m_ptr.
+
+    explicit GjsGLogFieldInstance(JSContext* cx, JS::HandleObject obj);
+    ~GjsGLogFieldInstance(void);
+
+    // Don't set GIWrapperBase::m_ptr directly. Instead, use one of these
+    // setters to express your intention to own the pointer or not.
+    void own_ptr(void* boxed_ptr) {
+        g_assert(!m_ptr);
+        m_ptr = boxed_ptr;
+        m_owning_ptr = true;
+    }
+
+    void share_ptr(void* unowned_boxed_ptr) {
+        g_assert(!m_ptr);
+        m_ptr = unowned_boxed_ptr;
+        m_owning_ptr = false;
+    }
+
+    // JS constructor
+
+    GJS_JSAPI_RETURN_CONVENTION
+    bool constructor_impl(JSContext* cx, JS::HandleObject obj,
+                          const JS::CallArgs& args);
+
+ public:
+    [[nodiscard]] GLogField* ptr() const {
+        return static_cast<GLogField*>(m_ptr.get());
+    }
+
+    struct NoCopy {};
+
+ private:
+    GJS_JSAPI_RETURN_CONVENTION
+    bool init_from_c_struct(JSContext* cx, GLogField* glogfield);
+    GJS_JSAPI_RETURN_CONVENTION
+    bool init_from_c_struct(JSContext* cx, GLogField* glogfield, NoCopy);
+
+    template <typename... Args>
+    GJS_JSAPI_RETURN_CONVENTION static JSObject* new_for_c_struct_impl(
+        JSContext* cx, GIStructInfo* info, void* glogfield, Args&&... args);
+
+ public:
+    GJS_JSAPI_RETURN_CONVENTION
+    static JSObject* new_for_c_struct(JSContext* cx, GIStructInfo* info,
+                                      void* gboxed);
+    GJS_JSAPI_RETURN_CONVENTION
+    static JSObject* new_for_c_struct(JSContext* cx, GIStructInfo* info,
+                                      void* gboxed, NoCopy);
+};
+
+#endif
\ No newline at end of file
diff --git a/gi/repo.cpp b/gi/repo.cpp
index 87030fd4..414bc6a3 100644
--- a/gi/repo.cpp
+++ b/gi/repo.cpp
@@ -33,6 +33,7 @@
 #include "gi/function.h"
 #include "gi/fundamental.h"
 #include "gi/gerror.h"
+#include "gi/glogfield.h"
 #include "gi/interface.h"
 #include "gi/ns.h"
 #include "gi/object.h"
@@ -395,8 +396,12 @@ gjs_define_info(JSContext       *context,
         /* Fall through */
 
     case GI_INFO_TYPE_BOXED:
-        if (!BoxedPrototype::define_class(context, in_object, info))
+        if (gjs_struct_info_is_log_field(info)) {
+            if (!GjsGLogFieldPrototype::define_class(context, in_object, info))
+                return false;
+        } else if (!BoxedPrototype::define_class(context, in_object, info)) {
             return false;
+        }
         break;
     case GI_INFO_TYPE_UNION:
         if (!gjs_define_union_class(context, in_object, (GIUnionInfo*) info))
diff --git a/gi/value.cpp b/gi/value.cpp
index df427464..5bb8b40d 100644
--- a/gi/value.cpp
+++ b/gi/value.cpp
@@ -31,6 +31,7 @@
 #include "gi/foreign.h"
 #include "gi/fundamental.h"
 #include "gi/gerror.h"
+#include "gi/glogfield.h"
 #include "gi/gtype.h"
 #include "gi/js-value-inl.h"
 #include "gi/object.h"
@@ -963,7 +964,9 @@ gjs_value_from_g_value_internal(JSContext             *context,
         }
 
         GIInfoType type = info.type();
-        if (type == GI_INFO_TYPE_BOXED || type == GI_INFO_TYPE_STRUCT) {
+        if (gjs_struct_info_is_log_field(info)) {
+            obj = GjsGLogFieldInstance::new_for_c_struct(context, info, gboxed);
+        } else if (type == GI_INFO_TYPE_BOXED || type == GI_INFO_TYPE_STRUCT) {
             if (no_copy)
                 obj = BoxedInstance::new_for_c_struct(context, info, gboxed,
                                                       BoxedInstance::NoCopy());
diff --git a/gjs/mem-private.h b/gjs/mem-private.h
index e4bdc750..2ec46525 100644
--- a/gjs/mem-private.h
+++ b/gjs/mem-private.h
@@ -12,6 +12,8 @@
 #define GJS_FOR_EACH_COUNTER(macro) \
     macro(boxed_instance)           \
     macro(boxed_prototype)          \
+    macro(logfield_instance)        \
+    macro(logfield_prototype)       \
     macro(closure)                  \
     macro(function)                 \
     macro(fundamental_instance)     \
diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build
index e11f1418..615fc967 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -94,6 +94,7 @@ subdir('libgjstesttools')
 jasmine_tests = [
     'self',
     'ByteArray',
+    'Console',
     'Exceptions',
     'Format',
     'Fundamental',
diff --git a/installed-tests/js/testConsole.js b/installed-tests/js/testConsole.js
new file mode 100644
index 00000000..5daa942c
--- /dev/null
+++ b/installed-tests/js/testConsole.js
@@ -0,0 +1,41 @@
+// / <reference types="jasmine" />
+
+const {GLib} = imports.gi;
+
+describe('Console log levels', function () {
+    /** @type {jasmine.Spy<(_level: any, _fields: any) => any>} */
+    let writer_func;
+
+    beforeAll(function () {
+        writer_func = jasmine.createSpy(
+            'Log test writer func',
+            function (_level, _fields) {
+                return GLib.LogWriterOutput.HANDLED;
+            }
+        );
+
+        writer_func.and.callThrough();
+
+        GLib.log_set_writer_func(writer_func);
+    });
+
+    beforeEach(function () {
+        writer_func.calls.reset();
+    });
+
+    it('logs a message', function () {
+        console.log('a message');
+
+        expect(writer_func.calls.mostRecent().args).toEqual(
+            [GLib.LogLevelFlags.LEVEL_MESSAGE, jasmine.arrayContaining([jasmine.objectContaining({key: 
'MESSAGE', value: 'a message'})])]
+        );
+    });
+
+    it('logs a warning', function () {
+        console.warn('a warning');
+
+        expect(writer_func.calls.mostRecent().args).toEqual(
+            [GLib.LogLevelFlags.LEVEL_WARNING, jasmine.arrayContaining([jasmine.objectContaining({key: 
'MESSAGE', value: 'a warning'})])]
+        );
+    });
+});
diff --git a/meson.build b/meson.build
index 3280f4e8..77a90b99 100644
--- a/meson.build
+++ b/meson.build
@@ -383,6 +383,7 @@ libgjs_sources = [
     'gi/function.cpp', 'gi/function.h',
     'gi/gerror.cpp', 'gi/gerror.h',
     'gi/gjs_gi_trace.h',
+    'gi/glogfield.cpp', 'gi/glogfield.h',
     'gi/gobject.cpp', 'gi/gobject.h',
     'gi/gtype.cpp', 'gi/gtype.h',
     'gi/interface.cpp', 'gi/interface.h',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]