Coverage for mlprodict/grammar/cc/c_compilation.py: 100%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# pylint: disable=R0401
2"""
3@file
4@brief Helpers to compile C.
5"""
6import os
7import sys
8import shutil
9import numpy
12_header_c_float = """
13void concat_float_float(float* xy, float x, float y)
14{
15 xy[0] = x;
16 xy[1] = y;
17}
19void adot_float_float(float* res, float* vx, float* vy, int dim)
20{
21 *res = 0;
22 for(; dim > 0; --dim, ++vx, ++vy)
23 *res += *vx * *vy;
24}
26void aadd_float(float* res, float* vx, float* vy, int dim)
27{
28 for(; dim > 0; --dim, ++vx, ++vy, ++res)
29 *res = *vx + *vy;
30}
32void asub_float_float(float* res, float* vx, float* vy, int dim)
33{
34 for(; dim > 0; --dim, ++vx, ++vy, ++res)
35 *res = *vx - *vy;
36}
38void amul_float_float(float* res, float* vx, float* vy, int dim)
39{
40 for(; dim > 0; --dim, ++vx, ++vy, ++res)
41 *res = *vx * *vy;
42}
44void adiv_float_float(float* res, float* vx, float* vy, int dim)
45{
46 for(; dim > 0; --dim, ++vx, ++vy, ++res)
47 *res = *vx / *vy;
48}
50void sign_float(float* res, float x)
51{
52 *res = x >= 0 ? (float)1 : (float)0 ;
53}
55void atake_float_int(float* res, float * vx, int p, int dim)
56{
57 *res = vx[p];
58}
60void atake_int_int(int* res, int* vx, int p, int dim)
61{
62 *res = vx[p];
63}
65typedef int bool;
67"""
69_header_c_double = _header_c_float.replace("float", "double")
72class CompilationError(Exception):
73 """
74 Raised when a compilation error was detected.
75 """
76 pass
79def compile_c_function(code_c, nbout, dtype=numpy.float32, add_header=True,
80 suffix="", additional_paths=None, tmpdir='.', fLOG=None):
81 """
82 Compiles a C function with :epkg:`cffi`.
83 It takes one features vector.
85 :param nbout: number of expected outputs
86 :param code_c: code C
87 :param dtype: numeric type to use
88 :param add_header: add common function before compiling
89 :param suffix: avoid avoid the same compiled module name
90 :param additional_paths: additional paths to add to the module
91 :param tmpdir: see below
92 :param fLOG: logging function
93 :return: compiled function
95 The function assumes the first line is the signature.
96 If you are using Windows with Visual Studio 2017, make sure
97 you are using :epkg:`Python` 3.6.3+
98 (see `Issue 30389 <https://bugs.python.org/issue30389>`_).
99 Parameter *tmpdir* is used by function `compile
100 <http://cffi.readthedocs.io/en/latest/cdef.html?
101 highlight=compile#ffibuilder-compile-etc-compiling-out-of-line-modules>`_.
102 """
103 if sys.platform.startswith("win"):
104 if "VS140COMNTOOLS" not in os.environ: # pragma: no cover
105 raise CompilationError(
106 "Visual Studio is not installed.\n{0}".format(
107 "\n".join("{0}={1}".format(k, v) for k, v in sorted(os.environ.items()))))
109 sig = code_c.split("\n")[0].strip() + ";"
110 name = sig.split()[1]
111 include_paths = []
112 lib_paths = []
113 if additional_paths is None:
114 additional_paths = []
116 # ~ if len(additional_paths) == 0 and sys.platform.startswith("win") and \
117 # ~ 'VSSDK140Install' not in os.environ: # last condition is for the installed VisualStudio.
118 # ~ if fLOG:
119 #~ fLOG("[compile_c_function] fix PATH for VS2017 on Windows")
120 # ~ # Update environment variables.
121 # ~ adds = [r"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\amd64",
122 # ~ r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.15063.0\x64"]
123 # ~ vcvars64 = os.path.join(adds[0], 'vcvars64.bat')
124 #~ subprocess.run(vcvars64)
126 # ~ # Add paths for VS2017.
127 # ~ includes = [r'C:\Program Files (x86)\Windows Kits\10\Include\10.0.15063.0\shared',
128 #~ r'C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include',
129 # ~ r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\SDK\ScopeCppSDK\SDK\include\ucrt']
130 # ~ libs = [r'C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\lib\amd64',
131 #~ r'C:\Program Files (x86)\Windows Kits\10\Lib\10.0.15063.0\um\x64',
132 # ~ r'C:\Program Files (x86)\Windows Kits\10\Lib\10.0.15063.0\ucrt\x64']
133 # ~ opaths = os.environ['PATH'].split(';')
134 # ~ for add in adds:
135 # ~ if os.path.exists(add) and add not in opaths:
136 #~ additional_paths.append(add)
137 # ~ oinc = os.environ.get('INCLUDE', '').split(';')
138 # ~ for inc in includes:
139 # ~ if os.path.exists(inc) and inc not in oinc:
140 #~ include_paths.append(inc)
141 # ~ for lib in libs:
142 # ~ if os.path.exists(lib):
143 #~ lib_paths.append(lib)
145 if additional_paths:
146 if fLOG: # pragma: no cover
147 for p in additional_paths:
148 fLOG("[compile_c_function] PATH += '{0}'".format(p))
149 os.environ["PATH"] += ";" + ";".join(additional_paths)
151 if lib_paths and sys.platform.startswith("win"): # pragma: no cover
152 libs = ['msvcrt.lib', 'oldnames.lib', 'kernel32.lib', 'vcruntime.lib',
153 'ucrt.lib']
154 libs = {k: False for k in libs}
155 for lib in lib_paths:
156 for name in list(libs):
157 if libs[name]:
158 continue
159 msv = os.path.join(lib, name)
160 if os.path.exists(msv):
161 dst = os.getcwd()
162 msvd = os.path.join(dst, name)
163 if not os.path.exists(msvd):
164 shutil.copy(msv, dst)
165 if fLOG:
166 fLOG("[compile_c_function] copy '{0}'".format(msv))
167 libs[name] = True
168 copied = len([k for k, v in libs.items() if v])
169 if copied < len(libs):
170 raise CompilationError('Unable to find those libraries ({0}<{1}) {2} in\n{3}'.format(
171 copied, len(libs), ','.join(sorted(libs)), '\n'.join(lib_paths)))
173 if include_paths:
174 if fLOG: # pragma: no cover
175 for p in include_paths:
176 fLOG("[compile_c_function] INCLUDE += '{0}'".format(p))
177 if 'INCLUDE' in os.environ: # pragma: no cover
178 os.environ["INCLUDE"] += ";" + ";".join(include_paths)
179 else: # pragma: no cover
180 os.environ["INCLUDE"] = ";".join(include_paths)
182 is_float = dtype == numpy.float32
183 header = _header_c_float if is_float else _header_c_double
184 code = code_c if not add_header else (header + code_c)
186 from cffi import FFI
187 ffibuilder = FFI()
188 try:
189 ffibuilder.cdef(sig)
190 except Exception as e: # pragma: no cover
191 raise CompilationError(
192 "Signature is wrong\n{0}\ndue to\n{1}".format(sig, e)) from e
193 ffibuilder.set_source("_" + name + suffix, code)
194 try:
195 ffibuilder.compile(verbose=False, tmpdir=tmpdir)
196 except Exception as e: # pragma: no cover
197 raise CompilationError(
198 "Compilation failed \n{0}\ndue to\n{1}".format(sig, e)) from e
199 mod = __import__("_{0}{1}".format(name, suffix))
200 fct = getattr(mod.lib, name)
202 def wrapper(features, output, cast_type, dtype):
203 "wrapper for a vector of features"
204 if len(features.shape) != 1:
205 raise TypeError( # pragma: no cover
206 "Only one dimension for the features not {0}.".format(
207 features.shape))
208 if output is None:
209 output = numpy.zeros((nbout,), dtype=dtype)
210 else:
211 if len(output.shape) != 1:
212 raise TypeError( # pragma: no cover
213 "Only one dimension for the output not {0}.".format(
214 output.shape))
215 if output.shape[0] != nbout:
216 raise TypeError( # pragma: no cover
217 "Dimension mismatch {0} != {1} (expected).".format(
218 output.shape, nbout))
219 if output.dtype != dtype:
220 raise TypeError( # pragma: no cover
221 "Type mismatch {0} != {1} (expected).".format(
222 output.dtype, dtype))
223 ptr = features.__array_interface__['data'][0]
224 cptr = mod.ffi.cast(cast_type, ptr)
225 optr = output.__array_interface__['data'][0]
226 cout = mod.ffi.cast(cast_type, optr)
227 fct(cout, cptr)
228 return output
230 def wrapper_double(features, output=None):
231 "wrapper for double"
232 return wrapper(features, output, "double*", numpy.float64)
234 def wrapper_float(features, output=None):
235 "wrapper for float"
236 return wrapper( # pragma: no cover
237 features, output, "float*", numpy.float32)
239 return wrapper_float if is_float else wrapper_double