This post is going to introduce all the individual parts that make up NexusRemoting. It is not going to go into excessive detail on any single part, but instead show the overall design and introduce the terminology that will be used in future posts.
While it is not strictly necessary to have read the posts about Interface Fundamentals, Advanced Interface Usage and Patterns and Interface Aggregation the terms and concepts introduced in these posts will be helpful in understanding the overall design and will be necessary to understand the detailed look at individual components in future posts.
I will be starting with a line of code that I have given in my Introduction post and follow the execution flow from there, introducing parts as they are encountered.
RemotingClient.CreateInstance(CLSID_Calc, nil, InxCalc, Calc);
RemotingClient in this case is an instance of TnxRemotingClient. TnxRemotingClient, together with TnxRemotingCommandHandler and TnxRemotingServer form what in NexusDB Transport terminology is referred to as a Plugin. Plugins are a feature of the NexusDB Transport Architecture and allow the exchange of custom messages. These classes are used to connect the NexusRemoting framework with the NexusDB Transport Architecture.
An important point of the design is that no part of the remaining NexusRemoting framework directly references these classes or any of the base classes of the NexusDB Transport Architecture. While this is currently the only implementation of an out-of-process Class Activator and Channel Layer for NexusRemoting, it is possible to implement different Class Activators and Channel Layers in the future which do not sit on top of the NexusDB Transport Architecture.
Class Activator
A class activator is anything implementing the following interface:
type
InxClassActivator = interface(InxInterface)
['{2AF05701-C1AB-4259-BE37-571285940A81}']
function CreateInstance(const aClassID : TnxGuid;
const aOuter : InxInterface;
const aInterfaceID : TnxGuid;
out aClass )
: HRESULT;
end;
TnxRemotingClient is one of two class activators implemented in NexusRemoting currently. The second implementation of a Class Activator is the Class Factory Registry. The difference is that the Class Factory Registry, when used as a Class Activator, will create local instances in the current process while TnxRemotingClient will use the NexusDB Transport to communicate with a remote process, use the Class Factory Registry there to create an instance in that remote process and then use the other parts that make up NexusRemoting to provide a transparent proxy in the local process.
For code which uses a Class Activator it normally makes no difference which specific implementation of a Class Activator it uses. A specific class (identified by it's ClassID) will be created and an interface reference to a specific interface (identified by it's InterfaceID) will be returned. This returned interface can then be used as if it belongs to a local instance. This makes it easily possible to run the same client code either out-of-process or in-process by using either TnxRemotingClient or the Class Factory Registry as the Class Activator.
The aOuter parameter of CreateInstance makes it possible to use the returned class for interface aggregation. It can be nil if aggregation is not required, but if an outer interface has been specified then the requested InterfaceID must be for InxInterface. Usage then follows the pattern I've shown in my recent post about Interface Aggregation.
Registries, Factories and Controls
A pattern which is repeated in many parts of NexusRemoting involves a Registry (no relationship to the Windows Registry), where Factory instances can be registered, returning a Control interface which can be used to manage the registration. The registered Factories can then later be used to create different instances through the Registry.
As one example, here is how this looks for the Class Factory Registry:
type
InxClassFactoryRegistry = interface(InxClassActivator)
['{54F6F091-45F0-4E16-8788-170EA7385BA4}']
function RegisterFactory(const aClassID : TnxGuid;
const aFactory : InxClassFactory;
out aControl : InxClassFactoryControl)
: HRESULT;
//...
end;
As mentioned before, the Class Factory Registry is a Class Activator. In addition to the CreateInstance method it implements in it's role as Class Activator, it provides a method to register InxClassFactory instances for a specific ClassID (only one factory can be registered per ClassID). When factory is returned a reference to a InxClassFactoryControl instance is returned which is used to control this specific registration.
type
InxClassFactory = interface(InxInterface)
['{4470B7A4-7E67-4013-ACDC-2C59E479141D}']
function CreateInstance(const aOuter : InxInterface;
const aInterfaceID : TnxGuid;
out aClass )
: HRESULT;
end;
The Class Factory provides a CreateInstance method to create an instance of the class represented by this Factory. Unlike the Class Activator interface it no longer takes a ClassID as each Class Factory is registered for one specific ClassID.
type
InxFactoryControl = interface(InxInterface)
['{5D181EA6-888A-4314-AB3A-0F496854CD69}']
function Unregister: HRESULT;
end;
InxClassFactoryControl = interface(InxFactoryControl)
['{CA98630F-A76E-41C9-B384-A675D2E0CBFA}']
//...
end;
All Factory Controls provide an Unregister method. Control interfaces for the different Registries can provide additional functionality that's specific to the type of Registry and Factory. When the last reference to the Control interface is released the Factory is implicitly unregistered, so it's important to hold on to the Control interface as long as the Factory should stay registered.
All other Registries in NexusRemoting follow the same general pattern.
Class Proxy Factory Registry
The first thing an implementation of an out-of-process Class Activator needs is some local class instance which takes the place of the remote instance in the local process. This is called a Class Proxy.
There is a Class Proxy Registry where Factories for different Class Proxy implementations can be registered. Class Proxies do not need to be specifically for a single ClassID. In fact, NexusRemoting currently only comes with a single Class Proxy implementation which can be used as a Class Proxy for any ClassID. The functionality provided by this default implementation should be sufficient for the large majority of usage scenarios, but there are some advanced cases where it might be beneficial to provide specialized Class Proxies and NexusRemoting is designed to handle these as well.
Given the ClassID the Class Proxy Registry will return a list of possible Class Proxies (identified by ClassProxyIDs) sorted by their priority. This list of possible Class Proxies can now be send to the server together with the requested ClassID and InterfaceID.
Class Factory Registry
When an activation request from an out-of-process Class Activator arrives at the server it first has to determine if the requested ClassID is actually available. This is done by using the local Class Factory Registry (details of which have already been shown above) as a Class Activator to create an instance of the requested class.
Class Stub Factory Registry
Once the class has been successfully created the server uses the Class Stub Factory Registry to match up the list of available Class Proxies with the registered Class Stubs to find the highest priority pair of Class Proxy and Stub supported by Client and Server. The ID of the chosen Proxy/Stub pair is part of the reply to the client.
The Registry is then used to create a Class Stub, giving it the reference to the created Class instance.
Class Stub
A Class Stub is an object in the server process which holds the reference to the InxInterface interface of the Class instance. It will receive messages from the Class Proxy in the client process and will use the information in these messages to make calls against the Class instance on behalf of the Class Proxy.
When a Class Stub is created it is given the opportunity to provide information which will be returned to the client and made available to the matching Class Proxy that will be created in the client.
As with the Class Proxy, there is only one default implementation of a Class Stub in NexusRemoting currently (Proxies and Stubs always must come in matched pairs).
Channel Exits
Once the Class Stub has been successfully created the only thing missing is a way for the Class Stub to receive messages from the server. This is done by creating a Channel which will connect the Class Proxy on the client side with the Class Stub on the server side. The first step to establishing a Channel is to create a Channel Exit in the server process.
Creating a Channel Exit produces some information which will be returned to the client and used to create a Channel Entrance, resulting in a complete Channel.
Channel Sinks
When a Channel Exit is created a Channel Sink must be specified which will receive the request and create a reply.
Class Stubs are Channel Sinks:
type
InxChannelSink = interface(InxRoutingPath)
['{3CD324C8-EEF8-488D-837E-4EA297286359}']
procedure Connected(const aChannel : InxChannelExit);
procedure Disconnected(const aChannel : InxChannelExit);
function Process(const aChannel : InxChannelExit;
const aRequest : InxReader;
const aReply : InxWriter)
: HRESULT;
end;
InxClassStub = interface(InxChannelSink)
//...
Multiple Channel Exits can be connected to the same Channel Sink. Channel Exits disconnect themselves from their Channel Sink if the Channel has been explicitly closed by the Channel Entrance or if the connection to the Channel Entrance has been permanently lost by some external factor.
This allows the Class Stub to release it's references to the Class instance it represents once no Channels are connected to it anymore.
Readers and Writers
Incoming messages are provided as typed stream of values that can be read from an InxReader:
type
InxReader = interface(InxInterface)
['{0042DFCD-DA51-4DA0-B7C1-A4871B8E61D6}']
function ReadValue: TValueType;
function ReadBoolean: Boolean;
function ReadChar: Char;
function ReadAnsiChar: AnsiChar;
function ReadWideChar: WideChar;
function ReadFloat: Extended;
function ReadSingle: Single;
function ReadDouble: Double;
function ReadCurrency: Currency;
function ReadDate: TDateTime;
function ReadIdent: string;
function ReadInteger: Integer;
function ReadInt64: Int64;
procedure ReadListBegin;
procedure ReadListEnd;
function ReadString: string;
function ReadWideString: WideString;
function ReadVariant: Variant;
function ReadGuid: TnxGuid;
procedure ReadBinary(out Value; Size: Integer);
{ peek ahead }
function EndOfList: Boolean;
function NextValue: TValueType;
end;
Outgoing messages are written as a typed stream of values using an InxWriter:
type
InxWriter = interface(InxInterface)
['{F2E2EBE1-408C-4061-B2AF-0F986EA28E9E}']
procedure WriteValue(Value: TValueType);
procedure WriteBoolean(Value: Boolean);
procedure WriteAnsiChar(Value: AnsiChar);
procedure WriteWideChar(Value: WideChar);
procedure WriteFloat(const Value: Extended);
procedure WriteSingle(const Value: Single);
procedure WriteDouble(const Value: Double);
procedure WriteCurrency(const Value: Currency);
procedure WriteDate(const Value: TDateTime);
procedure WriteIdent(const Ident: string);
procedure WriteInteger(Value: Integer); overload;
procedure WriteInteger(Value: Int64); overload;
procedure WriteListBegin;
procedure WriteListEnd;
procedure WriteString(const Value: string);
procedure WriteWideString(const Value: WideString);
procedure WriteVariant(const Value: Variant);
procedure WriteGuid(const Value: TnxGuid);
procedure WriteBinary(const Value; Size: Integer);
end;
Channel Entrance
With the information that was produced when the Channel Exit was created on the server it is now possible to create a Channel Entrance on the client side and complete the Channel. A Channel Entrance is then represented by the following interface:
type
InxChannelEntrance = interface(InxChannel)
['{AF602A06-2FAC-4610-AFAC-590D68A5F5CF}']
function GetWriter(out aWriter : InxWriter)
: HRESULT;
function Process(const aWriter : InxWriter;
out aReader : InxReader)
: HRESULT;
end;
GetWriter returns an InxWriter which is used to prepare the outgoing message. The writer is then passed to Process which will transmit the information to the Channel Exit where it will be passed as an InxReader to the Channel Sink. The Channel Sink can use an InxWriter to prepare a reply which will then be returned as an InxReader from the Channel Entrance.
The Channel will be maintained as long as a reference to the Channel Entrance exists. When the last reference to the Channel Entrance is released the Channel will be closed and the Channel Exit on the server side will disconnect from it's Channel Sink.
Class Proxy
The reply from the server also contains the ID of the Stub which has been chosen. With that ID it is now possible to create an instance of the correct Class Proxy using the Class Proxy Factory Registry. On creation, the Class Proxy is provided with the Channel Entrance linking it to the Class Stub and with whatever information the Class Stub has provided for the Class Proxy when it was created.
It is now the job of the Class Proxy to represent the remote Class instance and behave as if the remote Class instance was running locally. All further communication that takes place between the Class Proxy and Class Stub is a private implementation detail of that matched pair of Proxy and Stub.
Default Class Proxy and Stub Implementation
The following is specific to the current default Class Proxy and Stub implementation that comes with NexusRemoting. Other implementations can be very different.
The Default Class Proxy itself only supports a InxInterface and IMultiQI (which I introduced in the Interface Aggregation post already and which is basically just a performance tweak to replace multiple round trips of individual QueryInterface calls with a single call that queries multiple interfaces).
QueryInterface requests for any other interface supported through the use of so Interface Proxies.
Interface Proxy and Stub Factory Registries
The Factory Registries for Interface Proxies and Interface Stubs and their usage follows the same pattern as that which was already established by the Class Proxy and Stub Factory Registries. Multiple possible Interface Proxies can be registered for the same InterfaceID, a list of all InterfaceProxyIDs that apply to a specific InterfaceID can be generated and send to the server where the highest priority matching pair is then chosen.
While Interface Proxies and Stubs can be statically registered for specific ClassIDs and/or InterfaceIDs, it is also possible for the Factory to decided at runtime if it can support a specific InterfaceID or not.
Interface Proxy and Stub
When a specific InterfaceID is requested from the Default Class Proxy a message exchange between Proxy and Stub establishes which (if any) is the highest priority Interface Proxy/Stub pair that supports this InterfaceID and if the Class instance actually supports this interface.
If yes an Interface Stub is created on the server side which holds a reference to this specific interface of the Class instance. The Default Class Stub keeps a list of all Interface Stubs it created.
Like with the Class Stub, the Interface Stub can provide information on creation which is returned to the client and available to the Interface Proxy when it is created.
Interface Proxies and Stubs do not establish their own Channels. Instead the Interface Proxy gets it's InxWriter from the Class Proxy which then sends the message through it's Channel to the Class Stub which forwards the request to the correct Interface Stub.
Interface Proxies must implement the specific interface they claim to support (calls to QueryInterface with that InterfaceID must succeed). It must support Interface Aggregation and will be aggregated to the Class Proxy which servers as it's Host.
The Default Class Proxy keeps a list of all Interface Proxies it created and if the same InterfaceID is requested again the existing Interface Proxy can be reused without further message exchange between client and server. Interface Proxies (and Stubs), once created when first needed, will be retained for the full lifetime of the Class Proxy and Stub that hosts them, even if at some point there are no references to their specific interface. The Class Proxy itself will be retained as long as there is an active reference to any interface it implements directly or to any Interface Proxy that it hosts.
It is now the job of the Interface Proxy to represent this specific interface of the remote Class instance and behave as if the remote Class instance was running locally. All further communication that takes place between the Interface Proxy and Interface Stub is a private implementation detail of that matched pair of Proxy and Stub. There is no requirement that every call against an interface method on the Proxy will result in a message exchange between Proxy and Stub. Only the externally observable behavior is important.
The overall effect of this design is that a client can use a Class Activator to get a reference to an instance that behaves as if it is a single locally running Class instance. But in reality it receives an aggregate of a Class Proxy and any number of Interface Proxies.
Interface Proxy and Stub Implementations
NexusRemoting currently comes with 2 different pairs of Interface Proxies and Stubs.
The first is a handwritten pair to support one specific interface: IDispatch.
This makes it possible to pass any object which supports IDispatch between client and server. It is important to point out here that it is not an requirement of NexusRemoting that the Class instance that is being remoted is implemented by you in your code. e.g. it is possible to use OLE Automation to get an IDispatch reference to e.g. Microsoft Word and then pass this reference using NexusRemoting to a different process.
The second one is a proxy and stub pair which is able to support any interface that can be described by interface RTTI.
This is what I used in my Introduction post. In that example, InxCalc derives from InxInvokable which causes the compiler to generate detailed runtime type information for this interface. This detailed type information just needs to be added to the Invoke Registry with a single line of code:
nxInvokeRegistry.RegisterInterface(TypeInfo(InxCalc));
With the detailed type information available the Invokeable Interface Proxy and Stub Pair is then able to generate the required code to implement this interface at runtime on the Proxy and performing the calls against the actual interface implemented by the Class from the Stub.
More to come...
This post laid out the big picture, showing all the major parts of NexusRemoting. In a future post I'll introduce the Invokeable framework in more detail, which has many uses beyond just automatic Interface Proxy and Stub generation at runtime.