C++ Question How exactly does Orbiter... well work?

BruceJohnJennerLawso

Dread Lord of the Idiots
Addon Developer
Joined
Apr 14, 2012
Messages
2,585
Reaction score
0
Points
36
So I have studied C++ enough that I can write my own programs, although not quite at a really advanced level just yet. One of the things that puzzles me is how Orbiter works with regards to vessel classes defined in external modules.

As I understood it, polymorphism works something like this:


A very simple definition of two classes, obviously this is missing an incredible number of things, but please bear with me

Code:
class Vessel
{   // define some good vessel stuff that the child classes derived from
    // Vessel will inherit, like variables and member functions
    virtual void callbackFrameUpdate();
};

class DeltaGlider: public Vessel
{   // define some additional good things specific to the vessel, like
    // the mesh of the dg and whatever
   DeltaGlider(arguments argies);
   void callbackFrameUpdate();
};

class ShuttleA: public Vessel
{   // define some additional good things specific to the vessel, like
    // the mesh of the dg and whatever
   ShuttleA(arguments argies);
   void callbackFrameUpdate();
};

Then in our int main function, we can do something like

Code:
int main()
{   Vessel * GL1, * SH2;
     GL1 = new DeltaGlider(argieee);
     SH2 = new ShuttleA(argier);
     // then we stuff those two Vessel pointers into a vector or something
     // similar
     while(we_aint_done)
     {
       // we call callbackFrameUpdate(); for every pointer in the vector
     }
     delete GL1;
     delete SH2;
     // because someone will howl if I dont do this ;)
    return 0;
}


which is simpler than storing lists of every single type of vessel in the sim and updating those individually, because we know that update will need to be called for every object that inherits from Vessel anyways

But... The thing that puzzles me is exactly how Orbiter does this. When Martins compiles Orbiter, he cannot know the exact type name of every vessel type that addon developers will define, like XR2, Energia, Flying_banana, and so on... So when the program loads vessels from a scn file or creates them from the scenario editor, what can the program possibly call when creating that new object?

When developing an Orbiter module, I do know that MSVC++ always spits out a dll, which gets copied to the appropriate orbiter folder. This is perfectly normal as a development strategy, since dynamic link libraries allow for modifying the contents of functions without recompiling a programs original source, only if the program included the original headers. But as we already knew, Orbiter cant have those headers when its source executable is compiled. The one thing I do notice is that Orbiter config files for dlled vessels require a definition of a classname variable or something similar? Its almost as if Orbiter can dynamically load that typename and use it in the program, but how I dont know. I feel as if I am missing something obvious here.

Any help here is appreciated :hailprobe:
 

Linguofreak

Well-known member
Joined
May 10, 2008
Messages
5,031
Reaction score
1,271
Points
188
Location
Dallas, TX
But... The thing that puzzles me is exactly how Orbiter does this. When Martins compiles Orbiter, he cannot know the exact type name of every vessel type that addon developers will define, like XR2, Energia, Flying_banana, and so on... So when the program loads vessels from a scn file or creates them from the scenario editor, what can the program possibly call when creating that new object?

I'm not sure of the specifics in C++, but the general idea in an object oriented language is that when you hand the compiler something like this (this will look vaguely Java-ish, since that's the syntax I'm more familiar with, but it's meant to be pseudocode, not anything in a real language):

Code:
class A
{
    int x;
    int y;
    void z()
    {
       //do stuff
    }
}

The compiler makes some notes to itself that look like this:

"If we have an object belonging to class A, that object takes up 16 bytes. The first data member of the object is an int called x. It is located 0 bytes from the beginning of the object and takes up the first four bytes of the object. The second data member is an int called y, it is located 4 bytes from the beginning of the object, and takes up the next four bytes after the end of x. The third data member of the object is a pointer to a void function called z(). If the function is not overridden by a subclass, the value of the pointer should be the address of A.z(), which is 12345. The function pointer is located 8 bytes from the beginning of the object, and takes up the final 8 bytes of the object"

The compiler then writes the code for A.z() at address 12345. If we create an object of class A named "Foo" and call Foo.z(), the compiler will create code that looks like this:

Code:
mov pointer_to_Foo to registerA
add 8 to registerA
call registerA

When this code is run, it will load a pointer to Foo, add 8 to it to get a pointer to Foo.z(), and the call the function that that pointer is pointing at. If Foo isn't a member of a subclass of A that overrides A.z(), the pointer will contain address 12345, which points to A.z().

If we create a class that looks like this:

Code:
class B extends A
{
    int w;
    void z()
    {
         //do different stuff
    }
}

The compiler will make notes that look like this:

"An object of class B takes up 20 bytes. Class B extends A, so the first 16 bytes are occupied by the same data as for class A. However, the pointer to z() at offset 8 into the object should be filled in with the address of B.z() rather than A.z(). B.z() is at address 67890. There is also a new data member, w, which is an int beginning 16 bytes after the beginning of the object, and occupying the final four bytes of the object."

If we make Foo an object of type B and call Foo.z(), the compiler will generate the same code, which will do the same thing:

Code:
mov pointer_to_Foo to registerA
add 8 to registerA
call registerA

The one difference is that the pointer at offset 8 into Foo now contains the value 67890 instead of 12345 and thus points to different code.
 

martins

Orbiter Founder
Orbiter Founder
Joined
Mar 31, 2008
Messages
2,448
Reaction score
462
Points
83
Website
orbit.medphys.ucl.ac.uk
Or with a slightly higher-level explanation attempt:

C++ defers the binding of virtual methods to runtime. It does so by means of a virtual table: instead of linking to the method directly, it links to the appropriate entry in the virtual table, which in turn contains the address of the actual function.

Derived classes inherit the virtual table of their parent, and so by default link to the parent's virtual methods. Whenever the derived class overloads a virtual method, the entry in the virtual table is replaced, pointing to the new method instead.

This additional dereferencing step at runtime makes virtual methods (marginally) less efficient than non-virtual ones, which is why in particularly performance-critical code polymorphism is sometimes replaced with link-time resolution approaches, e.g. by extensive use of templates. Also obviously you can't have virtual methods defined inline.
 

jedidia

shoemaker without legs
Addon Developer
Joined
Mar 19, 2008
Messages
10,869
Reaction score
2,128
Points
203
Location
between the planets
When Martins compiles Orbiter, he cannot know the exact type name of every vessel type that addon developers will define

That's why it's called a dll... a dynamic link library (as opposed to a static library, which has extension .lib and needs to be linked at compile time). So in the bare basics, the code looks for dll's of a specific name (defined either in the config, i.e. Launchpad, or in a vessels config file) and establishes the neccessary links at runtime.
 

BruceJohnJennerLawso

Dread Lord of the Idiots
Addon Developer
Joined
Apr 14, 2012
Messages
2,585
Reaction score
0
Points
36
Or with a slightly higher-level explanation attempt:

C++ defers the binding of virtual methods to runtime. It does so by means of a virtual table: instead of linking to the method directly, it links to the appropriate entry in the virtual table, which in turn contains the address of the actual function.

Derived classes inherit the virtual table of their parent, and so by default link to the parent's virtual methods. Whenever the derived class overloads a virtual method, the entry in the virtual table is replaced, pointing to the new method instead.

Okay...

But that is mostly explaining polymorphism in general isnt it? Dont we need to know the type name when a new class is instantiated at runtime? For example, in the original pseudocode I posted,

Code:
GL1 = new DeltaGlider(arghhhhhs);

We had to already have the type DeltaGlider defined, which is fine if the class type was defined in the executables code before compilation, but it isnt if the class is defined in an external module.

For all the world, it seems as if you are doing something like this:

Code:
// we open up our config file, and set a string equal to the contents of classname (the parameter that we set in the vessels config file
std::string classname = classnamefromfile(blah);

GL1 = new typename_at(classname)(argies);
// ^ What is this witchcraft???

which makes no sense, since types in C++ have to be defined at compile time of the executable that uses them, dont they? (isnt that one of the defining aspects of a statically typed language?)

More specific to what jedidia posted, how does Orbiter perform this search for dlls of a given name, then use them as the definition of a specific class?

To narrow the question down a bit, what exactly does the scenario editor do when a user enters a name, selects a type, then hits the 'create' button?

---------- Post added at 19:29 ---------- Previous post was at 19:25 ----------

the code looks for dll's of a specific name (defined either in the config, i.e. Launchpad, or in a vessels config file) and establishes the neccessary links at runtime.

So my question is specifically this, how is it done???
 

kamaz

Unicorn hunter
Addon Developer
Joined
Mar 31, 2012
Messages
2,298
Reaction score
4
Points
0
Code:
// we open up our config file, and set a string equal to the contents of classname (the parameter that we set in the vessels config file
std::string classname = classnamefromfile(blah);

GL1 = new typename_at(classname)(argies);
// ^ What is this witchcraft???

which makes no sense, since types in C++ have to be defined at compile time of the executable that uses them, dont they? (isnt that one of the defining aspects of a statically typed language?)

The DLL must export this function:

DLLCLBK VESSEL *ovcInit (OBJHANDLE hvessel, int flightmodel)
{
return new ShuttlePB (hvessel, flightmodel);
}

As you have pointed out, the core cannot call "new" directly because it does not know the derived type definition. So instead it calls ovcInit() function of the module, which calls new() and returns the object pointer to the core. The core then calls methods of the spacecraft via VESSEL/VESSEL2/VESSEL3 interface thanks to the wonders of polymorphism:

http://www.learncpp.com/cpp-tutorial/124-early-binding-and-late-binding/
http://stackoverflow.com/questions/18035293/what-is-early-static-and-late-dynamic-binding-in-c

You can also ask how the core can call functions in a DLL module if does not have the corresponding .h and .lib files. Well, it does not need .h file, because ovcInit() is already declared in the SDK headers. And instead of using the .lib file to get the function address, you can get the function address at runtime like this:

HANDLE hModule = LoadLibrary("ShuttlePB.dll");
OVCINITPTR ovcInit = GetProcAddress(hModule, "ovcInit");
VESSEL *v = ovcInit(...);
v->DoWhateverWithVessel();

...look up MDSN docs for LoadLIbrary and GetProcAddress.

You can easily check what vehicle classes you have available just by getting a list of all loaded DLLs via EnumProcessModules() and calling GetProcAddress(hModule, "ovcInit"). If the module does not implement ovcInit (i.e. it is not a vessel), it will return NULL.
 
Last edited:

BruceJohnJennerLawso

Dread Lord of the Idiots
Addon Developer
Joined
Apr 14, 2012
Messages
2,585
Reaction score
0
Points
36
The DLL must export this function:

DLLCLBK VESSEL *ovcInit (OBJHANDLE hvessel, int flightmodel)
{
return new ShuttlePB (hvessel, flightmodel);
}

As you have pointed out, the core cannot call "new" directly because it does not know the derived type definition. So instead it calls ovcInit() function of the module, which calls new() and returns the object pointer to the core. The core then calls methods of the spacecraft via VESSEL/VESSEL2/VESSEL3 interface thanks to the wonders of polymorphism:

Yes, I think that is what I am looking for.

From the looks of the function, I am going to guess that is a WindowsAPI specific definition? Is there a cross-platform way of doing it? (please say yes :shifty:)
 

kamaz

Unicorn hunter
Addon Developer
Joined
Mar 31, 2012
Messages
2,298
Reaction score
4
Points
0
Yes, I think that is what I am looking for.

From the looks of the function, I am going to guess that is a WindowsAPI specific definition? Is there a cross-platform way of doing it? (please say yes :shifty:)

Not sure what you mean platform-indendent. It's a Windows DLL entry point, so it must have appropriate decorations.

If you want to use some platform-indenpendent code, then just add a wrapper:

void *MyPlatformIndependentFunction(int p1, int p2) {
...
}

DLLCLBK void WindowsEntryPoint(int p1, int p2) {
return MyPlatformIndependentFunction(p1, p2);
}

---------- Post added at 09:19 PM ---------- Previous post was at 09:17 PM ----------

Or, if you want to build both Windows and Linux DLLs form the same .c file, then you do something like that:

#ifdef __WIN32__
DLLCLBK void EntryPoint(int p1, int p2) {
return MyPlatformIndependentFunction(p1, p2);
}
#endif

#ifdef __LINUX__
void EntryPoint(int p1, int p2) {
return MyPlatformIndependentFunction(p1, p2);
}
#endif

Also, Linux equivalent of LoadLibrary() is dlopen().
 
Last edited:

jedidia

shoemaker without legs
Addon Developer
Joined
Mar 19, 2008
Messages
10,869
Reaction score
2,128
Points
203
Location
between the planets
So my question is specifically this, how is it done???

To the processor, a function is just a memory address at the end of the day. If the process arrives at the memory address of a function, that function will get executed. It must not be known to the processor. All it needs to know is where to go (this can lead to some really fun stuff when you mess up your memory, like the process jumping several hundred lines of code and continuing in a completely unrelated function. Gives you the willies when tracing it in the debugger.... :facepalm:).

In context: Orbiter doesn't need to know what class it is working with. All it needs is the memory address of the new instance, which is perfectly compatible with any instance of the VESSEL class. And that address is returned to it, as Kamaz pointed out, by ovcInit, the address of which it knows perfectly fine when the dll is loaded. The compilation of the dll handles the linking inside the class itself, and all Orbiter ever needs to know is that there's another VESSEL instance around. It doesn't care that this instance calls functions it doesn't know about.
 

kamaz

Unicorn hunter
Addon Developer
Joined
Mar 31, 2012
Messages
2,298
Reaction score
4
Points
0
To the processor, a function is just a memory address at the end of the day.

It's actually more complicated than that because you must account for calling conventions and C++ name mangling.

The reason the ovcInit() hack is needed is that the DLL interface does not know about C++ -- it uses C-style functions.

Also, passing a C++ object between DLL and executable literally relies on the assumption that both sides have been built with the same compiler (i.e. that the DLL uses the same vtable layout and name mangling scheme as the core). If you were to compile the add-on with GCC it would crash the moment an object method is called.
 

jedidia

shoemaker without legs
Addon Developer
Joined
Mar 19, 2008
Messages
10,869
Reaction score
2,128
Points
203
Location
between the planets
It's actually more complicated than that because you must account for calling conventions and C++ name mangling.

It's always more complicated :lol:
 

BruceJohnJennerLawso

Dread Lord of the Idiots
Addon Developer
Joined
Apr 14, 2012
Messages
2,585
Reaction score
0
Points
36
Not sure what you mean platform-indendent. It's a Windows DLL entry point, so it must have appropriate decorations.

If you want to use some platform-indenpendent code, then just add a wrapper:

void *MyPlatformIndependentFunction(int p1, int p2) {
...
}

DLLCLBK void WindowsEntryPoint(int p1, int p2) {
return MyPlatformIndependentFunction(p1, p2);
}

---------- Post added at 09:19 PM ---------- Previous post was at 09:17 PM ----------

Or, if you want to build both Windows and Linux DLLs form the same .c file, then you do something like that:

#ifdef __WIN32__
DLLCLBK void EntryPoint(int p1, int p2) {
return MyPlatformIndependentFunction(p1, p2);
}
#endif

#ifdef __LINUX__
void EntryPoint(int p1, int p2) {
return MyPlatformIndependentFunction(p1, p2);
}
#endif

Also, Linux equivalent of LoadLibrary() is dlopen().

And the linux version is returning a void pointer there?
 

Hielor

Defender of Truth
Donator
Beta Tester
Joined
May 30, 2008
Messages
5,580
Reaction score
2
Points
0
I think kamaz left out a couple *s.
 

kamaz

Unicorn hunter
Addon Developer
Joined
Mar 31, 2012
Messages
2,298
Reaction score
4
Points
0
Yes, there should be void * everywhere instead of void.

Thanks for catching this.

---------- Post added at 11:29 PM ---------- Previous post was at 11:27 PM ----------

So is calling return in a void function implicitly returning a function pointer of type void?

Again, there is an error in the example, because the return type should be void *, not void. The return type of the wrapper function should be the same as the return type of the wrapped function.

In general:
- Calling return inside a void function is legal.
- Calling return with a value inside a void function will give you a warning.
 

BruceJohnJennerLawso

Dread Lord of the Idiots
Addon Developer
Joined
Apr 14, 2012
Messages
2,585
Reaction score
0
Points
36
One other thing, I cant seem to find where OVCINITPTR is defined anywhere in the OrbiterSDK, is it just a wrapper for a function pointer that returns a type *VESSEL?
 

kamaz

Unicorn hunter
Addon Developer
Joined
Mar 31, 2012
Messages
2,298
Reaction score
4
Points
0
One other thing, I cant seem to find where OVCINITPTR is defined anywhere in the OrbiterSDK, is it just a wrapper for a function pointer that returns a type *VESSEL?

Where does OVCINITPTR appear?
 

BruceJohnJennerLawso

Dread Lord of the Idiots
Addon Developer
Joined
Apr 14, 2012
Messages
2,585
Reaction score
0
Points
36
Where does OVCINITPTR appear?


HANDLE hModule = LoadLibrary("ShuttlePB.dll");
OVCINITPTR ovcInit = GetProcAddress(hModule, "ovcInit");
VESSEL *v = ovcInit(...);
v->DoWhateverWithVessel();

And would the module handle be closed at the end of the program, or after the vessel was loaded with ovcInit using its handle?
 
Top