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

Plugin-as-a-Library #145

Open
ThistleSifter opened this issue Mar 18, 2022 · 8 comments
Open

Plugin-as-a-Library #145

ThistleSifter opened this issue Mar 18, 2022 · 8 comments
Labels
enhancement New feature or request

Comments

@ThistleSifter
Copy link
Member

I was toying with the idea of writing plugins in such a way so that they can be called by other plugins, aka plugin-as-a-library (not the best name, I know). At the moment this is just an idea and I haven't tried it out, but combined with the state management offered by finenv.RetainLuaState, this could open up some interesting opportunities for interactions between plugins.

I came across a method for checking if a script is included as a library or whether it is the main script.
Using it in a plugin means the following is possible:

src/my_plugin.lua

-- True if this script is included as a library, false if it's the main script
local is_library = pcall(debug.getlocal, 4, 1)

...
-- plugin defining code
...

-- At the end of the file
if is_library then
    -- As library
    return dialog
else
    -- As main script
    dialog:ExecuteModal(nil)
end

If exposing the whole dialog object is not desirable (and I imagine it wouldn't be), we could have a factory for creating wrapping tables:

-- function finalemix.create_wrapper(object, exposed_methods)

if is_library then
    return finalemix.create_wrapper(dialog, {'ExecuteModal', 'MyCustomFunction', 'MyOtherCustomFunction'})
end

And if control over preserving/refeshing state is needed, the plugin could return a create function, which then returns a dialog or a wrapper as desired. Then the calling plugin would be in control of when the plugin-as-a-library is retained or garbage collected.

Possible usage:

src/consuming_plugin.lua

-- my_plugin returns a create function when run as a library
local my_plugin = require('my_plugin')

...
-- plugin defining code
...

-- create a button that invokes my_plugin
-- to save some lines, named controls are taken from the dialog mixin I'm currently working on
dialog:CreateButton(10, 10, 'my_plugin_ctrl')

dialog:RegisterHandleControlEvent(dialog:GetControl('my_plugin_ctrl'), function(ctrl)
    if not dialog.my_plugin then
        dialog.my_plugin = my_plugin()
    end

    dialog.my_plugin.ExecuteModal(dialog)
    -- Additional tasks or testing the return value can be done here as well
end)
@ThistleSifter ThistleSifter added the enhancement New feature or request label Mar 18, 2022
@rpatters1
Copy link
Collaborator

rpatters1 commented Mar 18, 2022

Using the techniques you describe, it should be fairly straightforward to write a script that is both callable by another script as well as directly callable by RGP/JW Lua. I don't see what finenv.RetainLuaState brings to the table. Lua states are managed by RGP (or JW) Lua. For RGP Lua, there is potentially an active Lua state per configured menu option in RGP Lua. (For JW Lua there is one Lua state at a time.)

If you create, for example, a modeless palette that runs any of a number of scripts, the palette and any script it runs share the same Lua state. (Unless RGP Lua is enhanced to somehow allow a script to spin off another script in a separate Lua state. This, perhaps, could be done but currently does not exist.) This sharing means that care must be taken that scripts do not define the same global variable names. Use of global variables is much more likely in a script that retains its Lua state than one that doesn't, because the script's state has to survive across chunks. That implies that it would probably be safer for called scripts not to use finenv.RetainLuaState. The one Lua state would remain active as long as the palette is open, whether you modify RetainLuaState or not.

What you describe is doable. In fact I think @jwink75 may be doing something like that with the Jetstream Controller project. But it will require careful discipline and design.

@Nick-Mazuk
Copy link
Member

How would this be different than simply creating a library function for the underlying plugin?

You could then move the entirety of one plugin into a library function, and call it from any new plugin.

@ThistleSifter
Copy link
Member Author

@rpatters1 Thanks for the clarification about state and global variables. I agree that avoiding global variable naming conflicts would be a must for this to work.

@Nick-Mazuk One difference is keeping related code grouped together in one file rather than spreading it around between files (ie plugindef, code for creating plugin, and code for displaying/running plugin).

Plugins could also control how their scope is used. For example, whether it forces the same state to be reused between all uses or whether it allows a consuming plugin to create a localised copy with its own scope.

I have some ideas about how it could look (who knows, it might still fall flat on its face) but I'll leave playing with it until after I've finished writing mixins.

@Nick-Mazuk
Copy link
Member

@ThistleSifter ah I see. Thanks for the clarification!

+1 on this feature

One other case to consider is what should happen if one of the needed scripts is missing.

@Nick-Mazuk
Copy link
Member

It seems like #287 solves the core issue here of calling one script from another script.

Is there any aspect of this issue that's not addressed by #287? If not, I'm thinking of closing this issue since it looks like it would be superseded by #287.

@rpatters1
Copy link
Collaborator

I'm not sure. #287 allows one script to invoke another. What say, you @ThistleSifter, does #287 cover it?

@ThistleSifter
Copy link
Member Author

It's pretty similar, although not exactly the same. Most plugins have options and some have multiple variations on their functionality. What I was describing was also about being able to call a plugin (or a particular part of a plugin's functionality, depending on what it exposed) with parameters. Return values can also be used to report success, failure, status, etc.

I think I originally had this in mind so that multiple plugins could be strung together either for a workflow thing, to create a singular combined result, or something else. Whatever is desired.


I've just thought of another idea, and that is to separate plugins from their functionality. At the moment, scripts handle most of the 'what', the 'why', and the 'how' of what needs to be done (yes we have library functions, but I'd put these mostly in the category of helper functions). This is getting further into the realms of developing a framework, but splitting these up (perhaps similar to how you might envisage an MVC framework) could also open up more possibilities for both reusing and combining functionality.

@rpatters1
Copy link
Collaborator

FWIW: The FCLuaScriptItem functionality would allow you to modify the prefix for ad-hoc scripts, so effectively you could pass parameters to other scripts. This is not the same a true framework, however.

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

No branches or pull requests

3 participants