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