| | """ |
| | Build a c-extension module on-the-fly in tests. |
| | See build_and_import_extensions for usage hints |
| | |
| | """ |
| |
|
| | import os |
| | import pathlib |
| | import subprocess |
| | import sys |
| | import sysconfig |
| | import textwrap |
| |
|
| | __all__ = ['build_and_import_extension', 'compile_extension_module'] |
| |
|
| |
|
| | def build_and_import_extension( |
| | modname, functions, *, prologue="", build_dir=None, |
| | include_dirs=None, more_init=""): |
| | """ |
| | Build and imports a c-extension module `modname` from a list of function |
| | fragments `functions`. |
| | |
| | |
| | Parameters |
| | ---------- |
| | functions : list of fragments |
| | Each fragment is a sequence of func_name, calling convention, snippet. |
| | prologue : string |
| | Code to precede the rest, usually extra ``#include`` or ``#define`` |
| | macros. |
| | build_dir : pathlib.Path |
| | Where to build the module, usually a temporary directory |
| | include_dirs : list |
| | Extra directories to find include files when compiling |
| | more_init : string |
| | Code to appear in the module PyMODINIT_FUNC |
| | |
| | Returns |
| | ------- |
| | out: module |
| | The module will have been loaded and is ready for use |
| | |
| | Examples |
| | -------- |
| | >>> functions = [("test_bytes", "METH_O", \"\"\" |
| | if ( !PyBytesCheck(args)) { |
| | Py_RETURN_FALSE; |
| | } |
| | Py_RETURN_TRUE; |
| | \"\"\")] |
| | >>> mod = build_and_import_extension("testme", functions) |
| | >>> assert not mod.test_bytes('abc') |
| | >>> assert mod.test_bytes(b'abc') |
| | """ |
| | if include_dirs is None: |
| | include_dirs = [] |
| | body = prologue + _make_methods(functions, modname) |
| | init = """ |
| | PyObject *mod = PyModule_Create(&moduledef); |
| | #ifdef Py_GIL_DISABLED |
| | PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); |
| | #endif |
| | """ |
| | if not build_dir: |
| | build_dir = pathlib.Path('.') |
| | if more_init: |
| | init += """#define INITERROR return NULL |
| | """ |
| | init += more_init |
| | init += "\nreturn mod;" |
| | source_string = _make_source(modname, init, body) |
| | mod_so = compile_extension_module( |
| | modname, build_dir, include_dirs, source_string) |
| | import importlib.util |
| | spec = importlib.util.spec_from_file_location(modname, mod_so) |
| | foo = importlib.util.module_from_spec(spec) |
| | spec.loader.exec_module(foo) |
| | return foo |
| |
|
| |
|
| | def compile_extension_module( |
| | name, builddir, include_dirs, |
| | source_string, libraries=None, library_dirs=None): |
| | """ |
| | Build an extension module and return the filename of the resulting |
| | native code file. |
| | |
| | Parameters |
| | ---------- |
| | name : string |
| | name of the module, possibly including dots if it is a module inside a |
| | package. |
| | builddir : pathlib.Path |
| | Where to build the module, usually a temporary directory |
| | include_dirs : list |
| | Extra directories to find include files when compiling |
| | libraries : list |
| | Libraries to link into the extension module |
| | library_dirs: list |
| | Where to find the libraries, ``-L`` passed to the linker |
| | """ |
| | modname = name.split('.')[-1] |
| | dirname = builddir / name |
| | dirname.mkdir(exist_ok=True) |
| | cfile = _convert_str_to_file(source_string, dirname) |
| | include_dirs = include_dirs or [] |
| | libraries = libraries or [] |
| | library_dirs = library_dirs or [] |
| |
|
| | return _c_compile( |
| | cfile, outputfilename=dirname / modname, |
| | include_dirs=include_dirs, libraries=libraries, |
| | library_dirs=library_dirs, |
| | ) |
| |
|
| |
|
| | def _convert_str_to_file(source, dirname): |
| | """Helper function to create a file ``source.c`` in `dirname` that contains |
| | the string in `source`. Returns the file name |
| | """ |
| | filename = dirname / 'source.c' |
| | with filename.open('w') as f: |
| | f.write(str(source)) |
| | return filename |
| |
|
| |
|
| | def _make_methods(functions, modname): |
| | """ Turns the name, signature, code in functions into complete functions |
| | and lists them in a methods_table. Then turns the methods_table into a |
| | ``PyMethodDef`` structure and returns the resulting code fragment ready |
| | for compilation |
| | """ |
| | methods_table = [] |
| | codes = [] |
| | for funcname, flags, code in functions: |
| | cfuncname = f"{modname}_{funcname}" |
| | if 'METH_KEYWORDS' in flags: |
| | signature = '(PyObject *self, PyObject *args, PyObject *kwargs)' |
| | else: |
| | signature = '(PyObject *self, PyObject *args)' |
| | methods_table.append( |
| | "{\"%s\", (PyCFunction)%s, %s}," % (funcname, cfuncname, flags)) |
| | func_code = f""" |
| | static PyObject* {cfuncname}{signature} |
| | {{ |
| | {code} |
| | }} |
| | """ |
| | codes.append(func_code) |
| |
|
| | body = "\n".join(codes) + """ |
| | static PyMethodDef methods[] = { |
| | %(methods)s |
| | { NULL } |
| | }; |
| | static struct PyModuleDef moduledef = { |
| | PyModuleDef_HEAD_INIT, |
| | "%(modname)s", /* m_name */ |
| | NULL, /* m_doc */ |
| | -1, /* m_size */ |
| | methods, /* m_methods */ |
| | }; |
| | """ % {'methods': '\n'.join(methods_table), 'modname': modname} |
| | return body |
| |
|
| |
|
| | def _make_source(name, init, body): |
| | """ Combines the code fragments into source code ready to be compiled |
| | """ |
| | code = """ |
| | #include <Python.h> |
| | |
| | %(body)s |
| | |
| | PyMODINIT_FUNC |
| | PyInit_%(name)s(void) { |
| | %(init)s |
| | } |
| | """ % { |
| | 'name': name, 'init': init, 'body': body, |
| | } |
| | return code |
| |
|
| |
|
| | def _c_compile(cfile, outputfilename, include_dirs, libraries, |
| | library_dirs): |
| | link_extra = [] |
| | if sys.platform == 'win32': |
| | compile_extra = ["/we4013"] |
| | link_extra.append('/DEBUG') |
| | elif sys.platform.startswith('linux'): |
| | compile_extra = [ |
| | "-O0", "-g", "-Werror=implicit-function-declaration", "-fPIC"] |
| | else: |
| | compile_extra = [] |
| |
|
| | return build( |
| | cfile, outputfilename, |
| | compile_extra, link_extra, |
| | include_dirs, libraries, library_dirs) |
| |
|
| |
|
| | def build(cfile, outputfilename, compile_extra, link_extra, |
| | include_dirs, libraries, library_dirs): |
| | "use meson to build" |
| |
|
| | build_dir = cfile.parent / "build" |
| | os.makedirs(build_dir, exist_ok=True) |
| | with open(cfile.parent / "meson.build", "wt") as fid: |
| | link_dirs = ['-L' + d for d in library_dirs] |
| | fid.write(textwrap.dedent(f"""\ |
| | project('foo', 'c') |
| | py = import('python').find_installation(pure: false) |
| | py.extension_module( |
| | '{outputfilename.parts[-1]}', |
| | '{cfile.parts[-1]}', |
| | c_args: {compile_extra}, |
| | link_args: {link_dirs}, |
| | include_directories: {include_dirs}, |
| | ) |
| | """)) |
| | native_file_name = cfile.parent / ".mesonpy-native-file.ini" |
| | with open(native_file_name, "wt") as fid: |
| | fid.write(textwrap.dedent(f"""\ |
| | [binaries] |
| | python = '{sys.executable}' |
| | """)) |
| | if sys.platform == "win32": |
| | subprocess.check_call(["meson", "setup", |
| | "--buildtype=release", |
| | "--vsenv", ".."], |
| | cwd=build_dir, |
| | ) |
| | else: |
| | subprocess.check_call(["meson", "setup", "--vsenv", |
| | "..", f'--native-file={os.fspath(native_file_name)}'], |
| | cwd=build_dir |
| | ) |
| |
|
| | so_name = outputfilename.parts[-1] + get_so_suffix() |
| | subprocess.check_call(["meson", "compile"], cwd=build_dir) |
| | os.rename(str(build_dir / so_name), cfile.parent / so_name) |
| | return cfile.parent / so_name |
| |
|
| |
|
| | def get_so_suffix(): |
| | ret = sysconfig.get_config_var('EXT_SUFFIX') |
| | assert ret |
| | return ret |
| |
|