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 IHYVCLVisualPlugin.
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.