Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to emit signals on an object #281

Open
HumblePresent opened this issue Feb 2, 2022 · 26 comments
Open

How to emit signals on an object #281

HumblePresent opened this issue Feb 2, 2022 · 26 comments

Comments

@HumblePresent
Copy link

Does LGI support emitting existing signals on an object? I have an object type that has two signals associated with it according to the result of GObject.signal_list_ids(), but I do not know how to emit those signals on an instance of the object. I have tried calling obj:on_<signal>() but just get an error saying no 'on_<signal>'.

@psychon
Copy link
Collaborator

psychon commented Feb 4, 2022

@psychon
Copy link
Collaborator

psychon commented Feb 4, 2022

...but that is what you are already trying to do.

Edit: Hm, dunno 🤷

@HumblePresent
Copy link
Author

Hey @psychon, thanks for responding. I've looked into this a bit more and it looks like there is a method defined in lgi/override/GObject-Object.lua called Object:_access_signal() that will return a table with an emit() method. This at least looks promising as far as being able to emit signals, but while this method is technically available through lgi it doesn't look like it's necessarily meant to be called directly. For one thing I'm not sure what all the arguments to this function are supposed to be as it is not documented. In particular the info parameter seems to carry some description of the objects signals, but I'm not totally certain what needs to be in there. Do you have any idea if there is a more indirect or idiomatic way that this method is called?

@psychon
Copy link
Collaborator

psychon commented Feb 20, 2022

I guess (based on git grep)

local handler = self['_access' .. category]
and
if not category or not (origin or self)['_access' .. category] then
do something with _access. Time to look for the category "signal", I guess... According to this comment, it should be called from core:

lgi/lgi/component.lua

Lines 154 to 155 in 9eaad42

-- Implementation of _access method, which is called by _core when
-- instance of repo-based type is accessed for reading or writing.

Perhaps this? This is just the __index metamethod for objects.

lgi/lgi/object.c

Lines 384 to 386 in 9eaad42

/* Check that 1st arg is an object and invoke one of the forms:
result = type:_access(objectinstance, name)
type:_access(objectinstance, name, val) */

If I get this right, __index simply forwards to __access...?

Hm...

lgi/lgi/component.lua

Lines 211 to 214 in 9eaad42

-- Decompose symbol name, in case that it contains category prefix
-- (e.g. '_field_name' when requesting explicitely field called
-- name).
local category, name = string.match(symbol, '^(_.-)_(.*)$')
sounds like one should be able to do .signal_foo to explicitly access foo in the category signal.

I'll do some print-debugging and see if one actually ends up in _access_signal. I'll edit this comment with progress.

@psychon
Copy link
Collaborator

psychon commented Feb 20, 2022

Okay, no edit, but a new post instead.

New insights: One has to call :emit on the signal, but just calling it should have the same effect (which I only noticed now and not while investigating, whoops):

function pad:emit(...)
return emit_signal(object, gtype, info, nil, ...)
end
function mt:__call(_, ...)
return emit_signal(object, gtype, info, nil, ...)
end

This code here seems wrong and causes the actual error we see (it tries to set up things for the signal's return value, a boolean):

if self.ret then retval.type = self.ret.gtype end
if self.phantom then retval.type = self.phantom.gtype end

There is no type property on Value. It needs to be gtype. But when changing this, I get an error when the signal is actually called:

(lua5.3:45690): Lgi-CRITICAL **: 09:49:23.301: attempt to steal record ownership from unowned rec

This error comes from here and I do not really understand what is going on:

g_critical ("attempt to steal record ownership from unowned rec");

@HumblePresent
Copy link
Author

HumblePresent commented Feb 20, 2022

Thanks for doing some digging! So based on that match pattern when determining the category and name, one should be able to do something like obj._signal_name in order to access the "name" signal? I haven't had any luck doing that with the object I created this issue about but I might still be doing something wrong. Are you able to do something like require("gears.debug").dump(obj._signal_name) without errors?

@psychon
Copy link
Collaborator

psychon commented Feb 20, 2022

Oh, whoops, I totally forgot to post the test case that I came up with:

local dialog = require("lgi").Gtk.AboutDialog.new()
print(dialog._signal_on_activate_link)
dialog._signal_on_activate_link = function(...) print("signal was called", ...) return true end

print(dialog.on_activate_link.emit)
print("signal returns:", dialog.on_activate_link("foo"))

Which object/signal are you working with?

Are you able to do something like require("gears.debug").dump(obj._signal_name) without errors?

Nope, no gears. ;-) However, this works fine:

local dialog = require("lgi").Gtk.AboutDialog.new()
for k, v in pairs(dialog._signal_on_activate_link) do
	print(k, v)
end

@psychon
Copy link
Collaborator

psychon commented Feb 20, 2022

Anyway, I do not really know what I am doing.

The following change turns the Lua error into a segfault (but at a later point) and the "attempt to steal" message from above:

diff --git a/lgi/override/GObject-Closure.lua b/lgi/override/GObject-Closure.lua
index 577f64f..dde56b0 100644
--- a/lgi/override/GObject-Closure.lua
+++ b/lgi/override/GObject-Closure.lua
@@ -231,9 +231,15 @@ function CallInfo:pre_call(...)
    end
 
    -- Prepare return value.
-   local retval = Value()
-   if self.ret then retval.type = self.ret.gtype end
-   if self.phantom then retval.type = self.phantom.gtype end
+   local retvaltype
+   local retval
+   if self.ret then retvaltype = self.ret.gtype end
+   if self.phantom then retvaltype = self.phantom.gtype end
+   if retvaltype then
+       retval = Value(retvaltype)
+   else
+       retval = Value()
+   end
    return retval, params, marshalling_params
 end
 

I tried to work around that "attempt to steal" stuff, but yeah, this thing confuses me. To emit the signal, g_signal_emitv is called: https://docs.gtk.org/gobject/func.signal_emitv.html

This function has a GValue* as its last argument for the return value. This argument is marked (inout) and I do not really understand the in part: https://github.com/GNOME/glib/blob/a57c33fc1d657f8f9f788b9b042c6ded19bb3c59/gobject/gsignal.c#L3116

Due to this (inout), the parameter is also (transfer full):

Default Annotations: To avoid having the developers annotate everything the introspection framework is providing sane default annotation values for a couple of situations:
[...]
(inout) and (out) parameters: (transfer full)
https://wiki.gnome.org/Projects/GObjectIntrospection/Annotations#Type_signature

I guess (but am not really sure), that something around this stuff is fishy. Hence my patch above. But apparently/obviously, I still got it wrong.

If only we had someone who understood this stuff... :-(

@HumblePresent
Copy link
Author

Haha sorry for the gears reference, I just know you're an AwesomeWM guy :). I am trying to use the Wireplumber library to monitor audio sinks managed by Pipewire. There is a loadable plugin that facilitates querying for the current default node that requires emitting signals for use. The object is of type WpDefaultNodesApi which inherits from Wp.Plugin. The actual plugin is undocumented but here is the source code. There are four primary signals that are used to interface with the plugin and they are all listed by

local GObject = require("lgi").GObject
for _, id in ipairs(GObject.signal_list_ids("WpDefaultNodesApi")) do
    print(GObject.signal_query(id).signal_name)
end

as

get-default-node
get-default-configured-node-name
set-default-configured-node-name
changed

Unfortunately, after creating the object, trying to call

print(default_nodes._signal_on_get_default_node)

still produces the no `_signal_on_get_default_node` error. There may be some GLib weirdness going on that lgi is not equipped to handle. I really appreciate your help on this though as the lgi internals are a bit harder to decipher.

@psychon
Copy link
Collaborator

psychon commented Feb 20, 2022

Unfortunately, after creating the object,

How exactly are you creating the object? I didn't find any API documentation, but I also did not look at anything but module-default-nodes-api.c.

Does the changed signal work? It doesn't have a return type and it seems like the signal's return type is part of the problem here.

Edit: I just installed gir1.2-wp-0.4. When I run the following:

local GObject = require("lgi").GObject
for _, id in ipairs(GObject.signal_list_ids("WpDefaultNodesApi")) do
    print(GObject.signal_query(id).signal_name)
end

I only get:

(process:54335): GLib-GObject-CRITICAL **: 18:34:13.568: g_signal_list_ids: assertion 'G_TYPE_IS_INSTANTIATABLE (itype) || G_TYPE_IS_INTERFACE (itype)' failed

@psychon
Copy link
Collaborator

psychon commented Feb 20, 2022

Okay, I do not understand wireplumber. I managed to get a segfault:

local lgi = require("lgi")
local Wp = lgi.Wp
Wp.init(0)
local core = Wp.Core.new(nil, nil)
[E] pw.context [pipewire.c:256 load_spa_handle()] load lib: plugin directory undefined, set SPA_PLUGIN_DIR
[E] pw.loop [loop.c:86 pw_loop_new()] 0x55f150b1a800: can't make support.system handle: No such file or directory
zsh: segmentation fault  lua /tmp/wp.lua

Somehow I feel like the (nullable) here are wrong: https://pipewire.pages.freedesktop.org/wireplumber/c_api/core_api.html#c.wp_core_new

@HumblePresent
Copy link
Author

Here is how I am creating the object

local Wp = require("lgi").Wp

Wp.init(Wp.InitFlags.ALL)
local core = Wp.Core()
core:connect()

core:load_component("libwireplumber-module-default-nodes-api", "module")
local default_nodes = Wp.Plugin.find(core, "default-nodes-api")

default_nodes:activate(Wp.Pluginfeatures.Enabled, nil, function(self, res)
    if not self:activate_finish(res) then
        print("FAILED to activate plugin: " .. self.name)
    end
end)

@psychon
Copy link
Collaborator

psychon commented Feb 20, 2022

Ah, the flags to init are apparently the important difference.

Now :connect() crashes after complaining about a missing client.config file. 🤷

@HumblePresent
Copy link
Author

Hmm, that sounds like something is not right with your pipewire installation maybe? I'm also running this code inside AwesomeWM's Lua runtime but that shouldn't matter I don't think.

@psychon
Copy link
Collaborator

psychon commented Feb 20, 2022

I have no pipewire installation. ;-)

There is not even a pipewire daemon running and I think there should be. I only installed the GObject introspection files and libwireplumber in the hope that that would be enough to.... do something.

@HumblePresent
Copy link
Author

Ah that would explain it. From what I have read Wireplumber is a library/session manager that operates on top of the pipewire daemon so it might not work correctly without the daemon running.

@psychon
Copy link
Collaborator

psychon commented Mar 6, 2022

I guess/think that the problem might be that there is no such signal in the GObject-Introspection data (10 is the depth to print; this data structure really is self-referential and recursive and thus infinite; e.g. the information about a method refers to the types of its arguments and from there one can get to the methods on that type and an endless loop is formed):

$ lua dump-typelib.lua 10 Wp | grep -4 default
        is_pointer : false (boolean)
        tag : guint32 (string)
        is_basic : true (boolean)
        deprecated : false (boolean)
    log_writer_default : table: 0x556ee40ac900
      type : function (string)
      deprecated : false (boolean)
      return_type : table: 0x556ee40accf0
        type : type (string)
--
        tag : interface (string)
        is_basic : false (boolean)
        deprecated : false (boolean)
      flags : table: 0x556ee40b1a00
      name : log_writer_default (string)
      return_transfer : none (string)
      namespace : Wp (string)
      cats : table: 0x556ee40b20a0
        args : table: 0x556ee40b2a20

However, the code really needs the list of signals in the GI data.

So, time to figure out how to emit a signal "the hard way"...

P.S.: Yes, I installed pipewire just for you

@psychon
Copy link
Collaborator

psychon commented Mar 6, 2022

So, time to figure out how to emit a signal "the hard way"...

Argh. The answer really is "don't". Without information about the signals arguments and return type, everything is complicated. At least a bit.

local Wp = require("lgi").Wp

Wp.init(Wp.InitFlags.ALL)
local core = Wp.Core()
core:connect()

core:load_component("libwireplumber-module-default-nodes-api", "module")
local default_nodes = Wp.Plugin.find(core, "default-nodes-api")
print(default_nodes)

local GObject = require("lgi").GObject

local signal_id = GObject.signal_lookup("get-default-node", default_nodes._gtype)
print("Signal is", signal_id)
assert(signal_id ~= 0)

local Value = GObject.Value
local function emit_get_default_node(obj, arg)
	local params = {
		Value(obj._gtype, obj),
		Value(GObject.Type.STRING, arg)
	}
	local retval = Value(GObject.Type.UINT, 0)
	print("doing call")
	--GObject.signal_emitv(params, signal_id, 0, retval)
	foo = GObject.signal_emitv(params, signal_id, 0, nil)
	print("done call")
	foo:init(GObject.Type.UINT)
	return retval.value
end

print(emit_get_default_node(default_nodes, "foo"))
print("after call")

collectgarbage("collect")
collectgarbage("collect")
collectgarbage("collect")

require("lgi").GLib.MainLoop():run()

The above does not crash. If one removes the start of a main loop at the end, everything crashes and burns when the object foo is garbage collected. And I don't really understand why. g_signal_emitv returns void!?.

If one enables the commented out line to provide a place to save the retval of the signal, things crash during signal emission.

@HumblePresent
Copy link
Author

So based on some C source code that uses the "default-nodes-api" the "get-default-node" signal takes one string argument that specifies a media class to query, something like "Audio/Sink". It returns a guint32 which is the ID of the default node. As far as g_signal_emitv() returning void, it looks like a pointer to a GValue is passed as a parameter to obtain any return value. It looks like the GObject.Closure.CallInfo object is meant to help translate the C style arguments to lua.

-- Marshalls Lua arguments into Values suitable for invoking closures
-- and signals. Returns Value (for retval), array of Value (for
-- params) and keepalive value (which must be kept alive during the
-- call)
function CallInfo:pre_call(...)

Not sure exactly how to use this but this is how the code we were looking at before formats arguments for signal_emitv()
-- Emits signal on specified object instance.
local function emit_signal(obj, gtype, info, detail, ...)
-- Compile callable info.
local call_info = Closure.CallInfo.new(info)
-- Marshal input arguments.
local retval, params, marshalling_params = call_info:pre_call(obj, ...)
-- Invoke the signal.
signal_emitv(params, signal_lookup(info.name, gtype),
detail and quark_from_string(detail) or 0, retval)
-- Unmarshal results.
return call_info:post_call(params, retval, marshalling_params)
end

@psychon
Copy link
Collaborator

psychon commented Mar 8, 2022

Yup, that code in pre_call and emit_signal is what I stared at when I wrote the above. I think the pre_call just creates the array with GValue instances that I created above.

A random guess would be that "return a value from a signal" is just always broken in lgi (and retval in the code you linked to would be nil in that case and possibly the whole issue disappears), but I am really not sure.

Do you happen to have some knowledge about e.g. the Gtk API? Is there some other signal returning a value? Possibly one where gobject-introspection information is available, so that one could "just" use LGI without much extra magic?
I would expect that signal to cause the same kind of problems, but it would still feel like a confirmation...

@HumblePresent
Copy link
Author

Yeah I have wondered if there are other API's that have return values from signals that we could try to use. I'm not familiar with any off the top of my head but I'll let you know if I find any.

@necauqua
Copy link

necauqua commented Sep 2, 2022

Hello, I was trying to do this exact thing (interfacing with wireplumber from awesome) and this thread was an interesting read - especially at the point you had the same code I had, I spent some time to achieve that before finding this :)

The only thing I can add is that I simply did this

local wtf = GObject.signal_emitv(params, signal_id, 0, retval)
getmetatable(wtf).__gc = nil

to avoid those free() errors (not sure if we're leaking memory here tho), now the only hard roadblock is the attempt to steal record ownership from unowned rec followed by a segfault.

The other thing is that using mixer and default-nodes plugins is a convenience and technically it should be possible to reimplement them in lua with lgi - without having to emit signals that return a value - although this promises to be a huge pita ¯\_(ツ)_/¯

edit:
Well, the main thing you need to interface with it directly (instead of the cringe path of calling shell commands) for is the feedback whenever something else changes the volume so you can update the widget without doing polling - and this kind of works, yay:

Wp.Plugin.find(core, 'default-nodes-api'):activate(Wp.PluginFeatures.ENABLED, nil, function(self, res)
    if not self:activate_finish(res) then
        print("FAILED to activate plugin: " .. self.name)
    end

    GObject.signal_connect_closure(self, 'changed', GObject.Closure(function()
        -- here we don't get anything besided the actual event of the default sink changing
        print('default_nodes changed!')
    end), true)
end)

Wp.Plugin.find(core, 'mixer-api'):activate(Wp.PluginFeatures.ENABLED, nil, function(self, res)
    if not self:activate_finish(res) then
        print("FAILED to activate plugin: " .. self.name)
    end

    GObject.signal_connect_closure(self, 'changed', GObject.Closure(function(...)
        print('mixer changed!', ...) -- we even get the id of the node that changed here
    end), true)
end)

@HumblePresent
Copy link
Author

Thanks for looking into this a bit more. I did consider reimplementing the mixer and default-nodes plugins in lua but it looked complicated based on their C source code and I worried that having to process all that info within the context of the Awesome main loop would slow things down or cause lag.

@necauqua
Copy link

necauqua commented Sep 5, 2022

nah I actually tried and got stuck at one point since they really like varargs that are uncallable (it seems) with lgi

The only thing, I can kind of super-hacky extract is the value of mute by node-id that you get in mixer.changed callback.

hack.lua
local object_manager = Wp.ObjectManager.new(core)
object_manager:add_interest_full(Wp.ObjectInterest.new_type(Wp.Node))

object_manager:request_object_features(Wp.GlobalProxy,
    Wp.ProxyFeatures.PROXY_FEATURE_BOUND |
    Wp.ProxyFeatures.PIPEWIRE_OBJECT_FEATURE_INFO |
    Wp.ProxyFeatures.PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS
)

core:install_object_manager(object_manager)

local objects = {}

-- here the signal syntax works totally fine as those signals are present in the typelib, eh
function object_manager.on_object_added(_, object)
    objects[object:get_properties():get('object.id')] = object
end

function object_manager.on_object_removed(_, object)
    objects[object:get_properties():get('object.id')] = nil
end

--- snip: following code is when we have an activated mixer plugin instance

GObject.signal_connect_closure(mixer, 'changed', GObject.Closure(function(_, node)
    local object = objects[tostring(math.floor(node))]

    print('process changed their volume/mute:', object:get_properties():get('application.process.id'))

    Gio.Async.start(function()

        local mute = nil

        -- the enum_params(_sync) does not work, no idea why
        -- it says 'returns null when nothing was cached'
        -- but we more or less repeat the mixer C code here 🤷
        -- and C calls the sync variant and it works fine
        local prop_iter = object:async_enum_params('Props')

        -- so we get the first (and only) prop object and iterate over its 'fields'
        -- in C code they iterate but we have no way of knowing
        -- if the boolean in the prop object is 'mute' so we just grab
        -- the first-first one (instead of the first 'mute' one like C does)
        local iter = prop_iter:next().value:new_iterator()
        local item = iter:next()
        while item do
            local b = item.value:get_boolean()
            if b ~= nil then -- just the first boolean, as I've said
                mute = b
                break
            end
            item = iter:next()
        end

        -- to get something BY NAME they ONLY have a vararg getter which gets multiple things
        -- guess it looks fine for the C caller, but we cannot use that sadly
        print('mute', mute)
    end)()

end), true)

And since there is no wpctl get-mute I am actually using this)

tl;dr; of this entire issue - it consists of two parts:

  • first is that Wp does not have plugin signals in the typelib (you can see that in the gir) so the nice lgi syntax for signal handling does not work
  • if it did work (or we did everything the syntax does directly, as you guys did above) - the argument for emitting a signal with a return is inout which lgi does not seem to handle properly

Actually one more thing I also just tried is this

local retval = require 'lgi.core' .record.new(Value, nil, 1, true)
retval.gtype = GObject.Type.UINT

which makes a GValue that does not log the attempt to steal record ownership from unowned rec thing - but it still just sigservs inside signal_emitv ¯\_(ツ)_/¯

@Elv13
Copy link

Elv13 commented Sep 5, 2022

Some possible, untested, paths forward

Keep in mind you can use C code modules in Awesome. The drawback if that now you get to compile part of your config, which makes it hard to redistribute. It also let you do real multi threading, so that's a win if there are worries that it will slow down Awesome too much. Also note that if it's done (partially) in C and is actually just a normal native Lua module, I would consider merging it in Awesome itself. Sound support has been a really frequent feature request over the years. Since PipeWire/WirePlumber finally seem like an actual long term solution, making it an optional dependency and give users what they want outweigh the portability and out-of-scope concerns IMHO.That would solve the issue of making configs using it hard to distribute.

According to this link, callable.c would need to ffi_prep_cif_var, but according to gobject introspection doc, it doesn't seem it's something that supported well. But GI newest release seems to have some improvements toward this. This also means it's too new for this scanner to be in most distros, then really, really too new to actually make those types usable. So that's a dead end.

Implementing the whole function using Raw LibFFI code is also an option, but that works only for luajit or with the 3rd party libffi module for Rio Lua.

@HumblePresent
Copy link
Author

I agree, implementing some of this functionality in C is the only real way forward right now.

Interestingly, WirePlumber actually has a Lua API based on GI that abstracts some of the lower level details of the library and has an easy to use interface for loading plugins and emitting signals. Unfortunately, it can only be used from sandboxed environment with special behavior and a subset of the standard libraries. Still, the C code that the Lua API is based on could be a good starting point for creating a standalone Lua module. I myself have not delved into the Lua library C API but could be interesting.

As far as redistribution could it become something like this library that uses LuaRocks for distribution?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants