Passing Interfaces between Host and Plugins

While Hydra's base library provides implementations for the most general tasks involved while working with plugins (for example embedding a visual plugin into your form), most non-trivial applications will need some application-specific communication between hosts and plugins.

This communication is handled with the use of custom interfaces that can be implemented on the plugin, the host, or both, allowing the two to call each other. Plugins can implement interfaces to expose the functionality they are providing, just as the host can implement interfaces to make itself accessible to any plugin that is active.

Custom interfaces basics

To enable this communication across the boundaries of Delphi and .NET, the Hydra IDE integration provides sophisticated support for converting interface declarations between the two development platforms. You can write your custom interface(s) using .NET code, and then import the DLL into Delphi to generate a Delphi unit with the appropriate interface declarations, or you can write your interface in Delphi, and then import the unit to generate code that compiles under .NET for any of the supported .NET languages.

Creating a basic plugin interface

Let's create a basic interface that will provide the ability to pass a message from host to plugin:

uses
  RemObjects.Hydra.CrossPlatform,
  System.Runtime.InteropServices;

type
  [Guid('EEE74183-854D-4A5F-AF2A-6A00AB10FB95')]
  IMessageProcessor = public interface(IHYCrossPlatformInterface)
    method ProcessMessage(Message: String);
  end;
using RemObjects.Hydra.CrossPlatform;
using System.Runtime.InteropServices;

[Guid("EEE74183-854D-4A5F-AF2A-6A00AB10FB95")]
public interface IMessageProcessor: IHYCrossPlatformInterface
{
  void ProcessMessage(string Message);
}

Note that we have to use two assemblies, RemObjects.Hydra.CrossPlatform and System.Runtime.InteropServices, since all custom interfaces must be inherited from the IHYCrossPlatformInterface and the attribute must also be set.

After we create the basic interface, it needs to be implemented by a plugin, for example:

interface

type
  [Plugin(), VisualPlugin()]
  NetVisualPlugin = public partial class(VisualPlugin, IMessageProcessor)
  public
    method ProcessMessage(Message: String);
  end;

implementation

method NetVisualPlugin.ProcessMessage(Message: String);
begin
  MessageBox.Show('Message from the host: ' + Message)
end;
[Plugin, VisualPlugin]
public partial class NetVisualPlugin : VisualPlugin, IMessageProcessor
{
  public void ProcessMessage(string Message)
  {
    MessageBox.Show("Message from the host: " + Message);
  }
}

Importing interfaces

Now we're finished with the plugin, we need a host that will send messages to our plugin. First of all, we need to import an interface to the host; this can easily be done by using our converter that is built in the IDEs. The following set of screenshots shows how we can import an interface to a Delphi host:

Select Hydra|Import Interfaces from .Net Assemblies:

Select an assembly that holds the interfaces' definitions and mark the interfaces you want to import:

Press the Import button and a file named YourPluginName_Import.pas will be added to your project. Let's take a look at its content:

uses
  uHYCrossPlatformInterfaces;
type
  IMessageProcessor = interface;

  // Original Name: Net_Plugin.IMessageProcessor
  IMessageProcessor = interface(IHYCrossPlatformInterface)
  ['{eee74183-854d-4a5f-af2a-6a00ab10fb95}']
    procedure ProcessMessage(const Message: WideString); safecall;
  end;

As you can see, our interface, written in .NET (either C# or Oxygene), is automatically converted to Delphi.

Of course you can also create interfaces in Delphi and then convert them to .Net by selecting Import Interfaces from Delphi Unit from the solution explorer:

Select your unit that holds the interfaces' definitions and mark the interfaces you want to import:

Press the Import button and a file named DelphiUnitName_Import.cs will be added to your project.

Using the Interface

Now that we have a custom interface that was created in .Net and then imported into the Delphi project, let's take a look at how we can use this interface in the host:

procedure TMainForm.bLoadNetPluginClick(Sender: TObject);
var
  MessageProcessor: IMessageProcessor;
begin
  ModuleManager.LoadModule('Net_Plugin.dll');
  ModuleManager.CreateVisualPlugin('Net_Plugin.NetVisualPlugin', fVisualPlugin, Panel1);

  if fVisualPlugin.QueryInterface(IMessageProcessor, MessageProcessor) = S_OK then
    MessageProcessor.ProcessMessage('Hello from the Delphi host');
end;

Note: CreateVisualPlugin is passed an 'out' parameter called fVisualPlugin and this needs to be added to the host form as a private variable of type IHYVisualPlugin.

But what if a plugin wants to send a message to the host? We will modify our plugin and host to achieve this.

First, let's implement our interface in a host:

type
  TMainForm = class(TForm, IMessageProcessor)
  [..]
  private
    procedure ProcessMessage(const Message: WideString); safecall;
  end;

procedure TMainForm.ProcessMessage(const Message: WideString);
begin
  Self.Caption := Message;
end;

Please note that all cross-platform interfaces are inherited from IDispatch, so all objects that implement it must also provide an implementation for that interface, but we don't implement it here directly since TForm already takes care of that. Custom implementation of the interface will be discussed later in this article.

Now, we can add code to our plugin that will call the host method:

method bSendToHost_Click(sender: Object; e: EventArgs);
begin
  if self.Host is IMessageProcessor then
    (IMessageProcessor(self.Host)).ProcessMessage('Message from the plugin')
end;
private void bSendToHost_Click(object sender, EventArgs e)
{
  if (this.Host is IMessageProcessor)
    (this.Host as IMessageProcessor).ProcessMessage("Message from the plugin");
}

That's it, now we can send messages between host and plugin.

Note: If you need to use custom interfaces within a managed host that uses managed plugins, you must place your interfaces in a shared assembly, and both host and plugins must references to that assembly; then all work with interfaces can be done as usual.

Data marshaling

Marshaling is the process of passing calls between managed code and unmanaged code. When you pass data from an unmanaged host to a managed plugin, or vice versa, this data is converted to a data format suitable for storage or transmission. When you use our converter (described above) data types are automatically converted to the appropriate types on the other side, but in some cases you may need to manually tune data types.

Simple data types

The following table shows data types used in the .NET Framework and their corresponding Delphi built-in value types.

.NET Delphi
System.Boolean WordBool
System.Char WideChar
System.Int16 SmallInt
System.UInt16 Word
System.Int32 LongInt
System.UInt32 LongWord
System.Int64, System.UInt64 Int64
System.Single Single
System.Double Double
System.IntPtr, System.UIntPtr Pointer
System.String WideString
System.Guid TGUID
System.DateTime TDateTime

Note: Always use WideString when passing string data between host and plugins. WideString is a special string type that is provided to be compatible with the COM BSTR type.

Arrays

Passing arrays will require some additional work to be done. First of all, .NET arrays are marshaled as PVarArray. Our converter tool adds a comment next to the array parameter declaration, which shows what data type is used in this array, for example:

GetBytes(): array of Byte;
byte[] GetBytes();

will be converted to:

function GetBytes: PVarArray{Byte}; safecall;

Now you can work with this array. The following example shows how to convert a PVarArray with bytes (as shown below) to a Delphi array of byte and vice versa:

uses
  VarUtils;

type
  ByteArray = array of byte;

function SafeArrayAsByteArray(Arr: PVarArray): ByteArray;
var
 LBound, UBound, i: Integer;
 Indices: array [0..0] of Integer;
begin
 SafeArrayGetLBound(Arr, 1, LBound);
 SafeArrayGetUBound(Arr, 1, UBound);
 SetLength(result, UBound-LBound+1);

 for i := LBound to UBound do begin
   Indices[0] := i;
   SafeArrayGetElement(Arr, @Indices, @result[i-LBound]);
 end;
end;

function ByteArrayAsSafeArray(Arr: ByteArray): PVarArray;
var
  pvData: PByteArray;
  Bounds: TVarArrayBoundArray;
begin
   Bounds[0].LowBound:=0;
   Bounds[0].ElementCount:=Length(Arr);

   Result := SafeArrayCreate(varByte, 1, Bounds);
   SafeArrayAccessData(Result, Pointer(pvData));
   Move(Arr[0], pvData[0], Length(Arr));
end;

Records

Unmanaged structures can be marshaled as managed structures, and this case will also require additional work. To be able to pass structures, they must be defined properly in a managed environment, all fields must have the same order, size and layout.

Let's convert the following Delphi record to a managed structure:

  TSampleRecord = record
    IntVal: integer;
    DoubleVal: double;
    StrVal: WideString;
    BoolVal: boolean;
    DateVal: TDateTime;
    ArrayVal: PVarArray;
  end;

Managed structures must have the same fields order as unmanaged ones, so we need to use the StructLayout attribute and set the layout to LayoutKind.Sequential, which defines that the members of the object are laid out sequentially in the order in which they appear when exported to unmanaged memory. The following code shows how our managed structure should look like:

  [StructLayout(LayoutKind.Sequential)]
  TSampleRecord = public record
  public
    var     IntVal: Integer;
    var     DoubleVal: Double;
    [MarshalAs(UnmanagedType.BStr)]
    var     StrVal: String;
    var     BoolVal: Boolean;
    var     DateVal: DateTime;
    var     ArrayVal: array of String;
  end;
  [StructLayout(LayoutKind.Sequential)]
  public struct TSampleRecord
  {
    public int IntVal;
    public double DoubleVal;
    [MarshalAs(UnmanagedType.BStr)]
    public string StrVal;
    public bool BoolVal;
    public DateTime DateVal;
    public string[] ArrayVal;
  }

Please note that you must directly specify how string fields should be marshaled; [MarshalAs(UnmanagedType.BStr)] defines that this string should be passed to unmanaged code as BSTR.

If you use packed records, you must set "Pack=1" in the StructLayout attribute; this field controls the alignment of the data fields of a structure in memory. Also, in this case, the size of the field becomes crucial. In the example above, we use a field of boolean type in the Delphi record. This type occupies only 1 byte in memory, but by default, bool fields are marshaled as 4 bytes, so we must define that this field should be marshaled as 1 byte (alternatively, we can change the Delphi records to use LongBool instead of Boolean). The following example shows how the structure should be modified if a packed record is used:

  [StructLayout(LayoutKind.Sequential, Pack := 1)]
  TSampleRecord = public record
  public
    var     IntVal: Integer;
    var     DoubleVal: Double;
    [MarshalAs(UnmanagedType.BStr)]
    var     StrVal: String;
    [MarshalAs(UnmanagedType.I1)]
    var     BoolVal: Boolean;
    var     DateVal: DateTime;
    var     ArrayVal: array of String;
  end;
  [StructLayout(LayoutKind.Sequential, Pack=1)]
  public struct TSampleRecord
  {
    public int IntVal;
    public double DoubleVal;
    [MarshalAs(UnmanagedType.BStr)]
    public string StrVal;
    [MarshalAs(UnmanagedType.I1)]
    public bool BoolVal;
    public DateTime DateVal;
    public string[] ArrayVal;
  }

Now, when we created a proper managed structure, we can use this structure in interfaces, for example:

  [Guid('F9373593-9DF3-4218-B0A3-E0F08EE7694E')]
  IRecordTester = public interface(IHYCrossPlatformInterface)
    method GetRecord: TSampleRecord;
    method SetRecord(var R: TSampleRecord);
  end;
  [Guid("F9373593-9DF3-4218-B0A3-E0F08EE7694E")]
  public interface IRecordTester : IHYCrossPlatformInterface
  {
    TSampleRecord GetRecord();
    void SetRecord(ref TSampleRecord R);
  }

Please note that you must pass records by reference.

Using IDispatch

As stated above, all cross-platform interfaces are inherited from IDispatch and therefore objects that implement these interfaces must also provide an implementation of the IDispatch. In most cases, these methods are not needed and the THYFakeIDispatch class can be used as base class for objects that implement cross-platform interfaces.

Let's create a small sample, starting with defining our interfaces:

IParameters = interface(IHYCrossPlatformInterface)
['{02D0CC22-444F-4BAC-B2DA-0BBA54E0A485}']
  function Put(const Value: WideString): integer; safecall;
  function Get(const Index: integer): WideString; safecall;

  function Get_Count: integer; safecall;
  property Count: integer read Get_Count;
end;

IParametersProvider = interface (IHYCrossPlatformInterface)
['{6C1F6757-51CB-4F2D-B677-511061E1B598}']
  function Get_Parameters: IParameters; safecall;
  property Parameters: IParameters read Get_Parameters;
end;

and implementing them:

TMainForm = class(TForm, IParametersProvider)
[..]
private
  function Get_Parameters: IParameters; safecall;
public
  property Parameters: IParameters read Get_Parameters;
end;

TParameters = class(THYFakeIDispatch, IParameters)
private
  fList: TStringList;
  function Get_Count: integer; safecall;
public
  constructor Create;
  destructor Destroy; override;

  function Put(const Value: WideString): integer; safecall;
  function Get(const Index: integer): WideString; safecall;

  property Count: integer read Get_Count;
end;

function TMainForm.Get_Parameters: IParameters;
begin
  if not Assigned(fParameters) then
    fParameters := TParameters.Create;

  Result := fParameters;
end;

constructor TParameters.Create;
begin
  fList := TStringList.Create;
end;

destructor TParameters.Destroy;
begin
  FreeAndNil(fList);
  inherited;
end;

function TParameters.Get(const Index: integer): WideString;
begin
  Result := fList[Index];
end;

function TParameters.Get_Count: integer;
begin
  Result := fList.Count;
end;

function TParameters.Put(const Value: WideString): integer;
begin
  Result := fList.Add(Value);
end;

Now we can import our interfaces into a .Net project and use them:

if self.Host is IParametersProvider then
begin
  var Parameters: IParameters := (IParametersProvider(self.Host)).Get_Parameters();

  Parameters.Put('Sample parameter');
  MessageBox.Show(Parameters.Get(0))
end;
if (this.Host is IParametersProvider)
{
  IParameters Parameters = (this.Host as IParametersProvider).Get_Parameters();

  Parameters.Put("Sample parameter");
  MessageBox.Show(Parameters.Get(0));
}

Note: If your object is a descendant of the THYFakeIDispatch class, you can't use the implements directive or your method resolution goes wrong.

Interfaces and threads

In .Net, you can't access COM objects created in one thread from another one. All threads that want to use the Host interface must synchronize their calls with the main thread. Let's look at a small sample that shows how to do that.

We will define two interfaces, one for the plugin and a second for the host:

  [Guid('A4EEC900-3D27-42A0-B708-46C76FA9389D')]
  ICustomHost = public interface(IHYCrossPlatformInterface)
    method WorkFinished;
  end;

  [Guid('319DEA26-8B6B-4E63-9344-161A71838622')]
  IAsyncWorker = public interface(IHYCrossPlatformInterface)
    method StartWork;
  end;
  [Guid("A4EEC900-3D27-42A0-B708-46C76FA9389D")]
  public interface ICustomHost : IHYCrossPlatformInterface
  {
    void WorkFinished();
  }

  [Guid("319DEA26-8B6B-4E63-9344-161A71838622")]
  public interface IAsyncWorker : IHYCrossPlatformInterface
  {
    void StartWork();
  }

Our host will implement the ICustomHost interface:

TMainForm = class(TForm, IMessageProcessor, IParametersProvider, ICustomHost)
[..]
private
    procedure WorkFinished; safecall;

procedure TMainForm.WorkFinished;
begin
  ShowMessage('Async work in the plugin is done');
end;

And the plugin will start the BackgroundWorker and notify the host when the work is done.

Synchronizing in VisualPlugins

In the visual plugins, calls to a host method can easily be synchronized by calling the Control.Invoke method:

method StartWork;
begin
  var Worker: BackgroundWorker := new BackgroundWorker();
  Worker.DoWork += new DoWorkEventHandler(Worker_DoWork);
  Worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Worker_RunWorkerCompleted);
  Worker.RunWorkerAsync()
end;

method NotifyHost;
begin
  if self.Host is ICustomHost then
    (ICustomHost(self.Host)).WorkFinished()
end;

method Worker_RunWorkerCompleted(sender: Object; e: RunWorkerCompletedEventArgs);
begin
  if self.InvokeRequired then
    self.Invoke(new NotifyDelegate(NotifyHost))
end;

method Worker_DoWork(sender: Object; e: DoWorkEventArgs);
begin
  Thread.Sleep(2000)
end;
public void StartWork()
{
  BackgroundWorker Worker = new BackgroundWorker();
  Worker.DoWork += new DoWorkEventHandler(Worker_DoWork);
  Worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Worker_RunWorkerCompleted);
  Worker.RunWorkerAsync();
}

private delegate void NotifyDelegate();

void NotifyHost()
{
  if (this.Host is ICustomHost)
    (this.Host as ICustomHost).WorkFinished();
}

void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
  if (this.InvokeRequired)
    this.Invoke(new NotifyDelegate(NotifyHost));
}

void Worker_DoWork(object sender, DoWorkEventArgs e)
{
  Thread.Sleep(2000);
}

Synchronizing in NonVisualPlugins

For NonVisualPlugins, we will require more work to be able to synchronize calls with the main thread, since it's not inherited from Control. We have to create our own hidden window that we can use to synchronize calls:

interface

type
  SyncWindow = class(System.Windows.Forms.Form)
  public
    constructor;
  end;

  NetNonVisualPlugin = class(NonVisualPlugin, IAsyncWorker)
  private
    var     fSync: SyncWindow;
  public
    constructor;
  private
    method Worker_RunWorkerCompleted(sender: Object; e: RunWorkerCompletedEventArgs);
  end;

implementation

constructor SyncWindow;
begin
  CreateHandle()
end;

constructor NetNonVisualPlugin;
begin
  InitializeComponent();
  fSync := new SyncWindow()
end;

method NetNonVisualPlugin.Worker_RunWorkerCompleted(sender: Object; e: RunWorkerCompletedEventArgs);
begin
  if fSync.InvokeRequired then
    fSync.Invoke(new NotifyDelegate(NotifyHost))
end;
class SyncWindow: System.Windows.Forms.Form
{
  public SyncWindow()
  {
    CreateHandle();
  }
}

public partial class NetNonVisualPlugin : NonVisualPlugin, IAsyncWorker
{
  private SyncWindow fSync;
  public NetNonVisualPlugin()
  {
    InitializeComponent();
    fSync = new SyncWindow();
  }

void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
  if (fSync.InvokeRequired)
    fSync.Invoke(new NotifyDelegate(NotifyHost));
}

Note that we use the same code to launch the BackgroundWorker in a VisualPlugin, we only changed Worker_RunWorkerCompleted to use our SyncWindow.

Custom Interfaces and WPF

Custom interface usage in a WPF plugin needs some additional code. Special wrapper classes are needed when using custom interfaces inside WPF plugins, because of WPF limitations that block COM interop inside WPF classes.

We will use the "Delphi WPF" sample to show how to use custom interfaces within WPF plugins.

First of all, let's define an interface:

  [Guid('8032a51c-5961-41f5-9582-c77d98ea4d93')]
  IVisualizerControl = public interface(IHYCrossPlatformInterface)
    property BarsVisible: Boolean read write;
    method Randomize;
    method Sinus(aRange: Double);
  end;
[Guid("8032a51c-5961-41f5-9582-c77d98ea4d93")]
public interface IVisualizerControl : IHYCrossPlatformInterface
{
  bool BarsVisible { get; set; }
  void Randomize();
  void Sinus(Double aRange);
}

Then we need to create a wrapper that will receive calls from the host and pass it to the plugin:

interface

uses
  System.Runtime.InteropServices,
  RemObjects.Hydra.WPF.Internal;

type
  [Guid('6df54f69-0022-4d6f-b755-d855d92d4b96')]
  VisualizerWrapper = public class(VisualPluginWrapper, IVisualizerControl)
  private
    property PluginInstance: Plugin read Plugin(inherited PluginInstance);
  public
    property BarsVisible: Boolean read PluginInstance.BarsVisible write PluginInstance.BarsVisible;
    method Randomize;
    method Sinus(aRange: Double);
  end;

implementation

method VisualizerWrapper.Randomize;
begin
  PluginInstance.Randomize()
end;

method VisualizerWrapper.Sinus(aRange: Double);
begin
  PluginInstance.Sinus(aRange)
end;
using System.Runtime.InteropServices;
using RemObjects.Hydra.WPF.Internal;

  [Guid("6df54f69-0022-4d6f-b755-d855d92d4b96")]
  public class VisualizerWrapper : VisualPluginWrapper, IVisualizerControl
  {
    private new Plugin PluginInstance { get { return base.PluginInstance as Plugin; } }

    public bool BarsVisible
    {
      get { return PluginInstance.BarsVisible; }
      set { PluginInstance.BarsVisible = value; }
    }

    public void Randomize()
    {
      PluginInstance.Randomize();
    }

    public void Sinus(Double aRange)
    {
      PluginInstance.Sinus(aRange);
    }
  }

When using Oxygene instead of C#, Interface Delegation will be able to safe you some code:

uses System.Runtime.InteropServices,
     RemObjects.Hydra.WPF.Internal;

  [Guid('6df54f69-0022-4d6f-b755-d855d92d4b96')]
  VisualizerWrapper = public class (VisualPluginWrapper, IVisualizerControl)
  private
    property PluginInstance: Plugin read (inherited PluginInstance) as Plugin; implements public IVisualizerControl; reintroduce;
  end;

Now we need to inform the host that a wrapper will be used to invoke the plugin method:

  [Plugin(), VisualPluginAttribute(), NeedsManagedWrapper(typeOf(VisualizerWrapper))]
  Plugin = public partial class(RemObjects.Hydra.WPF.VisualPlugin, IVisualizerControl)
  end;
[Plugin, VisualPluginAttribute, NeedsManagedWrapper(typeof(VisualizerWrapper))]
public partial class Plugin : RemObjects.Hydra.WPF.VisualPlugin, IVisualizerControl

And our WPF plugin is ready. You can look at the Delphi WPF sample shipped with Hydra to see wrapper classes in action.