lgi-devs / lgi

Dynamic Lua binding to GObject libraries using GObject-Introspection
MIT License
434 stars 68 forks source link

Example of how to implement *DBusInterfaceSkeleton* abstract class #275

Open joseigor opened 2 years ago

joseigor commented 2 years ago

Hi all,

I am working in a project using lgi where I need to implement a instance of the abstract class DBusInterfaceSkeleton. After many trails I still did not get any success so I was wondering if anyone could help me. I found some documentation of how to implement a sublcass using lgi.package but still not clear for me how I can get a instance of the class DBusInterfaceSkeleton.

Just to put in context I need a instance of this class in order to add a DBUS interface to a DBUS object using Gio.DBusObjectSkeleton.add_interface(...) method.

Best Regards,

psychon commented 2 years ago

Do you happen to have some C example code for me to translate?

Last time I looked at this, I gave up since I failed to figure out how "all the pieces fit together".

(From the top of my head: I also don't know how the subclassing works exactly. But hopefully I can figure this out when I don't have to figure out Gio's dbus code at the same time.)

joseigor commented 2 years ago

@psychon thanks for the quick replay. In this moment I am able to create a instance of DBusInterfaceSkeleton, see code below, but now by some reason I get a SEGMENTATION FAULT when I try to add an interface to a object. I tried to debug the reason of the error but without success. Last line of code below just cause the SEGMENTATION FAULT.

local lgi = require("lgi")
local Gio = lgi.Gio
local GObject = lgi.GObject
local GLib = lgi.GLib

local MyApp = lgi.package 'MyApp'
MyApp:class('MyDBusInterfaceSkeleton',  Gio.DBusInterfaceSkeleton)

-- Override get_info() method of Gio.DBusInterfaceSkeleton
function MyApp.MyDBusInterfaceSkeleton:do_get_info()
      return Gio.DBusInterfaceInfo{
        name = "org.test",
        methods = nil,
        signals = nil,
        properties = nil,
        annotations = nil
    }
end

-- create a instace of MyDBusInterfaceSkeleton which implements Gio.DBusInterfaceSkeleton
local interface_ske = MyApp.MyDBusInterfaceSkeleton()

local obj_man = Gio.DBusObjectManagerServer{object_path ="/object"}  
local obj_ske = Gio.DBusObjectSkeleton.new("/object/1")

obj_man:export(obj_ske)

-- Line below causes SEGMENTATION FAULT
obj_ske:add_interface(interface_ske)
psychon commented 2 years ago

Sorry, but that does not segfault here :-(

joseigor commented 2 years ago

It works on you side? Weird, let me check again... Thanks for the feedback

Cold you share how you tested it?

psychon commented 2 years ago
$ cat > /tmp/t.lua && lua /tmp/t.lua 
local lgi = require("lgi")
local Gio = lgi.Gio
local GObject = lgi.GObject
local GLib = lgi.GLib

local MyApp = lgi.package 'MyApp'
MyApp:class('MyDBusInterfaceSkeleton',  Gio.DBusInterfaceSkeleton)

-- Override get_info() method of Gio.DBusInterfaceSkeleton
function MyApp.MyDBusInterfaceSkeleton:do_get_info()
      return Gio.DBusInterfaceInfo{
        name = "org.test",
        methods = nil,
        signals = nil,
        properties = nil,
        annotations = nil
    }
end

-- create a instace of MyDBusInterfaceSkeleton which implements Gio.DBusInterfaceSkeleton
local interface_ske = MyApp.MyDBusInterfaceSkeleton()

local obj_man = Gio.DBusObjectManagerServer{object_path ="/object"}  
local obj_ske = Gio.DBusObjectSkeleton.new("/object/1")

obj_man:export(obj_ske)

-- Line below causes SEGMENTATION FAULT
obj_ske:add_interface(interface_ske)

First line is what I entered in my shell. The rest is copy&paste from your post (with a final ctrl+d for "end of file). I tested this with all of lua5.1, 5.2, 5.3 and luajit.

joseigor commented 2 years ago

Now I see, actually I narrowed down the case of segmentation fault, it happens when set the dbus-connection to object manager. I put all code I used to test below. And indeed the way you tested there is no segfault. If you try the code below just after we set the connection to the object-manager segfault happens in the line obj_ske:add_interface(interface_ske). Still not clear to me why.


local lgi = require("lgi")
local Gio = lgi.Gio
local GObject = lgi.GObject
local GLib = lgi.GLib

local MyApp = lgi.package 'MyApp'
MyApp:class('MyDBusInterfaceSkeleton',  Gio.DBusInterfaceSkeleton)

-- Override get_info() method of Gio.DBusInterfaceSkeleton
function MyApp.MyDBusInterfaceSkeleton:do_get_info()
      return Gio.DBusInterfaceInfo{
        name = "org.test",
        methods = nil,
        signals = nil,
        properties = nil,
        annotations = nil
    }
end

-- create a instace of MyDBusInterfaceSkeleton which implements Gio.DBusInterfaceSkeleton
local interface_ske = MyApp.MyDBusInterfaceSkeleton()

local obj_man = Gio.DBusObjectManagerServer{object_path ="/object"}  
local obj_ske = Gio.DBusObjectSkeleton.new("/object/1")

obj_man:export(obj_ske)

local function on_bus_acquire(conn, _, _)
  obj_man:set_connection(conn)
  -- Line below causes SEGMENTATION FAULT
  obj_ske:add_interface(interface_ske)
end

local function on_name_lost(conn, _, _)
    -- if con is nil, then no connection to the bus could be made
    if (conn) then
        print('Could not acquire the requested name.')
    else
        print('Connection to the bus cannot be established.')
    end
end

local mainloop

local function main()
  Gio.bus_own_name(
        Gio.BusType.SESSION,
        "com.org.luadbus1",
        Gio.BusNameOwnerFlags.NONE,
        GObject.Closure(on_bus_acquire), nil, GObject.Closure(on_name_lost)
    )

    mainloop = GLib.MainLoop.new()
    print("daemon started")

    mainloop:run()

    dbus_service.stop()
    print("daemon stopped")
end

main()

If we change the order of the calls, then segfault happens on obj_man:set_connection(conn).


local lgi = require("lgi")
local Gio = lgi.Gio
local GObject = lgi.GObject
local GLib = lgi.GLib

local MyApp = lgi.package 'MyApp'
MyApp:class('MyDBusInterfaceSkeleton',  Gio.DBusInterfaceSkeleton)

-- Override get_info() method of Gio.DBusInterfaceSkeleton
function MyApp.MyDBusInterfaceSkeleton:do_get_info()
      return Gio.DBusInterfaceInfo{
        name = "org.test",
        methods = nil,
        signals = nil,
        properties = nil,
        annotations = nil
    }
end

-- create a instace of MyDBusInterfaceSkeleton which implements Gio.DBusInterfaceSkeleton
local interface_ske = MyApp.MyDBusInterfaceSkeleton()

local obj_man = Gio.DBusObjectManagerServer{object_path ="/object"}  
local obj_ske = Gio.DBusObjectSkeleton.new("/object/1")

obj_man:export(obj_ske)

local function on_bus_acquire(conn, _, _)
    obj_ske:add_interface(interface_ske)

  -- Line below causes SEGMENTATION FAULT
  obj_man:set_connection(conn)
end

local function on_name_lost(conn, _, _)
    -- if con is nil, then no connection to the bus could be made
    if (conn) then
        print('Could not acquire the requested name.')
    else
        print('Connection to the bus cannot be established.')
    end
end

local mainloop

local function main()
  Gio.bus_own_name(
        Gio.BusType.SESSION,
        "com.org.luadbus1",
        Gio.BusNameOwnerFlags.NONE,
        GObject.Closure(on_bus_acquire), nil, GObject.Closure(on_name_lost)
    )

    mainloop = GLib.MainLoop.new()
    print("daemon started")

    mainloop:run()

    dbus_service.stop()
    print("daemon stopped")
end

main()

Below I put the code implementationo of GIO g_dbus_object_manager_server_set_connection

/**
 * g_dbus_object_manager_server_set_connection:
 * @manager: A #GDBusObjectManagerServer.
 * @connection: (nullable): A #GDBusConnection or %NULL.
 *
 * Exports all objects managed by @manager on @connection. If
 * @connection is %NULL, stops exporting objects.
 */
void
g_dbus_object_manager_server_set_connection (GDBusObjectManagerServer  *manager,
                                             GDBusConnection           *connection)
{
  g_return_if_fail (G_IS_DBUS_OBJECT_MANAGER_SERVER (manager));
  g_return_if_fail (connection == NULL || G_IS_DBUS_CONNECTION (connection));

  g_mutex_lock (&manager->priv->lock);

  if (manager->priv->connection == connection)
    {
      g_mutex_unlock (&manager->priv->lock);
      goto out;
    }

  if (manager->priv->connection != NULL)
    {
      unexport_all (manager, FALSE);
      g_object_unref (manager->priv->connection);
      manager->priv->connection = NULL;
    }

  manager->priv->connection = connection != NULL ? g_object_ref (connection) : NULL;
  if (manager->priv->connection != NULL)
    export_all (manager);

  g_mutex_unlock (&manager->priv->lock);

  g_object_notify (G_OBJECT (manager), "connection");
 out:
  ;
}

Once again, thank you so much for the support!

psychon commented 2 years ago

The crash happens here:

GDBusInterfaceVTable *
g_dbus_interface_skeleton_get_vtable (GDBusInterfaceSkeleton *interface_)
{
  GDBusInterfaceVTable *ret;
  g_return_val_if_fail (G_IS_DBUS_INTERFACE_SKELETON (interface_), NULL);
  ret = G_DBUS_INTERFACE_SKELETON_GET_CLASS (interface_)->get_vtable (interface_);
  g_warn_if_fail (ret != NULL);
  return ret;
}

Specifically, it happens when calling the get_vtable function. This function pointer is NULL.

psychon commented 2 years ago

Hm... I tried to add the following, but the function is not called (still same NULL pointer as before). Now I am back in "I do not know how this work and would like to have a working example in C that can be translated". Sorry.

function MyApp.MyDBusInterfaceSkeleton:do_get_vtable()
    print("You have to somehow implement this method")
end
joseigor commented 2 years ago

Hi @psychon as per my investigation it seems the LGI lua bidding do not implement the get_vtable which is required, so it does not work if we try to override because it is not part of the DBusInterfaceSkeletongenerated by lig this is the reason I guess it is NULL.

Regarding a work example please find below attached files from the official GIO repository.

https://drive.google.com/drive/folders/1B3iyrStz2rVw8alPOr9TMjkgHfC5Ylhw?usp=sharing

psychon commented 2 years ago

Hrm. Sorry, I give up. That's 3.8k lines of code, 3.1k of which come from a code generator. Gio's DBus code is just not meant to be used from anything other than C.

Here is my state when I gave up:

local lgi = require("lgi")
local Gio = lgi.Gio
local GObject = lgi.GObject
local GLib = lgi.GLib

local MyApp = lgi.package 'MyApp'
MyApp:class('ExampleObjectSkeleton',  Gio.DBusObjectSkeleton)
MyApp:class('ExampleAnimalSkeleton',  Gio.DBusInterfaceSkeleton)

MyApp.ExampleAnimalSkeleton._property.mood = GObject.ParamSpecString("mood", "Mood", "Mood", nil, { "READWRITE" })
MyApp.ExampleObjectSkeleton._property.animal = GObject.ParamSpecObject("animal", "animal", "animal", MyApp.ExampleAnimalSkeleton, { "READWRITE" })

function MyApp.ExampleAnimalSkeleton._property_set:mood(value)
    self.priv.mood = value
end

function MyApp.ExampleAnimalSkeleton:set_mood(value)
    self.mood = value
end

local vtable = Gio.DBusInterfaceVTable{
    method_call = function(...) print("method call", ...) end,
    get_property = function(...) print("get property", ...) end,
    set_property = function(...) print("set property", ...) end
}
function MyApp.ExampleAnimalSkeleton:do_get_vtable()
    print("get vtable 1 - WHY THE HECK IS THIS NEVER CALLED?")
end

local info = Gio.DBusInterfaceInfo {
    name = "org.gtk.GDBus.Example.ObjectManager.Cat",
    -- Lots of NULLs follow
}
function MyApp.ExampleAnimalSkeleton:do_get_info()
    return info
end

function MyApp.ExampleObjectSkeleton._property_set:animal(value)
    self.priv.animal = value
    -- Not sure if I got this right... In fact, I am almost sure I got this wrong
    self:add_interface(value)
end

function MyApp.ExampleObjectSkeleton:set_animal(value)
    self.animal = value
end

local function example_object_skeleton_new(path)
    return MyApp.ExampleObjectSkeleton{["g-object-path"] = path}
end

local function example_animal_skeleton_new()
    return MyApp.ExampleAnimalSkeleton()
end

local function on_animal_poke(animal, invocation, make_sad, make_happy, ...)
    print("on_animal_poke", animal, invocation, make_sad, make_happy)

    if (make_sad and make_happy) or (not make_sad and not make_happy) then
        invocation:return_dbus_error("org.gtk.GDBus.Examples.ObjectManager.Error.Failed", "Exactly one of make_sad or make_happy must be TRUE")
        return true -- The C code uses a constant for this, but I did not find this anywhere
    end

    if make_sad then
        if animal:get_mood() == "Sad" then
            invocation:return_dbus_error("org.gtk.GDBus.Examples.ObjectManager.Error.SadAnimalIsSad", "Sad animal is already sad")
            return true
        end
        animal:set_mood("Sad")
        animal:complete_poke(animal, invocation)
    elseif make_happy then
        if animal:get_mood() == "Happy" then
            invocation:return_dbus_error("org.gtk.GDBus.Examples.ObjectManager.Error.HappyAnimalIsHappy", "Happy animal is already happy")
            return true
        end
        animal:set_mood("Happy")
        animal:complete_poke(animal, invocation)
    end
    assert(0)
end

local function on_bus_acquired(conn, name, data)
    print("Acquired a message bus connection")

    manager = Gio.DBusObjectManagerServer.new("/example/Animals")
    for n = 0, 10 do
        local object = example_object_skeleton_new(string.format("/example/Animals/%03d", n))
        local animal = example_animal_skeleton_new()
        animal:set_mood("Happy")
        object:set_animal(animal)

        --[[ No thanks, let's skip this for now
        if n % 2 == 1 then
            local cat = example_cat_skeleton_new()
            object:set_cat(cat)
        end
        --]]

        -- FIXME: How to I add the handle-poke signal?!?
        --animal.on_handle_poke = on_animal_poke

        manager:export(object)
    end
    manager:set_connection(conn)
end

local function on_name_acquired(conn, name, data)
    print("Acquired the name", con, name, data)
end

local function on_name_lost(conn, name, data)
    print("Lost the name", con, name, data)
end

-- main

local loop = GLib.MainLoop.new(nil, false)
id = Gio.bus_own_name(Gio.BusType.SESSION,
    "org.gtk.GDBus.Examples.ObjectManager",
    Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT + Gio.BusNameOwnerFlags.REPLACE,
    GObject.Closure(on_bus_acquired), GObject.Closure(on_name_lost), GObject.Closure(loop), nil)

loop:run()
Gio.bus_unown_name(id)

print("end of main")

Perhaps the following code helps you? It shows how to use Gio without using DBusInterfaceSkeleton (but of course you then lose its automatic introspection capabilities): https://github.com/awesomeWM/awesome/blob/ebc9b99ae2817ddd95b5b9b2238881fcd5c777e0/lib/naughty/dbus.lua#L429-L430