I'm trying to use CFFI for dynamic linking of dependent C libraries in Python; am I mis-understanding it?
In the following extremely simplified example, library foo_b depends on library foo_a. Specifically, it depends on bar, and it exposes its own function baz.
from cffi import FFI
from pathlib import Path
Path('foo_a.h').write_text("""\
int bar(int x);
""")
Path('foo_a.c').write_text("""\
#include "foo_a.h"
int bar(int x) {
return x + 69;
}
""")
Path('foo_b.h').write_text("""\
int baz(int x);
""")
Path('foo_b.c').write_text("""\
#include "foo_a.h"
#include "foo_b.h"
int baz(int x) {
return bar(x * 100);
}
""")
ffi_a = FFI()
ffi_b = FFI()
ffi_a.cdef('int bar(int x);')
ffi_a.set_source('ffi_foo_a', '#include "foo_a.h"', sources=['foo_a.c'])
ffi_a.compile()
ffi_b.cdef('int baz(int x);')
ffi_b.include(ffi_a)
ffi_b.set_source('ffi_foo_b', '#include "foo_b.h"', sources=['foo_b.c'])
ffi_b.compile()
import ffi_foo_a
if ffi_foo_a.lib.bar(1) == 70: print('foo_a OK')
else: raise AssertionError('foo_a ERR')
import ffi_foo_b # Crashes on _this_ line due to undefined symbol "bar", DESPITE the fact that we included ffi_a, which should provide that symbol
if ffi_foo_b.lib.baz(420) == 42069: print('foo_b OK')
else: raise AssertionError('foo_b ERR')
However, it doesn't compile, instead crashing on the indicated line with the indicated error message.
I don't understand why this example isn't working, considering the following in the CFFI documentation:
For out-of-line modules, the
ffibuilder.include(other_ffibuilder)line should occur in the build script, and theother_ffibuilderargument should be another FFI instance that comes from another build script. When the two build scripts are turned into generated files, say_ffi.soand_other_ffi.so, then importing_ffi.sowill internally cause_other_ffi.soto be imported. At that point, the real declarations from_other_ffi.soare combined with the real declarations from_ffi.so.
If ffibuilder.include() isn't the right way to dynamically link together multiple CFFI-based libraries, what is?
Or if ffibuilder.include() is the right way to dynamically link together multiple CFFI-based libraries, what am I doing wrong?
The library
ffi_foo_b.cpython-XXX.sofails to import because of the C-level issue that it doesn't find the symbolbar. Looking at the tests in CFFI, it seems that this case is not supported: theffi_foo_b.include(ffi_foo_a)syntax is not enough to cause the C-levelffi_foo_b.cpython-XXX.soto be compiled specially. If a symbol fromffi_foo_a.cpython-XXX.sois needed at the C level forffi_foo_b.cpython-XXX.soto load, then it won't work. The CFFI documentation is kinda misleading. It means rather that, say, you can take things likestructtype definitions that appear inffi_aand use them fromlib_foo_b.ffi.In terms of CFFI implementation, I'm not quite sure how this case could be supported: for example, on Windows you need special tricks to export a symbol from a DLL (with the extension
.dllor.pydfor CPython extension modules). In other words your example on Windows produces affi_foo_athat doesn't automatically exportbarat all at the level of C. You can still callffi_foo_a.bar(), because the look up ofbarinsideffi_foo_ais done at a different level (on any platform) than the raw C level. You could also call it asffi_foo_b.bar()thanks to theffi.include(), if there wasn't the problem thatffi_foo_btries to usebarat the C level directly. But you can't use the C symbolbarfromfoo_b.set_source().For now I'd recommend one of these solutions:
consolidating everything inside a single
ffi.make standard, CFFI- and Python-independant
.soas you need them, e.g.foo_a.soandfoo_b.so, with the proper C-level Makefile or something, withfoo_bcompiled in such a way to say that it depends onfoo_a---using some gcc arguments like-L . -lfoo_a.so. Then you can wrap these two.sowith two CFFI libraries. You can useffi.include()then; this is really howffi.include()was meant to work.as a mixture of 1 and 2, you can probably have
foo_a.socompiled as a standalone module, and then wrapped inffi_a, but keep the existing solution forffi_bbecause no symbol fromffi_bis expected to be found at the C level from somewhere else.EDIT: two more possible solutions:
You can probably have it working just like you wrote, but you need to add platform- and compiler-specific options. I'd not recommend that option.
You can bypass the problem by not calling
bar()fromfoo_b.c. Instead, you'd write code like this:This removes the dependency at the C level. You need to initialize the global variable once after import: