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