Merge: doc: fixed some typos and other misc. corrections
[nit.git] / contrib / jwrapper / src / code_generator.nit
1 # This file is part of NIT (http://www.nitlanguage.org).
2 #
3 # Copyright 2014 Frédéric Vachon <fredvac@gmail.com>
4 # Copyright 2015 Alexis Laferrière <alexis.laf@xymus.net>
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17
18 # Services to generate extern class `in "Java"`
19 module code_generator
20
21 import gen_nit
22
23 intrude import model
24
25 class CodeGenerator
26
27 # Path to the output file
28 var file_name: String
29
30 # Model of Java class being wrapped
31 var model: JavaModel
32
33 # Comment out methods with unknown (unwrapped) types
34 var comment_unknown_types: Bool
35
36 # Generate stub classes for unknown types used in the generated module
37 var stub_for_unknown_types: Bool
38
39 # Output file
40 var file_out: Writer = new FileWriter.open(file_name) is lazy, writable
41
42 # Name of the Nit module to generate
43 var module_name: nullable String is lazy do
44 if file_name.file_extension == "nit" then
45 # Output file ends with .nit, we expect it to be a valid name
46 return file_name.basename(".nit")
47 else return null
48 end
49
50 # Generate the Nit module into `file_out`
51 fun generate
52 do
53 # License
54 file_out.write license
55
56 # Module declaration
57 var module_name = module_name
58 if module_name != null then file_out.write "module {module_name} is no_warning(\"useless-superclass\")\n"
59 file_out.write "\n"
60
61 # All importations
62 var imports = new HashSet[String]
63 imports.add "import java\n"
64 for key, jclass in model.classes do
65 for import_ in jclass.imports do imports.add "import android::{import_}\n"
66 end
67 file_out.write imports.join("\n")
68 file_out.write "\n"
69
70 # Sort classes from top-level classes (java.lang.Object) to leaves
71 var standard_classes = new Array[JavaClass]
72 for name, jclass in model.classes do
73 if not jclass.class_type.is_anonymous then standard_classes.add jclass
74 end
75 var linearized = model.class_hierarchy.linearize(standard_classes)
76
77 for jclass in linearized do
78 # Skip classes with an invalid name at the Java language level
79 if jclass.class_type.extern_equivalent.has("-") then continue
80
81 generate_class_header(jclass)
82
83 if not sys.opt_no_properties.value then
84
85 for id, signatures in jclass.local_intro_methods do
86 for signature in signatures do
87 assert not signature.is_static
88 generate_method(jclass, id, id, signature.return_type, signature.params)
89 file_out.write "\n"
90 end
91 end
92
93 # Constructors
94 for constructor in jclass.constructors do
95 var complex = jclass.constructors.length != 1 and constructor.params.not_empty
96 var base_name = if complex then "from" else ""
97 var name = jclass.nit_name_for(base_name, constructor.params, complex, false, local_only=true)
98
99 generate_constructor(jclass, constructor, name)
100 end
101
102 # Attributes
103 for id, attribute in jclass.attributes do if not attribute.is_static then
104 generate_getter_setter(jclass, id, attribute)
105 end
106 end
107
108 # JNI services
109 generate_jni_services jclass.class_type
110
111 # Close the class
112 file_out.write "end\n\n"
113
114 if not sys.opt_no_properties.value then
115
116 # Static functions as top-level methods
117 var static_functions_prefix = jclass.class_type.extern_name.to_snake_case
118 for id, signatures in jclass.methods do
119 for signature in signatures do if signature.is_static then
120 var nit_id = static_functions_prefix + "_" + id
121 generate_method(jclass, id, nit_id, signature.return_type, signature.params, is_static=true)
122 file_out.write "\n"
123 end
124 end
125
126 # Static attributes as top-level getters and setters
127 for id, attribute in jclass.attributes do if attribute.is_static then
128 generate_getter_setter(jclass, id, attribute)
129 end
130 end
131
132 # Primitive arrays
133 for d in [1..opt_arrays.value] do
134 generate_primitive_array(jclass, d)
135 end
136 end
137
138 if stub_for_unknown_types then
139 for jtype, nit_type in model.unknown_types do
140 generate_unknown_class_header(jtype)
141 file_out.write "\n"
142 end
143 end
144
145 file_out.close
146 end
147
148 # Serialize `model` to a file next to `file_name`
149 fun write_model_to_file
150 do
151 if not sys.opt_save_model.value then return
152
153 # Write the model to file next to the Nit module
154 var model_path = file_name.strip_extension + ".jwrapper.bin"
155 var model_stream = model_path.to_path.open_wo
156 var serializer = new MsgPackSerializer(model_stream)
157 serializer.serialize model
158 model_stream.close
159 end
160
161 # License for the header of the generated Nit module
162 var license = """
163 # This file is part of NIT (http://www.nitlanguage.org).
164 #
165 # Licensed under the Apache License, Version 2.0 (the "License");
166 # you may not use this file except in compliance with the License.
167 # You may obtain a copy of the License at
168 #
169 # http://www.apache.org/licenses/LICENSE-2.0
170 #
171 # Unless required by applicable law or agreed to in writing, software
172 # distributed under the License is distributed on an "AS IS" BASIS,
173 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
174 # See the License for the specific language governing permissions and
175 # limitations under the License.
176
177 # This code has been generated using `jwrapper`
178 """ is writable
179
180 private fun generate_class_header(java_class: JavaClass)
181 do
182 var java_type = java_class.class_type
183 var nit_type = model.java_to_nit_type(java_type)
184
185 var super_java_types = new HashSet[JavaType]
186 super_java_types.add_all java_class.extends
187 super_java_types.add_all java_class.implements
188
189 var supers = new Array[String]
190 var effective_supers = 0
191 for java_super in super_java_types do
192 var nit_super = model.java_to_nit_type(java_super)
193
194 # Comment out unknown types
195 var c = ""
196 if not nit_super.is_known and comment_unknown_types then c = "# "
197
198 supers.add "{c}super {nit_super}"
199 if c != "# " then effective_supers += 1
200 end
201
202 if effective_supers == 0 then
203 if java_class.class_type.java_full_name == "java.lang.Object" or
204 not model.knows_the_object_class then
205 supers.add "super JavaObject"
206 else supers.add "super Java_lang_Object"
207 end
208
209 file_out.write """
210 # Java class: {{{java_type.extern_equivalent}}}
211 extern class {{{nit_type}}} in "Java" `{ {{{java_type.extern_equivalent}}} `}
212 {{{supers.join("\n\t")}}}
213
214 """
215 end
216
217 private fun generate_unknown_class_header(jtype: JavaType)
218 do
219 var nit_type = jtype.extern_name
220
221 file_out.write "extern class {nit_type} in \"Java\" `\{ {jtype.extern_equivalent} `\}\n"
222 file_out.write "\tsuper JavaObject\n\nend\n"
223 end
224
225 private fun generate_method(java_class: JavaClass, java_method_id, method_id: String,
226 java_return_type: JavaType, java_params: Array[JavaType], is_static: nullable Bool)
227 do
228 var java_args = new Array[String]
229 var nit_params = new Array[String]
230 var nit_id = "arg"
231 var nit_id_no = 0
232 var c = ""
233
234 # Parameters
235 for jparam in java_params do
236 var nit_type = model.java_to_nit_type(jparam)
237
238 if not nit_type.is_known and comment_unknown_types then c = "#"
239 if jparam.is_vararg then c = "#"
240
241 java_args.add "{jparam.param_cast}{nit_id}{nit_id_no}"
242 nit_params.add "{nit_id}{nit_id_no}: {nit_type}"
243
244 nit_id_no += 1
245 end
246
247 # Method identifier
248 method_id = method_id.to_nit_method_name
249 method_id = java_class.nit_name_for(method_id, java_params, java_class.methods[java_method_id].length > 1, is_static == true)
250
251 # Build the signature
252 var nit_signature = new Array[String]
253 nit_signature.add "fun {method_id}"
254 if not java_params.is_empty then nit_signature.add "({nit_params.join(", ")})"
255
256 # Return value
257 var return_type = null
258 if not java_return_type.is_void then
259 return_type = model.java_to_nit_type(java_return_type)
260
261 if not return_type.is_known and comment_unknown_types then c = "#"
262 if java_return_type.is_vararg then c = "#"
263
264 nit_signature.add ": " + return_type.to_s
265 end
266
267 # Build the call in Java
268 var java_call
269 if is_static == true then
270 java_call = java_class.class_type.java_full_name
271 else java_call = "self"
272 java_call += ".{java_method_id}({java_args.join(", ")})"
273
274 if return_type != null then java_call = "return {java_return_type.return_cast}" + java_call
275
276 # Tabulation
277 var t = "\t"
278 if is_static == true then t = ""
279 var ct = c+t
280
281 # Write
282 file_out.write """
283 {{{t}}}# Java implementation: {{{java_return_type}}} {{{java_class.class_type.extern_equivalent}}}.{{{java_method_id}}}({{{java_params.join(", ")}}})
284 {{{ct}}}{{{nit_signature.join}}} in "Java" `{
285 {{{ct}}} {{{java_call}}};
286 {{{ct}}}`}
287 """
288 end
289
290 # Generate getter and setter to access an attribute, of field
291 private fun generate_getter_setter(java_class: JavaClass, java_id: String,
292 attribute: JavaAttribute)
293 do
294 var java_type = attribute.java_type
295 var nit_type = model.java_to_nit_type(java_type)
296
297 var nit_id = java_id
298 if attribute.is_static then nit_id = java_class.class_type.extern_name.to_snake_case + "_" + nit_id
299 nit_id = nit_id.to_nit_method_name
300 nit_id = java_class.nit_name_for(nit_id, [java_type], false, attribute.is_static)
301
302 var c = ""
303 if not nit_type.is_known and comment_unknown_types then c = "#"
304 if java_type.is_vararg then c = "#"
305
306 var recv
307 if attribute.is_static then
308 recv = java_class.class_type.java_full_name
309 else recv = "self"
310
311 # Tabulation
312 var t = "\t"
313 if attribute.is_static then t = ""
314 var ct = c+t
315
316 file_out.write """
317 {{{t}}}# Java getter: {{{java_class.class_type.extern_equivalent}}}.{{{java_id}}}
318 {{{ct}}}fun {{{nit_id}}}: {{{nit_type}}} in "Java" `{
319 {{{ct}}} return {{{recv}}}.{{{java_id}}};
320 {{{ct}}}`}
321
322 {{{t}}}# Java setter: {{{java_class.class_type.extern_equivalent}}}.{{{java_id}}}
323 {{{ct}}}fun {{{nit_id}}}=(value: {{{nit_type}}}) in "Java" `{
324 {{{ct}}} {{{recv}}}.{{{java_id}}} = value;
325 {{{ct}}}`}
326
327 """
328 end
329
330 # Generate getter and setter to access an attribute, of field
331 private fun generate_constructor(java_class: JavaClass, constructor: JavaConstructor, name: String)
332 do
333 var c = ""
334 var nit_params_s = ""
335 var java_params_s = ""
336
337 if constructor.params.not_empty then
338 var nit_params = new Array[String]
339 var java_params = new Array[String]
340 var param_id = 'a'
341 for java_type in constructor.params do
342
343 java_params.add "{java_type.param_cast}{param_id}"
344
345 var nit_type = model.java_to_nit_type(java_type)
346 nit_params.add "{param_id}: {nit_type}"
347 param_id = param_id.successor(1)
348
349 if not nit_type.is_known and comment_unknown_types then c = "#"
350 if java_type.is_vararg then c = "#"
351 end
352
353 nit_params_s = "(" + nit_params.join(", ") + ")"
354 java_params_s = java_params.join(", ")
355 end
356
357 file_out.write """
358 # Java constructor: {{{java_class.class_type.extern_equivalent}}}
359 {{{c}}} new {{{name}}}{{{nit_params_s}}} in "Java" `{
360 {{{c}}} return new {{{java_class.class_type.java_full_name}}}({{{java_params_s}}});
361 {{{c}}} `}
362
363 """
364 end
365
366 private fun generate_primitive_array(java_class: JavaClass, dimensions: Int)
367 do
368 var base_java_type = java_class.class_type
369 var java_type = base_java_type.clone
370 java_type.array_dimension = dimensions
371
372 var base_nit_type = model.java_to_nit_type(base_java_type)
373 var nit_type = model.java_to_nit_type(java_type)
374
375 file_out.write """
376 # Java primitive array: {{{java_type.extern_equivalent}}}
377 extern class {{{nit_type}}} in "Java" `{ {{{java_type.extern_equivalent}}} `}
378 super AbstractJavaArray[{{{base_nit_type}}}]
379
380 # Get a new array of the given `size`
381 new(size: Int) in "Java" `{ return new {{{base_java_type}}}[(int)size]; `}
382
383 redef fun [](i) in "Java" `{ return self[(int)i]; `}
384
385 redef fun []=(i, e) in "Java" `{ self[(int)i] = e; `}
386
387 redef fun length in "Java" `{ return self.length; `}
388
389 """
390 generate_jni_services(java_type)
391 file_out.write """
392 end
393
394 """
395 end
396
397 # Generate JNI related services
398 #
399 # For now, mostly avoid issue #845, but more services could be generated as needed.
400 private fun generate_jni_services(java_type: JavaType)
401 do
402 var nit_type = model.java_to_nit_type(java_type)
403
404 file_out.write """
405 redef fun new_global_ref import sys, Sys.jni_env `{
406 Sys sys = {{{nit_type}}}_sys(self);
407 JNIEnv *env = Sys_jni_env(sys);
408 return (*env)->NewGlobalRef(env, self);
409 `}
410
411 redef fun pop_from_local_frame_with_env(jni_env) `{
412 return (*jni_env)->PopLocalFrame(jni_env, self);
413 `}
414 """
415 end
416 end
417
418 redef class Sys
419 # List of Nit keywords
420 #
421 # These may also be keywords in Java, but there they would be used capitalized.
422 private var nit_keywords: Set[String] is lazy do
423 var set = new HashSet[String]
424 set.add_all keywords
425 set.add_all methods_in_pointer
426 return set
427 end
428
429 # Name of methods used at the top-level
430 #
431 # Used by `JavaClass::nit_name_for` with static properties.
432 private var top_level_used_names = new HashSet[String]
433
434 # Option to _not_ generate properties (static or from classes)
435 var opt_no_properties = new OptionBool("Do not wrap properties, only classes and basic services", "-n", "--no-properties")
436
437 # Should the model be serialized to a file?
438 var opt_save_model = new OptionBool("Save the model next to the generated Nit module", "-s", "--save-model")
439 end
440
441 redef class String
442
443 # Convert the Java method name `self` to the Nit style
444 #
445 # * Converts to snake case
446 # * Strips `Get` and `Set`
447 # * Add suffix `=` to setters
448 fun to_nit_method_name: String
449 do
450 var name = self.to_snake_case
451
452 # Strip the '_' prefix
453 while name.has_prefix("_") do name = name.substring(1, name.length-1)
454
455 # Escape Nit keywords
456 if nit_keywords.has(name) then name += "_"
457
458 # If the name starts by something other than a letter, prefix with `java_`
459 if not name.chars.first.is_letter then name = "java_" + name
460
461 name = name.replace("$", "_")
462
463 return name
464 end
465 end
466
467 redef class JavaClass
468 # Property names used in this class
469 private var used_names = new HashSet[String] is serialize
470
471 # Get an available property name for the Java property with `name` and parameters
472 #
473 # If `use_parameters_name` then expect that there will be conflicts,
474 # so use the types of `parameters` to build the name.
475 private fun nit_name_for(name: String, parameters: Array[JavaType], use_parameters_name: Bool, is_static: Bool, local_only: nullable Bool): String
476 do
477 # Append the name of each parameter
478 if use_parameters_name then
479 for param in parameters do
480 var id = param.id
481 id += "Array"*param.array_dimension
482 name += "_" + id
483 end
484 end
485
486 # Set of sets of property names, local or top-level
487 var local_used_names
488 var used_names
489 if is_static then
490 # Top-level methods
491 local_used_names = sys.top_level_used_names
492 used_names = sys.top_level_used_names
493 else if local_only == true then
494 # Local only: constructors
495 local_used_names = self.used_names
496 used_names = self.used_names
497 else
498 # Avoid conflicts with all super classes
499 local_used_names = self.used_names
500 used_names = new HashSet[String]
501 for sup in in_hierarchy.greaters do
502 used_names.add_all sup.used_names
503 end
504 end
505
506 # As a last resort, append numbers to the name
507 var base_name = name
508 var count = 1
509 while used_names.has(name) do
510 name = base_name + count.to_s
511 count += 1
512 end
513
514 local_used_names.add name
515 return name
516 end
517 end