A lot of the design decisions were made directly as counters to issues we had encountered when working with other applications.
Therefore we have used an interface based system that allows for replacement of implementations, as long as the same interfaces are implemented. Combined with the use of run-time loadable components, we are able to replace an implementation without affecting any other part of the system. By considering the binary design of the interfaces, we are able to make it so an implementation can be replaced without needing any recompilation.
An example of this occuring was with our physics simulation component. The initial system was unfortunately abandoned by its developer. Because of the component seperation, we have been able to use the strengths of the original implementations design and interfaces, but reimplement it using a different library.
Components are loaded based on the capabilites they offer. Each component exports a function, GetComponentInfo, which is called by the Core to discover what capabilites they offer. The capabilites required for a simulation are specified in the .space file. Each capability is identified by a 128 bit number that is randomly generated by the developer. It is intended that a 128 bit number should provide enough range that the capability identifier should never conflict with the identifier for a different capability.
When more then one component offers the specified capability the Core uses a scoring system to determine which combination of components will fullfil all the required capabilites with the minimum number of components.
To allow for development using different development tools and compilers, rather then using the C++ "interface" keyword, we use our own system of macros to create the DInterfaces. These use C style function calls and data structures to ensure binary compatibility across development platforms.
One of the restrictions placed when designing DInterfaces that all components should be responsible for any memory they allocate. This allows different components to use different memory management techniques. An example advantage this provides is being able to run a debug build component in a release built installation of Digital Spaces. This should also allow different components to be implemented in different languages, using different memory libraries, as long as they support the C style calling convention. Almost all languages that compile to a binary support this, as this is the oldest and most widely used calling convention.
In order to provide a "point of first contact" between components, each component will provide at least one "Factory" object. This factory object will provide functions that allow access to sub-objects. An example of this is the SGManager, which manages access to the scene graph, by providing functions to access the scene graph organization objects.
The exported factory objects are also scheduled by the Core. This means that periodically a function "PerformHeartbeat" is called for that object. In this function, the component is expected to do any repetative work that is required. The scheduled object also has the functions PerformPrivateStartupStep, PerformPublicStartupStep and StopFactory. PerformPrivateStartupStep is to allow the component to do any initialization that doesn't require interaction with other components. PerformPublicStartupStep is to allow the component to do initialization that does require interaction. StopFactory is called to perform any shut down procedures that require inter-component interaction. Any remaining shut down procedures should be performed in a way that is triggered as the component is unloaded. In C++, this may be achieved using the destructor of a global or static object.