NiceLib

NiceLib

NiceLib is a package for rapidly developing “nice” Python bindings to C libraries, using cffi.

It lets you turn a C function like this

int my_c_function(int arg1, float arg2, uint* out_arr, int out_arr_size, int reserved);

into a Python function that can be called like this

out_arr = my_c_function(arg1, arg2)

just by defining a signature like this

class MyLib(NiceLib):
    ...
    my_c_function = Sig('in', 'in', 'arr', 'len', 'ignore')

while giving you easy error handling and more.

Installing

NiceLib is available on PyPI:

$ pip install nicelib

If you would like to use the development version, download and extract a zip of the source from our GitHub page or clone it using git. Now install:

$ cd /path/to/NiceLib
$ pip install .

Feedback / Contributing

For contributing, reporting issues, and providing feedback, see our GitHub page. Feedback is greatly appreciated!

Introduction

NiceLib essentially consists of two layers:

  • Tools for directly using C headers to generate an importable Python module
  • An interface for quickly and cleanly defining a Pythonic mid-level binding

Mid-level means that functions take input arguments, return output arguments, and raise Exceptions on error. NiceLib tries to simplify commonly-seen C idioms, like using user-created buffers for returning output strings. Take, for example, this toy binding for the NIDAQmx library:

from nicelib import load_lib, NiceLib, Sig

class NiceNI(NiceLib):
    _info_ = load_lib('ni')
    _prefix_ = 'DAQmx'

    GetSysDevNames = Sig('buf', 'len')

Now, we can simply call NiceNI.GetSysDevName(code), which is equivalent to the following:

lib_info = load_lib('ni')
ffi, lib = lib_info._ffi, lib_info.lib

def GetSysDevNames():
    buflen = 512
    buf = ffi.new('char[]', buflen)
    ret = lib.DAQmxGetSysDevNames(buf, buflen)
    return ffi.string(buf), ret

Many of our C library’s functions may use status codes as their return value. To automatically check the return code and raise an exception if warranted, we can use _ret_:

from nicelib import RetHandler

# Define a new return handler
@RetHandler(num_retvals=0)
def daq_errorcheck(ret):
    if ret != 0:
        raise DAQError(ret)

class NiceNI(NiceLib):
    _ret_ = daq_errorcheck
    [...]

Often a C library will have method-like functions, each of which take a handle as its first argument. These translate naturally into Python objects, obviating you of the need to pass the handle all the time. For example, here’s a partial definition of a Task object, again wrapping NIDAQmx:

class NiceNI(NiceLib):
    [...]
    CreateTask = Sig('in', 'out')

    class Task(NiceObject):
        _init_ = 'CreateTask'

        StartTask = Sig('in')
        ReadAnalogScalarF64 = Sig('in', 'in', 'out', 'ignore')

For both of these function signatures, the first 'in' argument is the Task handle. We can then use the class to make and use Task objects:

task = NiceNI.Task('myTask')
task.StartTask()

By specifying _init_ when defining Task, the string 'myTask' is passed to NiceNI.CreateTask, whose return value (a task handle) is stored in the Task instance.

Automatically Processing Headers

The awesome cffi package greatly improves wrapping C libraries in Python by allowing you to load header files directly, instead of manually churning out mind-numbing ctypes boilerplate. However it’s not perfect—in particular, it makes a fairly limited attempt at preprocessing header files, meaning that in many cases you’ll have to preprocess them yourself, either manually or by running e.g. the gcc preprocessor. NiceLib provides preprocessing facilities that aim to allow you to use unmodified header files to generate a “compiled” ffi module, without requiring a C compiler.

NiceLib’s preprocessor has basic support for both object-like and simple function-like macros, which it translates into equivalent [1] portable Python source code. For example

#define CONST_VAL       (1 << 4) | (1 << 1)
#define LTZ(val)        ((val) < 0)

becomes the Python code:

CONST_VAL = (1 << 4) | (1 << 1)
def LTZ(val):
    return (val) < 0

The preprocessor also supports conditionals (#ifdef and friends), #includes, and platform-specific predefined macros (like __linux__, __WIN64, and __x86_64).

Currently, #pragma once is supported, but other #pragma directives are ignored.

[1]Note that, due to the nature of the C preprocessor, this generated code cannot always be truly equivalent. However, in the overwhelming majority of cases, the macros defined in library header files are quite simple.