DarkMod and Doom3 linkage

From The DarkMod Wiki
Revision as of 11:37, 1 May 2012 by Tels (talk | contribs) (add note)
Jump to navigationJump to search
This article is outdated, Doom3 is open source and TDM has now it's own executable file.

Closed part of Doom 3 engine resides in module "Doom3.exe" which is built by ID software long time ago and cannot be changed. The TDM part of code builds itself into "gamex86.dll" module which is then linked dynamically by Doom 3 when the mod is launched. This situation complicates the communication between TDM engine and Doom 3 engine. And it can't be changed unless Doom 3 engine goes open source.

General C++ info

The code in C++ is a just set of functions calling each other. The functions are divided into virtual and non-virtual ones: they are very different with respect to linking issues.

Non-virtual functions

Non-virtual function is by default local and resides in the module it is defined in. So there is no legal way to call it from another module unless:

1. It is explicitly declared as DLL export.

or 2. Somewhere in the module its pointer is taken.

or 3. Maybe there are other cases...

In the first case linker knows that the function will be called from outside, so it preserves it. In the second case linker has to preserve the function because later someone can call it by pointer. Otherwise linker decides the that the function is internal and is free to optimize it in any way it wants. The whole program optimization can change function calling conventions or perform input-sensitive optimization without preserving the original function. As the result, the function cannot be called from outside.

Virtual functions

Virtual function is called by special mechanism. If "pObject" is a pointer to object and we call its virtual "Method" function, compiler does the following:

 	DWORD addrVTable = *(DWORD*)pObject;
 	DWORD addrMethod = addrVTable + 4 * IndexOf(Method);
 	/*return value*/ = ((MethodType)addrMethod) (/*all the arguments you specify*/);

So the virtual function is called by pointer to function which is obtained from vtable array of function addresses. Note the "IndexOf" thing. It finds the index of method based on caller's class declaration. If it is different from callee's class declaration, you'll get a horrible crash=) Also note that linker is free to delete virtual function if it is never called in a module.

How to call

To sum up, you can call function from another module in one of three ways:

1. Specify function is as DLL import and call it as usual.

2. Get valid pointer to function and use it to call the function.

3. Get valid object with virtual method and call this method.

Doom3 and TDM communication

GetGameAPI

Ok, now let's return back to Doom3 and TDM. First of all DLL exports. Doom3 has no DLL exports at all, TDM has the only one: "GetGameAPI". So you can forget about using 1st way to call Doom3's functions=)The other two ways require valid objects or valid function pointers. That's why "GetGameAPI" shares pointers to the most important objects between modules. By calling virtual functions of these important objects you can retrieve pointers to other objects and so on. The pointer to function method is not used in Doom3-TDM communication. I guess it is not as reliable as it looks like.

Closed source: virtual functions

Virtual function calls are the only way to call Doom3's functions. The whole "framework", "renderer" and "sound" directories are filled with header files only. These files contain declarations of various classes packed with virtual functions (like "idRenderWorld", "idSoundSystem", "idFile"). Since we don't have the sources of their implementations, these classes serve as calling interfaces only. If we have pointer to such a class we can call any of its virtual methods thanks to these headers. Also note that:

1. You cannot call any of non-virtual methods even if they are declared.

2. You cannot construct the object. If you want to create the object, search for virtual factory method in other class.

3. You cannot delete the object even if it has virtual destructor (because of CRT issue). Search for virtual "Free*" or "Close*" methods in other classes.

4. Most of these interface class declarations are not interfaces in C++ sense. Because their virtual functions are not pure virtual. In this case you cannot derive your implementation from such interface - you can only use the Doom3's implementation.

5. If you add/delete/permute declarations of virtual functions in these headers, Doom3 will crash.

idClass hierarchy: TDM only

idClass hierarchy lives only inside "gamex86.dll" and its objects are never passed in or out of this module. So you won't face any intermodule problems by using them.

idLib: two instances

idLib is the trickiest piece of code because it is used in both Doom3 and TDM. But since almost all of the idLib routines are non-virtual (often even inline) functions, it is not shared between modules. Instead of it, two instances of idLib coexist. One is statically linked into "Doom3.exe" module, the other is linked into "gamex86.dll". Each of them has its own state (do not believe words in "Lib.h" that idLib is stateless).

CRT: two instances

Now the worst part: CRT (C runtime library) also exists in two instances. That's because "Doom3.exe" linked CRT statically. There is no way to change it now. It means that all dynamic memory allocation/deallocation is local to the module. You cannot allocate memory in "Doom3.exe" and deallocate it in "gamex86.dll" and vice versa. You must be completely sure that allocation and deallocation of any object happens in the same module. Of course you can freely use the allocated memory in both modules - only allocation/deallocation is restricted.

Editing idLib containers

The separate idLib+CRT instances imply that idLib containers can be edited only in their own module. Suppose that you pass pointer to your idList object to Doom3. Doom3 tries to add one element to it. This insertion causes array reallocation. Doom3's idLib instance calls free from Doom3's CRT instance. This CRT instance tries to free the memory in its own heap. But there is no such memory in its heap -> you get a crash similar to double-free crash.