[sldev] Plugin work so far

Soft Noel soft at softnoel.org
Sat Feb 24 17:54:57 PST 2007


110% agreed that some big questions are unanswered. I dumped my progress
so far on the list to try to encourage continued discussion. Several
people have complained about the talk:code ratio, suggesting it isn't
being written. I *am* writing code, and the discussions *are* useful.

Please please discuss the UI API specifically. I'm still hoping for a way
to expose this without a hundred individual accessors, and without having
the plugin interface becoming incompatible with every last UI system
change. If we can tackle the problem there, the same concepts will be
useful with other APIs.

When I get to the UI stuff, I'm going brute force on accessors to support
versioned interfaces, which will mean a lot of work on my part. If a nice,
clean alternative is found on the list, it could save many evenings!


On Sat, February 24, 2007 5:22 pm, Tim Shephard wrote:
> I think the immediate and key challenges in any plugin solution are:
>
>   1. how do UI widgets get instantiated?
>   2. how do we hook into the message protocol?
>
> I think the answer to the second problem isn't that mysterious (they
> probably all involve simple hooks) but there are different approaches
> for 1.
>
> I've detailed them elsewhere, but a couple of them are:
>
> a) Inheritance across DLLS.   Fast to develop, but stability could be an
> issue
> b) NPAPI style plugin - seperate thread with a plugin widget loop?
> callbacks from a proxy llFloater object?
>
> I believe these are the critical issues because they will most impact
> plugin developers.
>
> Issues such as loading, initialization, destruction, and specification
> are important, however the time to refactor our code to match those
> contracts will probably be minor compared to reworking all the UI /
> Communication code.
>
>
>
>
>
>
> On 2/24/07, Soft Noel <soft at softnoel.org> wrote:
>> I wish the plugin discussions would continue without people worrying
>> that
>> no code is being written. I've been working, and the discussions have
>> been
>> helpful. I'll go over what I've got so far and where I'm going so people
>> can make comments on the broad strategy, or at least have confidence
>> that
>> we're not wasting time.
>>
>> Currently my work is Mac-only, though I hope to get my hands on a
>> Windows
>> machine this weekend so I can do the DLL work. I'll drop a version on
>> the
>> list after I get Windows and Mac both working to my satisfaction, but
>> before Linux. Linux should be easy once Mac is 100%. Sadly, my only
>> Debian
>> install within viewing distance is on a G4 mini, so it's useless here.
>> :(
>>
>> I'm not averse to purging or rewriting big chunks of this after I drop
>> my
>> first release if it does things we don't want, or if others suggest
>> better
>> ways of doing things.
>>
>>
>> Here's what a very basic plugin looks like. It's (on mac) a dylib linked
>> against the static lib libPluginBase:
>>
>>
>> ### start HelloWorld.cpp ###
>>
>> #include "linden_plugin_common.h"
>>
>> // Find extra interface types in llpluginterfaceshared.h
>> PlugInterfaceType interfaces_requested[] =
>> {
>>         PI_TYPE_IO,
>>
>>         PI_TYPE_END
>> };
>>
>> // plugin name, your plugin version, SL name, plugin homepage, plugin
>> flags, interfaces requested
>> PLUGIN_DECLARE( "Hello Printer", "0.01", "Soft Noel", "http://foo.org/",
>> PLUGIN_FLAGS_DEFAULT, interfaces_requested );
>>
>>
>> bool PluginStartup()
>> {
>>         pIO->LogPrint( "Hello world." );
>>
>>         return true;
>> }
>>
>>
>> void PluginShutdown( bool crash )
>> {
>>         if( !crash )
>>                 pIO->LogPrint( "Goodbye world." );
>> }
>>
>> ### end HelloWorld.cpp ###
>>
>> These are the interesting things here:
>>
>> interfaces_requested is a taglist of enums representing interfaces we
>> need. These would be things like IO, UI, SYSTEM. END terminates the
>> list.
>>
>> PLUGIN_DECLARE tells the library that "Hello Printer" is the name of
>> this
>> plugin, "0.01" is our version number, "Soft Noel" is the author, and
>> "http://foo.org" is where the user can go browse for updates. All of the
>> information so far will (later) be displayed in a floater in an SL
>> plugin
>> manager. For now, I hard-load plugins unconditionally. The flags will be
>> used for identifying plugins that should default to being disabled in
>> certain circumstances, for example when an author wants to send out a
>> plugin he prefers to be used on a beta grid for now.
>>
>> Everything in PLUGIN_DECLARE gets turned into an accessor function so
>> that
>> the viewer can inspect a plugin's requirements before deciding whether
>> to
>> activate it. In addition, PLUGIN_DECLARE creates an accessor with the
>> version of libPluginBase in use, which is used in negotiating an
>> interface
>> bundle. (More on that in a bit.)
>>
>> PluginStartup gets called when a plugin is activated. By this point, all
>> interfaces have already been acquired. No plugin developer code is ever
>> called before this point.
>>
>> PluginShutdown is called when a plugin is terminated, either politely by
>> (later) the plugin management floater or viewer exit, or rudely by crash
>> protection. Ideally, interface functions will scrutinize input for
>> dangerous data/behaviors, print a warning to the log, and terminate a
>> plugin before it has a chance to crash the viewer. I'm thinking more
>> about
>> plugin developer iteration time here than I am about end users. I'm
>> adamant about quick iteration times for casual developers, or they lose
>> interest fast, so if we can unload/deactivate (Mac doesn't unload
>> dylibs,
>> just abandons them!) their plugin without booting them from the viewer
>> and
>> sit ready to load their next plugin build, it's a big win.
>>
>> Note that I'm far from married to the pINTERFACENAME->Method() calling
>> convention in the example above. Suggestions about what methods and
>> accessors should look like to plugin developers are welcome.
>>
>>
>> On to the viewer...
>>
>> Currently all new viewer files reside in indra/llplug, and patches
>> outside
>> this directory are minimal. A plug (as in the directory name) is the
>> client side of plugin. Here are the major actors and their roles:
>>
>> LLPlugInterface, base of LLPlugInterfaceIO, LLPlugInterfaceUI, etc
>>
>> The LLPlugInterfaceIO was that pIO we saw in the plugin. Each
>> LLPlugInterface derivative implements a subset of the overall available
>> API. Originally I was going to put it all in one glom, but after a
>> discussion with Rob Linden about the specialized interfaces in Real's
>> plugin SDK, I came to realize that parceling this out would be helpful.
>> The main reason is that some interfaces like a graphic interface will
>> deprecate more quickly than something like an IO interface. If we try to
>> maintain backward compatibility, it will be better if it's not an
>> all-or-nothing affair. (More on that in a bit.)
>>
>> LLPlugInterfaces come from an LLPlugInterfaceFactory and are retained by
>> an LLPlugInterfaceManager. LLPlugInterfaceFactory takes a request for an
>> LLPlugInterface type and a version and returns a compatible interface or
>> declines the request if an interface version is deprecated or newer than
>> the viewer. Interface versioning was the source of some contentious
>> discussion, but it really isn't the headache it sounds like, so I'll
>> prove
>> that out here:
>>
>> ### start llpluginterfacefactory.cpp ###
>>
>> #include "linden_common.h"
>>
>> #include "llplugshared.h"
>> #include "llpluginterfaceio.h"
>> #include "llpluginterfacesystem.h"
>> #include "llpluginterfaceui.h"
>>
>> #include "llpluginterfacefactory.h"
>>
>>
>> typedef LLPlugInterface *(*IFMakerFunc)();
>>
>>
>> struct InterfaceMakerEntry
>> {
>>         const char *min_ver, *max_ver;
>>         PlugInterfaceType type;
>>         IFMakerFunc maker_func;
>>         bool nearly_deprecated;
>> };
>>
>>
>> // C++ has no type variable or name-based construction, so:
>> LLPlugInterface *PIFMakeIO_Head() { return new LLPlugInterfaceIO; }
>> LLPlugInterface *PIFMakeSystem_Head() { return new
>> LLPlugInterfaceSystem; }
>> LLPlugInterface *PIFMakeUI_Head() { return new LLPlugInterfaceUI; }
>>
>>
>> static InterfaceMakerEntry InterfaceMakers[]=
>> {
>>         // Always put newest interfaces at the top. Engine uses the
>> first fit.
>>         { "2007.02.22 00", pluginEngineVersion, PI_TYPE_IO,
>> PIFMakeIO_Head, false },
>>         { "2007.02.22 00", pluginEngineVersion, PI_TYPE_SYSTEM,
>> PIFMakeSystem_Head, false },
>>         { "2007.02.22 00", pluginEngineVersion, PI_TYPE_UI,
>> PIFMakeUI_Head, false },
>>
>>         // Old interfaces on the way out:
>> };
>>
>>
>> static S16 InterfaceMakerFindIndex( const char *version,
>> PlugInterfaceType
>> type )
>> {
>>         for( S16 i = 0; i <
>> sizeof(InterfaceMakers)/sizeof(*InterfaceMakers); i++ )
>>         {
>>                 InterfaceMakerEntry *ime;
>>                 ime = &InterfaceMakers[i];
>>
>>                 if( type != ime->type )
>>                         continue;
>>
>>                 // This is not intuitive. Think of it like this:
>> "v01Bogo", "v00Zippy" -
>> strcmp
>>                 // will get to the third char and return '1'-'0' = 1,
>> meaning the requested
>>                 // version of v00Zippy is too old, as it's "less" than
>> v01Bogo.
>>                 if( strcmp( ime->min_ver, version ) > 0 )
>>                         continue;
>>
>>                 if( strcmp( ime->max_ver, version ) < 0 )
>>                         continue;
>>
>>                 return i;
>>         }
>>
>>         return -1;
>> }
>>
>>
>> static IFMakerFunc InterfaceMakerFind( const char *version,
>> PlugInterfaceType type )
>> {
>>         S16 i;
>>         index = InterfaceMakerFindIndex( version, type );
>>
>>         if( i >= 0 )
>>                 return InterfaceMakers[i].maker_func;
>>
>>         return NULL;
>> }
>>
>>
>> static bool InterfaceMakerIsNearlyDeprecated( const char *version,
>> PlugInterfaceType type )
>> {
>>         S16 index;
>>         index = InterfaceMakerFindIndex( version, type );
>>
>>         if( index >= 0 )
>>                 return InterfaceMakers[index].nearly_deprecated;
>>
>>         return false;
>> }
>>
>>
>> bool LLPlugInterfaceFactory::canCreate( const char *version,
>> PlugInterfaceType type )
>> {
>>         IFMakerFunc ifm;
>>
>>         ifm = InterfaceMakerFind( version, type );
>>
>>         return ifm ? true : false;
>> }
>>
>>
>> LLPlugInterface *LLPlugInterfaceFactory::create( const char *version,
>> PlugInterfaceType type )
>> {
>>         LLPlugInterface *pi = NULL;
>>         IFMakerFunc ifm;
>>
>>         ifm = InterfaceMakerFind( version, type );
>>         if( ifm )
>>                 pi = ifm();
>>
>>         if( pi )
>>         {
>>                 if( InterfaceMakerIsNearlyDeprecated( version, type ) )
>>                         pi->deprecationWarningSet();
>>         }
>>
>>         return pi;
>> }
>>
>> ### end llpluginterfacefactory.cpp ###
>>
>> The main item of interest is that InterfaceMakerEntry. For a given plug
>> PlugInterfaceType, it declares the earliest and latest supported
>> version,
>> and tells how to make that interface. LLPlugInterfaceFactory::create
>> (through helpers) scans for a compatible interface and builds it. Note
>> that there can be multiple entries per PlugInterfaceType, so if
>> PlugInterfaceIO is revised, we can keep an older version of the class
>> around with a slightly different name and everything where older plugins
>> expect it to be. This won't mean a bunch of duplicated code thanks to
>> the
>> PlugInterface compatibility_slave construct. (More on that in a bit.)
>> The
>> deprecation flag is for (later) informing a user via his plugin
>> management
>> panel that his plugin is likely to break in a coming release. These
>> would
>> be set when planning to prune the InterfaceMakers/legacy
>> LLPlugInterfaces.
>>
>> The compatibility_slave construct: When freezing a version of an
>> interface
>> for backward compatibility, we create a version of the interface with an
>> old version number, ie LLPlugInterfaceIOv1 which is derived from
>> LLPlugInterface (not LLPlugInterfaceIO), and copy the old list of
>> virtual
>> functions into the new class. The new class would then create the next
>> NEWER class and stuff it in its compatibility_slave pointer, and all
>> functions would be direct calls to the compatibility_slave except where
>> we
>> need to add/remove function arguments, or otherwise make tweaks to make
>> old-style calls continue to work. We can keep an arbitrary number of
>> compatibility interfaces without further work, as they all simply chain
>> upward through their compatibility slaves. This isn't done with class
>> inheritance as we want to be able to reorder, add, and remove items in
>> newer versions. We can't inherit older classes from newer classes
>> without
>> affecting the old structure. We can't inherit new classes from older
>> classes without losing the ability to remove functions or change data
>> formats.
>>
>> The LLPlugInterfaces are passed to the client in an LLPlugBundle. The
>> bundle is never seen by plugin developers, only libPluginBase. The
>> plugin
>> developer just sees all those LLPlugInterfaces. LLPlugBundle comes from
>> LLPlugBundleManager/LLPlugBundleFactory. The LLPlugBundle is an old
>> fashioned struct, which contains LLPlugInterface pointers for all
>> possible
>> LLPlugInterface types, ex:
>>
>> struct LLPlugBundle
>> {
>>         LLPlugBundle()
>>         {
>>                 memset( this, 0, sizeof( *this ) );
>>         }
>>
>>         char *version;
>>
>>     // 1. Don't re-order
>>         // 2. New interfaces must always be added to the BOTTOM.
>>         //
>>         // This struct may get passed to a plugin anticipating an older
>>         // version of the struct.
>>
>>         // These are deliberately anonymous/downcast because we'll stuff
>>         // combinations of versions in to satisfy older interface
>> version
>>         // requests.
>>
>>         LLPlugInterface *plug_io;
>>         LLPlugInterface *plug_sys;
>>         LLPlugInterface *plug_ui;
>> };
>>
>>
>> Jumping back up a bit, we've got our LLPlugBundle with LLPlugInterfaces
>> getting passed to the plugin and turned into globals. Where this all
>> happens is in an LLPlug, via LLPlugManager/LLPlugFactory. An LLPlug is
>> created with the path of the corresponding plugin dylib as an argument.
>> Straight away, LLPlugin::load() dlopen()s the plugin with RTLD_LAZY
>> binding, and grabs its version and user displayable information. From
>> this
>> point on, LLPlugBundleFactory can tell us whether it will be able to
>> create a compatible set of LLInterfaces, and we can decide if we want to
>> LLPlugin::enable(), which acquires the interfaces and tells
>> libPluginBase
>> to set things up and call the plugin developer's PluginStartup().
>>
>> Of note, each LLPlug has a unique serial number that will be attached to
>> and used for cleaning up orphaned resources like floaters, menu entries,
>> and hooks. And this brings us to hooks.
>>
>> I'm in the middle of a rework on hooks right now after a discussion at
>> Rob
>> Linden's office. Originally, I was having the plugin author create a
>> class
>> implementing all possible hooks, just as a hack to get things working.
>> This isn't maintainable of course, and I'm waffling between two
>> approaches. One is a class like the above, but specialized for each API
>> subset. The other is moving to a subscription model similar to what
>> Bushing proposed. You can see his sample at
>> https://wiki.secondlife.com/wiki/Hook_example_code ... the main
>> differences would be that I'd want to work with templated versions based
>> on parameter structures to avoid anonymous data, and I'd have an
>> instance
>> of the class per hookable code point rather than trying to keep it
>> generic
>> like the above example.
>>
>>
>> I'll hand off the above soon, then everyone can start nitpicking about
>> the
>> implementation and I'll happily iterate to make this shiny. I'm hoping
>> people will step in at that point to start filling out the
>> LLPlugInterfaces with a useful set of API calls and hooks. I want to get
>> Kelly's suggested data floater example going, then convert Dale's
>> scanner
>> early on, so my own API calls will revolve around those. If you have a
>> pet
>> project, now's a good time to start proposing APIs.
>>
>> There are other useful areas for ongoing discussion if you want to
>> continue to help me. One that I'm coming up against when we start adding
>> plugin-driven floaters is the question of where we want to install
>> plugins
>> and supplemental data. My thinking is something like this:
>>
>> SLDATADIR = ~/Library/Application Support/SecondLife (or Windows/Linux
>> equivalent)
>> SLPLUGDIR = $SLDATADIR/plugins
>>
>> SLPLUGDIR would contain all the plugin dylibs, DLLs, etc
>>
>> Optional folders for data would use the the plugin's self-provided name,
>> ex:
>>   SLPLUGDIR/Hello World/hellotext.xml
>>
>> For simplicity, we could give plugin developers a function that opened
>> files with this path.
>>
>> Similarly, for dynamic data, do we want functions to facilitate creating
>> and accessing per-user-perplugin data directories like this?
>> SLPLUGDATADIR = SLDATADIR/$SLNAME/$PLUGINNAME/settings.xml ?
>>
>> Another coming issue is that I'd like to avoid doing too much UI work
>> myself. If someone's eager to create the plugin management floater xml
>> and
>> related dialogs and own future changes, I'd be grateful. What the
>> floater
>> should look like and what dialogs it needs would be another good area
>> for
>> discussion.
>>
>>
>> _______________________________________________
>> Click here to unsubscribe or manage your list subscription:
>> /index.html
>>
>




More information about the SLDev mailing list