Generating C Sources for CFFI Extensions

CFFI ships a command-line tool, cffi-gen-src, that produces the same output as FFI.emit_c_code(): a .c source file ready to be compiled into a CPython c-extension module. This is useful for projects that want CFFI to generate source code for a later compilation step without depending on setuptools.

Installing CFFI installs the cffi-gen-src script; running python -m cffi.gen_src invokes the same command line and behaves identically. Use the former where a script with a Python shebang makes more sense (e.g. cross-compiling) and use the latter when the console script is not on PATH but a Python interpreter is.

In a packaged project, cffi-gen-src is usually called by a script or rule executed by a build backend, and the build backend then consumes the generated C file. Any Python build backend that can run a source generator, such as meson-python, scikit-build-core, or similar, can use cffi-gen-src the same way. The rest of this page uses meson-python in the examples.

The command line is the only supported interface; the Python API inside cffi._cffi_gen_src is private.

The implementation is based on the cffi-buildtool project by Rose Davidson (@inklesspen on GitHub). It is included in CFFI with permission of the original author.

The cffi-gen-src Command-line Tool

cffi-gen-src has two subcommands. The first, exec-python is most useful if you already have a Python script that sets up an FFI definition. The second, read-sources is most useful if you are wrapping a large API surface and want a more structured way to specify a set of FFI definitions.

cffi-gen-src exec-python

This mode executes a Python script that creates a cffi.FFI object as a module-level global and calls FFI.set_source() on it, then emits the generated C source. By default cffi-gen-src looks for a global named ffibuilder; pass --ffi-var if the object has another name. Apart from being run by the tool instead of by hand, it is the same script the CFFI docs show under Main mode of usage.

Let’s say we want to create an extension module that wraps a single C function named square. The square function has the following signature:

int square(int n);

Let’s also say this function definition is exposed inside a header named square.h. We could create a set of FFI bindings for this function given this _squared_build.py:

from cffi import FFI

ffibuilder = FFI()

ffibuilder.cdef("int square(int n);")

ffibuilder.set_source(
    "squared._squared",
    '#include "square.h"',
)

To generate the source code for the C extension, you would run:

$ cffi-gen-src exec-python _squared_build.py _squared.c

Many CFFI FFI definition scripts have an if __name__ == "__main__" section that triggers a compilation step. This is not needed for a script run by cffi-gen-src, which only writes the generated C source. If the script does have such a section it is harmless: the script is executed with __name__ set to "cffi.gen_src", so the block is skipped and an existing FFI definition script works unchanged.

If the cffi.FFI is bound to a name other than ffibuilder, pass --ffi-var. To make that concrete, let’s say your FFI definition script creates an FFI object named make_ffi:

from cffi import FFI

make_ffi = FFI()

In that case, you would pass --ffi-var=make_ffi to cffi-gen-src:

$ cffi-gen-src exec-python --ffi-var=make_ffi _squared_build.py _squared.c

Note

cffi-gen-src emits C source for out-of-line API mode modules. It cannot emit source for ABI mode modules where FFI.set_source() is called with None.

Note

CFFI’s setuptools integration supports passing libraries=, library_dirs=, include_dirs=, and extra_compile_args= arguments to FFI.set_source(). When using cffi-gen-src, these arguments are ignored. Link and include settings are the responsibility of the compilation step that happens after generating the C extension sources. If you are using meson-python following the examples in this section, you would express them through the dependencies, include_directories, and c_args arguments of py.extension_module().

cffi-gen-src read-sources

For larger modules, keeping the FFI definition and any necessary C source prelude in separate files tends to be easier to work with – you can configure your editor to treat them as plain C, and write presubmit tooling that parses the FFI definition directly without extracting it from a Python script.

Given squared.cdef.txt:

int square(int n);

and squared.csrc.c:

#include "square.h"

you would run the following command to generate the C source code for a CFFI extension:

$ cffi-gen-src read-sources squared._squared squared.cdef.txt squared.csrc.c _squared.c

With all other details left exactly the same as the exec-python example.

The first positional argument passed to the read-sources command is the fully qualified module name that will be embedded in the generated C source code (equivalent to the first argument to FFI.set_source()).

A Worked Example Using meson-python

Project layout:

squared/
├── pyproject.toml
├── meson.build
└── src/
    ├── squared/
    │   ├── __init__.py
    │   └── _squared_build.py
    └── csrc/
        ├── square.h
        └── square.c

pyproject.toml:

[build-system]
build-backend = 'mesonpy'
requires = ['meson-python', 'cffi']

[project]
name = 'squared'
version = '0.1.0'
description = 'Small self-contained example project that builds a CFFI extension via meson-python.'
requires-python = '>=3.9'
dependencies = ['cffi']

meson.build:

project(
    'squared',
    'c',
    version: '0.1.0',
    default_options: ['warning_level=2'],
)

py = import('python').find_installation(pure: false)

install_subdir('src/squared', install_dir: py.get_install_dir())

cffi_gen_src = find_program('cffi-gen-src')

square_lib = static_library(
    'square',
    'src/csrc/square.c',
    include_directories: include_directories('src/csrc'),
)
square_dep = declare_dependency(
    link_with: square_lib,
    include_directories: include_directories('src/csrc'),
)

squared_ext_src = custom_target(
    'squared-cffi-src',
    command: [
        cffi_gen_src,
        'exec-python',
        '@INPUT@',
        '@OUTPUT@',
    ],
    output: '_squared.c',
    input: ['src/squared/_squared_build.py'],
)

py.extension_module(
    '_squared',
    squared_ext_src,
    subdir: 'squared',
    install: true,
    dependencies: [square_dep],
)

src/squared/__init__.py:

from ._squared import ffi, lib


def squared(n):
    return lib.square(n)

src/squared/_squared_build.py:

from cffi import FFI

ffibuilder = FFI()

ffibuilder.cdef("int square(int n);")

ffibuilder.set_source(
    "squared._squared",
    '#include "square.h"',
)

if __name__ == "__main__":
    ffibuilder.compile(verbose=True)

src/csrc/square.h:

#ifndef SQUARE_H
#define SQUARE_H

int square(int n);

#endif

src/csrc/square.c:

#include "square.h"

int square(int n) {
    return n * n;
}

Build and install the project with any Python build front-end. For example, with pip, in the root squared directory:

$ python -m pip install .
$ python -c "from squared import squared; print(squared(7))"
49

To switch this project to read-sources mode, replace _squared_build.py with two files, so that the project layout becomes:

squared/
├── pyproject.toml
├── meson.build
└── src/
    ├── squared/
    │   ├── __init__.py
    │   ├── squared.cdef.txt
    │   └── squared.csrc.c
    └── csrc/
        ├── square.h
        └── square.c

The first new file, squared.cdef.txt, contains the FFI definition:

int square(int n);

and the second, squared.csrc.c, contains the C source prelude:

#include "square.h"

then change two spots in the meson.build file. First, update the custom_target command to call cffi-gen-src read-sources with two input arguments:

command: [
  cffi_gen_src,
  'read-sources',
  'squared._squared',
  '@INPUT0@',
  '@INPUT1@',
  '@OUTPUT@',
],

and then list both of the FFI specification files under input:

input: ['src/squared/squared.cdef.txt', 'src/squared/squared.csrc.c']

Distributing CFFI Extensions using Setuptools

You can (but don’t have to) use CFFI’s Distutils or Setuptools integration when writing a setup.py. For Distutils (only in out-of-line API mode; deprecated since Python 3.10):

# setup.py (requires CFFI to be installed first)
from distutils.core import setup

import foo_build   # possibly with sys.path tricks to find it

setup(
    ...,
    ext_modules=[foo_build.ffibuilder.distutils_extension()],
)

For Setuptools (out-of-line only, but works in ABI or API mode; recommended):

# setup.py (with automatic dependency tracking)
from setuptools import setup

setup(
    ...,
    setup_requires=["cffi>=1.0.0"],
    cffi_modules=["package/foo_build.py:ffibuilder"],
    install_requires=["cffi>=1.0.0"],
)

Note again that the foo_build.py example contains the following lines, which mean that the ffibuilder is not actually compiled when package.foo_build is merely imported—it will be compiled independently by the Setuptools logic, using compilation parameters provided by Setuptools:

if __name__ == "__main__":    # not when running with setuptools
    ffibuilder.compile(verbose=True)
  • Note that some bundler tools that try to find all modules used by a project, like PyInstaller, will miss _cffi_backend in the out-of-line mode because your program contains no explicit import cffi or import _cffi_backend. You need to add _cffi_backend explicitly (as a “hidden import” in PyInstaller, but it can also be done more generally by adding the line import _cffi_backend in your main program).

Note that CFFI actually contains two different FFI classes. The page Using the ffi/lib objects describes the common functionality. It is what you get in the from package._foo import ffi lines above. On the other hand, the extended FFI class is the one you get from import cffi; ffi_or_ffibuilder = cffi.FFI(). It has the same functionality (for in-line use), but also the extra methods described below (to prepare the FFI). NOTE: We use the name ffibuilder instead of ffi in the out-of-line context, when the code is about producing a _foo.so file; this is an attempt to distinguish it from the different ffi object that you get by later saying from _foo import ffi.

The reason for this split of functionality is that a regular program using CFFI out-of-line does not need to import the cffi pure Python package at all. (Internally it still needs _cffi_backend, a C extension module that comes with CFFI; this is why CFFI is also listed in install_requires=.. above. In the future this might be split into a different PyPI package that only installs _cffi_backend.)

Note that a few small differences do exist: notably, from _foo import ffi returns an object of a type written in C, which does not let you add random attributes to it (nor does it have all the underscore-prefixed internal attributes of the Python version). Similarly, the lib objects returned by the C version are read-only, apart from writes to global variables. Also, lib.__dict__ does not work before version 1.2 or if lib happens to declare a name called __dict__ (use instead dir(lib)). The same is true for lib.__class__, lib.__all__ and lib.__name__ added in successive versions.