# Try to Integrate Python and C++
###### tags: `Python`
Make a extension with c++ code for python3.
Not included cPython, pybind11, boost.python, ctype, SWIG.
This only use native and official method.
Table:
[TOC]
## Official Document
* [Intro](https://docs.python.org/3.7/extending/extending.html#a-simple-example)
* [python c-api](https://docs.python.org/3.7/c-api/)
## Concept
All of python instances are object(`PyObject*`), so the c++ function get pyobject as input and send pyobject as output.
Then, you need to wrap your function as a module for your python code to import, just like the module you install.
## Minimal example
`test1.cpp`
``` c
#define PY_SSIZE_T_CLEAN
#include <Python.h>
PyObject * spam_six(PyObject*, PyObject*);
static PyMethodDef SpamMethods[] = {
{"six", spam_six, METH_VARARGS, "return 6"},
{NULL},
};
static struct PyModuleDef spammodule = {
PyModuleDef_HEAD_INIT, "spam", NULL, -1, SpamMethods
};
PyMODINIT_FUNC PyInit_spam(void)
{
return PyModule_Create(&spammodule);
}
PyObject* spam_six(PyObject *self, PyObject *args)
{
return PyLong_FromLong(6);
}
```
`setup.py`
``` python
from distutils.core import setup, Extension
module = Extension('spam',
sources = ['test1.cpp'])
setup (name = 'spam',
version = '1.0',
description = 'This is a demo package',
ext_modules = [module])
```
`test.py`
``` python
import spam
print(spam.six())
print(type(spam.six()))
```
and then run
``` shell
python3 setup.py install
python3 test.py
```
## Explain for minimal example
Let me explain it from top to down.
### python code
In `test.py`, me import a module called `spam` and it has a method `six` which always return a constant interger 6.
### setup code
In `setup.py`, the `spam` module can be built by `python3 setup.py install`, where you can see the extension built by gcc from `test1.cpp`.
``` python
module = Extension('spam',
sources = ['test1.cpp'])
```
### Module Init
When calling `import spam` in Python, the interpreter will find `PyInit_{name}` to get the module instance, in this case is `PyInit_spam`.
``` python
PyMODINIT_FUNC PyInit_spam(void)
{
return PyModule_Create(&spammodule);
}
```
The spammodule define module name, documents
and its methods.
``` c
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
};
```
And the methods defined in here
``` c
static PyMethodDef SpamMethods[] = {
{"six", spam_six, METH_VARARGS, "return 6"},
{NULL},
};
```
`"six"` is the name of method,
`spam_six` is the self-defined function you want to call.
`METH_VARARGS` is about to deal with the parameters in your function, I will talk it later.
`"return 6"` is the documents of this function.
Practically, you can add as many entries as you want in here, note that you should add `{NULL}` in the last.
### Main function
``` c
PyObject* spam_six(PyObject *self, PyObject *args)
{
return PyLong_FromLong(6);
}
```
This function only return 6, however it wrapped into pyobject by `PyLong_FromLong`. The wrapper can find in [c-api doc](https://docs.python.org/3.7/c-api/concrete.html).
## Second, try to read parameters
We take `arange` for example.
In Python, we pass a number to this method
``` python
print(spam.arange(4))
print(spam.arange(14))
```
Add
``` c
{"arange", spam_arange, METH_VARARGS, "arange"},
```
in `SpamMethods`.
The function:
``` c
PyObject* spam_arange(PyObject *self, PyObject *args)
{
int n;
if (!PyArg_ParseTuple(args, "i", &n))
return NULL;
PyObject* obj = PyList_New(n);
for(int i=0; i<n; ++i)
PyList_SetItem(obj, i, PyLong_FromLong(i));
return obj;
}
```
The parameters can be parsed by `Pyarg_ParseTuple`, and the whole function return a list wrapped by pyobject.
The detail usage of parsing, you can see [tutorial](https://docs.python.org/3.7/extending/extending.html#extracting-parameters-in-extension-functions) or the [api](https://docs.python.org/3.7/c-api/arg.html).
## Next, read list
``` python
spam.array([1, 3 ,2])
```
``` c
PyObject* spam_array(PyObject *self, PyObject *args)
{
// parse into list
PyObject *listObj;
if (!PyArg_ParseTuple(args, "O!", &PyList_Type, &listObj))
return NULL;
// get length
Py_ssize_t numLines = PyList_Size(listObj);
if (numLines < 0)
return NULL; /* Not a list */
// foreach
PyObject* obj = PyList_New(numLines);
for (int i=0; i<numLines; i++){
long x = PyLong_AsLong(PyList_GetItem(listObj, i));
printf("%d -> %ld\n", i, x);
PyList_SetItem(obj, i, PyLong_FromLong(x));
}
return obj;
}
```
reference https://stackoverflow.com/questions/3253563/pass-list-as-argument-to-python-c-module
## Same thing for python dictionary
``` python
print(spam.get({'a': 123, 'b': 345}, 'a'))
```
``` c
PyObject* spam_get(PyObject *self, PyObject *args)
{
PyObject* dictObj;
char* s;
// parse into list
if (!PyArg_ParseTuple(args, "O!s", &PyDict_Type, &dictObj, &s))
return NULL;
long v = PyLong_AsLong(PyDict_GetItem(dictObj, PyUnicode_FromString(s)));
printf("%s -> %ld\n", s, v);
return PyLong_FromLong(v);
}
```
## Finally, raising error
``` c
static PyObject* SpamError = PyErr_NewException("spam.error", NULL, NULL);
PyObject* spam_arange(PyObject *self, PyObject *args)
{
...
if (n < 0) {
PyErr_SetString(SpamError, "Number should be nonzero");
// PyErr_SetString(PyExc_IndexError, "Number should be nonzero");
return NULL;
}
...
````
and the object should be carefully removed if you don't want. **No auto garbage collection in here.**
``` c
PyObject* tmpobj = PyList_New(10000000);
// Py_XDECREF(tmpobj); // obj can be NULL
Py_DECREF(tmpobj); // obj NOT NULL
```
The default exceptions are listed [here](https://docs.python.org/3.7/c-api/exceptions.html#standard-exceptions).
## Extra
Bulid with c++17
``` python
module = Extension('spam',
extra_compile_args=['-std=c++17'],
sources = ['test1.cpp'])
```
## Overall
test.py
``` python
import spam
print(spam.six())
print(spam.array([1, 2, 3]))
print(spam.get({'a': 123, 'b': 345}, 'a'))
print(spam.get({'a': 123, 'b': 345}, 'b'))
print(spam.arange(3))
print(spam.arange(-3))
```
test1.cpp
``` c
#define PY_SSIZE_T_CLEAN
#include <Python.h>
PyObject * spam_six(PyObject*, PyObject*);
PyObject * spam_arange(PyObject*, PyObject*);
PyObject * spam_array(PyObject*, PyObject*);
PyObject * spam_get(PyObject*, PyObject*);
static PyMethodDef SpamMethods[] = {
{"six", spam_six, METH_VARARGS, "return 6"},
{"arange", spam_arange, METH_VARARGS, "arange"},
{"array", spam_array, METH_VARARGS, "array"},
{"get", spam_get, METH_VARARGS, "get from dictionary"},
{NULL},
};
static struct PyModuleDef spammodule = {
PyModuleDef_HEAD_INIT, "spam", NULL, -1, SpamMethods
};
PyMODINIT_FUNC PyInit_spam(void)
{
return PyModule_Create(&spammodule);
}
PyObject* spam_six(PyObject *self, PyObject *args)
{
return PyLong_FromLong(6);
}
static PyObject* SpamError = PyErr_NewException("spam.error", NULL, NULL);
PyObject* spam_arange(PyObject *self, PyObject *args)
{
int n;
PyObject* tmpobj = PyList_New(10000000);
// Py_XDECREF(tmpobj); // obj can be NULL
Py_DECREF(tmpobj); // obj NOT NULL
if (!PyArg_ParseTuple(args, "i", &n))
return NULL;
if (n < 0) {
PyErr_SetString(SpamError, "Number should be nonzero");
// PyErr_SetString(PyExc_IndexError, "Number should be nonzero");
return NULL;
}
PyObject* obj = PyList_New(n);
for(int i=0; i<n; ++i)
PyList_SetItem(obj, i, PyLong_FromLong(i));
return obj;
}
PyObject* spam_array(PyObject *self, PyObject *args)
{
PyObject *listObj;
// parse into list
if (!PyArg_ParseTuple(args, "O!", &PyList_Type, &listObj))
return NULL;
// get length
Py_ssize_t numLines = PyList_Size(listObj);
if (numLines < 0)
return NULL; /* Not a list */
// foreach
PyObject* obj = PyList_New(numLines);
for (int i=0; i<numLines; i++){
long x = PyLong_AsLong(PyList_GetItem(listObj, i));
printf("%d -> %ld\n", i, x);
PyList_SetItem(obj, i, PyLong_FromLong(x));
}
return obj;
}
PyObject* spam_get(PyObject *self, PyObject *args)
{
PyObject *dictObj;
char* s;
// parse into dict
if (!PyArg_ParseTuple(args, "O!s", &PyDict_Type, &dictObj, &s))
return NULL;
printf("%s\n", s);
long v = PyLong_AsLong(PyDict_GetItem(dictObj, PyUnicode_FromString(s)));
printf("%s -> %ld\n", s, v);
return PyLong_FromLong(v);
}
```
## On Windows
Buld cpp in windows is not such easy thing.
First, make sure Python and your g++ are 64bits.
The g++ (MinGW32) in 64 bits can be download here
https://sourceforge.net/projects/mingw-w64/
To check:
* `g++ -v` the target should be x86_64-w64-mingw32
* `python -c 'import sys;print("%x" % sys.maxsize)'` The output of 64bits python should be `7fffffffffffffff`
If there is not PATH to g++, you can setup manually.
`set PATH=/your/path/MinGW/bin:$PATH`
Add below compiler setting in `.../Python37/Lib/distutils/distutils.cfg`
```
[build]
compiler = mingw32
```
Also, patch `.../Python37/Lib/distutils/cygwinccompiler.py`
```
def get_msvcr():
...
if msc_ver == '1300':
...
else:
return []
```
Reference:
https://stackoverflow.com/questions/1405913/how-do-i-determine-if-my-python-shell-is-executing-in-32bit-or-64bit-mode-on-os
https://stackoverflow.com/questions/34135280/valueerror-unknown-ms-compiler-version-1900