BetterMUD s Python Library

[ LiB ]

BetterMUD's Python Library

I found that all those C-style function calls and the manual reference counting management were extremely annoying parts of the API. Maybe it's just me, but I take routes that make my life easier. I've always said that programming is the art of avoiding work . If you think about it, it makes sense. Why would you waste your time coding 800 manual dereferences of Python objects, when you could write a simple wrapper around them to automatically handle it for you?

You can find the code for my Python library in the directory /BetterMUD/BetterMUD/ scripts/python.

PythonObject Class

My version of a Python object is called PythonObject . It's a simple wrapper around a PyObject* , but it uses constructors and destructors to automatically increment and decrement reference counts. You can find this class inside the PythonHelpers.h and .cpp files.

Here is the class skeleton:

 class PythonObject { public:     PythonObject( PyObject* p_object = 0 );     PythonObject( const PythonObject& p_object );     ~PythonObject();     PythonObject& operator=( const PythonObject& p_object );     PythonObject& operator=( PyObject* p_object );     PyObject* get() const;     bool Has( const std::string& p_name ) const;     std::string GetNameOfClass();     std::string GetName(); protected:     PyObject* m_object; }; 

Managing Reference Counts

The constructors are pretty simple; you can construct a PythonObject from either a pointer to a PyObject , or a reference to another PythonObject class.

The first constructor that takes a PyObject* simply assigns the parameter to m_object . It doesn't do anything else. It is designed this way because I am assuming that whenever you pass a PyObject* into the first constructor, it assumes that you're passing in a new reference that is returned from a Python function call.

On the other hand, if you construct an object from another object, the constructor is a little more complex:

 PythonObject( const PythonObject& p_object ) :     m_object( p_object.m_object ) {     Py_XINCREF( m_object ); } 

The first thing this does is assign p_object 's PyObject* to m_object . Once that is done, the function increases its reference count by calling Py_INCREF . Why do I do this? If you're copying something over from another PythonObject , that means you now have two PythonObject s, each with pointers to the same PyObject* , so you need to tell Python that you're now pointing to the object from two places.

On the other hand, the reference count is decremented when the object is destructed:

 ~PythonObject() {     Py_XDECREF( m_object ); } 

There are also two operator= s, which operate along the same principles:

 PythonObject& operator=( const PythonObject& p_object ) {     Py_XDECREF( m_object );     m_object = p_object.m_object;     Py_XINCREF( m_object );     return *this; } PythonObject& operator=( PyObject* p_object ) {     Py_DECREF( m_object );     m_object = p_object;     return *this; } 

The difference with these functions (as opposed to the previous two) is that when they are called, the PyObject* inside the class already exists and owns a reference count, so you can't simply overwrite m_object with the new pointer. If you do that, Python thinks that you're using the previous object and never deletes it, even after you've long forgotten it.

NOTE

Forgetting to dereference PyObject* s is a great way to introduce huge memory leaks into your program. Amaze your friends , be the life of the party! Leak memory now!

Once the outdated objects are dereferenced, you can reassign the pointer, increasing the count if you're copying it over from another PythonObject , or doing nothing if it's from a PyObject* .

Because of this behavior, you can now rewrite some lines from Demo 17.2:

 { PythonObject p; p = PyObject_CallMethod( mod, "printfunc", "s", "HELLO PYTHON!" ); p = PyObject_CallMethod( mod, "printfunc", "i", 42 ); } 

Now, when you issue the second call to PyObject_CallMethod , the operator= of the PythonObject class automatically dereferences the return value of the outdated object. Whenever p goes out of scope (which should be after the last curly bracket in the previous code), it automatically dereferences its current object (which is the return value from the second call, in this example).

Not only is the code cleaner, but I shaved off two lines and eliminated a possible source of huge errorsforgetting to dereference the return values. We're human, and we often forget things. So why bother ourselves with remembering every little detail?

See how you can make your life easy? I love avoiding work.

PythonObject Helper Functions

The helper functions assist you in performing specific tasks . For example, I didn't include a conversion operator that automatically converts a PythonObject into a PyObject* , because that would wreak havoc with the constructors and produce lots of errors (sometimes C++ can be a pain).

Instead, I provide a get function, which simply returns the pointer it is holding. This way, you can pass the object into Python calls. Here is a modified version of some code from Demo 17.2:

 PythonObject p = PyObject_CallMethod( mod, "returnstring", null ); std::string str = PyString_AsString( p.get() ); std::cout << str << std::endl; 

Another helper is the Has function, which determines if a Python object contains an object (could be a class, an instance, a variable; anything!) with a given name . Believe it or not, this is simple to accomplish, because the API has a function like that built right in:

 bool Has( const std::string& p_name ) const {     return (bool)PyObject_HasAttrString(         m_object, const_cast<char*>(p_name.c_str()) ); } 

This checks to see if an object contains an object with the given name. Again, since Python doesn't use const strings, I need to manually cast away its const characteristics. It's a bit ugly, but it gets the job done.

Every Python object has a name, so it makes sense to design the code to make it easy to extract these names. The two functions that receive names from objects are shown in this code snippet:

 std::string PythonObject::GetName() {     PythonObject name = PyObject_GetAttrString( m_object, "__name__" );     return StringFromPy( name ); } std::string PythonObject::GetNameOfClass() {     PythonObject cls = PyObject_GetAttrString( m_object, "__class__" );     PythonObject name = PyObject_GetAttrString( cls.get(), "__name__" );     return StringFromPy( name ); } 

The first function should be fairly straightforward. Every Python object has an attribute called __name__ , which is a string representing its name. I extract that object from the current object and call the custom StringFromPy function to convert it into a C++ std::string .

The second function is a little trickier; it's meant to be called on objects that are assumed to be instances of a class. If you call it on an element that isn't an instance of a class, you may crash the program. Two steps are required to get the class name of an instance.The first step is to get an object representing the class, which you can do by grabbing the __class__ attribute. Then, you need to grab the name of the class, by getting its __name__ attribute. Finally, you can return the value as a string.

Data Conversion Helpers

The bunch of helper functions I've included for converting types to and from Python automatically use PythonObject s to manage the reference counting. I'm going to show you the implementation of one extractor, and one inserter, and then just the names of the rest. Here are the implementations :

 inline PythonObject LongToPy( long p_obj ) {     return PyInt_FromLong( p_obj ); } inline long LongFromPy( const PythonObject& p_obj ) {     return PyInt_AsLong( p_obj.get() ); } 

They're just simple wrappers, utilizing my object class, so that there are no memory leaks. Here's a listing of all the converters:

 inline PythonObject IntToPy( int p_obj ); inline PythonObject LongToPy( long p_obj ); inline PythonObject EntityToPy( entityid p_obj ); inline PythonObject LongLongToPy( BasicLib::sint64 p_obj ); inline PythonObject DoubleToPy( double p_obj ); inline PythonObject FloatToPy( float p_obj ); inline PythonObject StringToPy( std::string p_obj ); inline long LongFromPy( const PythonObject& p_obj ); inline entityid EntityFromPy( const PythonObject& p_obj ); inline BasicLib::sint64 LongLongFromPy( const PythonObject& p_obj ); inline double DoubleFromPy( const PythonObject& p_obj ); inline std::string StringFromPy( const PythonObject& p_obj ); 

NOTE

I originally implemented these functions as specialized template functions, in which you could write ToPython( blah ) , and the function would automatically convert whatever type you had into a Python object, or FromPython<double>( obj ) , which does the opposite . Using templates would have allowed you to write prettier code, but alas, it just wasn't in the cards. VC6 has major problems with template special ization, and absolutely refused to make the template functions work, so I had to resort to creating separate conversion functions for each type. VC6 is a worthless piece of junk, and you should upgrade to VC7 as soon as you can. This has been a public service announcement. Thank you.

Automating Callable Objects

To make calling Python functions and classes easier, I created the concept of a PythonCallable class, which essentially calls a method inside a Python object. This class is located within the PythonScript.h and .cpp files.

The Class

Here is a listing of the class:

 class PythonCallable { public:     PythonCallable();     PythonCallable( PythonObject& p_object );     PythonObject Call( std::string p_name );     PythonObject Call( std::string p_name, const PythonObject& p_arg1 );     // *****SEE EXPLANATION*****     bool Has( const std::string& p_name ) const;     PyObject* get() const { return m_module.get(); } protected:     PythonObject m_module; }; 

This class actually has more functions than I've shown you. There are another five versions of the Call function, each one taking another parameter. There are versions for 0, 1, 2, 3, 4, 5, and 6 PythonObject arguments. Let me make it extremely clear to you right now: this is an ugly hack . However, it is necessary. The flexibility of Python allows passing dynamic datatypes and lists of variable arguments, but when you do this you're obviously going to run into a problem when you try to interface it with C++, a static language with fixed numbers of arguments.

While it is true that C supports variable argument lists, their support is not standard. There are tiny little quirks that tend to screw things up at the most inconvenient times. Rather than mess with all that, I chose to avoid it, and use this hack instead.

The other functions of this class simply wrap around a PythonObject.

Calling Python

Since I basically copy the same function over a few times ( Call ), I decided to make the process a little less painful by creating a helper macro:

 #define PYTHONCALL( CALL )                                               \     PythonObject r;                                                      \     try{ r = CALL }                                                      \     catch( ... ) {                                                       \         PyErr_Print();                                                   \         throw Exception( "Python Function Call Failed" );                \     }                                                                    \     if( r.get() == 0 ) {                                                 \         PyErr_Print();                                                   \         throw Exception( "Python Function Call Failed" );                \     }                                                                    \     return r; 

This macro is designed to wrap around some Python API function calls that return a new PyObject* . The macro creates a result object named r and then tries calling the function.

In case anything throws, the function catches it, prints out the Python error, and then rethrows the exception inside a BetterMUD::Exception class.

If the function returns 0, it failed, and again, the Python error is printed, and an exception is thrown.

Finally, if all goes well, the result object is returned.

Here's the 1-argument version of Call (which passes 0 arguments into the Python function):

 PythonObject PythonCallable::Call( std::string p_name ) {     PYTHONCALL(         PyObject_CallMethodObjArgs(             m_module.get(),             StringToPy( p_name ).get(),             null ); ) } 

I use the PyObject_CallMethodObjArgs function, which requires a Python string of the name of the object you are calling, and it has a variable argument list of all the objects you're passing in, which must be terminated with null . Since the PYTHONCALL macro wraps around the call, a result object named r is created and returned (or an exception is thrown if an error occurs).

For comparison, here is the two-argument version, which passes in one argument to the Python function (changes from the original are in bold):

 PythonObject PythonCallable::Call(     std::string p_name,  const PythonObject& p_arg1 ) {  PYTHONCALL(         PyObject_CallMethodObjArgs(             m_module.get(),             StringToPy( p_name ).get(),  p_arg1.get(),  null ); ) } 

The other five versions are similar, but have more arguments. If you ever need more than six arguments passed into a Python function, you can easily copy and paste more functions into the code.

Finally, here is how you would call a PythonCallable object named obj , with hypothetical function names:

 obj.Call( "testfunction" ); obj.Call( "needsargument", IntToPy( 42 ) ); std::string str; str = StringFromPy( obj.Call( "returnsstring" ) ); str = StringFromPy( obj.Call( "returnsstringneedsarg" ), IntToPy( 42 ) ); obj.Call( "needs2args", IntToPy( 42 ), FloatToPy( 3.14159 ) ); 

See how easy that is? If you know the code is not going to return anything, you don't care about return values, because the functions all deal with self-managing PythonObject s.

I love it when everything works out without worrying about all the minor details. Don't you?

Python Modules

I've abstracted Python modules into my own class, named (very originally, I might add) PythonModule . The PythonModule class is somewhat complex, and that's for a very good reason: script reloading.

Problem With Reloading

To gain some experience with the reloading problem, open your Python interpreter again, and start playing around with code like this:

 >>> class reloaden: ...     def funky(self): ...         print "OLD!!!" ... >>> a = reloaden() >>> a.funky() OLD!!! 

Now that you've done that, "reload" the class, by redefining it:

 >>> class reloaden: ...     def funky(self): ...         print "NEW!!!" ... >>> b = reloaden() >>> b.funky() NEW!!! >>> a.funky() OLD!!! 

So what happened here? You created a class named reloaden , and defined a function named funky , which printed out OLD!!! After that, you created an instance of that class, and called its funky function.

Then, you "reloaded" the class, by typing in a new definition, created a new instance named b , and called its funky function, which printed out NEW!!! , just as expected.

The last line, however, calls a.funky() again, which, instead of printing out NEW!!! , still prints out OLD!!! !

Argh! What the heck! Yeah, those were my first reactions too. Well, this behavior actually makes sense. When you loaded the first class object of reloaden (by typing the definition), you created a class object called reloaden . When you typed a = reloaden() , you created a new instance object that points to the class object .

When you redefined reloaden , the old reloaden had not been deleted; it was just hidden because a still references it. But whenever you create new instances of reloaden , the new instances all point to the new version, even though the old version still exists. In fact, the old version of reloaden exists until a stops referencing it. At that point, Python knows that the old reloaden is longer in use, and cleans up after itself.

Figure 17.5 illustrates the process from the code I showed you previously.

Figure 17.5. Reloading classes an illustration of the process of the previous example.

graphic/17fig05.gif


Not having automatic reloading can give you a major headache in a game situation. Imagine that you have a bunch of characters using a logic module that has a flaw. You reload the module with a fixed version, but there's a huge problem! Every instance that used the old module still points to the flawed module! Only new instances of the module use the correct code. Stupid Python! (BLASPHEMY!)

NOTE

You can still create new instances of the old class, if you're so inclined. Try typing this into the interpreter: c = a.__class__() . That copies the old class, even though you can no longer create it by calling reloaden() . This is an example of one of the many things you can do with the flexibility of Python.

Fixing the Problem

If you still have your interpreter open, you can easily fix a so that it uses the new version of reloaden . Just type this:

 >>> a.__class__ = reloaden >>> a.funky() NEW!!! 

You just re-assigned the reloaden class object. The reference count of the old version of reloaden is decreased by this operation, and whenever you call functions on a , it references the new version of reloaden , instead of the old one.

You can easily do the same thing from within C++

 PyObject_SetAttrString( instanceobj, "__class__", classobj ); 

This code assumes that instanceobj is a PyObject* pointing to an instance of the old class, and classobj points to the new version of the class.

Unfortunately, when you reload a module, the module has no idea what instances it created previously, so it must search out all these instances and tell them to reload themselves .

That is a difficult problem, but I've managed to solve it and automate it by using two classes. My PythonModule class generates new PythonInstance objects, and whenever it generates one, it adds a pointer to it into a list. Whenever you tell the module to reload, it goes through the whole list, and tells every instance to reload itself with a new version of its class.

This has disadvantages, however. Whenever an instance is deleted, the instance must tell its module that it no longer exists.

Figure 17.6 shows you how modules and instances form a simple tree, in which modules tell each instance when it must be reloaded, and each instance tells its module when it no longer exists.

Figure 17.6. The simple layering structure of Python modules and instances.

graphic/17fig06.gif


Module Class Interface

The PythonModule class interface looks like this:

 class PythonModule : public PythonCallable { public:     std::string Name() { return m_module.GetName(); }     void Load( const std::string& p_module );     void Reload( PYTHONRELOADMODE p_mode );     PythonInstance* SpawnNew( const std::string p_str );     void DeleteChild( PythonInstance* p_instance ); protected:     typedef std::list<PythonInstance*> spawnlist;     spawnlist m_spawns; }; 

PythonModule objects are loaded by passing in a string to the Load function. If you want to

load a file named "testpython.py", you call Load( "testpython" ) . In response, the Reload function takes a mode in which you can reload. There may be times (however rare) when you want to reload a module, but allow everyone to temporarily keep the old version of the script, and the function allows you to specify if you want to do that. You can pass in two values: LEAVEEXISTING , and RELOADFUNCTIONS . The first mode simply reloads the module and doesn't touch the instances at all. The second mode goes through all the instances and updates their class objects.

If the module you loaded has a class named "pies", you can create an instance of that class by calling SpawnNew( "pies" ) . You should note that this returns a brand new PythonInstance* , and when you finish using it, you should manually delete it (because the module won't).

The final function is the DeleteChild function, which PythonInstance s call when they are destructed, so that the module knows that it no longer has to update that instance.

Loading a Module

Loading modules is easy:

 void PythonModule::Load( const std::string& p_module ) {     PythonObject p =         PyImport_ImportModule( const_cast<char*>( p_module.c_str() ) );     if( p.get() == 0 )         throw Exception( "Couldn't load python module: " + p_module );     m_module = p; } 

The code simply tries to load the module from disk, and throws an exception if it can't.

Spawning New Instances

Spawning new class instances is a more complex process, since many things can go wrong. Here's the code:

 PythonInstance* PythonModule::SpawnNew( const std::string p_str ) {     try {     PythonObject c =         PyObject_GetAttrString(             m_module.get(),             const_cast<char*>(p_str.c_str()) );     if( c.get() == 0 )         throw Exception( "Could not find python class: " + p_str ); 

The previous code chunk tries to load a class object from the module ( pies from my example previously). Not being able to get that class object means that this module doesn't have a class with that name, and it just throws.

 PythonObject i = PyInstance_New( c.get(), null, null );     if( i.get() == 0 )         throw Exception( "Could not create python class instance: " + p_str ); 

Now the code tries to create a new instance of that class using PyInstance_New . If the instance couldn't be created, then it also throws:

 PythonInstance* mod = new PythonInstance( i, this );     if( !mod )         throw Exception( "Error allocating memory for python module" );     m_spawns.push_back( mod );     return mod;     } 

The previous code then creates a new PythonInstance object by passing in the PythonObject representing the instance ( i ), as well as a pointer to this , the module that the instance was spawned from. The new module is added to the list of instances, and the pointer to the brand new PythonInstance class is returned.

 catch( Exception& e ) { throw; }     catch( ... )        // catch any extra errors we didn't grab before     {         PyErr_Print();         throw Exception( "Unknown error attempting to create class "                          "instance: " + p_str );     } } 

Finally, the code catches and rethrows any exceptions (so that the next block of code can work), or catches anything else that happened, prints out a Python error, and throws.

Reloading

Reloading is a simple affair:

 void PythonModule::Reload( PYTHONRELOADMODE p_mode ) {     m_module = PyImport_ReloadModule( m_module.get() );     if( p_mode == LEAVEEXISTING )         return;     spawnlist::iterator itr = m_spawns.begin();     while( itr != m_spawns.end() ) {         (*itr)->Reload();         ++itr;     } } 

The module is reloaded using PyImport_ReloadModule , and then (if the caller wishes) the function loops through all its instances and reloads them.

Using a Module

You can use the module class quite easily. Since it inherits from PythonCallable , you can even use it to call functions inside the module. Here is a hypothetical example of using the class on a module named pies , which has a function named foobar , and a class named blarg :

 PythonModule mod; mod.Load( "pies" ); mod.Call( "foobar" ); PythonInstance* inst1 = mod.SpawnNew( "blarg" ); mod.Reload( LEAVEEXISTING ); PythonInstance* inst2 = mod.SpawnNew( "blarg" ); mod.Reload( RELOADFUNCTIONS ); delete inst1; delete inst2; 

The code loads a module named pies , and then calls its foobar function. It then creates a new instance of class blarg and stores it in inst1 . After that, the module is told to reload, but it keeps all the existing instances and does not update them.

Then, the code spawns a new instance, inst2 . At this point, both instances are pointing to different classes, with inst1 as the old version of blarg , and inst2 as the new version of blarg .

NOTE

In fact, I've hidden the copy con structor and operator= for both the PythonInstance and PythonModule class, to make absolutely sure that you never copy them to another loca tion, ever . Copying them completely messes up the pointer hierarchy that they must have to reload modules properly.

Finally, the code reloads the module again, this time reloading everything, so that both inst1 and inst2 are pointing to the very newest version of blarg . In the last step, the code deletes both instances; this is an important action, because the module, when spawning a new instance, actually spawns a new C++ instance object, which you must delete, or face a memory leak.

I chose to do things this way because of the complex relationship between instances and modules. An instance must point to its module, and a module points to its instances, so you can easily reload them when needed. It's a bad idea to copy these objects, since copying them automatically changes their location in memory.

Python Instances

The PythonInstance class also inherits from PythonCallable , so that you can call class functions on it quite easily.

Class Skeleton

Here is the class skeleton:

 class PythonInstance : public PythonCallable { public:     PythonInstance(         PythonObject p_instance,        // instance object         PythonModule* p_parent );       // pointer to parent     ~PythonInstance();     std::string Name();     void Reload()     void Load( std::istream& p_stream );     void Save( std::ostream& p_stream ); protected:     PythonModule* m_parent; }; 

The class is simpler than a module, and represents a class instance. Instances have names, can be reloaded, and can also be loaded and saved to disk. I discussed this idea in Chapter 13. All class instances must have the ability to load and save themselves from streams, because they may have extra data that should be retained (such as a command object that remembers the last time it was executed). When the entity that owns the class is reloaded from disk from the BetterMUD, the data associated with the Python module should be loaded.

Deleting the Instance

Whenever an instance is destructed, it must notify its parent that it no longer exists:

 PythonInstance::~PythonInstance() {     if( m_parent )         m_parent->DeleteChild( this ); } 

Reloading

Whenever an instance is told to reload from disk, it must overwrite its __class__ attribute. This isn't a difficult task:

 void PythonInstance::Reload() {     std::string clsname = m_module.GetNameOfClass();     PythonObject cls = PyObject_GetAttrString(         m_parent->get(),         const_cast<char*>(clsname.c_str()) );     if( cls.get() == 0 )         throw Exception( "Could not find python class: " + clsname );     PyObject_SetAttrString( m_module.get(), "__class__", cls.get() ); } 

The function first gets the name of the current class object of the instance (assuming it is loaded. PythonInstance objects should not exist unless they have been loaded), and then it retrieves a pointer to the Python class object and stores it into cls .

If cls doesn't exist, that means the class was probably deleted when the module was reloaded, and that's no good. Therefore an exception is thrown.

The final step is to reset the __class__ attribute and make it point to the new class object.

Once this has been completed, the instance object points to a reloaded version of its class.

Disk Operations

As I mentioned earlier, all PythonInstance objects know how to write themselves out to streams. This is required because instances may hold data that must be preserved.

The standard way of representing an instance is like this:

 instancename [DATA] ... data goes here ... [/DATA] 

Even if there is no data, the tags need to be there. This makes your datafiles look a little ugly, but eventually, you should have a nice editor for the MUD, so that you can't see the ugliness.

NOTE

The stream format for all script objects in the BetterMUD requires that the [DATA] and [/DATA] tags be on their own separate lines. I did this because of the way I pass streamed data into Python, which is the topic I touch on next.

Loading from Streams

When a PythonInstance must be loaded from disk, whatever is loading it first reads its name (usually the entity in charge of the script does this), and that entity creates the instance object. Once the object has been created, the stream is passed into the PythonInstance class, so that it can suck out all the data, and load it into itself. Here is the function:

 void PythonInstance::Load( std::istream& p_stream ) {     std::string str;     std::string temp;     bool done = false;     // read in the "[DATA] tag:     std::getline( p_stream, temp );     // loop until you hit "[/DATA]"     while( !done ) {         std::getline( p_stream, temp );         if( temp == "[/DATA]" )             done = true;         else             str += temp + "\n";     }     // send everything in between to the script     Call( "LoadScript", StringToPy( str ) ); } 

If you're using just primitive objects such as strings and numbers, interfacing between Python and C++ is very easy. There is no easy way to pass a C++ iostream into Python however, so instead of bothering with that, I've decided to use strings to pass streams.

The loading function plucks out the [DATA] tag from the stream using std::getline , which means that the tag must be on a line of its own; everything else on that line is ignored and discarded.

After that, I load things line by line, and store them into str , until I find a [/DATA] tag. By the time the loop finishes, everything between (but not including) the data tags is within str .

I used the std::getline function for a reason. If I just used the operator>> stream extractor, it would eat up all the whitespace between every word inside the tags. Look at this hypothetical example file:

 [DATA] This             string          has             long           spaces [/DATA] 

If I extracted each word until I found [/DATA] , I would end up with a string that contains This string has long spaces , but there wouldn't actually be any long spaces, because the stream extractor automatically discarded them! There may be times when the whitespace inside a data tag has meaning, and I don't want to destroy that. So the function loads the data line by line, and str holds the appropriate number of spaces between its words.

The final line of the function calls the script's LoadScript function, passing in the string as its parameter. Your scripts should know what to do with this data.

Saving to Streams

Luckily, saving to streams is much easier:

 void PythonInstance::Save( std::ostream& p_stream ) {     p_stream << "[DATA]\n";     p_stream << StringFromPy( Call( "SaveScript" ) );     p_stream << "[/DATA]\n"; } 

This simply saves the data tags, as well as the string returned from the Python objects' SaveScript function. It is perfectly legal for the Python script to return ("") if you don't have any data.

NOTE

Please remember to return a string. If you don't return anything, the function returns the null object, which can't be converted to a string, and your program crashes.

Python Databases

In Chapter 12, I told you about the PythonDatabase class, which is a special database class that loads and stores Python scripts. A PythonDatabase is actually a simple class that wraps around a collection of PythonModules . Here is the class skeleton:

 class PythonDatabase { public:     PythonDatabase( const std::string& p_directory );     ~PythonDatabase();     void Load();     void AddModule( const std::string& p_module );     void Reload( const std::string& p_module, PYTHONRELOADMODE p_mode );     PythonInstance* SpawnNew( const std::string p_str ); protected:     void Load( const std::string& p_module );     typedef std::list<PythonModule*> modules;     modules m_modules;     std::string m_directory; }; 

The functions should immediately remind you of the PythonModule class. This class was designed to enable you to separate your related scripts into many files, so that you don't have to shove them all into the same .py file on disk, which can become rapidly disorganized.

Using a managed collection of modules, on the other hand, allows you to do a few things. You can load completely new modules at any time, and you can reload specific modules whenever you want.

I'm not going to show you the Reload or the SpawnNew functions; they simply loop through every module in m_modules until they find a module matching the requested name. Then they call the requested function on the right module. I also won't show the destructor, which simply loops through every module and deletes it.

Loading a Database

Loading the database from a directory is similar to the Database::LoadDirectory function I showed you in Chapter 12. The names of all Python modules that you want loaded are stored in a manifest file:

 void PythonDatabase::Load() {     std::string filename = m_directory + "manifest";     std::ifstream manifest( filename.c_str(), std::ios::binary );     manifest >> std::ws;     while( manifest.good() ) {         std::string modulename;         manifest >> modulename >> std::ws;         Load( modulename );     } } 

Nothing new here. Each module name is loaded from the manifest, and then the Load(std::string) helper is called to add the module to the database.

Adding a Module

Adding a new module while the program is running is also a relatively simple task:

 void PythonDatabase::AddModule( const std::string& p_module ) {     Load( p_module );     std::string filename = m_directory + "manifest";     std::ofstream manifest(         filename.c_str(), std::ios::binary  std::ios::app );     manifest << "\n" << p_module << "\n"; } 

To load the module, the function invokes the Load helper again, and then it opens up the manifest file in append mode, and writes the name of the new module.

Loading Modules

I've written a helper that loads modules by their names. It's a relatively simple process with a few quirks here and there:

 void PythonDatabase::Load( const std::string& p_module ) {     std::string modname = m_directory + p_module;     // convert "/" or "\" to "." for proper module loading     modname = BasicLib::SearchAndReplace( modname, "/", "." );     modname = BasicLib::SearchAndReplace( modname, "\", "." ); 

First I created the name of the module by adding the name of the directory to the name of the module to be loaded. If you created this database with the directory data/logic/characters/ , and you wanted to load a module named "defaultplayerlogic", it would create the string data/logic/ characters/defaultplayerlogic .

The next step is to convert all slashes into dots, because Python loads modules based on dots.The module name would convert to "data.logic.characters.defaultplayerlogic".

 PythonModule* mod = new PythonModule();     if( !mod )  throw Exception( "Not enough memory to load python module" ); 

The previous code creates a new module, and throws if it can't be created. Now here's the tricky part:

 try {         mod->Load( modname );         m_modules.push_back( mod );     }     catch( ... ) {         delete mod;         throw;     } } 

You should try loading the module. Since you created mod using new , if the loading throws an exception, you must be sure to delete the module if something throws, or else you're going to end up with a memory leak.

If the loading goes fine, the new module is added to the list of modules.

BetterMUD Script Databases

The BetterMUD has three database classes that utilize the features of the PythonDatabase class. You've seen them all used before: the CommandDatabase , the ConditionDatabase , and the LogicDatabase . Luckily they inherit most of their functions from PythonDatabase , so you don't have to implement many of the functions in them. In fact, they are so simple, that I'm not even going to waste your time by showing them to you. They're boring and uninteresting, and I want to move on to showing you how to expose C++ classes to Python.

[ LiB ]


MUD Game Programming
MUD Game Programming (Premier Press Game Development)
ISBN: 1592000908
EAN: 2147483647
Year: 2003
Pages: 147
Authors: Ron Penton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net