Jump to content

Inter-Plugin communication


SPD13

Recommended Posts

What do you consider as the best way to create a communication between two plugins?

My target is to be able to fire a method call on plugin B when i'm inside plugin A code.

My first idea is to use, from plugin A

AssemblyLoader.loadedAssemblies

to get the list of loaded assembly modules. Catch the module B assembly using the name

m.DeclaringType.Assembly.GetName().Name

and use the method

Invoke

to call methods on it.

Is is the safest way to create a functionality in plugin A without altering plugin B ?

Link to comment
Share on other sites

First, can the communication be done via KSPFields somehow? Squad has given us a decent set of tools that include the ability to read/write KSPFields in other mods without having to jump through any other hoops.

Otherwise, yes, the method you outlined is the only way I am aware of to actually call a method in another mod.

D.

Link to comment
Share on other sites

4 hours ago, SPD13 said:

What do you consider as the best way to create a communication between two plugins?

My target is to be able to fire a method call on plugin B when i'm inside plugin A code.

My first idea is to use, from plugin A


AssemblyLoader.loadedAssemblies

to get the list of loaded assembly modules. Catch the module B assembly using the name


m.DeclaringType.Assembly.GetName().Name

and use the method


Invoke

to call methods on it.

Is is the safest way to create a functionality in plugin A without altering plugin B ?

-If- you know that plugin B will always be present, you can add it as a compile-time reference to plugin A and just use its public classes/fields normally, no reflection needed.

However if you don't want to induce a hard-dependency, then as Diazo stated, what you have is about the only way that I've found.

Link to comment
Share on other sites

I happened to get pointed to your scripting module for MechJeb, and then found this post off your profile.  I though I'd mention the method that kOS has taken to recommending for developing kOS "Addons".  The trick I ran into was finding a way to make it so that basic addons didn't need to use reflection or do their own assembly walk (which is how all of our own addons work).  Reflection is a pain to work with, and it's easy to break.  So I opted for recommending defining the KSPAssemblyDependency attribute on the assembly itself, that way if KSP doesn't find the dependency, AssemblyLoader won't load your assembly.  Then kOS uses reflection to create an instance of objects that inherit from a class, or interface, or just have an attribute defined on them.

What I really like about this method is that by carefully defining interfaces in your main assembly, you can easily create a utility to interface with another hard-linked dll without throwing errors.

The big downside to it however is that you need the functionality to be in a stand alone assembly.  So if you wanted to create an addon that interfaced between kOS and MechJeb (oh right, you do :D) you would make a shim assembly that hard links to both, and has the KSP dependency for both, allowing all of your classes within that assembly interact with both kOS and MechJeb directly.  Your class can then inherit from both the Addon type (or other supported type/interface) and a Mechjeb interface, allowing both to access data from your class directly.

It also doesn't help with accessing private/sealed/protected methods/properties/fields.

I'm sure we'd be happy to help you with your interaction with kOS.  Particularly so that we don't break compatibility with your systems with an update.  I'll try to walk through your code tonight to get a feel for what you're trying to do, and see how we can help you interact.  

https://github.com/KSP-KOS/KOS/blob/develop/src/kOS/AddOns/AddonManager.cs

https://github.com/KSP-KOS/KOS/blob/develop/src/kOS.Safe/Utilities/AssemblyWalkAttribute.cs

https://github.com/KSP-KOS/KOS/blob/develop/src/kOS/AddOns/Addon Readme.md

I should add a note to the readme that exactly one instance of the Addon will be created per processor, and will exist until the processor is unloaded.

Link to comment
Share on other sites

Hi @hvacengi,

Thank you for your message and for the details about how to connect MJ and kOS.

Some Background: I created a "scripting" module for MechJeb, intended to automate MechJeb tasks (Ascent guidance, docking autopilot, ...) and basic flight or control tasks (Target a body, Target a docking port, crew transfer, ...) to create fully autonomous flights. I'm done with most of the basic MechJeb tasks and now i would like to integrate tasks that could control additional plugins. I started with IR Sequencer and kOS.

My target with kOS: Be able to send a command to a processor and monitor the task execution to be able to wait for completion.

How i did that:

Step 1: Check if kOS is installed to enable the kOS features in MJ

foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
  else if (assembly.FullName.Contains("kOS"))
  {
  	this.compatiblePluginsInstalled.Add("kOS");
  }
}

This will check for the assembly

Step 2: Identify kOS processors

foreach (Vessel vessel in FlightGlobals.Vessels)
{
  if (vessel.state != Vessel.State.DEAD)
  {
    foreach (Part part in vessel.Parts)
    {
      foreach (PartModule module in part.Modules)
      {
        if (module.moduleName.Contains("kOSProcessor"))
        {
          //add the processor to a list
          this.kosModules.Add(module);
        }
      }
    }
  }
}

Step 3: When the action is triggered, use reflection to send the command on the Interpreter of ShareObjects inside the kOSProcessor module.

if (this.selectedPartIndex < this.kosModules.Count)
{
	if (openTerminal)
    {
      	//Invoke "OpenWindow" on the kOSProcessor module to open the terminal window
    	this.kosModules[this.selectedPartIndex].GetType().InvokeMember("OpenWindow", System.Reflection.BindingFlags.InvokeMethod, null, this.kosModules[this.selectedPartIndex], null);
    }
    //Catch the SharedObjects on the kOSProcessor module
    var sharedObjects = this.kosModules[this.selectedPartIndex].GetType().GetField("shared", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(this.kosModules[this.selectedPartIndex]);
    if (sharedObjects != null)
    {
        //Catch the Interpreter in SharedObjects
    	var interpreter = sharedObjects.GetType().GetProperty("Interpreter").GetValue(sharedObjects, null);
        if (interpreter != null)
        {
            //Send the command to the Interpreter
        	interpreter.GetType().InvokeMember("ProcessCommand", System.Reflection.BindingFlags.InvokeMethod | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance, null, interpreter, new object[] { command });
            ...

Step 4: Wait for the action to complete. Here, i used a "trick". I regularly watch for the result of "IsWaitingCommand" on the interpreter object. Not very clean, but it the only way i found to know a command has been completed.

public bool isCPUActive(object module)
{
  var sharedObjects = this.kosModules[this.selectedPartIndex].GetType().GetField("shared", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(this.kosModules[this.selectedPartIndex]);
  if (sharedObjects != null)
  {
    var interpreter = sharedObjects.GetType().GetProperty("Interpreter").GetValue(sharedObjects, null);
    if (interpreter != null)
    {
      //We check if the interpreter is waiting to know if our program has been executed
      bool waiting = (bool)interpreter.GetType().InvokeMember("IsWaitingForCommand", System.Reflection.BindingFlags.InvokeMethod, null, interpreter, null);
      if (waiting)
      {
      	//Action ended !
        return true;
      }
    }
  }
  return false;
}

I know reflection is not very clean, but it's very convenient as it greatly simplifies the setup for the user and creates no dependency between both plugins.

Anyway, the way you describe seems really cleaner and i will try to have a look.

Thank you for your feedback.

Link to comment
Share on other sites

The obvious addition to the above, cache anything and everything you can when using reflection, particularly if you need to do something often.

PartModule kosModule;
Type kosModuleType;
FieldInfo kosSharedField;
public bool isCPUActive(object module)
{
  if (kosModule == null) // only executed once per part
  {
    kosModule = this.kosModules[this.selectedPartIndex];
    kosModuleType = kosModule.GetType();
    kosSharedField = GetField("shared", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
  }
  // bare minimum required to be called multiple times
  var sharedObjects = kosSharedField.GetValue(kosModule);
}

I would take that even further and make anything I could static and only cache once per KSP instance (eg. type is often either fixed or has a very limited number of options).

Edited by Crzyrndm
Link to comment
Share on other sites

First a question about the concept:  

Would you consider a parallel effort to expose MechJeb features to kOS scripts?  I haven't had a chance to check out your implementation yet, but it seems like we could leverage off of each other pretty decently.  You're looking to add script-ability to MechJeb, kOS is a ready made scripting option.  While the premise of the kOS mod itself is "do it yourself autopilot", there wouldn't be anything wrong with MechJeb being able to add some hooks to kOS when both are installed.  I'm kind of assuming that your system is essentially a window where you queue different actions, and MechJeb just executes them sequentially, waiting for each to complete before moving to the next.  Are you offering a way to save up these lists of actions?  I wouldn't remove the system you have in place, I think you'll see better adoption if users aren't expected to download another mod, but kOS itself might serve as a nice "advanced" option.  A user just starting with scripting probably wants to script things like [Ascent autopilot => circularize => transfer to mun => land on mun].  But a more advanced script might be [Ascent autopilot => circularize => calculate transfer to jool => if encounter with mun or minmus adjust the transfer time => if jool encounter isn't close enough fine tune orbit => if there isn't enough fuel to capture at jool, aerocapture or gravity assist capture].

 

Second, conversation on the implementation:

Absolutely cache everything.  There is more cost in walking the methods and creating the delegate than there is for executing the delegate.  And just as importantly, repeated call chains to functions like GetType and GetField would be garbage heavy since the object is instantiated and orphaned immediately.

I'm not a huge fan of how the kOS-RemoteTech api is structured, but one of the things I really like is how it sets up the API access.  There is a class RemoteTechAPI whose properties are Action<> and Func<> types to match up with the RT internal methods.  Then it just walks the RT API class using reflection to set the property value to delegates created from the internal API methods.  Because the internal RT API uses static methods only one instance of kOS's API class needs to be constructed, which would be a difference in your case.  I could see a class definition like the following working well:

    public class WrappedSharedObjects
    {
        private Func<object> GetInterpreter { get; set; }

        private WrappedInterpreter interpreter;
        public WrappedInterpreter Interpreter
        {
            get
            {
                if (interpreter == null) // if the instance is null, instantiate a new one
                {
                    interpreter = new WrappedInterpreter(GetInterpreter());
                }
                return interpreter;
            }
        }

        public WrappedSharedObjects(object rawObject)
        {
            var type = rawObject.GetType();
            var interpreterProperty = type.GetProperty("Interpreter");
            var getInterpreterMethod = interpreterProperty.GetGetMethod();
            GetInterpreter = (Func<object>)Delegate.CreateDelegate(typeof(Func<object>), rawObject, getInterpreterMethod);
        }
    }

That way you can internally keep track of strongly typed objects (like WrappedSharedObjects).  (Warning, I didn't test the above, and didn't take the time to include error catching).  Alternatively, instead of storing the delegate itself you could store rawObject locally and getInterpreterMethod statically, calling getInterpreterMethod.Invoke(rawObject) to return the interpreter's object.  I want to say that Invoke is slightly slower than a delegate, but I don't recall the source of that info so your mileage may vary.  You'll need to use the field's GetValue method anyways, since FieldInfo can't be used to create a delegate, so calling Invoke and GetValue would maintain code consistency.

I've actually wanted to set up a templating system that would allow you to create such wrapper classes automatically instead of by hand, but obviously haven't done it yet.

Link to comment
Share on other sites

I should also mention that if you need us to expose any specific API methods from kOS to make this easier, we can probably get that done without too much issue.  Specifically I'm sure we can provide a cleaner way to figure out if a command is being executed.  We might even be able to get MechJeb to be treated like it's own interpreter instead of needing to use reflection.  I'll keep pondering how to do that cleanly.

Link to comment
Share on other sites

I also favor kOS making some sort of new "official" public API for other mods who want to "feed it some code".  This is because even if you can make the reflection more efficient by caching it, and even if you can make things work relatively smoothly, you still have the basic problem that there's two different reasons a programmer can choose to make a member "public" and nothing in looking at the code differentiates which reason it is:

Reason 1: It is public because it's part of the intended API for use by anyone in the world.  As such I will dedicate a bit of effort to try to keep it working the same way on the surface even when I refactor the insides of the class and change everything about how it works under the hood.  And if I do have to change it, I'll make sure to make it a big announcement that tells everyone what the change was.

Reason 2: It is public only because some of my *own* other classes within my project have to access it, but I still consider it part of the internal implementation of my project instead of something I'm obligated to document for outsiders, or to make any effort to keep it working the same way in the future.  If anyone does reverse engineer how it works and makes use of it, it could easily break right away in the next version.


The "IsWaitingForCommand" member is only pubic because of Reason 2 here.  We *just* added it recently to handle a different problem we were having and it's really just part of the ad-hoc solution to that problem.  Thus who knows if it will stay that way or not?

Link to comment
Share on other sites

The more difficult way around this, which is a lot more work to set up but in the long run is a lot more flexible, is to use sockets.

Basically you let your add-on expose a socket (TCP or UDP, or any other domain if you want) on the local host so other plugins can connect to it. The .NET TCPSocket class is quite easy to use so I would recommend trying it out.

The big plus of this is that you can set your own message format and set permissions about what plugins can and cannot do on the interaction with your add-on, and even replace functionality without having to link anything. You could expose your plugin to the local network if you want (with certain restrictions) so even apps on other machines can connect to it.

Big downside is that you need to design your add-on to have interaction in the first place, so it won't work with existing addons which may not have any communication in mind.

Link to comment
Share on other sites

Sorry for the late Reply. I have been pretty busy (had a baby ^_^).

On 03/11/2016 at 3:22 PM, hvacengi said:

Would you consider a parallel effort to expose MechJeb features to kOS scripts?  I haven't had a chance to check out your implementation yet, but it seems like we could leverage off of each other pretty decently.  You're looking to add script-ability to MechJeb, kOS is a ready made scripting option.

That was the first option i considered. As a software engineer, I love kOS and the idea to be able to write lines of code to control KSP, but my idea was to provide something for people that does not know/want to write code to automate tasks. That's the reason why my script module offers far less possibility than kOS but it can be controlled from the UI.

The screenshot below can give you an idea of what i tried to do:

ksp_mechjeb_scripts.png

And the video below demonstrate it working

I tried to be as less intrusive as i can in the mechjeb code. That's the reason why exposing mechjeb functions for kOS should be a combined effort with @sarbian, the main MechJeb developer.

On 03/11/2016 at 3:22 PM, hvacengi said:

Are you offering a way to save up these lists of actions?

Yes, I have 8 slots. We can name the slots and load/save the list of actions.

On 03/11/2016 at 3:22 PM, hvacengi said:

kOS itself might serve as a nice "advanced" option.

Sure. And that's something i can dive in a near future.

That's one of the reason i considered a bridge between my system and kOS to offer the opportunity to connect both together.

On 03/11/2016 at 3:22 PM, hvacengi said:

But a more advanced script might be [Ascent autopilot => circularize => calculate transfer to jool => if encounter with mun or minmus adjust the transfer time => if jool encounter isn't close enough fine tune orbit => if there isn't enough fuel to capture at jool, aerocapture or gravity assist capture].

sure the "if" logic is something that does not exists for the moment in my MechJeb script system.

On 03/11/2016 at 3:22 PM, hvacengi said:

 I could see a class definition like the following working well:

Yes, i like this implementation, i will try to modify the code to look like what you suggest.

On 03/11/2016 at 3:51 PM, hvacengi said:

I should also mention that if you need us to expose any specific API methods from kOS to make this easier, we can probably get that done without too much issue.  Specifically I'm sure we can provide a cleaner way to figure out if a command is being executed.

That would be wonderful. I'm not proud with the way i used to wait until the command is executed.

Edited by SPD13
Link to comment
Share on other sites

This thread is quite old. Please consider starting a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...