aabecdbf08f186b5c6f9591a095f27e5f5677f67
[nit.git] / src / interpreter / dynamic_loading_ffi / on_demand_compiler.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Compiles extern code within a module to a static library, as needed
16 module on_demand_compiler
17
18 import modelbuilder
19 import c_tools
20 import nitni
21 import ffi
22 import naive_interpreter
23 import debugger_socket # To linearize `ToolContext::init`
24
25 redef class ToolContext
26
27 # --compile-dir
28 var opt_compile_dir = new OptionString("Directory used to generate temporary files", "--compile-dir")
29
30 init do option_context.add_option opt_compile_dir
31 end
32
33 redef class AMethPropdef
34 # Does this method definition use the FFI and is it supported by the interpreter?
35 #
36 # * Must use the nested foreign code block of the FFI.
37 # * Must not have callbacks.
38 # * Must be implemented in C.
39 fun supported_by_dynamic_ffi: Bool
40 do
41 var n_extern_code_block = n_extern_code_block
42 if not (n_extern_calls == null and n_extern_code_block != null and
43 n_extern_code_block.is_c) then return false
44
45 for mparam in mpropdef.msignature.mparameters do
46 var mtype = mparam.mtype
47 if not mtype.is_cprimitive then
48 return false
49 end
50 end
51
52 return true
53 end
54 end
55
56 redef class NaiveInterpreter
57 redef fun start(mainmodule)
58 do
59 super
60
61 # Delete temporary files
62 var compile_dir = compile_dir
63 if compile_dir.file_exists then compile_dir.rmdir
64 end
65
66 # Where to store generated C and extracted code
67 private var compile_dir: String is lazy do
68 # Prioritize the user supplied directory
69 var opt = modelbuilder.toolcontext.opt_compile_dir.value
70 if opt != null then return opt
71 return "/tmp/niti_ffi_{process_id}"
72 end
73
74 # Identifier for this process, unique between running interpreters
75 private fun process_id: Int `{ return getpid(); `}
76
77 # Path of the compiled foreign code library
78 #
79 # TODO change the ".so" extension per platform.
80 fun foreign_code_lib_path(mmodule: MModule): String
81 do
82 return compile_dir / mmodule.c_name + ".so"
83 end
84
85 # External compiler used to generate the foreign code library
86 private var c_compiler = "gcc"
87 end
88
89 redef class AModule
90
91 # Compile user FFI code and a standardized API into a `.so` file
92 #
93 # Returns `true` on success.
94 fun compile_foreign_lib(v: NaiveInterpreter): Bool
95 do
96 var mmodule = mmodule
97 assert mmodule != null
98
99 var compile_dir = v.compile_dir
100 var foreign_code_lib_path = v.foreign_code_lib_path(mmodule)
101
102 if not compile_dir.file_exists then compile_dir.mkdir
103
104 # Compile the common FFI part
105 ensure_compile_ffi_wrapper
106 for mclassdef in mmodule.mclassdefs do for mpropdef in mclassdef.mpropdefs do
107 var anode = v.modelbuilder.mpropdef2node(mpropdef)
108 if mpropdef isa MMethodDef and anode isa AMethPropdef and anode.supported_by_dynamic_ffi then
109 anode.compile_ffi_method(mmodule)
110 end
111 end
112 mmodule.finalize_ffi_wrapper(compile_dir, mmodule)
113
114 # Compile the standard API and its implementation for the .so file
115 var ccu = compile_foreign_lib_api(compile_dir)
116
117 var srcs = [for file in ccu.files do new ExternCFile(file, ""): ExternFile]
118 srcs.add_all mmodule.ffi_files
119
120 if mmodule.pkgconfigs.not_empty then
121 fatal(v, "NOT YET IMPLEMENTED annotation `pkgconfig`")
122 return false
123 end
124
125 var ldflags = mmodule.ldflags[""].join(" ")
126 # TODO pkgconfig
127
128 # Compile each source file to an object file (or equivalent)
129 var object_files = new Array[String]
130 for f in srcs do
131 f.compile(v, mmodule, object_files)
132 end
133
134 # Link everything in a shared library
135 # TODO customize the compiler
136 var cmd = "{v.c_compiler} -Wall -shared -o {foreign_code_lib_path} {object_files.join(" ")} {ldflags}"
137 if sys.system(cmd) != 0 then
138 v.fatal "FFI Error: Failed to link native code using `{cmd}`"
139 return false
140 end
141
142 return true
143 end
144
145 # Compile the standard API of the `.so` file
146 #
147 # * The shared structure `nit_call_arg`.
148 # * Standardized implementation functions entrypoints that relay calls
149 # to the FFI implementation functions.
150 private fun compile_foreign_lib_api(compdir: String): CCompilationUnit
151 do
152 var mmodule = mmodule
153 assert mmodule != null
154
155 # ready extern code compiler
156 var ecc = new CCompilationUnit
157
158 ecc.body_decl.add """
159
160 #include <string.h>
161 #include <stdio.h>
162 #include <inttypes.h>
163
164 // C structure behind `CallArg` from the interpreter
165 typedef union nit_call_arg {
166 long value_Int;
167 int value_Bool;
168 uint32_t value_Char;
169 uint8_t value_Byte;
170 int8_t value_Int8;
171 int16_t value_Int16;
172 uint16_t value_UInt16;
173 int32_t value_Int32;
174 uint32_t value_UInt32;
175 double value_Float;
176 void* value_Pointer;
177 } nit_call_arg;
178
179 """
180
181 # types
182 var used_types = collect_mtypes
183 for t in used_types do
184 if not t.is_cprimitive then
185 ecc.header_c_types.add "typedef void* {t.cname};\n"
186 end
187 end
188
189 # TODO callbacks & casts
190
191 for nclassdef in n_classdefs do for npropdef in nclassdef.n_propdefs do
192 if npropdef isa AMethPropdef and npropdef.supported_by_dynamic_ffi then
193 npropdef.mpropdef.compile_foreign_code_entry ecc
194 end
195 end
196
197 ecc.write_as_foreign_lib_api(mmodule, compdir)
198
199 return ecc
200 end
201
202 # Collect all `MType` use in extern methods within this module
203 private fun collect_mtypes: Set[MType]
204 do
205 var used_types = new HashSet[MType]
206
207 # collect callbacks
208 for nclassdef in n_classdefs do for npropdef in nclassdef.n_propdefs do
209 if npropdef isa AMethPropdef and npropdef.supported_by_dynamic_ffi then
210 var fcs = npropdef.foreign_callbacks
211 used_types.add_all fcs.types
212 end
213 end
214
215 return used_types
216 end
217 end
218
219 redef class CCompilationUnit
220 # Write this compilation unit as the API of a foreign code library
221 private fun write_as_foreign_lib_api(mmodule: MModule, compdir: String)
222 do
223 # The FFI expects the support header to end with `._nitni.h`
224 var base_name = mmodule.c_name + "._nitni"
225 var guard = mmodule.c_name.to_s.to_upper + "_API_H"
226 var header_comment = """
227 /*
228 Public API to foreign code of the Nit module {{{mmodule.name}}}
229 */
230 """
231
232 # Header file
233 var h_file = base_name+".h"
234 var stream = new FileWriter.open(compdir/h_file)
235 stream.write header_comment
236 stream.write """
237 #ifndef {{{guard}}}
238 #define {{{guard}}}
239 """
240 compile_header_core stream
241 stream.write """
242
243 #endif
244 """
245 stream.close
246
247 # Body file
248 var c_file = base_name+".c"
249 stream = new FileWriter.open(compdir/c_file)
250 stream.write header_comment
251 stream.write """
252 #include "{{{h_file}}}"
253 """
254 compile_body_core stream
255 stream.close
256
257 # Only the C files needs compiling
258 files.add compdir / c_file
259 end
260 end
261
262 redef class MMethodDef
263 # Name of the entry point to the implementation function in the foreign lib
264 fun foreign_lib_entry_cname: String do return "entry__{cname}"
265
266 # Compile the standardized entry point as part of the foreign lib API
267 private fun compile_foreign_code_entry(ecc: CCompilationUnit)
268 do
269 var msignature = msignature
270 if msignature == null then return
271
272 # Return type
273 var return_mtype = msignature.return_mtype
274 if mproperty.is_init then return_mtype = mclassdef.mclass.mclass_type
275
276 var c_return_type
277 if return_mtype != null then
278 c_return_type = return_mtype.cname_blind
279 else c_return_type = "void"
280
281 var is_init = mproperty.is_init
282
283 # Params
284 var params = new Array[String]
285 if not is_init then params.add mclassdef.mclass.mclass_type.cname_blind
286 for param in msignature.mparameters do params.add param.mtype.cname_blind
287
288 # Declare the implementation function as extern
289 var impl_cname = mproperty.build_cname(mclassdef.bound_mtype,
290 mclassdef.mmodule, "___impl", long_signature)
291 ecc.body_decl.add "extern {c_return_type} {impl_cname}({params.join(", ")});\n"
292
293 # Declare the entry function
294 var foreign_lib_entry_cname = "int {foreign_lib_entry_cname}(int argc, nit_call_arg *argv, nit_call_arg *result)"
295 var fc = new CFunction(foreign_lib_entry_cname)
296
297 # Check argument count on the library side
298 #
299 # This may detect inconsistencies between the interpreter and the generated code.
300 var expected_argc = msignature.arity
301 if not is_init then expected_argc += 1
302
303 fc.exprs.add """
304 if (argc != {{{expected_argc}}}) {
305 printf("Invalid argument count in `{{{mproperty.full_name}}}`, expected %d, got %d.\\n",
306 argc, {{{expected_argc}}});
307 return 1;
308 }
309 """
310
311 # Unpack and prepare args for the user code
312 var k = 0
313 var args_for_call = new Array[String]
314 if not is_init then
315 var mtype = mclassdef.mclass.mclass_type
316 var arg_name = "arg___self"
317
318 fc.decls.add " {mtype.cname_blind} {arg_name};\n"
319 fc.exprs.add " {arg_name} = argv[{k}].{mtype.call_arg_field};\n"
320 args_for_call.add arg_name
321
322 k += 1
323 end
324 for param in msignature.mparameters do
325 var mtype = param.mtype
326 var arg_name = "arg__"+param.name
327
328 fc.decls.add " {mtype.cname_blind} {arg_name};\n"
329 fc.exprs.add " {arg_name} = argv[{k}].{mtype.call_arg_field};\n"
330 args_for_call.add arg_name
331
332 k += 1
333 end
334
335 # Call implementation function
336 var args_compressed = args_for_call.join(", ")
337 var method_call = "{impl_cname}({args_compressed})"
338 if return_mtype != null then
339 fc.decls.add """
340 {{{return_mtype.cname_blind}}} return_value;
341 """
342 fc.exprs.add """
343 return_value = {{{method_call}}};
344 result->{{{return_mtype.call_arg_field}}} = return_value;
345 """
346 else
347 fc.exprs.add " {method_call};\n"
348 end
349
350 fc.exprs.add " return 0;\n"
351
352 ecc.add_exported_function fc
353 end
354 end
355
356 redef class MType
357 # The interpreter FFI use `void*` to represent intern data types
358 redef fun cname_blind do return "void*"
359
360 # Field to store this type in the C structure `nit_call_arg`
361 private fun call_arg_field: String do return "value_Pointer"
362 end
363
364 redef class MClassType
365 redef fun call_arg_field
366 do
367 if is_cprimitive and mclass.kind != extern_kind then
368 return "value_{name}"
369 else return super
370 end
371 end
372
373 redef class ExternFile
374 # Compile this source file
375 private fun compile(v: NaiveInterpreter, mmodule: MModule,
376 object_files: Array[String]): Bool is abstract
377 end
378
379 redef class ExternCFile
380 redef fun compile(v, mmodule, object_files)
381 do
382 var compile_dir = v.compile_dir
383 var cflags = mmodule.cflags[""].join(" ")
384 var obj = compile_dir / filename.basename(".c") + ".o"
385
386 var cmd = "{v.c_compiler} -Wall -c -fPIC -I {compile_dir} -g -o {obj} {filename} {cflags}"
387 if sys.system(cmd) != 0 then
388 v.fatal "FFI Error: Failed to compile C code using `{cmd}`"
389 return false
390 end
391
392 object_files.add obj
393 return true
394 end
395 end