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


Register forum user name Search FAQ

Gammon Forum

Notice: Any messages purporting to come from this site telling you that your password has expired, or that you need to "verify" your details, making threats, or asking for money, are spam. We do not email users with any such messages. If you have lost your password you can obtain a new one by using the password reset link.
[Folder]  Entire forum
-> [Folder]  MUSHclient
. -> [Folder]  Tips and tricks
. . -> [Subject]  Loading a utility table from another plugin

Loading a utility table from another plugin

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


Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Tue 21 Jul 2009 05:54 AM (UTC)

Amended on Tue 21 Jul 2009 06:02 AM (UTC) by Twisol

Message
A fair few of my plugins have wanted for an easy to use, native-looking interface to expose to other plugins. This is already possible by way of the CallPlugin() function, but to my eyes it's rather ugly, and doesn't immediately show your intent. I'd much rather call atcp.Send("ping", 0) than CallPlugin("asdfghjkl", "SendAtcp", "ping 0").

Sure, you can just define wrappers yourself. But then every plugin that wants to interface with the main plugin has to have that bulk, and that seems pretty inefficient. Plus, since plugins can load in potentially any order, yours may load before the main plugin has been initialized.

This script fixes the first problem, and alleviates the second. The loader is passed the ID of a plugin, and it returns a proxy object. This proxy will load the plugin's public interface when it's accessed in some way, and the process is invisible to the user.

In order to take advantage of this script, the main plugin needs to supply a MUSHclient variable called "Library", containing source code to return a table containing the public interface. Metamethods aren't guaranteed to work properly on the interface, except for __newindex and __index. For example:


<variables>
  <variable name="Library"><![CDATA[
    local atcp = {}
    local ID = "7c08e2961c5e20e5bdbf7fc5"

    atcp.EnableModule = function (...)
      CallPlugin(ID, "EnableModule", table.concat({...}, ","))
    end

    atcp.Send = function (msg)
      CallPlugin(ID, "SendATCP", tostring(msg))
    end

    atcp.Filter = function(id, msg, text)
      if id == ID then
        local sep = text:find("|") or #text+1
        id, msg, text = "ATCP", text:sub(1, sep-1), text:sub(sep+1)
      end

      return id, msg, text
    end

    return atcp
  ]]></variable>
</variables>



All the client plugin needs to do is add two lines:

loader = require("loadplug")
atcp = loader.load("7c08e2961c5e20e5bdbf7fc5")


In this case, I'm requesting the interface for my ATCP plugin. At a later point I can attempt to index into this proxy object. If it succeeds, it's as though I had the proxy object all along. If it doesn't, the script fires an error with a very obvious error message.

One call to loader.load() is all that's required to retrieve any plugin library interface.

Here's the script below:
Quote:

local liblist = {
  libraries = {
    __mode = "k",
  },

  load = function(self, tbl)
    if self.libraries[tbl] == nil then
      local lib = GetPluginVariable(tbl.id, "Library")

      if lib == nil then
        error("[Plugin Library Loader]: Unable to load interface for plugin " .. (tbl.id or "<invalid ID>"), 0)
      end
      self.libraries[tbl] = loadstring(lib)()
    end

    return self.libraries[tbl]
  end,
}
setmetatable(liblist.libraries, liblist.libraries)

local Proxy = {
  __metatable = false,

  __newindex = function(tbl, idx, val)
    liblist:load(tbl)[idx] = val
  end,

  __index = function(tbl, idx)
    return liblist:load(tbl)[idx]
  end,

  new = function(self, id)
    return setmetatable({id = id}, self)
  end,
}


local Loader = {
  __newindex = function()
    error("Please don't modify this table.")
  end,

  load = function(id)
    if type(id) ~= "string" then
      error("Plugin ID must be a string.")
    end

    return Proxy:new(id)
  end,
}
Loader.__index = Loader

-- getmetatable works as expected
-- setmetatable will not work
Loader.__metatable = Loader

return setmetatable({}, Loader)

'Soludra' on Achaea

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

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #1 on Sat 25 Jul 2009 03:49 AM (UTC)

Amended on Sun 26 Jul 2009 06:40 PM (UTC) by Twisol

Message
Here's an updated version of the same script. There's absolutely no difference to either the plugin or its clients, but the code itself has been tightened up a bit. I also wanted to add better metamethod support in the proxy, but there's no way to catch the metamethods before we know what they are in the library. If you want to do something with metamethods, give your PPI (public plugin interface) a function to return another table from a plugin variable. It's guaranteed that the plugin will at least have been loaded by that point, so you don't have to use a proxy.


load_ppi.lua
Quote:

local Proxies = {
  __mode = "k",

  load = function(self, tbl)
    local proxy = self[tbl]
    local lib = GetPluginVariable(proxy.id, "Library")

    if lib == nil then
      error("[LoadPPI]: Unable to load interface for plugin " .. (proxy.id or "<invalid ID>"), 0)
    end

    proxy.library = loadstring(lib)()
    return proxy.library
  end,
}
setmetatable(Proxies, Proxies)

local Proxy = {
  new = function(id)
    local proxy = {
      id = id,
      library = false,

      __metatable = false,

      __newindex = function(tbl, idx, val)
        (Proxies[tbl].library or Proxies:load(tbl))[idx] = val
      end,

      __index = function(tbl, idx)
        return (Proxies[tbl].library or Proxies:load(tbl))[idx]
      end,
    }

    local tbl = setmetatable({}, proxy)
    Proxies[tbl] = proxy
    return tbl
  end,
}

-- Creates new Proxies for a plugin library
local Loader = {
  __metatable = false,

  __newindex = function()
    error("Please don't modify this table.")
  end,

  __index = {
    load = function(id)
      if type(id) ~= "string" then
        error("Plugin ID must be a string.")
      end

      return Proxy.new(id)
    end,
  },
}

-- Returns a new read-only loader.
return setmetatable({}, Loader)

'Soludra' on Achaea

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

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #2 on Sun 26 Jul 2009 05:13 PM (UTC)

Amended on Sun 26 Jul 2009 06:41 PM (UTC) by Twisol

Message
I snuck a change in right before posting, and it killed the script *laughs*. Updated script above, but I forgot to add '.library' in the __index and __newindex metamethods of the proxy.

EDIT: New fix, this time it really does work. :P I made a mistake in Proxies.load().

'Soludra' on Achaea

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

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #3 on Wed 12 Aug 2009 08:18 PM (UTC)
Message
I was really hoping for a little feedback on this. =/ It seems like it would be pretty useful for writing plugins that expose extra functionality but want to keep it simple. I don't particularly care if nobody uses it, but even an "It's not going to work well because of this and this" would be appreciated. >_>

'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 #4 on Wed 12 Aug 2009 09:49 PM (UTC)

Amended on Wed 12 Aug 2009 09:50 PM (UTC) by WillFa

Message
If you have a module and a plugin, why not put everything in the module? (I vaguely recall that atcp has something to do with telnet options, so maybe you need the OnPacketReceived parsing...)

I could see a module that returns a table with metatables to encapsulate the CallPlugin functions, but I think this may be over-engineered. I don't like sticking code in MC variables that you need to loadstring(). Also, a specific module requires a potential user to only learn your object model, instead of your abstraction mechanism and object model.

I think of Plugins as little black boxes of functionality. Libraries and Modules are meant to be building blocks.

With all that said, I think I'm a little guilty of doing the same. On the mud I play, 3K, they implemented their own ATCP/MXP type thing, Portal MIP. I have several plugins that require another one that only does the core message parsing and uses broadcastplugin to notify the others. The core actually passes a serialized table that the others need to loadstring to get the data. :( I guess MXP, ATCP, and MIP are really niche functionality. (More than just mudclients in general!)


Although I can empathize with you about lack of feedback being disheartening. I think Larkin's the only person that uses my InfoBox module. It's like the only code worth using around here is Nick's. ;)

[Go to top] top

Posted by Nick Gammon   Australia  (23,043 posts)  [Biography] bio   Forum Administrator
Date Reply #5 on Wed 12 Aug 2009 11:25 PM (UTC)
Message
Your module looked good to me, I just didn't have a use at the time for cross-plugin communication.

I think the whole thing about needing shared plugins, and the slightly shonky interface between them, is one of the problems with the plugin design. Your post triggered a neuron or two in my brain to think "perhaps there is a better way". Maybe something in plugins that say another plugin (or module perhaps) is a requirement for *this* plugin to be loaded.

Quote:

I think Larkin's the only person that uses my InfoBox module.


The problem as usual with this stuff is keeping it noticed. I added a line the other day to this page:

http://www.gammon.com.au/forum/bbshowpost.php?bbtopic_id=108

That mentions your InfoBox module - particularly as a recent post was asking how to do things like that.

- Nick Gammon

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

Posted by Twisol   USA  (2,257 posts)  [Biography] bio
Date Reply #6 on Thu 13 Aug 2009 08:06 AM (UTC)

Amended on Thu 13 Aug 2009 08:11 AM (UTC) by Twisol

Message
Quote:
If you have a module and a plugin, why not put everything in the module? (I vaguely recall that atcp has something to do with telnet options, so maybe you need the OnPacketReceived parsing...)

I could see a module that returns a table with metatables to encapsulate the CallPlugin functions, but I think this may be over-engineered. I don't like sticking code in MC variables that you need to loadstring(). Also, a specific module requires a potential user to only learn your object model, instead of your abstraction mechanism and object model.

I think of Plugins as little black boxes of functionality. Libraries and Modules are meant to be building blocks.

With all that said, I think I'm a little guilty of doing the same. On the mud I play, 3K, they implemented their own ATCP/MXP type thing, Portal MIP. I have several plugins that require another one that only does the core message parsing and uses broadcastplugin to notify the others. The core actually passes a serialized table that the others need to loadstring to get the data. :( I guess MXP, ATCP, and MIP are really niche functionality. (More than just mudclients in general!)


Although I can empathize with you about lack of feedback being disheartening. I think Larkin's the only person that uses my InfoBox module. It's like the only code worth using around here is Nick's. ;)


Well, plugins are generally "living" entities on their own, whereas modules and other scripts are dead weight that need to be included into another plugin to do any work. Making ATCP a dead module wouldn't work, because only one "thing" can access/strip the ATCP information or the others will be starved. The functionality has to be centralized, with means one living thing that does the work itself, and passes it back to interested parties. I could implement the plugin's interfaces with two files, really, but it would come to the same thing, and then I'd just have two files to lug around.

A call to 'require' is really just a loadstring on a file's contents, isn't it? The point behind the LoadPPI script is to do a 'require' on a variable's contents, is all. The rest of the code just ensures the plugin has loaded before you try to get at its data. And it keeps everything in one nice little file, too.

Any plugin that does work, but wants to be able to interface with other scripts more easily, can utilize the LoadPPI extension. And if a client script doesn't want to use that, so be it - they can always go through the less kind CallPlugin function. This is just an easier, friendlier approach.

Quote:
Your module looked good to me, I just didn't have a use at the time for cross-plugin communication.

I think the whole thing about needing shared plugins, and the slightly shonky interface between them, is one of the problems with the plugin design. Your post triggered a neuron or two in my brain to think "perhaps there is a better way". Maybe something in plugins that say another plugin (or module perhaps) is a requirement for *this* plugin to be loaded.


If you could mark a plugin with dependencies, somehow ensuring that dependencies are loaded before the dependent, that would eliminate the need for LoadPPI entirely, and you could just get the variable and load its contents without having to worry about whether it's accessible yet. That would truly be awesome.


I'd like to point out (to everyone) that, while public plugin interfaces aren't always necessary, there are many times when they can still be very useful.


Thanks for the feedback guys! :)

'Soludra' on Achaea

Blog: http://jonathan.com/
GitHub: http://github.com/Twisol
[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.


21,017 views.

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 HostDash]