extbuild.py 7.6 KB

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