openage consists of python modules, which contain the program entry point,
and the library libopenage.so
, which contains all C++ code.
Cython is used for glue code.
Cython modules are written in .pyx
files, and roughly equivalent to .py files.
In addition to regular Python syntax, .pyx
files allow you to define typed
functions and objects:
cdef int square(int x):
return x * x
cdef cppclass Rectangle:
int h, w
int size():
return this.h * this.w
def foo():
cdef Rectangle r
r.h = 5
r.w = 6
return square(r.size())
More language information is in the official documentation.
.pyx
files are translated to .cpp
by Cython ("cythonized") as part of the
openage build process; syntax errors are shown in this step.
Each .cpp
file is then compiled to a Python extension module, which may be
used from everywhere.
def
functions andcdef class
classes can be used from Pythoncdef
functions can't be used directly- suitable for storage in a
C
function pointer - callable from
def
functions in the.pyx
file
- suitable for storage in a
$ cython --cplus -3 test.pyx
$ g++ -shared -fPIC $(python3-config --includes) test.cpp -o test.so
$ python3
>>> import test
>>> test.foo()
900
Cython can use any regular C++ function or type; for that purpose, they
need to be declared in .pxd
files (which are analogous to C++ .h
files):
cdef extern from "<stdlib.h>":
int atoi(const char *s)
To use this function declared in foo.pxd
,
from foo cimport atoi as c_atoi
def atoi(s):
# invokes the c implementation according to the
# interface defined in a .pxd file
return c_atoi(s)
openage
has a helper, pxdgen
, which auto-generates .pxd
files for .h
files in the libopenage/
subdirectory, from pxd:
annotations in these files, as part
of the build system.
The pxd annotations are really simple; just have a look at some of the headers.
You'll find the generated .pxd
files next to the C++ header files.
To cimport
a class Foo that was pxd-annotated in util/foo.h
, type
from libopenage.util.foo cimport Foo
Cython ships lots of .pxd
files for most C, C++ and CPython functions:
from libc.math cimport sin
from libcpp.vector cimport vector
cdef vector[float] vector_sin(vector[float]& args):
return [sin(arg) for arg in args]
print(vector_sin(range(10)))
From time to time, it may be useful to have a look at the generated .cpp
file,
especially if you require more exotic functionality or something doesn't work
out as you expected. For that purpose, .html
files are generated next to the .cpp
files.
To make a C++ function available for calling from Cython code, annotate the header
file with pxd
comments:
libopenage/example.cpp
namespace openage {
int foo(int arg0, std::string arg1) {
return 5;
}
}
libopenage/example.h
// pxd: from libcpp.string cimport string
#include <string>
#include "util/compiler.h"
namespace openage {
/**
* The famous foo function. Warning: might bar occasionally.
*
* pxd:
*
* int foo(int arg0, string arg1) except +
*/
OAAPI int foo(int arg0, std::string arg1);
}
Always pxd-declare your functions as except +
("may potentially rise a C++ exception"), unless the C++ function is marked noexcept
itself.
If a function that is not declared except +
throws anyways, the entire CPython interpreter is likely to be shredded.
(Sidenote: the except +
annotation enables openage-specific exception translation. To see how this is used, look in the exctranslate_tests.pyx/cpp
)
To enable pxdgen
, cmake
must be informed about the pxd
-annotated header file:
libopenage/CMakeLists.txt
pxdgen(example.h)
The function is now available from Cython. To make it available for pure-python modules, write a wrapper:
openage/foo.pyx
from libopenage.foo cimport foo as c_foo
def foo(int arg0, str arg1):
with nogil:
return c_foo(arg0, arg1)
openage/bar.py
from openage.foo import foo
print(foo(10, "test"))
The interface works one-way: Cython can access libopenage
, but not the other way round.
Thus, libopenage
must find its way back to Python:
- By
pyinterface::PyIfFunc
function pointers that are filled by Cython during initialization. - By
py::PyObj
objects that hold a Python object. It may be callable.
Any Python object can be stored in a PyObj
(from libopenage.pyinterface.pyobject
),
which C++ can use to do calls to Python callables.
This works in the way that you create C++ objects in Cython and then call a C++ function where you pass it.
// pxd: from libcpp.string cimport string
#include <string>
// pxd: from libopenage.pyinterface.pyobject cimport PyObj
#include "libopenage/pyinterface/pyobject.h"
/**
* pxd:
*
* cppclass demo_struct:
* PyObj obj
* string text
*/
struct OAAPI demo_struct {
py::PyObj obj;
std::string text;
};
/**
* pxd: int cpp_function(lol_struct arg, int another_arg) except +
*/
void cpp_function(lol_struct &arg, int another_arg) {
// native data can directly be used
std::cout << "native_arg: " << arg.text << std::endl;
// call the python
// with automatic argument conversion!
std::cout << "python call: "
<< arg.obj->getattr("py_func").call("some binary", another_arg).str()
<< std::endl;
}
from libopenage.pyinterface.pyobject cimport PyObj
from libopenage.main cimport demo_struct, cpp_function
class TestClass:
def __init__(self):
self.some_member = "rofl"
def py_func(self, arg0, arg1):
return "test: 0={} 1={} 2={}".format(self.some_member, arg0, arg1)
def entry():
# create the object and deliver it to c++
test_obj = TestClass()
# python object wrapping for c++
cdef PyObj pyobj_wrapped = PyObj(<PyObject*> some_object)
# create a c++ object
cdef demo_struct cppobj
cppobj.obj = pyobj_wrapped
cppobj.text = "behold the automatic type conversion!"
# call to c++
cpp_function(cpp_obj, 1337)
This means that Python calls to C++ with cpp_function
,
then C++ calls back to Python to the py_function
.
C++ can access a PyObj
in many more ways, perform casts, etc.
To access call Python functions some time later, they can be remembered in C++:
Any cdef
functions can be stored in C++ function pointers.
The openage::pyinterface::PyIfFunc
type has been created for this purpose;
it allows binding arguments and makes sure that the pointer is properly initialized.
We will call this pure-Python function from C++:
openage/bar.py
def bar(arg0, arg1):
"""
This function involves rainbows and unicorns.
arg0 shall be an integer, and arg1 a string.
"""
return 6.283185307179586
Declare, define and pxd-export the function pointer:
libopenage/foo.cpp
#include "foo.h"
PyIfFunc<float, int, std::string> bar;
libopenage/foo.h
// pxd: from libcpp.string cimport string
#include <string>
#include "pyinterface/functional.h"
// pxd: PyIfFunc2[float, int, string] bar
extern OAAPI PyIfFunc<float, int, std::string> bar;
Wrap the python function in a cdef
function, and define a method setup()
,
which binds the cdef
function to the PyIfFunc object.
openage/foo.pyx
from libopenage.foo cimport bar as c_bar
from .bar import bar as py_bar
cdef float bar(int arg0, string arg1) except * with gil:
return py_bar(arg0, <str> arg1)
def setup():
c_bar.bind0(bar)
PyIfFunc2
means that the function takes 2 arguments, and bind0
means that 0
arguments are bound (this is needed because Cython currently doesn't support variadic template arguments).
Add a call to openage.foo.setup()
to openage.cppinterface.setup.setup
.
openage/pyinterface/setup.pyx
def setup():
# (...)
from openage.foo import setup
setup()
# (...)
If you forget to do that, openage.pyinterface.setup.setup
will raise a fatal exception.
libopenage/bar.cpp
#include "foo.h"
std::cout << openage::bar.call(5, "test") << std::endl;
The only way of accessing Python code from C++ should be via the Func
or PyIfFunc
function wrappers, as those
guarantee that exceptions are properly translated, among other things.
- For code that wraps a C++ class for Python, see:
openage/cabextract/lzxd.pyx
libopenage/util/lzxd.h
- For code that wraps a Python class for C++, see:
libopenage/util/fslike/path.h
openage/util/fslike/cpp.pyx
- Search for
.pyx
files in the repo
The GIL must be acquired for any Python functionality (even as simple as PyErr_Occurred
).
However, GIL-safety is guaranteed by the combination of Cython, the pyinterface code,
and the fact that libopenage
doesn't link against Python itself / include any Python headers.
Any code in libopenage
can safely be run without the GIL.
Only functions that are marked with gil
can be bound to PyIfFunc
or Func
objects;
this ensures that the GIL is always re-acquired when jumping into Cython code.
Never use raw function pointers in the interface;
always use the PyIfFunc
or Func
objects; otherwise, you'll lose all safety guarantees.
OAAPI
marks the DLL entry-points which is necessary for the Windows build.
All pxd interface functions, classes, and extern
objects must be declared with OAAPI
in the header file.
See the samples above to understand the exact position of insertion for each of those.
This can be ignored for functions defined inline in the header.