extbuild.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. """
  2. Build a c-extension module on-the-fly in tests.
  3. See build_and_import_extensions for usage hints
  4. """
  5. import os
  6. import pathlib
  7. import subprocess
  8. import sys
  9. import sysconfig
  10. import textwrap
  11. __all__ = ['build_and_import_extension', 'compile_extension_module']
  12. def build_and_import_extension(
  13. modname, functions, *, prologue="", build_dir=None,
  14. include_dirs=[], more_init=""):
  15. """
  16. Build and imports a c-extension module `modname` from a list of function
  17. fragments `functions`.
  18. Parameters
  19. ----------
  20. functions : list of fragments
  21. Each fragment is a sequence of func_name, calling convention, snippet.
  22. prologue : string
  23. Code to precede the rest, usually extra ``#include`` or ``#define``
  24. macros.
  25. build_dir : pathlib.Path
  26. Where to build the module, usually a temporary directory
  27. include_dirs : list
  28. Extra directories to find include files when compiling
  29. more_init : string
  30. Code to appear in the module PyMODINIT_FUNC
  31. Returns
  32. -------
  33. out: module
  34. The module will have been loaded and is ready for use
  35. Examples
  36. --------
  37. >>> functions = [("test_bytes", "METH_O", \"\"\"
  38. if ( !PyBytesCheck(args)) {
  39. Py_RETURN_FALSE;
  40. }
  41. Py_RETURN_TRUE;
  42. \"\"\")]
  43. >>> mod = build_and_import_extension("testme", functions)
  44. >>> assert not mod.test_bytes('abc')
  45. >>> assert mod.test_bytes(b'abc')
  46. """
  47. body = prologue + _make_methods(functions, modname)
  48. init = """
  49. PyObject *mod = PyModule_Create(&moduledef);
  50. #ifdef Py_GIL_DISABLED
  51. PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED);
  52. #endif
  53. """
  54. if not build_dir:
  55. build_dir = pathlib.Path('.')
  56. if more_init:
  57. init += """#define INITERROR return NULL
  58. """
  59. init += more_init
  60. init += "\nreturn mod;"
  61. source_string = _make_source(modname, init, body)
  62. try:
  63. mod_so = compile_extension_module(
  64. modname, build_dir, include_dirs, source_string)
  65. except Exception as e:
  66. # shorten the exception chain
  67. raise RuntimeError(f"could not compile in {build_dir}:") from e
  68. import importlib.util
  69. spec = importlib.util.spec_from_file_location(modname, mod_so)
  70. foo = importlib.util.module_from_spec(spec)
  71. spec.loader.exec_module(foo)
  72. return foo
  73. def compile_extension_module(
  74. name, builddir, include_dirs,
  75. source_string, libraries=[], library_dirs=[]):
  76. """
  77. Build an extension module and return the filename of the resulting
  78. native code file.
  79. Parameters
  80. ----------
  81. name : string
  82. name of the module, possibly including dots if it is a module inside a
  83. package.
  84. builddir : pathlib.Path
  85. Where to build the module, usually a temporary directory
  86. include_dirs : list
  87. Extra directories to find include files when compiling
  88. libraries : list
  89. Libraries to link into the extension module
  90. library_dirs: list
  91. Where to find the libraries, ``-L`` passed to the linker
  92. """
  93. modname = name.split('.')[-1]
  94. dirname = builddir / name
  95. dirname.mkdir(exist_ok=True)
  96. cfile = _convert_str_to_file(source_string, dirname)
  97. include_dirs = include_dirs + [sysconfig.get_config_var('INCLUDEPY')]
  98. return _c_compile(
  99. cfile, outputfilename=dirname / modname,
  100. include_dirs=include_dirs, libraries=[], library_dirs=[],
  101. )
  102. def _convert_str_to_file(source, dirname):
  103. """Helper function to create a file ``source.c`` in `dirname` that contains
  104. the string in `source`. Returns the file name
  105. """
  106. filename = dirname / 'source.c'
  107. with filename.open('w') as f:
  108. f.write(str(source))
  109. return filename
  110. def _make_methods(functions, modname):
  111. """ Turns the name, signature, code in functions into complete functions
  112. and lists them in a methods_table. Then turns the methods_table into a
  113. ``PyMethodDef`` structure and returns the resulting code fragment ready
  114. for compilation
  115. """
  116. methods_table = []
  117. codes = []
  118. for funcname, flags, code in functions:
  119. cfuncname = "%s_%s" % (modname, funcname)
  120. if 'METH_KEYWORDS' in flags:
  121. signature = '(PyObject *self, PyObject *args, PyObject *kwargs)'
  122. else:
  123. signature = '(PyObject *self, PyObject *args)'
  124. methods_table.append(
  125. "{\"%s\", (PyCFunction)%s, %s}," % (funcname, cfuncname, flags))
  126. func_code = """
  127. static PyObject* {cfuncname}{signature}
  128. {{
  129. {code}
  130. }}
  131. """.format(cfuncname=cfuncname, signature=signature, code=code)
  132. codes.append(func_code)
  133. body = "\n".join(codes) + """
  134. static PyMethodDef methods[] = {
  135. %(methods)s
  136. { NULL }
  137. };
  138. static struct PyModuleDef moduledef = {
  139. PyModuleDef_HEAD_INIT,
  140. "%(modname)s", /* m_name */
  141. NULL, /* m_doc */
  142. -1, /* m_size */
  143. methods, /* m_methods */
  144. };
  145. """ % dict(methods='\n'.join(methods_table), modname=modname)
  146. return body
  147. def _make_source(name, init, body):
  148. """ Combines the code fragments into source code ready to be compiled
  149. """
  150. code = """
  151. #include <Python.h>
  152. %(body)s
  153. PyMODINIT_FUNC
  154. PyInit_%(name)s(void) {
  155. %(init)s
  156. }
  157. """ % dict(
  158. name=name, init=init, body=body,
  159. )
  160. return code
  161. def _c_compile(cfile, outputfilename, include_dirs=[], libraries=[],
  162. library_dirs=[]):
  163. if sys.platform == 'win32':
  164. compile_extra = ["/we4013"]
  165. link_extra = ["/LIBPATH:" + os.path.join(sys.base_prefix, 'libs')]
  166. elif sys.platform.startswith('linux'):
  167. compile_extra = [
  168. "-O0", "-g", "-Werror=implicit-function-declaration", "-fPIC"]
  169. link_extra = []
  170. else:
  171. compile_extra = link_extra = []
  172. pass
  173. if sys.platform == 'win32':
  174. link_extra = link_extra + ['/DEBUG'] # generate .pdb file
  175. if sys.platform == 'darwin':
  176. # support Fink & Darwinports
  177. for s in ('/sw/', '/opt/local/'):
  178. if (s + 'include' not in include_dirs
  179. and os.path.exists(s + 'include')):
  180. include_dirs.append(s + 'include')
  181. if s + 'lib' not in library_dirs and os.path.exists(s + 'lib'):
  182. library_dirs.append(s + 'lib')
  183. outputfilename = outputfilename.with_suffix(get_so_suffix())
  184. build(
  185. cfile, outputfilename,
  186. compile_extra, link_extra,
  187. include_dirs, libraries, library_dirs)
  188. return outputfilename
  189. def build(cfile, outputfilename, compile_extra, link_extra,
  190. include_dirs, libraries, library_dirs):
  191. "use meson to build"
  192. build_dir = cfile.parent / "build"
  193. os.makedirs(build_dir, exist_ok=True)
  194. so_name = outputfilename.parts[-1]
  195. with open(cfile.parent / "meson.build", "wt") as fid:
  196. includes = ['-I' + d for d in include_dirs]
  197. link_dirs = ['-L' + d for d in library_dirs]
  198. fid.write(textwrap.dedent(f"""\
  199. project('foo', 'c')
  200. shared_module('{so_name}', '{cfile.parts[-1]}',
  201. c_args: {includes} + {compile_extra},
  202. link_args: {link_dirs} + {link_extra},
  203. link_with: {libraries},
  204. name_prefix: '',
  205. name_suffix: 'dummy',
  206. )
  207. """))
  208. if sys.platform == "win32":
  209. subprocess.check_call(["meson", "setup",
  210. "--buildtype=release",
  211. "--vsenv", ".."],
  212. cwd=build_dir,
  213. )
  214. else:
  215. subprocess.check_call(["meson", "setup", "--vsenv", ".."],
  216. cwd=build_dir
  217. )
  218. subprocess.check_call(["meson", "compile"], cwd=build_dir)
  219. os.rename(str(build_dir / so_name) + ".dummy", cfile.parent / so_name)
  220. def get_so_suffix():
  221. ret = sysconfig.get_config_var('EXT_SUFFIX')
  222. assert ret
  223. return ret