The named interface has three attributes. It has (unsurprisingly) a text name, an integer-valued identifier, and a type constant. The name is actually optional -- it might be "" (the null string), in which case you can only look up the object in the directory by its identifier. In general, you should only use the name fleetingly to find the object you're looking for, and then use the identifier for any other operations -- especially network operations. If a name is given, then the identifier and the name form a one-to-one relationship: there is exactly one name for a given identifier, and one identifier for a given name. Since these "identify" a unique instance of an object, it makes sense that each object has only one name/identifier, and each name/identifier has only one object associated with it. The type attribute is a value that contains the type constant for the named interface that the object of which the object is an instance.
In addition to providing directory service, the named interface system also allows us to create instances of "named interfaces" at runtime. If the server asks a client to create a new "F15" object, then the client needs some platform-independent way to determine what an "F15" object is and how to create one. In this case, F15 is a named interface. That is, in some header there is an F15 interface class that will ultimately derive from the NamedInterface base class (so that it can use the directory service). The module that is in charge of the F15 will see to it that the F15 class's type constant is registered with the named interface directory. Then, we can request that the directory create an instance of the "F15" class by passing it the type constant for that class. The directory will then call the module that is responsible for the F15 type, which will return a new F15 object and add the F15 to the directory so that other objects can look it up. Note that the module might provide different implementations of the F15 object to us (in the extreme, every call might return an F15 with a different underlying implementation). However, these are all indistinguishable -- they are all instances of the F15 class and hence have the same type attribute (namely, their type attribute is just the type constant of the F15 class). So, the type attribute of a named interface refers to the most specific interface class of the object -- it does not (in fact, cannot) refer to the (more specific) implementation class of the object!
There are three general classes of objects that you'll create. The first are entities and value-types (see Entities and Values). Entities will usually be named interfaces (i.e., they'll derive from the NamedInterface class so that they can get all of the features described here). Entities are the objects that actually "do something" in our world, and will need to interact with each other. All of the entities that have been constructed as named interfaces will be available in the directory. i.e., you can access the directory to look up an entity by its name or identifier, and then interact with the entity to get your job done. Value-types should never be named interfaces since they have no sense of identity (which contradicts the purpose of this system).
A third class of objects is "descriptions". Descriptions are pieces of shared data that contain descriptive information about a kind of object in the simulator. For instance, a BuildingGeometry object could contain a 3D model of a building. Obviously, we don't want to store the building geometry separately for each building entity that uses it -- so we create a "description" of the building in question that can be shared between all of the objects that want to use that model. Descriptions are values because they are interchangeable -- they do represent a particular object. Meanwhile, we will generally refer to them by pointer, and avoid copying them because they are specifically intended to allow sharing of resources.
From a design standpoint, listeners can be seen as a way of defining an interface to a client. As implementors of an interface, we can provide the client with ways to acquire information from us on a "request" basis -- i.e., the client reads an attribute from our interface when it chooses. If we want to go the other way (that is, "push" information to the client), then our implementation needs to call a method in the client's interface to deliver the necessary information. Of course, not all of our clients will have the same interface. So we provide a listener class that defines the interface that we need and then invoke methods on instances of the listener class. The client then implements the listener interface that we've provided, which generally will result in a call back into the client object to perform the necessary processing. In this situation, the listener is an interface and the client's object is the implementation. So listeners are just a way of adapting the interface of a client object to the needs of our own system.
Unfortunately, one caveat with these types of pointers is circular referencing. If A has a pointer to B and B has a pointer to A then each object will have a reference count of 1. If we release all other pointers to these objects then we will have no way to access them, and yet the objects will not be deleted because each still has 1 reference. The solution to this is not to create circular references with smart pointers. If you must have such a relationship, then you should use raw pointers or a combination of smart/raw pointers.
A helpful tool in writing your code is to draw a bunch of boxes representing instances of your objects in a particular situation. Draw solid lines with arrows pointing to objects to represent smart-pointers to those objects (also called "strong" pointers), and draw dashed or dotted lines with arrows to represent raw pointers (also "weak" pointers). Obviously, you should avoid a situation in which your picture has a "cycle" of solid arrows. Also, if the order in which objects are deleted is important to you, then you should consider what happens as references to objects are deleted. For instance, if you have a long chain of references (e.g., A points to B which points to C, which points to D, etc.) Then when the last reference to A is destroyed, A's reference to B will be destroyed. This may result in the destruction of B, which might result in the destruction of C, etc. But note that if C's reference to D is destroyed, then A, B, and C will not be affected (unless there is a circular reference). In short: objects get destructed in the order implied by the direction of references. i.e., objects at the "tails" of the arrows are destructed before the objects at the "heads" of the arrows.
Some may find this a little hard to use at first -- it's somewhat different from the manual new/delete semantics that most are taught when they first learn C++. The strangest part is that you don't have direct control over when your object is deleted unless you are the only one that holds pointers to the object. If you hold all of the references to an object, then you simply set them all to 0 or otherwise cause the references to be deleted. This will result in the destruction of the object because no other references will exist at that point. However, if other objects might have references to your object, then the object will not be deleted until those other references are released. While this seems unusual at first, it's actually not much different than raw pointers. If you hold all of the raw pointers to a particular object, then you may call "delete" on one of the pointers to destroy the object and then simply throw away all of your raw pointers. If there are other objects with raw pointers to your object then you have a problem: you will need to be sure that all of the other clients know that the object is being deleted. Otherwise, a client might try to access the pointer the the deleted object, causing an error. In short, you still need to know who has references to your object, and you still can't delete the object until those references are no longer in use.
In general: It is always important to understand who is referring to your object, whether they're using smart or raw pointers, and how they will behave in the event that your object needs to be deleted.
A real-life example that will be important for everyone to understand is the named interface directory. This directory stores smart-pointers to all of the objects that it is keeping track of. Thus, for as long as your object is tracked by the directory, it will not be deleted. Even if you release all of your references to the object, your object will still exist in the directory. If you really want your object to be deleted, you'll have to remove it from the directory first.
typedef double Kilograms; ... void mass(Kilograms mass);
typedef double Slugs; void mass(Kilograms mass); ... void foo(void) { Slugs m = 1.0; mass(m); }
If we choose, we can provide "type isolation" that prevents such conversions by defining these types as classes (which cannot be converted between by the compiler). One way to do this is to write a separate class for each type:
class Slugs { public: Slugs operator+(Slugs) const; Slugs operator-(Slugs) const; Slugs operator/(Slugs) const; Slugs operator*(Slugs) const; ... private: double mSlugs; };
Using operator overloading, this type can be manipulated just like a double. Perhaps better, we can restrict our operators to prevent undesired behavior. For instance, a "SerialNumber" value might not allow operator< and operator> to be used, but only operator==. Also, if we similarly defined a Kilograms class as with Slugs above, the foo() method above will trigger a compile error -- because Slugs cannot be converted to Kilogram (unless one of the classes provided a conversion -- in which case we could actually convert the units automatically).
You needn't write this code yourself every time. Phoenix provides template classes that will wrap primitive types in this manner for you. See ValueType.
When you create an entity or description class, it should derive either directly from this class or from another class that derives ultimately from NamedInterface. As a result, all named interfaces will provide the standard type(), name() and identifier() attributes. (This is one nice property of "attribute only" interfaces -- the inherited interface merges cleanly with your own interface, which is also composed of attributes). One consequence of this is that named interface subclasses shouldn't use type, name or identifier as names for their own attributes.
If your named interface class only defines a base class (i.e., it is an interface to which other objects will conform, but you cannot instantiate it directly), then deriving from NamedInterface (directly or otherwise) is all that's necessary. Subclasses of your class will handle the rest.
If your named interface can be instantiated (i.e., a client can request new instances of your interface from the directory) then you should add two additional things to your class. These are const static member variables which define the type constant and, optionally, the type name of the interface type. By convention, these should be named INTERFACE_TYPE and INTERFACE_TYPE_NAME with types NamedInterface::Type and String respectively:
// PhxMyNamedInterface.h class MyNamedInterface : public NamedInterface { public: NamedInterface::Type INTERFACE_TYPE; String INTERFACE_TYPE_NAME; }; // PhxMyNamedInterface.cpp MyNamedInterface::Type MyNamedInterface::INTERFACE_TYPE_NAME(100); String MyNamedInterface::INTERFACE_TYPE_NAME("MyNamedInterface");
The type constant will be assigned by a centralized authority to make sure that they're unique (although complaints from the code will likely show up if you try one that isn't unique). The text name should be the fully qualified name of the class, excluding the Phx:: namespace prefix. So if the class is in the top-level Phx namespace, the text name is just the class name. Otherwise, it will be the names of the enclosing namespaces/classes, separated by ::, followed finally by the class name (e.g., MyNameSpace::MyClass::MyNamedInterface).
Note that the Manager class itself cannot be instantiated -- there is no publicly accessible implementation. In reality, the manager is partly integrated with the the core application code, but you will only ever see the directory service through the interface described by NamedInterface::Manager. You will usually be provided with a pointer to the interface manager when your module loads. After this point, you will not be able to access the manager -- so you'll want to store the pointer if you want to access the directory at runtime.
The class also wraps the C++ casting operators. The following code demonstrates:
Ptr<T> tPtr = new T(); Ptr<Y> yPtr = Ptr<Y>(dynamic_cast<Y*>(tPtr.ptr())); // long way Ptr<Y> yPtr2 = tPtr.dynamicCast<Y>(); // short way
The purpose of the smart pointer class is to wrap a raw pointer and keep track of how many references exist to that memory. For instance, if I create an Aircraft and there are three smart-pointers pointing the Aircraft, then its reference count is 3. If the reference count ever drops from 1 to 0 (i.e., the last smart pointer pointing to it was destroyed) then the object, by default, is freed automatically. An example of how this works:
Ptr<MyObject> smartPtr(new MyObject()); Ptr<MyObject> smartPtr2 = smartPtr; smartPtr->foo(); smartPtr = 0; // destroyed a reference to the object smartPtr2 = 0; // destroyed another reference to the object; no references left. // generally, the object would be freed by this point, automatically
Our smart pointer class (Ptr) is what's called an "intrusive" pointer. Unlike the Boost pointer class which stores the reference count in a hidden structure, this class embeds the reference counting code in the referenced object itself. This interface is:
class MyObject { void newReference(void) { /* new pointer to this was created */ } void deleteReference(void) { /* a pointer to this was destroyed */ } };
Note that this interface says nothing about the reference count itself, or what happens when references are created or deleted. The example given previously simply calls newReference() (twice - once when smartPtr is created and again when smartPtr2 is created), and deleteReference() (twice - once when smartPtr is set to 0, and again when smartPtr2 is set to 0). The typical use of these methods is to increment and decrement a reference count. When that count goes from 1 to 0, we delete the object. But this doesn't mean this must be the case -- we can implement this interface however we'd like to perform special processing when all of the references are destroyed, etc.
Most people will never write this code. To support the standard case, we provide two off-the-shelf implementations of this interface called PtrInterface and LockedPtrInterface. They are identical except that LockedPtrInterface adds locking to protect the reference counting process in a threaded program (and hence, this is the one that most objects will use). PtrInterface simply implements the scheme described above: when a new reference is created it increments its reference count; when a reference is deleted it decrements its count. When the count goes to 0, the object is deleted. The actual code to use this interface is:
class MyObject : public PtrInterface<MyObject> { };
This is sufficient to embed the standard reference counting code into MyObject. If MyObject derives from a base class that derives from PtrInterface, then that is sufficient (you don't want multiple PtrInterface base classes). The template parameter of PtrInterface is to guarantee that MyObject gets a PtrInterface base class (which is just mechanical goop and not conceptually important in our class hierarchy) that is a different type than the PtrInterface underlying other classes.
PtrInterface (and LockedPtrInterface) provides two additional methods:
references() returns the current reference count. For LockedPtrInterface this is unreliable, since the reference count may have changed from the time the method was called and the time you take any action based on the value it returns.
onZeroReferences() is a virtual function that can be overridden by the base class. This is called when the reference count reaches 0. Its default implementation does "delete this;" which deletes the object. You could override it to do other things instead or in addition. For instance, if the object is always going to be statically allocated (i.e., not allocated via "new") then you could override this with an empty function so that the object is not deleted when all references are destroyed (since this would be bad for a statically allocated object).
On this same note, you should NOT use smart-pointers on ANY statically allocated object unless something has been done to make this safe (as described above). The following is bad:
MyObject object; Ptr<MyObject> ptr(&object); // When ptr is destroyed, delete &object is executed. // But object is statically allocated!
For the same reason, you should not construct a smart pointer from a raw pointer that was passed to your code by a client unless you know for certain that the raw pointer is already being used with smart-pointers elsewhere. It may be the case that the raw pointer is a pointer to static storage, or is not referenced by other smart-pointers. In either case, creating a smart pointer will yield incorrect behavior as in the following examples:
void foo(MyObject* myObj) { Ptr myObjPtr(myObj); // BAD: don't know where myObj came from. // It might be created in any of the ways listed below, and we shouldn't assume // the caller always does it the same way. } ... MyObject* obj1 = new MyObject(); foo(obj1); // obj1 will be deleted before returning! // That is probably not what we intended. MyObject obj2; foo(obj2); // obj2 will be deleted before returning! // This is wrong because obj2 is statically allocated. Ptr obj3(new MyObject()); foo(obj3); // This is okay. We hold a reference to obj3, so it won't be deleted. // foo() only works in this case -- which is an unsafe assumption most of the time.
A handy use of smart-pointers is to assure proper memory cleanup after returning from a function, or when exceptions may be thrown.
void foo(void) { MyObject* myObjectPtr = new MyObject(); Ptr<MyObject> myObjectPtrSmart(new MyObject()); // do something }
When the function returns, myObjectPtr is orphaned and its memory is leaked. myObjectPtrSmart is destructed, the reference count of its object goes to 0, and the memory is freed correctly. Also, if the code in "do something" throws an unhandled exception, the stack cleanup will ensure that the memory pointed to by myObjectPtrSmart is freed correctly. Again, myObjectPtr will not get freed and its memory will be leaked.
LockHolder is a simple lock holder that acquires a lock in its constructor and unlocks the lock in its destructor. This is done as follows:
Lock gLock; void foo(void) { LockHolder lockHolder(&gLock); // calls gLock.lock() // do something } // end of scope: LockHolder::~LockHolder() calls gLock.unlock()
Note that the above code is exception safe. If an exception is thrown, the LockHolder will be destructed and the lock will be released.
ReadLockHolder and WriteLockHolder work similarly. ReadLockHolder acquires the "read" privilege of a ReadWriteLock; WriteLockHolder acquires the "write" privilege. Both of them release the lock on their destruction.
Sometimes we need to pass responsibility for a lock from one lock holder to another. If we've already constructed a LockHolder that holds a particular lock, no other LockHolder can acquire the lock until the existing LockHolder is destructed (and, hence, the lock is released). But if we want to transfer control of the lock from one holder to the other without releasing the lock in between, we can do so by constructing the new lock holder as follows:
Lock gLock; void foo(void) { LockHolder lockHolder1(&gLock); // calls gLock.lock(); LockHolder lockHolder2(&lockHolder1); // takes control of gLock from lockHolder1 // lockHolder1 is "inert". Its destructor does nothing. // lockHolder2 holds gLock. Its destructor calls gLock.unlock(); }
Any example of how this is used:
class MyClass { class BitCountClass; // declare, but never defined. public: typedef ValueType<BitCountClass, uint32_t> BitCount; };
In this example, MyClass::BitCount is an alias for ValueType<BitCountClass, uint32_t>. Because this is a type in itself (defined by the two template arguments), it cannot be converted to any other type based on different parameters. So, if we had another value type based on ValueType<BitIndexClass, uint32_t>, then it could not be converted to our BitCount type because the first template parameters are different classes. This trick provides "type isolation" between the two types, meaning that they can't accidentally be coerced into other types.
Any primitive type (more specifically, any type T for which "T t(0);" makes sense) can be used as the second template parameter. The default constructor of a ValueType<C, T> will construct a new T whose value is 0 (by calling T::T(0)).
The Core itself provides one facility to outside objects. The NamedInterface::Manager that keeps track of objects is stored within the Core. Modules will generally only need to interact directly with the core when they're initialized.
In Phx/Core/PhxModule.h, a base class is provided from which "Module" objects derive. A module has no interface. It can only be loaded (constructed) and unloaded (destructed). When a module is loaded, the Core calls (through the module loader) a C-style function that returns a smart-pointer to a Module object.
Modules can either be loaded from shared libraries (DLLs) or directly within the simulator (in the case of modules linked directly into the program). In either case, a module is loaded via a C-style function.
To create a module, two things are necessary. The first is to create a subclass of Module. This class will get created when your module loads, and deleted when your module unloads. If your module needs to do any setup (like register types with the named interface manager) or has any data to load, this should all be implemented in your Module class.
In addition, you need to define a function with C-style linkage. This function should have the name module_ModuleName(). Where "ModuleName" is the name of your module. If your module is loaded from a shared library, then ModuleName is the name of the shared library. e.g., if your library is PhxInput.so.0.0.0, or PhxInput.dll, then ModuleName is just PhxInput. For internal modules (those not loaded from a library), use something similar (i.e., use the name you'd use for the library if you ever created it). The prototype for this function is:
extern "C" Phx::Module* module_ModuleName(const Phx::Ptr<Phx::Description>&, Phx::Core*);
Your function should only appear in your source file. Because of the extern linkage, it can be linked against by outside code. For modules that are built in to the program, these prototypes will just be added to a file elsewhere to allow for direct calls. In the case of a shared library, these function will be located by the dynamic loader and called at runtime -- no prototype is needed in that case. In short: you do not need (or want) a publicly accessible prototype.
The function should be implemented to return a new, dynamically allocated, instance of your module. The dynamic allocate is a critical detail: you must use the 'new' operator to construct your module and return it -- DO NOT statically allocate your object. On return, the raw pointer will be converted to a smart pointer, which will do 'delete' on the pointer when it is no longer needed. Thus, statically allocating your module will result in a crash. Ideally, we'd simply return a Ptr<Module> here, but some compilers do not allow C-linkage functions to return C++ objects.
The first parameter is a pointer to a Description that can contain configuration information for your module. This will normally by parsed from an XML config file, but it can just as well be sent over the network or loaded from a binary file like other Descriptions. If you don't need any configuration information, you can just ignore this.
The second parameter passed to the function is a pointer to the core. The Core has a few methods that provide access to the simulator's named interface manager, description manager, etc. You can install your interface types and any objects you choose into these managers at startup. If you anticipate needing the managers at runtime, you should store the raw pointer to the core, or to any of the accessible components. The managers held by the Core are the last objects to be destructed, so these pointers will always be valid when your module is loaded.
Note that objects in the simulator that depend on your module (and its library!) will cease to function properly if the module or library is unloaded. Generally, your library is unloaded when your Module object is destructed. It is the client's responsibility to be sure that all objects constructed using your module are destroyed before your Module object is destroyed -- you do not need to worry about it. You may assume that your library and Module object are loaded and will not be destroyed/unloaded until all of the publicly accessible objects in the Core, etc. that use it are removed. The obvious exception, of course, is that the module must, when it is destructed, remove any objects that it created on its own -- the client obviously won't know anything about these.
This clearly puts a large burden on the client. In the case of the Core, most of the modules are loaded at startup and only unloaded when the program exits. A more intelligent system may, in the future, attempt to load inidividual modules only temporarily. For instance, a special campaign may attempt to load just a handful of libraries containing objects needed for the current campaign. In this case, the campaign would need to be certain that it had destroyed all of its objects first, then unloaded the module as the very last operation when it comes time to delete the campaign.
#include <Phx/Core/PhxCore.h> #include <Phx/Core/PhxClock.h> ... Ptr<Clock> frameClock = interfaceMgr->namedInterface(Core::WALL_CLOCK_NAME).dynamicCast<Clock>();
With your Task, you can install a listener that calls back into your code to do whatever processing you want. In order to get your listener called, you need to set its nextTime() attribute. This attribute is the next time at which your listener will be called. When the Clock for that Task reaches the time you set, your listener will be called and the nextTime attribute will be set to Clock::NEVER. If you set nextTime() to a time that is earlier than the current time, your listener will be called back as soon as possible and the nextTime attribute will be set back to NEVER. Note that setting nextTime() to 0 will always result in an immediate callback, since the current time on the clock is always positive. Also, a callback is initiated for EACH time that the nextTime() attribute becomes less than the current clock time. Hence, if you set nextTime() to zero once, it will be set back to NEVER and your listener will be called back as soon as possible. If you do this twice, your listener will be scheduled for TWO calls.
1.4.2