I have a two tier Delphi for Win32 application with a lot of business logic implemented in a god object I want to outsource into a separate service. This separate service should be accessed by multiple clients via TCP/IP telnet-style protocol.
How do I go about making the transition most simple?
Precisely, I'd like to keep this simplicity: I want to define every function just once. For example, if I want to add a pin code Login functionality to my app, I would just need to define the
function Login(Username: string; PinCode: integer): boolean;
function in some object on the server, then I can use it from clients without any additional work.
In the worst case, I have to implement three functions instead of one. First, the function body itself on the server, second, the unmarshaller which receives a text line from network, unpacks it and checks for validity:
procedure HandleCommand(Cmd: string; Params: array of string);
begin
...
if SameText(Cmd, 'Login') then begin
CheckParamCount(Params, 2);
ServerObject.Login(
Params[0],
StrToInt(Params[1])
);
end;
end;
Third, the marshaller, which when called by client, packs the params and sends them to server:
function TServerConnection.Login(Username: string; PinCode: integer): boolean;
begin
Result := StrToBool(ServerCall('Login '+Escape(Username)+' '+IntToStr(PinCode)));
end;
Obviously, I don't want this.
So far I have managed to rid myself of the unmarshaller. Working with Delphi RTTI, I wrote a generic unmarshaller which looks for a published method by name, checks the params and calls it.
So now I can just add a published method to the server object and I'm able to call it from telnet:
function Login(Username: string; PinCode: integer): boolean;
> login john_locke
Missing parameter 2 (PinCode: integer)!
But what do I do about writing a marshaller? I can't dynamically fetch server function list and add functions to the client object. I can probably keep some kind of dynamic pseudo-function collection but that would make my client calls ugly:
ServerConnection.Call('Login', [Username, Password]);
Plus, this defeats type-safety as every parameter is passed as a variant. If possible, I'd like to keep compile-time type-safety.
Maybe client code auto-generation? I can probably write "GetFunctionList()" and "GetFunctionPrototype(Name: string)" in my server:
> GetFunctionList
Login
Logout
IsLoggedIn
> GetFunctionPrototype Login
function Login(Username: string; PinCode: integer): boolean;
So that every time I need to update the client, I just re-query all the function prototypes from the server and automatically generate marshaller code for them. But that would mix compilation with execution: I'd have to first compile the server, then start it and query its functions, then build a client marshaller and recompile client only after that. Complicated!
Another option is to write a generic marshaller function and then call it from all the client prototype functions:
procedure TServerConnection.GenericMarshaller(); assembler;
asm
//finds the RTTI for the caller function, unwinds stack, pops out caller params,
//packs them according to RTTI and sends to the server.
//receives the result, pushes it to stack according to RTTI, quits
//oh god
end;
function TServerConnection.Login(Username: string; PinCode: integer): boolean; assembler;
asm
call GenericMarshaller
end;
This saves me writing manual packaging every time (less chance for errors), but still requires me to manually copy server function prototypes into the client object. Plus, writing this generic marshaller is probably going to be a living hell.
Then there's the option of using RPC, but I don't like it because I'd need to re-define all the functions in IDL. And Delphi's IDL editor sucks. And for OLE interfaces Delphi forcefully generates "safecall" functions which suck, too. And there's no way to implement auto-detect-disconnect and auto-restore-connection functionality other than checking EVERY SINGLE function call to the RPC class:
function TServerWrapper.Login(Username: string; PinCode: integer): boolean;
begin
try
RealObject.Login(Username, Pincode);
except
on E: EOleException do
if IndicatesDisconnect(E) then
Disconnect;
Reconnect;
RealObject.Login(Username, Pincode);
end;
end;
We're back to several functions instead of one.
So, what would you guys suggest? What are the other choices I'm missing? Are there common patterns or ready solutions for remoting in Delphi?