Compiling and linking Python C extensions on Windows
The most common implementation of Python is built directly on C and can be extended with C code as long as you use some Python-specific conventions and C functions/objects. The official Python docs generally provide a good introduction to this - in this post, I'm only filling in a couple of details for Windows that had me stumped for a bit.
Context
The official Python docs include an introduction to writing C (and C++) extensions for Python. I found it very helpful, but there was one Windows-specific point where I was confused. The introductory article brings up compiling and linking the sample extension, referring to a general section on building C extensions as well as a Windows-specific section on building. That section on Windows, however, doesn't explain where exactly to find the Python libraries and headers.
The docs generally suggest that you use setuptools, but I wanted to do the compilation and linking manually to keep things as simple as possible while troubleshooting an issue I'd run into with a project.
Setup
Install Python (I used 3.13.1) for Windows from python.org. During installation, note the installation path. In my case, it was "C:\Users\datalowe\AppData\Local\Programs\Python\Python313".
Install the Microsoft Visual Studio C++ build tools (selecting "Desktop development with C++" in the Visual Studio Installer), giving you access to the cl
CLI tool. You might have to do some additional setup for cl
to start working properly.
Manually compiling and linking a Python C extension on Windows
With regard to the C code itself, just copy the example code from the official tutorial into a spam.c
file (and replacing placeholder values like spam_doc
with NULL
), including the PyMODINIT_FUNC PyInit_spam()
function at the end:
// spam.c
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = system(command);
return PyLong_FromLong(sts);
}
static PyMethodDef SpamMethods[] = {
{"system", spam_system, METH_VARARGS,
"Execute a shell command."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static struct PyModuleDef spammodule = {
PyModuleDef_HEAD_INIT,
"spam", /* name of module */
NULL, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
SpamMethods
};
PyMODINIT_FUNC
PyInit_spam(void)
{
return PyModule_Create(&spammodule);
}
Now, open up "native tools command prompt" for your VS version (search for info if this is new to you - essentially it's command prompt, but with env vars etc required for Visual Studio tools usage set up). cd
to the directory with "spam.c". Now run:
REM replace path up to "Python313" by your own Python installation path
REM replace "python313.lib" based on your own Python version, e.g. "python312.lib" if using 3.12
cl /LD /IC:\Users\datalowe\AppData\Local\Programs\Python\Python313\include spam.c C:\Users\datalowe\AppData\Local\Programs\Python\Python313\libs\python313.lib
That handles getting ahold of the "Python.h" header (in the "...\include" directory) and the interface library ("python313.lib") so that compilation and linking is done correctly. You should now have multiple output files in the directory containing "spam.c", including "spam.dll" and "spam.lib".
Python uses a special file extension for Python C extension libraries on Windows, however. Instead of the usual ".dll", Python requires that the extension be ".pyd". So, we simply rename the DLL file:
move spam.dll spam.pyd
Use the C extension from Python
If you now run Python with py
(making sure you use the same Python version you linked against, e.g. 3.13 in my case), you should be able to call the sample code's system
function from the Python REPL:
>>> import spam
>>> spam.system("dir")
With that, you should see a list of all files in the current directory output.