PDA

View Full Version : Extendable Component Object Model (ECOM)



Pyrogine
22-05-2008, 02:43 AM
For a far amount of years I've been trying to come up with a way to get Delphi class from a standard Win32 DLL. A quick Google will reveal a few common ways you can do this (as flatten routines, all virtual methods or COM), but none really allows you to extend those classes in the natural Delphi OOP manner. Over the past few months I've developed bit and pieces to this puzzle that finally come together recently.

This is what I've come up with:

1. On the DLL side you have a base class (in my case TPyroObject) that has all virtual methods. There is a field call FIntfObject of type TPyroObject. There are two methods call CallingBaseMethod and IsCallingBaseMethod. I will explain about these a bit later.

2. Each class interface will have to exist on both the dll and the client side and this shared FIntfObject pointer will allow the client and dll versions to shadow each other.

3. You have one exported routine from the dll that can return an instance of each class interface. From this routine you also pass the client side version of this interface to the dll side. The FIntfObject field will hold the instance pointer to each other. In each method of the class on the client side you call FIntfObject.MethodName which will call the DLL side version of the code (having all virtual methods allow this to work). At this point you can create an instance of a class that reside inside the dll and use it on the client side in a natural way.

4. Now, how can we extend a class on the client side and still have the dll side be able to call the extended method in the expected manor? Well we have a copy of the client side class in the FIntfObject field so we can call any method we wish from the DLL side. So by default if the method is has been extended this extended method would be called unless you call the inherited version of it. So for us to do this we simply call FIntfObject.Methodname on the DLL side which will call into the client side version of the method and if it's extended this new code will be executed.

5. The problem now is how do we emulate a call to an inherited method? In this case the only time this construct will be called is in the base class on the client side. Since all methods are virtual we have to do something a little different. On the client side base method you would mark it's call such as this:

// client side
procedure TMyClass.MyMethod;
begin
FIntfObject.CallingBaseMethod;
FIntfObject.MyMethod;
end;

Now when the method on the DLL side is called, it will check CallingBaseMethod and see if we mean to call the base code or the extended code such as this:

// DLL side
procedure TMyClass.MyMethod
begin
if not IsCallingBaseMethod then // after this call the flag will be reset
begin
FIntfObject.MyMethod; // call extended method on client side
Exit;
end;

// call your base method code here
...
end;

I must add to that as an initial requirement for this to work as natural as possible on startup I make the PyroGine DLL share memory with the client EXE this way strings and other managed data types can be passed back and forth without problems. The SDK will handle setting this up automatically. If you create additional DLLs that you wish to share memory with the main client EXE you would only need to add PyroUseShareMem as the first unit in the DLL project.

I would like to use packages but here are the problems for us:
a) We develop downloadable games and gamedev tools from our website and the IDE by default will require those run-time BPLs. Combined they alone becomes larger than the game itself.

b) if I use one package, then we run into the problem if multiple units in a package if we want to use other packages.

c) They are version specific and we need our game engine to work with all versions of Delphi without having to maintain different packages for each version.

Standard win32 DLLs solves these problems, but of coarse Delphi has no support what so ever (that I'm aware of) at exposing classes or even variable (why not variables?). It would be soooo nice if it did. I've now converted the SDK to use this model and all seems to be working great. Every class inside the PyroGine DLL can now be easily extended on the client side. The method calls in the DLL can safely call the extended client side methods and vise versa. So for example if you create a new Archive class (RAR maybe) it will continue to work through the whole SDK. Sweet! I hope to release version 1.1 in the coming weeks.

BigJoe
23-05-2008, 08:54 AM
Hi Jarrod

Very impressive.
Good Work.

Bigoe

Pyrogine
23-05-2008, 01:11 PM
BigJoe

Thanks. I've been working on getting this to work for a while now. A few weeks ago I sat down and started to look at all the different parts that I've implemented in various SDK versions and realized that it would now finally work. It was indeed that "eureka" moment for me. :D

I've now converted the whole SDK use what I'm calling ECOM and it seems to be solid and working very well. What is very interesting is that if you use neutral language constructs you should be able to use your language of choice to make the ECOM DLL and then you ANY COM complaint language on the client side can be used such as c/c++ for example. When I get some time I will do some R&D on this.

arthurprs
23-05-2008, 01:55 PM
looks cool

nice work.

Pyrogine
23-05-2008, 02:09 PM
arthurprs

Thanks. I will post a sample project for peer review, hopefully tonight or the next day.

NecroDOME
23-05-2008, 03:28 PM
I also created a similair approach. The idea was to have the game (or game components) in seppareted DLL's.
On the client side I create a base class that inherids from a interface. This way the engine controls the behavior of each class. It actually works very nice and well.

When creating a game yhe only thing I need to do is create the classes, inherid from an interface (or base class that inherids that interface) and I can access all other object trough that class.

Disadvantage is that I created some overhead for performance.

Pyrogine
23-05-2008, 04:47 PM
NecroDOME

Yes this is exactly the overall concept I'm using. I wanted to take a step further and make it as general and natural as possible.

1. So the first thing was the have the DLL share memory with the client so that any object created on either side can be handled and uses transparently.

2. Next I wanted the classes on the client side to be able to be extended in the expected manner.

3. I wanted the client-side extended classes to be able to called from the DLL side in the normal and expected way.

I too was concerned about the additional overhead. The past few release of PGSDK have been using incremental parts of this and the performance has been great. The final specification has introduced one more layer of abstraction (all virtual methods on both the dll/client side). Now that I've got the PGSDK converted I can start to do performance testing to make sure things will be ok. I will release some sample code so that others may also do peer review. Together we can optimize/improve areas they may need it.

Pyrogine
23-05-2008, 05:28 PM
NecroDOME, in what areas did you notice effected performance and how did you resolve it?

Pyrogine
23-05-2008, 09:04 PM
This basic sample project is for peer review of the ECOM specification. It was made in Delphi 2007 but should work with any version of Delphi that supports COM (I think Version 3 and higher). You can download it here (http://pyroginegames.com/downloads/ECOM.zip).

Here is the listing for ECOM client sample:
program ECOMClient;

{$APPTYPE CONSOLE}

uses
SysUtils,
cliTypes,
cliSystem;

type
TTest = class(TPyroObject)
public
constructor Create; override;
destructor Destroy; override;
procedure OnVisit(aSender: TPyroObject; aEventId: Integer; var aDone: Boolean); override;
end;

TTest2 = class(TTest)
public
constructor Create; override;
destructor Destroy; override;
procedure OnVisit(aSender: TPyroObject; aEventId: Integer; var aDone: Boolean); override;
end;

TTest3 = class(TTest)
end;

TTest4 = class(TTest)
end;


constructor TTest.Create;
begin
inherited;
end;

destructor TTest.Destroy;
begin
inherited;
end;

procedure TTest.OnVisit(aSender: TPyroObject; aEventId: Integer; var aDone: Boolean);
begin
WriteLn('TTest.OnVisit --> ', Self.ClassName);
end;

constructor TTest2.Create;
begin
inherited;
end;

destructor TTest2.Destroy;
begin
inherited;
end;

procedure TTest2.OnVisit(aSender: TPyroObject; aEventId: Integer; var aDone: Boolean);
begin
WriteLn('TTest2.OnVisit --> ', Self.ClassName);
end;


procedure test1;
var
l: TPyroObjectList;
done: boolean;
begin
l := TPyroObjectList.Create;
l.Add(TTest.Create);
l.Add(TTest2.Create);
l.Add(TTest2.Create);
l.Add(TTest3.Create);
l.Add(TTest4.Create);
done := false;
l.ForEach(l, -1, 0, done);
l.Free;
Write('Press ENTER to continue...'); ReadLn;
end;

begin
test1;
end.

Although very basic this sample demonstrates how ECOM solves the fundamental problems the specification has been created to do:

1. The code for TPyroObject and TPyroObjectList resides inside the DLL. You are able to declare and use an instance of this class on the client side.

2. TTestXXX are extended versions of TPyroObject. You are able to create instances of them and use like you would expect.

3. The OnVisit event handler is called from the DLL side and is able to call into the extended versions of the classes as you would expect.

4. The class instances added and managed by to the linked list will be destroyed automatically on both side just as you would expect.

Apart from the small amount of manual upfront work to code to the ECOM specs, everything should "look and feel" normal. I'm very interested in your feedback.

Thanks.