python-cffi / cffi

A Foreign Function Interface package for calling C libraries from Python.
https://cffi.readthedocs.io/en/latest/
Other
87 stars 29 forks source link

Document how to use meson to build a cffi cextension #47

Open mattip opened 6 months ago

mattip commented 6 months ago

setuptools is not the only python build backend. Projects in the scientific python stack have moved to meson. It would be nice to provide an example of how to best use meson to build a cffi c-extension module.

mattip commented 6 months ago

I think this would help @minrk in ported pyzmq to meson.

inklesspen commented 5 months ago

I believe the big issue with this is that cffi relies on setuptools in some cases; specifically, it uses the distutils Distribution and Extension classes to compile extension modules, and as of Python 3.12 these have been removed from the stdlib and are only available in setuptools.

However, distutils is only actually needed for the step which automatically compiles the generated c source. Unfortunately, at the moment doing without this requires digging a little deeper into the internals of cffi. (I suspect it could be made simpler to access, but I don't know what constraints the cffi devs are working under here.)

from cffi import FFI

ffibuilder = FFI()

ffibuilder.cdef("int abs(int);")

ffibuilder.set_source("_simple", "#include <stdlib.h>")

if __name__ == "__main__":
    from cffi.recompiler import Recompiler
    # This code is based on FFI.emit_c_code and the functions it calls.
    # kwds are options which would be passed to the distutils Extension class
    # see https://setuptools.pypa.io/en/stable/userguide/ext_modules.html#setuptools.Extension
    # this may or may not be relevant when using meson
    module_name, source, source_extension, kwds = ffibuilder._assigned_source
    recompiler = Recompiler(ffibuilder, module_name)
    recompiler.collect_type_table()
    recompiler.collect_step_tables()
    filename = module_name + source_extension
    with open(filename, "w") as f:
        recompiler.write_source_to_f(f, source)

This Python script can be run with just cffi as a dependency. It generates the C extension source and writes it to a file; it should be obvious how to make it write to a different location. I suspect you can make this work with meson's Generating sources support.

arigo commented 5 months ago

@inklesspen This might be an oversight. Would it help if we move the line 1546 of recompile.py into the if call_c_compiler: branch a few lines below?

diff --git a/src/cffi/recompiler.py b/src/cffi/recompiler.py
index 4167bc05..19522470 100644
--- a/src/cffi/recompiler.py
+++ b/src/cffi/recompiler.py
@@ -1543,10 +1543,10 @@ def recompile(ffi, module_name, preamble, tmpdir='.', call_c_compiler=True,
             else:
                 target = '*'
         #
-        ext = ffiplatform.get_extension(ext_c_file, module_name, **kwds)
         updated = make_c_source(ffi, module_name, preamble, c_file,
                                 verbose=compiler_verbose)
         if call_c_compiler:
+            ext = ffiplatform.get_extension(ext_c_file, module_name, **kwds)
             patchlist = []
             cwd = os.getcwd()
             try:

EDIT: right, it needs more care because sometimes it also returns ext. The point is, this ext is not used when emit_c_code() is called, so my question is: is that the only line causing problem in your case? We can refactor the code a little bit to avoid calling ffiplatform.get_extension() in this situation.

inklesspen commented 5 months ago

The issue there is the else branch on if call_c_compiler; it returns the ext instance, for use in FFI.distutils_extension. Essentially you have three use cases for recompile: generate c source, generate c source and compile it, generate c source and distutils metadata. And the boolean logic doesn't work as well with that, as you've noticed.

My ideal scenario would be a version of emit_c_code which accepts a file-like object instead of a filename, and which can output c source to that file-like object (without using ext, and therefore without using distutils). But I suspect I will also want access to the contents of the _assigned_source tuple, and I would prefer not to use underscore-prefixed properties… (Specifically, I think module_name and kwds may be useful to a consuming build tool.)

arigo commented 5 months ago

OK, that makes sense. I'm going to check what makes sense (and still works generally instead of just in one case---there are a lot of other cases in recompile()...). But feel free to give a patch or open a pull request, too.

inklesspen commented 4 months ago

@mattip I put together https://github.com/inklesspen/meson-python-cffi-example which shows one possible way to do this.