Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Shared object files

Disclaimer: we will be working in notebooks, because it’s a great place to teach in. It’s not a great place to write compiled code in, so we will write out the files we want to compile from the notebooks. In real life, just write out the files directly!

What is meant by bindings?

Bindings allow a function(alitiy) in a library to be accessed from Python.

We will start with this example:

Writing simple.c

Depending on your platform, you may normally use gcc (Linux) or clang (macOS) to compile. If you are on Windows, you might want to work in the Linux-as-a-Subsystem for this one example. There are better, cross platform ways to compile extensions that we’ll see soon.

Desired usage in Python:

y = square(x)

ctypes

  • 0 dependencies

  • Shared libraries with C interfaces only

  • No help with creating/managing the SO’s.

C bindings are very easy. Just compile into a shared library, then open it in Python with the built in ctypes module:

Warning: You can only load a file once; future loads just reuse the existing library (much like Python imports). So if you change the file, you need to restart the kernel.

Also, this may inherit the changes to Windows DLL path loading in Python 3.8+, so keep that in mind if you have trouble loading an so (dll).

Now, we need to set the argument types - they can’t be inferred from an SO.

6.25

Notice what was required and why:

  1. First we load the library, just handing the file we want to open.

  2. Next, we set .argtypes (argument types) with a iterable of types and .restype with the return type. We had to use the types from ctypes since compiled types are not the same as Python types. SO’s do not store signatures!

There is an alternate way to do this:

  1. Create a new C Function type, listing the return type, then the argument type(s) if there are any. Yes, this is different order and structure than Type Hinting in modern Python...

Now “cast” the function with your function type.

And this also works.

4.0

However, it sets up a little bit of extra machinery so it’s a hair slower. However, this is itself now a valid c_type and can be passed to code! You can even wrap Python functions (won’t be faster, but powerful, nevertheless)!

Suggestion: wrap your library in a class!

4.0

There is a benefit to these types; they can wrap pure Python functions and make them fully callable from C, as well! Not fast, but can be done.

But what about C++ (and all other languages)?

This wasn’t really language specific, it is just a property of compiled libraries. However, C++ applies name mangling to function symbols by default, making the exported symbol names unpredictable and compiler-specific. To provide a stable C interface that ctypes or other FFI tools can reliably load, you explicitly use extern "C" to disable name mangling for specific functions and maintain C linkage:

Let’s try a slightly more advanced example, this time in C++:

Writing templates.cpp

This is not Python specific! If you want to load a SO (DLL) from any library, you need to do this.

4
4.0

NumPy tools

Let’s briefly mention NumPy has adapters to help you with ctypes, called numpy.ctypeslib. You can convert to/from any object providing an __array_interface__, like NumPy arrays. It has a loader with more consistent defaults across operating systems. You also have a special pointer that can do bounds checking and such for array arguments.

CFFI

  • The C Foreign Function Interface for Python

  • C only

  • Developed for PyPy, but available in CPython too

The same example as before:

4.0
<cdata 'float(*)(float)' 0x7f24e1f490f9>

Notice we were able to give a C header this time; no futzing around with C function types by hand.

Try it yourself

Let’s try it now. We’ll take a few minutes to play with the code; we are also available to help with troubleshooting at this time. Double the elements in this array (for simplicity, do it in-place):

Writing array_square.c
Ellipsis

Remember to match the types! This is a 32 bit float, not a 64 bit double!

Ellipsis
array([1., 2., 3., 4., 5.], dtype=float32)

Mix and match with Numba and more

We can mix ctypes (or cffi) with Numba, as well! Here’s another way to do the above problem:

45.1 μs ± 15.3 μs per loop (mean ± std. dev. of 7 runs, 1 loop each)

We capture lib.square in a variable because Numba’s @vectorize decorator needs to access the function at compilation time, and ctypes function objects have special behavior that works better when referenced this way.