Coverage for mlprodict/grammar/cc/c_compilation.py: 100%

49 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-02-04 02:28 +0100

1# pylint: disable=R0401 

2""" 

3@file 

4@brief Helpers to compile C. 

5""" 

6import os 

7import sys 

8import shutil 

9import numpy 

10 

11 

12_header_c_float = """ 

13void concat_float_float(float* xy, float x, float y) 

14{ 

15 xy[0] = x; 

16 xy[1] = y; 

17} 

18 

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} 

25 

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} 

31 

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} 

37 

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} 

43 

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} 

49 

50void sign_float(float* res, float x) 

51{ 

52 *res = x >= 0 ? (float)1 : (float)0 ; 

53} 

54 

55void atake_float_int(float* res, float * vx, int p, int dim) 

56{ 

57 *res = vx[p]; 

58} 

59 

60void atake_int_int(int* res, int* vx, int p, int dim) 

61{ 

62 *res = vx[p]; 

63} 

64 

65typedef int bool; 

66 

67""" 

68 

69_header_c_double = _header_c_float.replace("float", "double") 

70 

71 

72class CompilationError(Exception): 

73 """ 

74 Raised when a compilation error was detected. 

75 """ 

76 pass 

77 

78 

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. 

84 

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 

94 

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())))) 

108 

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 = [] 

115 

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) 

125 

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) 

144 

145 if additional_paths: 

146 if fLOG: # pragma: no cover 

147 for p in additional_paths: 

148 fLOG(f"[compile_c_function] PATH += '{p}'") 

149 os.environ["PATH"] += ";" + ";".join(additional_paths) 

150 

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(f"[compile_c_function] copy '{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))) 

172 

173 if include_paths: 

174 if fLOG: # pragma: no cover 

175 for p in include_paths: 

176 fLOG(f"[compile_c_function] INCLUDE += '{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) 

181 

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) 

185 

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 f"Signature is wrong\n{sig}\ndue to\n{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 f"Compilation failed \n{sig}\ndue to\n{e}") from e 

199 mod = __import__(f"_{name}{suffix}") 

200 fct = getattr(mod.lib, name) 

201 

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 f"Only one dimension for the features not {features.shape}.") 

207 if output is None: 

208 output = numpy.zeros((nbout,), dtype=dtype) 

209 else: 

210 if len(output.shape) != 1: 

211 raise TypeError( # pragma: no cover 

212 f"Only one dimension for the output not {output.shape}.") 

213 if output.shape[0] != nbout: 

214 raise TypeError( # pragma: no cover 

215 f"Dimension mismatch {output.shape} != {nbout} (expected).") 

216 if output.dtype != dtype: 

217 raise TypeError( # pragma: no cover 

218 f"Type mismatch {output.dtype} != {dtype} (expected).") 

219 ptr = features.__array_interface__['data'][0] 

220 cptr = mod.ffi.cast(cast_type, ptr) 

221 optr = output.__array_interface__['data'][0] 

222 cout = mod.ffi.cast(cast_type, optr) 

223 fct(cout, cptr) 

224 return output 

225 

226 def wrapper_double(features, output=None): 

227 "wrapper for double" 

228 return wrapper(features, output, "double*", numpy.float64) 

229 

230 def wrapper_float(features, output=None): 

231 "wrapper for float" 

232 return wrapper( # pragma: no cover 

233 features, output, "float*", numpy.float32) 

234 

235 return wrapper_float if is_float else wrapper_double