Start With C

For better or for worse C is currently the lingua franca of the programming world, almost every programming language talks to others via the C ABI. For this reason rusty-binder focuses on generating bindings via Rust's ability to export symbols with a C ABI.

Python actually has (broadly speaking) three different methods to talk to the outside world; CPython's native extensions, ctypes, and cffi. This tutorial is going to focus on the former method, but steelix will eventually support all three.

I'm going to run through this section fairly quickly because I'm guessing most of you probably don't actually care about Python. If you want to know more about Python extensions though I can suggest the official tutorial, this blog post, or just googling "python c extensions".

The Example In C

Let's start by recreating our little module in C (not forgetting the header file).

// vec.h

#ifndef vec_h
#define vec_h

typedef struct {
    double x;
    double y;
    double z;
} Vector;

Vector negate(Vector vec);
Vector add(Vector a, Vector b);
double dot(Vector a, Vector b);

#endif  // ndef vec_h
// vec.c

#include "vec.h"

Vector negate(Vector vec) {
    Vector new_vec = { -vec.x, -vec.y, -vec.z };
    return new_vec;
}

Vector add(Vector a, Vector b) {
    Vector new_vec = { a.x + b.x, a.y + a.y, a.z + b.z };
    return new_vec;
}

double dot(Vector a, Vector b) {
    double total = 0;
    total += a.x * b.x;
    total += a.y * b.y;
    total += a.z * b.y;
    return total;
}

Again not very fancy and I've not tested it so probably subtly wrong somewhere, it does compile at least though!

A C Module In Python

Next we need to turn this bit of C code into a Python module. Unsurprisingly Python doesn't really understand C types so everything goes through PyObject pointers. This means we'll have to wrap our functions from above in a Python-style C function. First we need the correct type signature.

static PyObject* vec_negate(PyObject* self, PyObject* args) {

We can call our function anything really, but the convention is <module name>_<function_name>.

Next we need to extract the arguments that were passed to the Python function, using PyArg_ParseTuple. To keep things simple the Python function will just take a 3-tuple of doubles, rather than than defining a Vector class.

    Vector vec = {0};
    if (!PyArgs_ParseTuple(args, "(ddd)", &vec.x, &vec.y, &vec.z)) {
        return NULL;  // couldn't parse args
    }

Finally we just need to apply the original function and return a 3-tuple.

    Vector new_vec = negate(vec);
    return Py_BuildValue("(ddd)", new_vec.x, new_vec.y, new_vec.z);
}

There's a lot of other bookkeeping to do that I won't go into until it becomes important, but the final file ends up looking something like this.

// vecmodule.c

#include <Python.h>

#include "vec.h"


static PyObject* vec_negate(PyObject* self, PyObject* args);
static PyObject* vec_add(PyObject* self, PyObject* args);
static PyObject* vec_dot(PyObject* self, PyObject* args);


// MODULE INITIALISATION
static PyMethodDef VecMethods[] = {
    {"negate", vec_negate, METH_VARARGS, "Negate a vector."},
    {"add", vec_add, METH_VARARGS, "Add two vectors."},
    {"dot", vec_dot, METH_VARARGS, "Take the dot product of two vectors."},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef vecmodule = {
    PyModuleDef_HEAD_INIT,
    "vec",
    NULL,
    -1,
    VecMethods
};

PyMODINIT_FUNC PyInit_vec(void) {
    return PyModule_Create(&vecmodule);
}


// PYTHON FUNCTIONS
static PyObject* vec_negate(PyObject* self, PyObject* args) {
    Vector vec = {0};
    if (!PyArg_ParseTuple(args, "(ddd)", &vec.x, &vec.y, &vec.z)) {
        return NULL;
    }

    Vector new_vec = negate(vec);
    return Py_BuildValue("(ddd)", new_vec.x, new_vec.y, new_vec.z);
}

static PyObject* vec_add(PyObject* self, PyObject* args) {
    Vector a = {0};
    Vector b = {0};
    if (!PyArg_ParseTuple(args, "(ddd)(ddd)", &a.x, &a.y, &a.z, &b.x, &b.y, &b.z)) {
        return NULL;
    }

    Vector new_vec = add(a, b);
    return Py_BuildValue("(ddd)", result);
}

static PyObject* vec_dot(PyObject* self, PyObject* args) {
    Vector a = {0};
    Vector b = {0};
    if (!PyArg_ParseTuple(args, "(ddd)(ddd)", &a.x, &a.y, &a.z, &b.x, &b.y, &b.z)) {
        return NULL;
    }

    double result = dot(a, b);
    return Py_BuildValue("d", result);
}

// C FUNCTIONS
Vector negate(Vector vec) {
    Vector new_vec = { -vec.x, -vec.y, -vec.z };
    return new_vec;
}

Vector add(Vector a, Vector b) {
    Vector new_vec = { a.x + b.x, a.y + a.y, a.z + b.z };
    return new_vec;
}

double dot(Vector a, Vector b) {
    double total = 0;
    total += a.x * b.x;
    total += a.y * b.y;
    total += a.z * b.y;
    return total;
}

Now we just have to figure out how to compile this, fortunately Python handles that for us we just need to write the setup.py file. Note that Python actually recommends using the more modern setuptools.

# setup.py

from setuptools import setup, Extension

vecmodule = Extension(
    "vec",
    sources=["vecmodule.c"],
)

setup(
    name="Vec",
    ext_modules=[vecmodule],
)

A quick pip install . at the command line and that is all compiled and installed for us.

A C Library In Python

Writing C straight into vecmodule.c is all well and good, but we can't transpile our Rust to C so it doesn't help us much. What we can do is compile our Rust to a static library so let's try doing that with our C version.

We can compile into a static library on Unix with

$ cc -o vec.o -c vec.c
$ ar -rcs libvec.a vec.o

and now all we need to do is tell setuptools that we want to link to the library in setup.py.

# setup.py

from setuptools import setup, Extension

vecmodule = Extension(
    "vec",
    sources=["vecmodule.c"],
    libraries=["vec"],
    library_dirs=["."],
)

setup(
    name="Vec",
    ext_modules=[vecmodule],
)

Also note that library_dirs=["."] is a terrible thing to do, but it works for now so let's not dwell on it too much.