Merge: Gamnit on iOS
[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 pkgconfig
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 # * Must not have a parameter or return typed with a Nit standard class.
40 fun supported_by_dynamic_ffi: Bool
41 do
42 # If the user specfied `is light_ffi`, it must be supported
43 var nats = get_annotations("light_ffi")
44 if nats.not_empty then return true
45
46 var n_extern_code_block = n_extern_code_block
47 if not (n_extern_calls == null and n_extern_code_block != null and
48 n_extern_code_block.is_c) then return false
49
50 for mparam in mpropdef.msignature.mparameters do
51 if not mparam.mtype.is_cprimitive then
52 return false
53 end
54 end
55
56 var return_mtype = mpropdef.msignature.return_mtype
57 if return_mtype != null and not return_mtype.is_cprimitive then
58 return false
59 end
60
61 return true
62 end
63 end
64
65 redef class NaiveInterpreter
66 redef fun start(mainmodule)
67 do
68 super
69
70 # Delete temporary files
71 var compile_dir = compile_dir
72 if compile_dir.file_exists then compile_dir.rmdir
73 end
74
75 # Where to store generated C and extracted code
76 private var compile_dir: String is lazy do
77 # Prioritize the user supplied directory
78 var opt = modelbuilder.toolcontext.opt_compile_dir.value
79 if opt != null then return opt
80 return "/tmp/niti_ffi_{process_id}"
81 end
82
83 # Identifier for this process, unique between running interpreters
84 private fun process_id: Int `{ return getpid(); `}
85
86 # Path of the compiled foreign code library
87 #
88 # TODO change the ".so" extension per platform.
89 fun foreign_code_lib_path(mmodule: MModule): String
90 do
91 return compile_dir / mmodule.c_name + ".so"
92 end
93
94 # External compiler used to generate the foreign code library
95 private var c_compiler = "cc"
96 end
97
98 redef class AModule
99
100 # Compile user FFI code and a standardized API into a `.so` file
101 #
102 # Returns `true` on success.
103 fun compile_foreign_lib(v: NaiveInterpreter): Bool
104 do
105 var mmodule = mmodule
106 assert mmodule != null
107
108 var compile_dir = v.compile_dir
109 var foreign_code_lib_path = v.foreign_code_lib_path(mmodule)
110
111 if not compile_dir.file_exists then compile_dir.mkdir(0o700)
112
113 # Compile the common FFI part
114 ensure_compile_ffi_wrapper
115 for mclassdef in mmodule.mclassdefs do for mpropdef in mclassdef.mpropdefs do
116 var anode = v.modelbuilder.mpropdef2node(mpropdef)
117 if mpropdef isa MMethodDef and anode isa AMethPropdef and anode.supported_by_dynamic_ffi then
118 anode.compile_ffi_method(mmodule)
119 end
120 end
121 mmodule.finalize_ffi_wrapper(compile_dir, mmodule)
122
123 # Compile the standard API and its implementation for the .so file
124 var ccu = compile_foreign_lib_api(compile_dir)
125
126 var srcs = [for file in ccu.files do new ExternCFile(file, ""): ExternFile]
127 srcs.add_all mmodule.ffi_files
128
129 # Compiler options specific to this module
130 var ldflags_array = mmodule.ldflags[""]
131 if ldflags_array.has("-lrt") and system("sh -c 'uname -s 2>/dev/null || echo not' | grep Darwin >/dev/null") == 0 then
132 # Remove -lrt on OS X
133 ldflags_array.remove "-lrt"
134 end
135 var ldflags = ldflags_array.join(" ")
136
137 # Protect pkg-config
138 var pkgconfigs = mmodule.pkgconfigs
139 var pkg_cflags = ""
140 if not pkgconfigs.is_empty then
141 var cmd = "which pkg-config >/dev/null"
142 if system(cmd) != 0 then
143 v.fatal "FFI Error: Command `pkg-config` not found. Please install it"
144 return false
145 end
146
147 for p in pkgconfigs do
148 cmd = "pkg-config --exists '{p}'"
149 if system(cmd) != 0 then
150 v.fatal "FFI Error: package {p} is not found by `pkg-config`. Please install it."
151 return false
152 end
153 end
154
155 pkg_cflags = "`pkg-config --cflags {pkgconfigs.join(" ")}`"
156 ldflags += " `pkg-config --libs {pkgconfigs.join(" ")}`"
157 end
158
159 # Compile each source file to an object file (or equivalent)
160 var object_files = new Array[String]
161 for f in srcs do
162 f.compile(v, mmodule, object_files, pkg_cflags)
163 end
164
165 # Link everything in a shared library
166 var cmd = "{v.c_compiler} -Wall -shared -o {foreign_code_lib_path} {object_files.join(" ")} {ldflags}"
167 if system(cmd) != 0 then
168 v.fatal "FFI Error: Failed to link native code using `{cmd}`"
169 return false
170 end
171
172 return true
173 end
174
175 # Compile the standard API of the `.so` file
176 #
177 # * The shared structure `nit_call_arg`.
178 # * Standardized implementation functions entrypoints that relay calls
179 # to the FFI implementation functions.
180 private fun compile_foreign_lib_api(compdir: String): CCompilationUnit
181 do
182 var mmodule = mmodule
183 assert mmodule != null
184
185 # ready extern code compiler
186 var ecc = new CCompilationUnit
187
188 ecc.body_decl.add """
189
190 #include <string.h>
191 #include <stdio.h>
192 #include <inttypes.h>
193
194 // C structure behind `CallArg` from the interpreter
195 typedef union nit_call_arg {
196 long value_Int;
197 int value_Bool;
198 uint32_t value_Char;
199 uint8_t value_Byte;
200 int8_t value_Int8;
201 int16_t value_Int16;
202 uint16_t value_UInt16;
203 int32_t value_Int32;
204 uint32_t value_UInt32;
205 double value_Float;
206 void* value_Pointer;
207 } nit_call_arg;
208
209 """
210
211 # types
212 var used_types = collect_mtypes
213 for t in used_types do
214 if not t.is_cprimitive then
215 ecc.header_c_types.add "typedef void* {t.cname};\n"
216 end
217 end
218
219 # TODO callbacks & casts
220
221 for nclassdef in n_classdefs do for npropdef in nclassdef.n_propdefs do
222 if npropdef isa AMethPropdef and npropdef.supported_by_dynamic_ffi then
223 npropdef.mpropdef.compile_foreign_code_entry ecc
224 end
225 end
226
227 ecc.write_as_foreign_lib_api(mmodule, compdir)
228
229 return ecc
230 end
231
232 # Collect all `MType` use in extern methods within this module
233 private fun collect_mtypes: Set[MType]
234 do
235 var used_types = new HashSet[MType]
236
237 # collect callbacks
238 for nclassdef in n_classdefs do for npropdef in nclassdef.n_propdefs do
239 if npropdef isa AMethPropdef and npropdef.supported_by_dynamic_ffi then
240 var fcs = npropdef.foreign_callbacks
241 used_types.add_all fcs.types
242 end
243 end
244
245 return used_types
246 end
247 end
248
249 redef class CCompilationUnit
250 # Write this compilation unit as the API of a foreign code library
251 private fun write_as_foreign_lib_api(mmodule: MModule, compdir: String)
252 do
253 # The FFI expects the support header to end with `._nitni.h`
254 var base_name = mmodule.c_name + "._nitni"
255 var guard = mmodule.c_name.to_s.to_upper + "_API_H"
256 var header_comment = """
257 /*
258 Public API to foreign code of the Nit module {{{mmodule.name}}}
259 */
260 """
261
262 # Header file
263 var h_file = base_name+".h"
264 var stream = new FileWriter.open(compdir/h_file)
265 stream.write header_comment
266 stream.write """
267 #ifndef {{{guard}}}
268 #define {{{guard}}}
269 """
270 compile_header_core stream
271 stream.write """
272
273 #endif
274 """
275 stream.close
276
277 # Body file
278 var c_file = base_name+".c"
279 stream = new FileWriter.open(compdir/c_file)
280 stream.write header_comment
281 stream.write """
282 #include "{{{h_file}}}"
283 """
284 compile_body_core stream
285 stream.close
286
287 # Only the C files needs compiling
288 files.add compdir / c_file
289 end
290 end
291
292 redef class MMethodDef
293 # Name of the entry point to the implementation function in the foreign lib
294 fun foreign_lib_entry_cname: String do return "entry__{cname}"
295
296 # Compile the standardized entry point as part of the foreign lib API
297 private fun compile_foreign_code_entry(ecc: CCompilationUnit)
298 do
299 var msignature = msignature
300 if msignature == null then return
301
302 # Return type
303 var return_mtype = msignature.return_mtype
304 if mproperty.is_init then return_mtype = mclassdef.mclass.mclass_type
305
306 var c_return_type
307 if return_mtype != null then
308 c_return_type = return_mtype.cname_blind
309 else c_return_type = "void"
310
311 var is_init = mproperty.is_init
312
313 # Params
314 var params = new Array[String]
315 if not is_init then params.add mclassdef.mclass.mclass_type.cname_blind
316 for param in msignature.mparameters do params.add param.mtype.cname_blind
317
318 # Declare the implementation function as extern
319 var impl_cname = mproperty.build_cname(mclassdef.bound_mtype,
320 mclassdef.mmodule, "___impl", long_signature)
321 ecc.body_decl.add "extern {c_return_type} {impl_cname}({params.join(", ")});\n"
322
323 # Declare the entry function
324 var foreign_lib_entry_cname = "int {foreign_lib_entry_cname}(int argc, nit_call_arg *argv, nit_call_arg *result)"
325 var fc = new CFunction(foreign_lib_entry_cname)
326
327 # Check argument count on the library side
328 #
329 # This may detect inconsistencies between the interpreter and the generated code.
330 var expected_argc = msignature.arity
331 if not is_init then expected_argc += 1
332
333 fc.exprs.add """
334 if (argc != {{{expected_argc}}}) {
335 printf("Invalid argument count in `{{{mproperty.full_name}}}`, expected %d, got %d.\\n",
336 argc, {{{expected_argc}}});
337 return 1;
338 }
339 """
340
341 # Unpack and prepare args for the user code
342 var k = 0
343 var args_for_call = new Array[String]
344 if not is_init then
345 var mtype = mclassdef.mclass.mclass_type
346 var arg_name = "arg___self"
347
348 fc.decls.add " {mtype.cname_blind} {arg_name};\n"
349 fc.exprs.add " {arg_name} = argv[{k}].{mtype.call_arg_field};\n"
350 args_for_call.add arg_name
351
352 k += 1
353 end
354 for param in msignature.mparameters do
355 var mtype = param.mtype
356 var arg_name = "arg__"+param.name
357
358 fc.decls.add " {mtype.cname_blind} {arg_name};\n"
359 fc.exprs.add " {arg_name} = argv[{k}].{mtype.call_arg_field};\n"
360 args_for_call.add arg_name
361
362 k += 1
363 end
364
365 # Call implementation function
366 var args_compressed = args_for_call.join(", ")
367 var method_call = "{impl_cname}({args_compressed})"
368 if return_mtype != null then
369 fc.decls.add """
370 {{{return_mtype.cname_blind}}} return_value;
371 """
372 fc.exprs.add """
373 return_value = {{{method_call}}};
374 result->{{{return_mtype.call_arg_field}}} = return_value;
375 """
376 else
377 fc.exprs.add " {method_call};\n"
378 end
379
380 fc.exprs.add " return 0;\n"
381
382 ecc.add_exported_function fc
383 end
384 end
385
386 redef class MType
387 # The interpreter FFI use `void*` to represent intern data types
388 redef fun cname_blind do return "void*"
389
390 # Field to store this type in the C structure `nit_call_arg`
391 private fun call_arg_field: String do return "value_Pointer"
392 end
393
394 redef class MClassType
395 redef fun call_arg_field
396 do
397 if is_cprimitive and mclass.kind != extern_kind then
398 return "value_{name}"
399 else return super
400 end
401 end
402
403 redef class ExternFile
404 # Compile this source file
405 private fun compile(v: NaiveInterpreter, mmodule: MModule,
406 object_files: Array[String], pkg_cflags: String): Bool is abstract
407 end
408
409 redef class ExternCFile
410 redef fun compile(v, mmodule, object_files, pkg_cflags)
411 do
412 var compile_dir = v.compile_dir
413 var cflags = mmodule.cflags[""].join(" ") + " " + pkg_cflags
414 var obj = compile_dir / filename.basename(".c") + ".o"
415
416 var cmd = "{v.c_compiler} -Wall -c -fPIC -I {compile_dir} -g -o {obj} {filename} {cflags}"
417 if sys.system(cmd) != 0 then
418 v.fatal "FFI Error: Failed to compile C code using `{cmd}`"
419 return false
420 end
421
422 object_files.add obj
423 return true
424 end
425 end