Loading a utility table from another plugin

Posted by Twisol on Tue 21 Jul 2009 05:54 AM — 7 posts, 31,317 views.

USA #0
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)
Amended on Tue 21 Jul 2009 06:02 AM by Twisol
USA #1
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)
Amended on Sun 26 Jul 2009 06:40 PM by Twisol
USA #2
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().
Amended on Sun 26 Jul 2009 06:41 PM by Twisol
USA #3
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. >_>
USA #4
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. ;)

Amended on Wed 12 Aug 2009 09:50 PM by WillFa
Australia Forum Administrator #5
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.
USA #6
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! :)