In my previous post, I told you all about the story around Viper. Now I'm going to tell you about the story inside Viper!
How Viper Embeds Python
Figuring out how to embed CPython has been an arduous task. There is little documentation on embedding, and few examples (Blender is a bad example, because it merely runs scripts -- it doesn't need to keep track of any details of the scripts). I did my best with those docs, the API reference, and the CPython source code. Oh, I can't forget the fabulous Mattie Casper, who helped me a lot with figuring out how to link CPython on both Windows and Linux.
I don't know if any of my issues have been rectified in CPython3000, but if not, the CPython community needs to do some real work before it becomes a viable solution for embedding.
Linking
Because Viper is not the main program that is run, the default search path for dynamically linked libraries does not include the directory where Viper is executed from. It only includes the srcds/bin/ directory. As most game server providers do not allow access to this directory, Viper needs some way to load CPython from a specified directory.
Windows
On Windows, Viper makes use of delay-loading. Delay-loading DLLs is a simplified way of saying MSVC turns all import references into calls to an import loading function. For example, it would take:
And turn it into:
__imp_LoadFromDelayLoadedDLL("PyInt_From_Long")(4);
This works out great, except when you need an instance managed by the DLL. For instance, Viper needs Py_None and PyExc_TypeError. MSVC can't handle the loading of that data, so you must do it manually.
Luckily, Py_None is:
#define Py_None &_Py_NoneStruct
This made it possible to do the following:
#undef Py_None
PyObject *Py_None = (PyObject *)GetProcAddress(python25_DLL, "Py_NoneStruct");
Thus, I didn't have to modify Viper's code at all. I wasn't so fortunate with other data. For instance, PyExc_TypeError is defined as a PyObject, and not a pointer to a PyObject. This made it impossible to just #undef it and redefine it. I had to declare my own variable and update all references to &PyExc_TypeError with my new variable:
PyObject *_PyExc_TypeError = (PyObject *)GetProcAddress(python25_DLL, "PyExc_TypeError");
Linux
On Linux, Viper uses the RPATH (runtime linking path) of the ELF format. The RPATH allows you to set an absolute path at compile time where the runtime linker will check for dynamically linked libraries.
For the most part, this works great. I don't need to change any code to use both data and functions from Python. However, there is one pitfall...
The RPATH used in Viper is this:
$ORIGIN/viper/lib/plat-linux2
$ORIGIN expands to the directory where viper.ext.so resides. Unfortunately, some game server providers (e.g., GameServers.com) use the server IP and port in the path to the server, meaning $ORIGIN might expand to:
/home/user/133.7.90.01:27015/srcds/cstrike/addons/sourcemod/extensions/
This causes problems, because RPATH accepts multiple paths separated by a colon. But because the runtime linker has no way to differentiate between a colon in a path and a colon separating paths, it just assumes all colons separate paths. So with our previous example, the runtime linker ends up searching for python25.so in both the /home/user/133.7.90.01 and 27015/srcds/cstrike/addons/sourcemod/extensions paths -- neither of which exist.
Currently, the only solution to this problem is to ask the game server provider for access to the srcds/bin/ directory, and manually place python25.so in it.
Plug-in Management
Separation of Contexts
A fun part of the Viper development process, and still a large zit in the face of widespread CPython embedding, is the separation of plug-in contexts. Though it's impossible to completely separate plug-ins, because low-level functions such as os.close can conflict, CPython does provide sub-interpreters which allow for as much independence as manageable. Each sub-interpreter has its own copies of the __main__, __builtin__, and sys modules, as well as new sys.path and sys.modules lists.
There are a few downfalls to using sub-interpreters, but most of them are only applicable in a multi-threaded environment, of which Viper is not. The major downfall to Viper, then, is a minor jump in complexity: every new sub-interpreter introduces a new thread state (not an actual thread), which must be switched to when calling functions from a plug-in.
When a plug-in calls a Viper function, the correct thread state is already set, but when the Source Engine calls a Viper function (say, because a client executed a console command), the wrong thread state may be set, so Viper must manually set the thread state. Fortunately, this is very easy to implement and manage:
// In CPlugin ctor
m_pThreadState = Py_NewInterpreter();
// Meanwhile...
PyObject *CPluginFunction::Execute(PyObject *args, PyObject *keywds)
{
PyThreadState *_save = PyThreadState_Get();
PyThreadState_Swap(m_pPlugin->m_pThreadState);
PyObject *result = PyObject_Call(m_pFunc, args, keywds);
PyThreadState_Swap(_save);
return result;
}
Viper saves the current thread state in _save, then resinstates it at the end of the function.
Identifying a Plug-in From a Function Call
A tougher and even more enjoyable problem was figuring out what plug-in called a function in Viper. I tried many things -- first, I tried setting a global variable inside the Python plug-in context with a pointer to the CPlugin, but then it was impossible to determine the plug-in if it called a Viper function from an imported module.
Next, I tried to store a pointer to the CPlugin in a dictionary managed by the thread state, but if any new thread states were created (e.g., because twisted created a new thread) there would be no way to determine the plug-in.
Finally, I figured out that every thread state stored a pointer to the sub-interpreter that owned it. Viper now stores a pointer to the sub-interpreter with the CPlugin, and maintains a list of all the sub-interpreters associated connected to their plug-ins. When a Viper function is called from within a plug-in, Viper grabs the current thread state, retrieves the sub-interpreter from that thread state, and finally finds it in the sub-interp list:
CPlugin::CPlugin(char const *file)
{
// ...
m_pThreadState = Py_NewInterpreter();
// ...
m_pInterpState = m_pThreadState->interp;
}
CPlugin *CPluginManager::GetPluginOfInterpreterState(PyInterpreterState *interp)
{
SourceHook::List<CPlugin *>::iterator iter;
for (iter=m_list.begin(); iter!=m_list.end(); iter++)
{
if ((*iter)->GetInterpState() == interp)
return (*iter);
}
return NULL;
}
// Elsewhere...
IViperPlugin *pPlugin;
PyThreadState *tstate = PyThreadState_Get();
pPlugin = g_VPlugins.GetPluginOfInterpreterState(tstate->interp);
Plug-in Metadata
Like SourceMod, Viper provides a way for plug-ins to supply some simple metadata -- title, author, description, version, and url. Unlike SourceMod/SourcePawn, there is no way in Python to know the global variables inside a plug-in before runtime, so Viper first executes the plug-in, then retrieves a dict variable named "myinfo" from the plug-in's global scope:
# In the plug-in
myinfo = {
'name': "Hello World!",
'author': "theY4Kman",
'description': "Salutations, Celestial Rock",
'version': "1.0"
}
To retrieve the fields:
void CPlugin::UpdateInfo()
{
#define RETRIEVE_INFO_FIELD(field)\
{ \
PyObject *_obj; \
if ((_obj = PyDict_GetItemString(myinfo, #field)) != NULL) \
{ \
PyObject *str = PyObject_Str(_obj); \
m_info.field = sm_strdup(PyString_AsString(str)); \
Py_DECREF(str); \
} \
}
PyObject *myinfo = PyDict_GetItemString((PyObject*)m_pPluginDict, "myinfo");
if (myinfo == NULL || !PyDict_Check(myinfo))
return;
RETRIEVE_INFO_FIELD(name);
RETRIEVE_INFO_FIELD(author);
RETRIEVE_INFO_FIELD(description);
RETRIEVE_INFO_FIELD(version);
RETRIEVE_INFO_FIELD(url);
#undef RETRIEVE_INFO_FIELD
}
Forwards
A forward is essentially a list of callback functions and the function prototype that callbacks need to adhere to. When an event occurs, the owner of the forward can fire it, executing each callback in succession. Depending on the configuration of the forward, a callback function may return a "stop" signal, telling the forward to cease execution. For example, SourceMod uses this kind of forward when a BanClient is called, so that a plug-in may cancel a ban.
Viper allows for the creation of two fundamental types of forwards:
- Named forwards: Viper manages these, and any plug-in may add a callback to the forward's list if it knows the name of the forward.
- Anonymous forwards: the owning plug-in has complete control over what functions are added and removed from the callback list.
In both cases, the owner plug-in controls when the forward is fired. Forwards also have many different ways to process the return values of callbacks. The following options affect what myforward.fire() returns:
- ET_Ignore: always returns None, and does nothing when a callback returns; useful for notifications, such as OnClientDisconnected.
- ET_Event: returns the highest integer returned by a callback
- ET_LowEvent: returns the lowest integer returned by a callback
- ET_Hook: returns the highest integer returned by a callback, but will stop executing callbacks if one returns the stop signal. Used in forwards such as BanClient, so that one plug-in can cancel a ban and stop the execution of other callbacks.
These options may work for some cases, but many times you need to process every return value of the callbacks. For this, Viper allows you to pass a function that will be called every time a callback returns. This function will be passed the return value of the callback function. The value that the processor function returns is substituted for the callback's return value when it is processed according to the options above.
A named Viper forward looks like this:
from sourcemod import *
def process_return(rvalue):
return rvalue
myforward = forwards.create('backwardforward', process_return, forwards.ET_Hook, int, str)
def callback(number, string):
print "%d: %s" % (number, string)
if number == 1:
return Plugin_Stop
else:
return Plugin_Continue
forwards.register('backwardforward', callback)
myforward.fire(1, 'the loneliest number')
# "1: the loneliest number" is printed, Plugin_Stop is returned, execution of callbacks ends,
# and myforward.fire() returns Plugin_Stop (which is an int of 2)
An anonymous forward:
from sourcemod import *
myforward = forwards.create('', None, forwards.ET_Ignore, int, str)
def callback(number, string):
print "%d: %s" % (number, string)
if number == 1:
return Plugin_Stop
else:
return Plugin_Continue
myforward.add_function( callback)
myforward.fire(1, 'the loneliest number')
# "1: the loneliest number" is printed, Plugin_Stop is returned, and myforward.fire() returns None
print len(myforward) # 1
myforward.remove_function(callback)
print len(myforward) # 0
The Client Object
Clients have tons of data associated with them -- death count, IP address, Steam ID, name, average latency, health, et cetera ad infinitum. Faced with writing getters for all of these properties, I made use of the extra data allowed by Python to be passed to the getter function:
enum clients__client__getsets_t
{
CLIENT_GETSET_ABS_ANGLES = 0,
CLIENT_GETSET_ABS_ORIGIN,
CLIENT_GETSET_ALIVE,
// ...
CLIENT_GETSET_MODEL,
CLIENT_GETSET_WEAPON,
};
static PyObject *
clients__Client__getter_valid_ingame_modsupport(clients__Client *self,
clients__client__getsets_t type)
{
// ... Check client is valid ...
switch (type)
{
case CLIENT_GETSET_ABS_ANGLES:
return CreatePyVector(pInfo->GetAbsAngles());
case CLIENT_GETSET_ABS_ORIGIN:
return CreatePyVector(pInfo->GetAbsOrigin());
case CLIENT_GETSET_ALIVE:
return PyBool_FromLong(!pInfo->IsDead());
// ...
case CLIENT_GETSET_MODEL:
return PyString_FromString(pInfo->GetModelName());
case CLIENT_GETSET_WEAPON:
return PyString_FromString(pInfo->GetWeaponName());
}
Py_RETURN_NONE;
}
static PyGetSetDef clients__Client__getsets[] = {
{"abs_angles", (getter)clients__Client__getter_valid_ingame_modsupport, NULL,
"The client's angles vector.\n"
"@throw ViperError: Invalid client, client not in-game, or no mod support.",
(void *)CLIENT_GETSET_ABS_ANGLES},
// ...
{NULL},
};
Essentially, I defined an enum with the available client properties, created a function that checks for the appropriate conditions (e.g., making sure the client is connected, not a bot, etc), and finally uses a switch to return the correct data. I'm sure that solution was obvious to the rest of you, but I copied and pasted and edited a getter function for all of those properties before I figured it out.
How Viper is Documented
Since Viper Mark II, documentation has been a considerate concern of mine. After all, good documentation can be the difference between "vital tool" and "useless crap." I wanted a tool that made it easy to add and edit functions and modules, and also had support for user comments. I wanted something like PHP's docs, which are amazing (and without those amazing docs, PHP would be a cluttered pile of excrement). With no luck whatsoever, I eventually settled on just having alright documentation.
Sphinx is the documentation tool used by CPython. Essentially, Sphinx translates specialized reStructuredText into HTML, and provides pretty CSS and a JavaScript search page. It doesn't have many extras, but what it does provide works very well.
You can view Viper's docs online here. On the left side of each page is a Show Source link that displays the reStructuredText source of the current page.
In the future, I want to write a documentation tool using Django that allows for user comments. I recently started using Markdown (this post is written using Markdown), and I'm impressed, so I'll probably use Markdown for the new docs.
How Viper is Built
On Windows, building Viper is pretty simple. I use Visual Studio to manage the project -- I punch in the settings I want in the configuration window (GUI) and hit Build.
For Linux, I used a Makefile for the longest time. I took the sample Makefile for extensions from SourceMod and changed it to what Viper needed. It worked fine, but, like any Makefile, it was ugly and hacky.
Recently, I switched Viper's build system (on Linux only, thus far) to SCons. SCons is a build system that uses Python for its configuration files. It allows you to specify possible actions by calling functions in the configuration file, then, on the command-line, lets you run those actions, just like a Makefile:
extension = env.SharedLibrary(target="$BUILD_DIR/viper", source=SOURCE)
env.Default(extension)
env.Alias("extension", extension)
install = env.Install(SRCDS_BASE + "/cstrike/addons/sourcemod/extensions/auto.1.ep1/", extension)
env.Alias("install", install)
With that SConstruct (the configuration file for SCons), one can run these commands from bash:
scons extension # builds the extension
scons install # builds the extension and moves it to a directory
scons extension install # builds the extension and moves it to a directory
The env.SharedLibrary function instructs SCons to build a shared library (.dll on Windows or .so on Linux), and the env.Install function instructs SCons to move a file from one place to another. The env.Alias function names an action, so it can be called from the command-line. Finally, the env.Default function sets the default action to run if no action is passed to scons on the command-line.
SCons is wonderful because it's written in Python, which enables me to do some pretty cool things. For example, when I call env.Install, I pass extension, which is the env.SharedLibrary action that builds viper.ext.so. This informs SCons that the "install" action is dependent upon building the shared library, and that it should copy the output of the env.SharedLibrary to the folder. It greatly simplifies building, and allows for less hacky code! Plus, it also supports creating archives, checking code out from a repository and compiling, and much, much more!
I know what you're thinking! You're thinking, "it's too good to be true." Well, good sir or madam, you are unfortunately correct. SCons still has its fair share of flaws.
SCons has you create an Environment object (that's what env is in the example above), which is used when building. By default, it's filled with useful variables, such as CXX, CPP, and SHLIBSUFFIX, that control what tools SCons executes, and how it executes them. That's all well and good, except that the provided values to most of these env vars are usually completely useless, in the best case. In the worst case, they cause tons of commotion, and you don't even know what env var is causing it.
For example, I was building Viper one time when I noticed -fPIC was among the options (-fPIC instructs the compiler to create position independent code, which is useless most of the time). I never wrote that flag in, so I spent hours determining the source. Finally, I discovered it: SCons was putting SHCCFLAGS on my build line to gcc, which contained "-fPIC" and nothing but "-fPIC". The easy fix was to set it to blank. After that, everything was fine.
SCons also has an odd way of placing object files. It makes you address files as if they existed under the build directory. So if you wanted to build sdk/smsdk_ext.cpp, you'd tell SCons to build build/debug/sdk/smsdk_ext.cpp. Maybe it helps 90% of the people using SCons, but I think it only helps 1% and annoys the hell out of everyone else. Luckily, Python gives me a quick fix:
_SOURCE = [... 'sdk/smsdk_ext.cpp', ...]
SOURCE = [env["BUILD_DIR"] + "/" + target for target in _SOURCE]
My last real complaint is with command-line variables. SCons provides a few different ways to extract data from the command-line. One of them is with EnumVariable. The goal of an EnumVariable is to map the string value of a variable entered by a user to a value specified in the configuration file.
env_var_ENGINE = EnumVariable(
"ENGINE", "The Source engine version to compile against",
None, ("original", "orangebox", "left4dead"),
map={
"original": SE_EPISODEONE,
"orangebox": SE_ORANGEBOX,
"left4dead": SE_LEFT4DEAD
},
# ignorecase=1 converts the user's supplied value to lowercase
ignorecase=1
)
The above example would allow the user to run:
Then SCons would translate the user's value ("original") into the mapped value (SE_EPISODEONE). Somewhere along the line, though, a SCons developer decided to use the following code to retrieve the value of an EnumVariable:
converter(env.subst("$ENGINE"))
env.subst returns the value of the passed variable. But because $ENGINE is an EnumVariable, env.subst() effectively becomes converter(). That's bad news bears, because the inner converter() call yields SE_EPISODEONE, but the outer call, *converter(SE_EPISODEONE), gives an error, because it is not a mapped value.
It's very stupid. My workaround is to provide the actual value of SE_EPISODEONE to the list of possible user entered values:
env_var_ENGINE = EnumVariable(
"ENGINE", "The Source engine version to compile against",
None, ("original", "orangebox", "left4dead",
str(SE_EPISODEONE), str(SE_ORANGEBOX), str(SE_LEFT4DEAD)),
map={
"original": SE_EPISODEONE,
"orangebox": SE_ORANGEBOX,
"left4dead": SE_LEFT4DEAD
},
ignorecase=1
)
If you'd like, you can check out Viper's full SConstruct file at github.