[Home] [Downloads] [Search] [Help/forum]

Gammon Forum

See www.mushclient.com/spam for dealing with forum spam. Please read the MUSHclient FAQ!

[Folder]  Entire forum
-> [Folder]  MUSHclient
. -> [Folder]  Lua
. . -> [Subject]  New feature in version 4.55 - extended CallPlugin syntax
Home  |  Users  |  Search  |  FAQ
Username:
Register forum user name
Password:
Forgotten password?

New feature in version 4.55 - extended CallPlugin syntax

It is now over 60 days since the last post. This thread is closed.     [Refresh] Refresh page


Pages: 1 2  3  

Posted by Nick Gammon   Australia  (21,322 posts)  [Biography] bio   Forum Administrator
Date Wed 28 Jul 2010 04:47 AM (UTC)

Amended on Wed 28 Jul 2010 05:06 AM (UTC) by Nick Gammon

Message
Version 4.55 of MUSHclient offers an extended version of the CallPlugin syntax, for Lua-to-Lua calls only.

First, some background ...

Recently MUSHclient plugins have become more sophisticated, as authors have been implementing miniwindows, telnet negotiation, mappers, and caching. From time to time, plugin authors want to share information from one plugin to another.

Up until now, there have been two main methods of doing this:


  • BroadcastPlugin
  • CallPlugin


Both of these let plugins pass information from one to another. In the case of BroadcastPlugin a "broadcast" is made, which all plugins can pick up. In the case of CallPlugin, a specific plugin is targeted.

However neither call allows information to be returned to the caller, which makes them less useful for situations like "give me the current mob's name", or "look up a cached item on the database and give me its value".

Up until now, this has been achieved reasonably well by letting the called plugin set a variable, which the calling plugin can query, however this is a bit cumbersome. Twisol has developed a PPI module (Plugin-to-Plugin Interface) but even that suffers a bit from being a bit unwieldy, in that it has to still set plugin variables, albeit in the background.

The old CallPlugin syntax looked like this:


result_code = CallPlugin (plugin_id, function_name, argument)


(Where result_code was 0 for success, and the argument was a single string value).

For example:


result_code = CallPlugin ("80cc18937a2aca27079567f0", "LogIt", "Data to be logged")


The new syntax (which is backwards compatible with the old) looks like this:



result_code [ , val1, val2, val3 ... ] = CallPlugin (plugin_id, function_name, arg1 [, arg2, arg3, arg4 ... ] )


For example:


rc, a, b, c, d = CallPlugin ("80cc18937a2aca27079567f0", "show_message", "red", "green", "message", 42)


An example of the implementation of show_message in the target plugin would be:


function show_message (fore_colour, back_colour, message, count)

  for i = 1, count do
    ColourNote (fore_colour, back_colour, message)
  end -- for

  return 1, 2, 3, 4
end -- function show_message


The calling plugin would call it like this:


rc, a, b, c, d = CallPlugin ("80cc18937a2aca27079567f0", "show_message", "red", "green", "message", 42)
print ("rc =", rc) --> rc = 0
print (a, b, c, d) --> 1 2 3 4


The backwards compatibility exists because the first result from CallPlugin is still the return code, which is still 0 in the case of no error.

If the return code is in fact 0 (otherwise defined as error_code.eOK) then any values returned by the called function are then returned as additional return values (which Lua lets you do, unlike most languages). Also the case of sending a single string argument (which the old CallPlugin did) is now a special case of sending multiple arguments.

If the return code is not zero, then it will be one of the return codes defined for CallPlugin. In addition to that, a second value will be returned which is a string explanation of the error.

For example:


rc, a, b, c, d = CallPlugin ("80cc18937a2aca27079567f0", "foo", "bar")
print ("rc =", rc)  --> rc = 30036
print (a, b, c, d)  --> No function 'foo' in plugin 'Test_Plugin' (80cc18937a2aca27079567f0) nil nil nil


This lets you find out in more detail why a CallPlugin call failed.

In order for all this to work:


  • Both the caller and called plugin must be written in Lua (the caller does not have to be a plugin)

  • You can pass between zero and any number of arguments to the requested function

  • The passed arguments can be one of the following types only:


    • nil
    • boolean
    • number
    • string


  • In particular, the following types are not supported and will result in an error code being returned:


    • function
    • userdata
    • thread
    • table


    Functions are specific to a Lua script space (for example, they may be implemented in a DLL) and are not portable across script spaces.

    Userdata is likely to represent non-portable data, such as open files.

    Threads (coroutines) will not be portable.

    Tables are not only likely to be deep (eg, the entire _G table), plus are likely to contain functions, userdata and threads.

    If you wish to pass a table (eg. a player's inventory) then this can be accomplished by using the serialize library to convert a table into a string.

  • It is permissible to pass no arguments at all. eg.

    
    CallPlugin ("80cc18937a2aca27079567f0", "my_func")
    


  • It is possible for the caller and callee to be in the same plugin (however there is not much point to doing that). If you do that, the restrictions on the types of data that may be passed and returned do not apply.


The return code(s) from CallPlugin are:

On failure:


  • A status code, as before, which will be non-zero

  • A string indicating the reason in human-readable form

  • In the case of a runtime error in the called function, a second string indicating the nature of the error


On success:


  • A status code, as before, which will be the number zero

  • Zero or more values as returned by the called function.

  • The returned values can be one of the following types only:


    • nil
    • boolean
    • number
    • string


    Thus it would be an error for the called function to return (for example) string.match as that is a function.

    The reason for this is the same reason as the restrictions on the parameter passing - the parameters and returned values have to be copied from one Lua script space to another, and only certain data types lend themselves to such a copy operation.


Because of the backwards compatibility, no changes should be required to existing plugins or code. Also if you do not use Lua, then you can continue to use existing methods, such as setting plugin variables, or using world.Execute to cause aliases to fire in other plugins.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #1 on Wed 28 Jul 2010 05:17 AM (UTC)

Amended on Wed 28 Jul 2010 05:21 AM (UTC) by Twisol

Message
Excellent! I believe PPI still has a place as a plugin-watcher, though. You can't use CallPlugin until the plugin you want to communicate with is available. And CallPlugin is still a little un-pretty just by virtue of passing the plugin's ID every time.

I also can't help but disagree with this:
Nick Gammon said:
*It is possible for the caller and callee to be in the same plugin (however there is not much point to doing that). If you do that, the restrictions on the types of data that may be passed and returned do not apply.


See bolded. I don't like the inconsistency you're introducing. It may make sense looking at it from the implementation of the function, but to user code it can be confusing. You expect it to have a specific contract, and the function even looks like you're calling out to another plugin. So if you break a plugin into two plugins and it relied on this behavior, you have a problem.

Not to mention anyone using CallPlugin for this kind of thing can only be new, and we should try even harder not to encourage this kind of thing. After all:

CallPlugin("f59a5eb513655f255efc07fe", "foofunc", {1, 2, 3, 4})
-- v.s.
foofunc({1, 2, 3, 4})


It's easier and clearer to use the second form.


I'll pull down the changes and see about refitting PPI. Thanks for adding this support!

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

Posted by WillFa   USA  (525 posts)  [Biography] bio
Date Reply #2 on Wed 28 Jul 2010 05:26 AM (UTC)

Amended on Wed 28 Jul 2010 05:31 AM (UTC) by WillFa

Message
In theory would this work if we wanted the result list packed into a table?



function CallPlugin (...)
  local function pack(...)
   return {...}
  end

  local t = pack(world.CallPlugin(...))

  if table.remove(t, 1) == 0 then
     return t 
  else
 -- error () -- handler...
  end
end




ret = CallPlugin("0000aaaabbbbcccc", "foofunc", 1, 1, 3, 4, 7)


[Go to top] top

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #3 on Wed 28 Jul 2010 05:36 AM (UTC)
Message
I don't see anything wrong with it, so my guess is yes.

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

Posted by Nick Gammon   Australia  (21,322 posts)  [Biography] bio   Forum Administrator
Date Reply #4 on Wed 28 Jul 2010 05:46 AM (UTC)
Message
It doesn't have to be that complex:



t = { CallPlugin ("80cc18937a2aca27079567f0", "show_message", "red", "green", "message", 42) }

tprint (t)



If a function returns multiple values and is put inside a table constructor, its values get turned into table items.

In other words, you don't need the helper function (unless you want to delete item 1 from the table, which you can do now anyway).


t = { CallPlugin ("80cc18937a2aca27079567f0", "show_message", "red", "green", "message", 42) }

if table.remove (t, 1) == error_code.eOK then
  -- t contains results
else
  error (t [1] )
end

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #5 on Wed 28 Jul 2010 05:49 AM (UTC)

Amended on Wed 28 Jul 2010 05:51 AM (UTC) by Twisol

Message
Or:

t = {assert(CallPlugin ("80cc18937a2aca27079567f0", "show_message", "red", "green", "message", 42))}


Ugly enough to warrant a wrapper, perhaps:

do
  local orig = CallPlugin
  function CallPlugin(...)
    return {assert(orig(...))}
  end
end

t = CallPlugin("80cc18937a2aca27079567f0", "show_message", "red", "green", "message", 42))

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

Posted by Nick Gammon   Australia  (21,322 posts)  [Biography] bio   Forum Administrator
Date Reply #6 on Wed 28 Jul 2010 05:52 AM (UTC)

Amended on Wed 28 Jul 2010 05:53 AM (UTC) by Nick Gammon

Message
Anyone that does this:


CallPlugin (GetPluginID (), "foofunc", {1, 2, 3, 4})


Rather than:


foofunc({1, 2, 3, 4})


... deserves all the pain he gets, honestly.

And why stop there?


CallPlugin (GetPluginID (), "CallPlugin", GetPluginID (), "foofunc", {1, 2, 3, 4})


... and so on.

The fact is, that with only one Lua script space, I can't just copy from L1 to L2 (as I do with two script spaces) because then you get all the parameters twice, and it is a lot of fiddling around (there is only one stack you see).

I just put that caveat in the documentation in case someone tried it.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by WillFa   USA  (525 posts)  [Biography] bio
Date Reply #7 on Wed 28 Jul 2010 05:55 AM (UTC)
Message
Twisol said:

Or:

t = {assert(CallPlugin ("80cc18937a2aca27079567f0", "show_message", "red", "green", "message", 42))}


Ugly enough to warrant a wrapper, perhaps:

do
  local orig = CallPlugin
  function CallPlugin(...)
    return {assert(orig(...))}
  end
end

t = CallPlugin("80cc18937a2aca27079567f0", "show_message", "red", "green", "message", 42))



assert is always true tho, because we're getting C-style error reporting, not a nil + error code Lua-style.
[Go to top] top

Posted by Nick Gammon   Australia  (21,322 posts)  [Biography] bio   Forum Administrator
Date Reply #8 on Wed 28 Jul 2010 05:56 AM (UTC)
Message
Twisol said:

Or:

t = {assert(CallPlugin ("80cc18937a2aca27079567f0", "show_message", "red", "green", "message", 42))}




You can't assert, it returns 0 on success, not nil on failure, for backwards compatibility. You would need a wrapper that asserts on (return_code == 0).

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Worstje   Netherlands  (899 posts)  [Biography] bio
Date Reply #9 on Wed 28 Jul 2010 06:00 AM (UTC)

Amended on Wed 28 Jul 2010 06:01 AM (UTC) by Worstje

Message
How difficult would it be to support Lua -> Non-Lua calls? If I get it right, we are only talking primitive types, which should be supported through the WSH APIs without much more trouble. Likewise, returning the value can be similarly solved by having a function return an array (list) rather than an int, and if that is the case unpack said array to end up with the Lua result.

Non-Lua -> Lua might be a bit harder, but even then I believe it possible without necessarily breaking compatibility.

Create a CallPluginEx(pid, func, params) function for non-Lua languages, the params parameter being typed as an array. Same primitive types stuff applies, of course. The result can be an array as well, an empty array meaning a nil-like value was returned.


Now, I even imagine it quite possible to have support for nested non-cyclic tables in Lua-Lua, and with a non-Lua plugin being involved that being limited to nested non-cyclic arrays. But that's nice stuff for the future if the basics are worked out right.
[Go to top] top

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #10 on Wed 28 Jul 2010 06:02 AM (UTC)
Message
Ah, right. Well then:

do
  local orig = CallPlugin
  function CallPlugin(...)
    local results = {assert(orig(...))}
    if table.remove(results, 1) ~= 0 then
      return error(results[1]) -- return is idiomatic
    end
    return results
  end
end

t = CallPlugin("80cc18937a2aca27079567f0", "show_message", "red", "green", "message", 42))



Also, you can do this to "import" a function to use a more natural interface:

function ImportPlugin(id, name)
  return function(...)
    local results = {assert(CallPlugin(...))}
    if table.remove(results, 1) ~= 0 then
      return error(results[1]) -- return is idiomatic
    end
    return unpack(results)
  end
end

Foo = ImportPlugin("80cc18937a2aca27079567f0", "Foo")

Foo("Hello, world!")


Now -that- I like.

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[Go to top] top

Posted by WillFa   USA  (525 posts)  [Biography] bio
Date Reply #11 on Wed 28 Jul 2010 06:04 AM (UTC)
Message
Nick Gammon said:

Anyone that does this:


CallPlugin (GetPluginID (), "foofunc", {1, 2, 3, 4})

... deserves all the pain he gets, honestly.

And why stop there?


CallPlugin (GetPluginID (), "CallPlugin", GetPluginID (), "foofunc", {1, 2, 3, 4})



Obfuscated Lua!


function wtf(...)
   return lpeg.Ct(lpeg.C(lpeg.P(1-lpeg.P(","))^1) * (lpeg.P(",") * lpeg.C(lpeg.P(1-lpeg.P(","))^1))^0 + lpeg.P(-1)):match(table.concat({CallPlugin(...)},","))
end


(You wanted LPEG...)
[Go to top] top

Posted by Nick Gammon   Australia  (21,322 posts)  [Biography] bio   Forum Administrator
Date Reply #12 on Wed 28 Jul 2010 06:09 AM (UTC)
Message
Twisol said:

Ah, right. Well then:


...
 local results = {assert(orig(...))}
...




You gotta lose the assert. The first return value is never nil, nor false.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (21,322 posts)  [Biography] bio   Forum Administrator
Date Reply #13 on Wed 28 Jul 2010 06:11 AM (UTC)
Message
WillFa said:


return lpeg.Ct(lpeg.C(lpeg.P(1-lpeg.P(","))^1) * (lpeg.P(",") * lpeg.C(lpeg.P(1-lpeg.P(","))^1))^0 + lpeg.P(-1)):match(table.concat({CallPlugin(...)},","))



My eyes! ....

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (21,322 posts)  [Biography] bio   Forum Administrator
Date Reply #14 on Wed 28 Jul 2010 06:14 AM (UTC)
Message
Worstje said:

How difficult would it be to support Lua -> Non-Lua calls?


Once defined, the prototype for a OLE function is not supposed to change. So I can't change it from returning a long to returning a table. Or accepting a table instead of a string.

Maybe CallPluginEx could be defined in a different way. Maybe we encourage everyone to use Lua.

As I said, you don't *have* to use the new method. The old one survived for quite a few years. And you have Twisol's language-neutral PPI as a fallback.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


19,270 views.

This is page 1, subject is 3 pages long: 1 2  3  [Next page]

It is now over 60 days since the last post. This thread is closed.     [Refresh] Refresh page

Go to topic:           Search the forum


[Go to top] top

Quick links: MUSHclient. MUSHclient help. Forum shortcuts. Posting templates. Lua modules. Lua documentation.

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.

[Home]


Written by Nick Gammon - 5K   profile for Nick Gammon on Stack Exchange, a network of free, community-driven Q&A sites   Marriage equality

Comments to: Gammon Software support
[RH click to get RSS URL] Forum RSS feed ( https://gammon.com.au/rss/forum.xml )

[Best viewed with any browser - 2K]    [Hosted at FutureQuest]