Writing the Maya Python standalone module

The entry point

The entry point, as mentioned previously, must have a specific function signature based off the desired module name.

static char MAYA_PYTHON_C_EXT_DOCSTRING[] = "An example Python C extension that makes use of Maya functionality.";

static PyMethodDef mayaPythonCExtMethods[] = {
    {"hello_world_maya", pyHelloWorldMaya, METH_VARARGS, HELLO_WORLD_MAYA_DOCSTRING},
    {NULL, NULL, 0, NULL}
};

extern "C" PyMODINIT_FUNC initmaya_python_c_ext()
{
    PyObject *module = Py_InitModule3("maya_python_c_ext",
                                      mayaPythonCExtMethods,
                                      MAYA_PYTHON_C_EXT_DOCSTRING);
    if (module == NULL) {
        return;
    }
}

Alright, let's break this down bit-by-bit again. The first line is fairly obvious; we're just defining a docstring for the module itself.

The second line, however, introduces a new PyMethodDef type: this is what we're going to use for defining the function table available in our module to the Python interpreter.

Listing the available methods

PyMethodDef is essentially a structure with 4 fields: the name of the function desired (i.e. what we will actually type in the Python interpreter when calling it), the callback to be executed, an enum indicating the type of arguments that it accepts, and the docstring for the function itself that will be displayed when someone calls .__doc__ on the function object in the Python interpreter. We define mayaPythonCExtMethods as an array of these structures, with a sentinel value of a structure with NULL fields to indicate the bounds of the array.

We then pass that to the call Py_InitModule3 in our entry point initmaya_python_c_ext, which will allow Python to register our module and register the table, thus making the functions available for use in the interpreter. We export the entry point with extern "C" so that the compiler doesn't mangle the name of the symbol when it is compiled.

Ok, now we have all the code written. (Wasn't that fast?) We just need to actually build the .pydnow so that we can use it.

Building the extension

This is where I will stray a little from past tutorials, and even the official Python documentation: we will not be making use of distutils or CMake or anything like that. We will write a build.bat. The build.bat will build the code, no other nonsense involved. (And trust me, if you go down the distutils route, there is a whole lot of involvement.)

The build.bat script is provided in the code for this repository. Most of it is fairly straightforward if you understand batch file syntax on Windows, so I'll only focus on the parts that concern this project.

Crossing the platforms

On Linux, I provide a build.sh script, which is functionally similar to the Windows build.bat. If you're only familiar with Bash syntax, you can take a look at that instead.

After the usual boilerplate of setting up the Visual Studio environment, creating a build directory and all that jazz, we get to something interesting in the script:

set CommonCompilerFlags=%CommonCompilerFlags% /I"%MayaRootDir%\include" /I "%MayaRootDir%\include\python2.7"

Notice that we're not including the system Python headers directly. We also don't link against the system Python either:

set CommonLinkerFlags=%CommonLinkerFlags% "%MayaLibraryDir%\OpenMaya.lib" "%MayaLibraryDir%\OpenMayaAnim.lib" "%MayaLibraryDir%\OpenMayaFX.lib" "%MayaLibraryDir%\OpenMayaRender.lib" "%MayaLibraryDir%\OpenMayaUI.lib" "%MayaLibraryDir%\Foundation.lib" "%MayaLibraryDir%\IMFbase.lib" "%MayaLibraryDir%\clew.lib" "%MayaLibraryDir%\Image.lib" "%MayaLibraryDir%\python27.lib"

Why is this?

Well, recall that for Maya specifically, there is a Python interpreter embedded directly in the application. It's also been modified slightly from the standard Python interpreter; in order to make sure that we don't encounter any funny issues down the line (read: crashes), we're going to link against Maya's version instead. This does mean, however, that you should not attempt to use the module we're building in a standard non-mayapy Python interpreter. (You probably wouldn't anyway, since the whole point is that we're making use of Maya-specific functionality in our module that wouldn't work in standard Python.)

There's one more thing we need to do:

set PythonModuleExtension=pyd

set PythonModuleLinkerFlagsCommon=/export:initmaya_python_c_ext /out:"%BuildDir%\%ProjectName%.%PythonModuleExtension%"

Remember how we declared our entry point with C-linkage above? This is where we export that symbol specifically. We need to do this on Windows, since symbols are stripped from the binary in release mode otherwise.

The rest of the build script is mostly boilerplate to actually compile and link the module. If you have trouble understanding it, I suggest reading my previous tutorial which goes over each section of the build script in greater detail.

Do note: if you're going to just use my build.bat wholesale, make sure you edit the MayaRootDir variable to point to the root of your Maya installation. You should also make sure that the include folder from your Maya devkit is placed in there as well, or else edit the MayaIncludeDirvariable to point to it as needed.

So what's wrong with distutils?

Other than the fact that's it's yet another unnecessary abstraction and would require me to run a seperate python setup.py build step outside of the normal build script, it also attempts to call the wrong Visual Studio compiler by default, since it's concerned with what the official Python distribution was built against. Unfortunately, we're working with a custom build of Python in the case of Maya's Python interpreter, which basically makes the entire distutils build system an annoying nuinsance to work around. Besides, we have more control over what the compiler/linker are actually doing this way, without worrying about overriding any flags being set by default or other such distractions.

Running it in mayapy

Run the following code in a mayapy interpreter:

import maya.standalone
maya.standalone.initialize()

import sys
sys.path.append('path to the msbuild folder')

from maya_python_c_ext import *
hello_world_maya('oh noes')

You should see:

Hello world from the Maya Python C extension!
oh noes
'oh noes'

Great! Now in the next chapter, we'll focus on implementing one of the things people have been complaining about: a missing OM1 to OM2 binding.