This page tells how to write a python interface to a C++ class. This isn't about adding a new function to an existing class, but rather how to add a brand new class, which wasn't present in vanilla.
The same goes for enums.
Do note that the version of boost python used by the Civilization IV engine is 1.32, which is from 2004 and is ancient. Online documentation on this topic tells of a lot of features, which doesn't appear to work on this old version. This page is intentionally a small subset of what boost python can do due to this issue. Everything listed here should work despite the age of the library.
Python classes are declared in files, which by convention are named Cy*Interface.cpp
The file contain a functions, which contain the declaration itself. A declaration looks like this:
python::class_<CvXInfo>("CvXInfo")
This will make a python class called CvXInfo and link it to the C++ class CvXInfo.
TODO: confirm that it is <C++ name="">("python name") if they differ, though it's a good idea to call them the same.
The following lines will then contain .def() to declare member functions and then it ends with ;
TODO type something about the boost 64k character limit and figure out if it is a function or file limit
It isn't enough to declare the class in a function. The function also has to be executed when the DLL starts up. This is done in the file called CvDLLPython.cpp.
In here the function is first declared and then called in DLLPublishToPython().
TODO figure out if order of calls in DLLPublishToPython matters. There might be a problem if one class refer to another class before the other class is declared.
The last part of DLLPublishToPython() is a workaround for the 64k limit. More about this later.
The .def() lines have up to 4 arguments. They are:
1. python function name
+ C++ class pointer to call, has to start with &
+ (optional) python behavior flag used when returning class pointers
+ (optional) debug text
Example:
.def("getType", &CvInfoBase::getType, "string ()")
The first mean it is accessed in python by typing ClassPointer.getType() while the next is the address of the C++ function, which is called. Notice that the & sign provides the address of the function, not the function itself. This is why the second argument should always start with &.
"string ()" is referred to as debug text. Presumably it can be made to show up in a debugger, but without a functioning python debugger, this is useless and can be skipped.
Do note that the declaration will look up the C++ header and use the types for arguments and return value. This mean if the function takes an enum value as argument or return value, you have to declare the enum to python as well. The same goes for class pointers.
In order to use the new class in python, the python files have to ask the DLL for a pointer. There are two approaches to this. They are nearly identical in code, but using the wrong approach can lead to memory leaks or freeing memory still in use by the DLL, which results in crashes.
This is to create a pointer to an existing object in the DLL. The DLL intend to use it after python is done using it, which mean python shouldn't free the memory when it's done.
Flag keyword: python::return_value_policy<python::reference_existing_object>()
Example:
.def("getBonusInfo", &CyGlobalContext::getBonusInfo, python::return_value_policy<python::reference_existing_object>(), "(BonusID) - CvInfo for BonusID")
The get function should return a pointer to the object, in this case CvBonusInfo.
This will allocate a new object and give it to python. The DLL is not supposed to keep a pointer to it, which mean python becomes responsible for freeing the memory. Luckily python does that automatically when it goes out of scope, meaning it will happen automatically.
Flag keyword: python::return_value_policy<python::manage_new_object>()
Example: .def("getCity", &CyPlayer::getCity, python::return_value_policy<python::manage_new_object>(), "CyCity* (int iID)")
The get function should use the new keyword to create an instance of the class in question and return the pointer.
In theory both approaches can be used in all cases. Obviously returning a pointer to an existing object requires way less code as it can link directly to classes, which are also used by C++.
Vanilla uses new objects a lot. The idea is that instead of accessing CvCity directly, an instance of CyCity is allocated. It contains a pointer to CvCity and it is supposed to work just like CvCity. The major difference is that the Cy classes have wrapper functions.
The key difference is that Cv functions will assume the arguments to be correct while Cy functions will not. For instance if the argument is an index in an array, the Cv function will assert if it is out of bounds, but always read from the array for performance reasons. The Cy function will check for out of bound indexes and return some default value if it is.
This mean if a bug in python calls the function with an out of bound index, the Cy wrapper function will catch the issue and return 0 or whatever number is "do nothing". A direct Cv call will try to read/write out of bounds, which will cause the DLL to crash.
In short: the approach to use depends on how much you trust your python code. There really is no difference if the python code is bug free.
Enums can be exposed to python. This is done in CyEnumsInterface.cpp, though more files could be added if needed.
It's pretty self explaining.
python::enum_<GameStateTypes>("GameStateTypes")
.value("GAMESTATE_ON", GAMESTATE_ON)
.value("GAMESTATE_OVER", GAMESTATE_OVER)
.value("GAMESTATE_EXTENDED", GAMESTATE_EXTENDED)
;
The first argument is the one used by python while the second is the C++ enum value.
TODO figure out if the C++ value is really just an int. If it is, it could be the return value of function calls, like GC.getNum*Infos().
C++ supports class inheritance and this too can be exposed to python.
Example:
python::class_<CvMissionInfo, python::bases<CvInfoBase=""> >("CvMissionInfo")
Notice that the python class now has a comma in the <> declaration. The second part is python::bases<CvInfoBase>. This mean whenever python has an instance of CvMissionInfo, that pointer can use all functions in both CvMissionInfo and CvInfoBase, meaning CvMissionInfo will not have to list functions like getType() as the base already did that.
This can be chained if needed, like
python::class_<CvTestInfo, python::bases<CvMissionInfo=""> >("CvTestInfo")
CvTestInfo can then use functions defined in CvMissionInfo and since CvMissionInfo can use CvInfoBase, those functions will also be available to CvTestInfo.
Python even supports multiple bases, like:
python::class_<CvUnitInfo, python::bases<CvInfoBase,="" CvScalableInfo=""> >("CvUnitInfo")
However this only makes sense because CvScalableInfo didn't set CvInfoBase as base. If the base is set consistently, there shouldn't be a need to set more than one base.
In C++, if you make a function return CvInfoBase, it will be CvInfoBase, even if the pointer actually points to say CvUnitInfo.
Python however will figure out that it is actually CvUnitInfo and can use functions defined for unit info even though the DLL told the pointer should be a base info pointer. Usually this difference shouldn't matter, but it is more flexible and might allow code, which doesn't work in C++, though you might gain something like this with dynamic_cast.
Boost has a 64 kB limit for reading characters when declaring classes and functions. Some big functions, like CyGlobalContext have to be split into multiple sections of less than 64 kB text and then merged.
TODO figure out if the limitation is for a file or for a function. In other words can a single file provide 100 kB code if it has two functions? Maybe it is characters between ;, which mean a single function is ok.
The merge takes place in CvDLLPython.cpp as the last thing to be published to python.
The conceptis fairly simple. First the function is declared.
python::class_<CyGlobalContext> gc ("CyGlobalContext");
This is followed by adding the different file functions to the class
CyGlobalContextPythonInterface1(gc);
CyGlobalContextPythonInterface2(gc);
and so on.
The interface functions then take an argument, which mean they look like this:
void CyGlobalContextPythonInterface1(python::class_<CyGlobalContext>& x)
{
OutputDebugString("Python Extension Module - CyGlobalContextPythonInterface1\n");x
.def("isDebugBuild", &CyGlobalContext::isDebugBuild, "() - returns true if running a debug build")
the x refers to the python class and it tells that the following .def calls should add functions to this class.