X-Git-Url: http://nitlanguage.org diff --git a/contrib/jwrapper/src/code_generator.nit b/contrib/jwrapper/src/code_generator.nit index f0be48c..4014c75 100644 --- a/contrib/jwrapper/src/code_generator.nit +++ b/contrib/jwrapper/src/code_generator.nit @@ -1,6 +1,7 @@ # This file is part of NIT (http://www.nitlanguage.org). # # Copyright 2014 Frédéric Vachon +# Copyright 2015 Alexis Laferrière # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,82 +18,148 @@ # Services to generate extern class `in "Java"` module code_generator +import gen_nit + intrude import model class CodeGenerator - var with_attributes: Bool - var comment_unknown_types: Bool - var file_out: FileWriter - var java_class: JavaClass - var nb_params: Int - var module_name: nullable String = null + # Path to the output file + var file_name: String - init (file_name: String, jclass: JavaClass, with_attributes, comment: Bool) - do - file_out = new FileWriter.open(file_name) + # Model of Java class being wrapped + var model: JavaModel - var nit_ext = ".nit" - if file_name.has_suffix(nit_ext) then - # Output file ends with .nit, we expect it to be a valid name - module_name = file_name.strip_extension(nit_ext) + # Comment out methods with unknown (unwrapped) types + var comment_unknown_types: Bool - # Otherwise, it may be anything so do not declare a module - end + # Generate stub classes for unknown types used in the generated module + var stub_for_unknown_types: Bool - self.java_class = jclass - self.with_attributes = with_attributes - self.comment_unknown_types = comment + # Output file + var file_out: Writer = new FileWriter.open(file_name) is lazy, writable + + # Name of the Nit module to generate + var module_name: nullable String is lazy do + if file_name.file_extension == "nit" then + # Output file ends with .nit, we expect it to be a valid name + return file_name.basename(".nit") + else return null end + # Generate the Nit module into `file_out` fun generate do - var jclass = self.java_class + # License + file_out.write license - var class_content = new Array[String] - class_content.add(gen_class_header(jclass.class_type)) + # Module declaration + var module_name = module_name + if module_name != null then file_out.write "module {module_name} is no_warning(\"useless-superclass\")\n" + file_out.write "\n" + + # All importations + var imports = new HashSet[String] + imports.add "import java\n" + for key, jclass in model.classes do + for import_ in jclass.imports do imports.add "import android::{import_}\n" + end + file_out.write imports.join("\n") + file_out.write "\n" - if with_attributes then - for id, jtype in jclass.attributes do class_content.add(gen_attribute(id, jtype)) + # Sort classes from top-level classes (java.lang.Object) to leaves + var standard_classes = new Array[JavaClass] + for name, jclass in model.classes do + if not jclass.class_type.is_anonymous then standard_classes.add jclass end + var linearized = model.class_hierarchy.linearize(standard_classes) + + for jclass in linearized do + # Skip classes with an invalid name at the Java language level + if jclass.class_type.extern_equivalent.has("-") then continue + + generate_class_header(jclass) + + if not sys.opt_no_properties.value then + + for id, signatures in jclass.local_intro_methods do + for signature in signatures do + assert not signature.is_static + generate_method(jclass, id, id, signature.return_type, signature.params) + file_out.write "\n" + end + end + + # Constructors + for constructor in jclass.constructors do + var complex = jclass.constructors.length != 1 and constructor.params.not_empty + var base_name = if complex then "from" else "" + var name = jclass.nit_name_for(base_name, constructor.params, complex, false, local_only=true) + + generate_constructor(jclass, constructor, name) + end - for id, methods_info in jclass.methods do - for method_info in methods_info do - var nid = id - if methods_info.length > 1 then nid += "{methods_info.index_of(method_info)}" - class_content.add gen_method(id, nid, method_info.return_type, method_info.params) + # Attributes + for id, attribute in jclass.attributes do if not attribute.is_static then + generate_getter_setter(jclass, id, attribute) + end end - end - class_content.add("\nend\n") - var wrappers = new Array[String] - for jtype in jclass.unknown_types do - if jtype == jclass.class_type then continue - wrappers.add("\n") - wrappers.add(gen_unknown_class_header(jtype)) - end + # JNI services + generate_jni_services jclass.class_type - var imports = new Array[String] - imports.add("import mnit_android\n") - for import_ in jclass.imports do - imports.add("import android::{import_}\n") - end + # Close the class + file_out.write "end\n\n" - file_out.write(gen_licence) + if not sys.opt_no_properties.value then - var module_name = module_name - if module_name != null then file_out.write "module {module_name}\n" + # Static functions as top-level methods + var static_functions_prefix = jclass.class_type.extern_name.to_snake_case + for id, signatures in jclass.methods do + for signature in signatures do if signature.is_static then + var nit_id = static_functions_prefix + "_" + id + generate_method(jclass, id, nit_id, signature.return_type, signature.params, is_static=true) + file_out.write "\n" + end + end + + # Static attributes as top-level getters and setters + for id, attribute in jclass.attributes do if attribute.is_static then + generate_getter_setter(jclass, id, attribute) + end + end + + # Primitive arrays + for d in [1..opt_arrays.value] do + generate_primitive_array(jclass, d) + end + end + + if stub_for_unknown_types then + for jtype, nit_type in model.unknown_types do + generate_unknown_class_header(jtype) + file_out.write "\n" + end + end - file_out.write("\n") - file_out.write(imports.join("")) - file_out.write("\n") - file_out.write(class_content.join("")) - file_out.write(wrappers.join("")) + file_out.close end - fun gen_licence: String + # Serialize `model` to a file next to `file_name` + fun write_model_to_file do - return """ + if not sys.opt_save_model.value then return + + # Write the model to file next to the Nit module + var model_path = file_name.strip_extension + ".jwrapper.bin" + var model_stream = model_path.to_path.open_wo + var serializer = new BinarySerializer(model_stream) + serializer.serialize model + model_stream.close + end + + # License for the header of the generated Nit module + var license = """ # This file is part of NIT (http://www.nitlanguage.org). # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -108,138 +175,271 @@ class CodeGenerator # limitations under the License. # This code has been generated using `jwrapper` -""" - end +""" is writable - fun gen_class_header(jtype: JavaType): String + private fun generate_class_header(java_class: JavaClass) do - var temp = new Array[String] - temp.add("extern class Native{jtype.id} in \"Java\" `\{ {jtype} `\}\n") - temp.add("\tsuper JavaObject\n\n") + var java_type = java_class.class_type + var nit_type = model.java_to_nit_type(java_type) - return temp.join("") - end + var super_java_types = new HashSet[JavaType] + super_java_types.add_all java_class.extends + super_java_types.add_all java_class.implements - fun gen_unknown_class_header(jtype: JavaType): String - do - var nit_type: NitType - if jtype.extern_name.has_generic_params then - nit_type = jtype.extern_name.generic_params.first - else - nit_type = jtype.extern_name + var supers = new Array[String] + var effective_supers = 0 + for java_super in super_java_types do + var nit_super = model.java_to_nit_type(java_super) + + # Comment out unknown types + var c = "" + if not nit_super.is_known and comment_unknown_types then c = "# " + + supers.add "{c}super {nit_super}" + if c != "# " then effective_supers += 1 + end + + if effective_supers == 0 then + if java_class.class_type.java_full_name == "java.lang.Object" or + not model.knows_the_object_class then + supers.add "super JavaObject" + else supers.add "super Java_lang_Object" end - var temp = new Array[String] - temp.add("extern class {nit_type} in \"Java\" `\{ {jtype.to_package_name} `\}\n") - temp.add("\tsuper JavaObject\n\nend\n") + file_out.write """ +# Java class: {{{java_type.extern_equivalent}}} +extern class {{{nit_type}}} in "Java" `{ {{{java_type.extern_equivalent}}} `} + {{{supers.join("\n\t")}}} - return temp.join("") +""" end - fun gen_attribute(jid: String, jtype: JavaType): String + private fun generate_unknown_class_header(jtype: JavaType) do - return "\tvar {jid.to_nit_method_name}: {jtype.to_nit_type}\n" + var nit_type = jtype.extern_name + + file_out.write "extern class {nit_type} in \"Java\" `\{ {jtype.extern_equivalent} `\}\n" + file_out.write "\tsuper JavaObject\n\nend\n" end - fun gen_method(jmethod_id: String, nmethod_id: String, jreturn_type: JavaType, jparam_list: Array[JavaType]): String + private fun generate_method(java_class: JavaClass, java_method_id, method_id: String, + java_return_type: JavaType, java_params: Array[JavaType], is_static: nullable Bool) do - var java_params = "" - var nit_params = "" + var java_args = new Array[String] + var nit_params = new Array[String] var nit_id = "arg" var nit_id_no = 0 - var nit_types = new Array[NitType] - var comment = "" + var c = "" # Parameters - for i in [0..jparam_list.length[ do - var jparam = jparam_list[i] - var nit_type = jparam.to_nit_type - - if not nit_type.is_complete then - if jparam.is_wrapped then - java_class.imports.add nit_type.mod.as(not null) - else - if comment_unknown_types then - comment = "#" - else - nit_type = jparam.extern_name - java_class.unknown_types.add(jparam) - end - end - end + for jparam in java_params do + var nit_type = model.java_to_nit_type(jparam) - var cast = "" + if not nit_type.is_known and comment_unknown_types then c = "#" + if jparam.is_vararg then c = "#" - if not jparam.is_collection then cast = jparam.param_cast - - nit_types.add(nit_type) - nit_type.arg_id = "{nit_id}{nit_id_no}" - - if i == jparam_list.length - 1 then - java_params += "{cast}{nit_id}{nit_id_no}" - nit_params += "{nit_id}{nit_id_no}: {nit_type}" - else - java_params += "{cast}{nit_id}{nit_id_no}" + ", " - nit_params += "{nit_id}{nit_id_no}: {nit_type}, " - end + java_args.add "{jparam.param_cast}{nit_id}{nit_id_no}" + nit_params.add "{nit_id}{nit_id_no}: {nit_type}" nit_id_no += 1 end # Method identifier - var method_id = nmethod_id.to_nit_method_name + method_id = method_id.to_nit_method_name + method_id = java_class.nit_name_for(method_id, java_params, java_class.methods[java_method_id].length > 1, is_static == true) + + # Build the signature var nit_signature = new Array[String] + nit_signature.add "fun {method_id}" + if not java_params.is_empty then nit_signature.add "({nit_params.join(", ")})" + + # Return value + var return_type = null + if not java_return_type.is_void then + return_type = model.java_to_nit_type(java_return_type) - nit_signature.add "\tfun {method_id}" + if not return_type.is_known and comment_unknown_types then c = "#" + if java_return_type.is_vararg then c = "#" - if not jparam_list.is_empty then - nit_signature.add "({nit_params})" + nit_signature.add ": " + return_type.to_s end - var return_type = null + # Build the call in Java + var java_call + if is_static == true then + java_call = java_class.class_type.java_full_name + else java_call = "self" + java_call += ".{java_method_id}({java_args.join(", ")})" + + if return_type != null then java_call = "return {java_return_type.return_cast}" + java_call + + # Tabulation + var t = "\t" + if is_static == true then t = "" + var ct = c+t + + # Write + file_out.write """ +{{{t}}}# Java implementation: {{{java_return_type}}} {{{java_class.class_type.extern_equivalent}}}.{{{java_method_id}}}({{{java_params.join(", ")}}}) +{{{ct}}}{{{nit_signature.join}}} in "Java" `{ +{{{ct}}} {{{java_call}}}; +{{{ct}}}`} +""" + end - if not jreturn_type.is_void then - return_type = jreturn_type.to_nit_type - - if not return_type.is_complete then - if jreturn_type.is_wrapped then - java_class.imports.add return_type.mod.as(not null) - else - if comment_unknown_types then - comment = "#" - else - return_type = jreturn_type.extern_name - java_class.unknown_types.add(jreturn_type) - end - end + # Generate getter and setter to access an attribute, of field + private fun generate_getter_setter(java_class: JavaClass, java_id: String, + attribute: JavaAttribute) + do + var java_type = attribute.java_type + var nit_type = model.java_to_nit_type(java_type) + + var nit_id = java_id + if attribute.is_static then nit_id = java_class.class_type.extern_name.to_snake_case + "_" + nit_id + nit_id = nit_id.to_nit_method_name + nit_id = java_class.nit_name_for(nit_id, [java_type], false, attribute.is_static) + + var c = "" + if not nit_type.is_known and comment_unknown_types then c = "#" + if java_type.is_vararg then c = "#" + + var recv + if attribute.is_static then + recv = java_class.class_type.java_full_name + else recv = "self" + + # Tabulation + var t = "\t" + if attribute.is_static then t = "" + var ct = c+t + + file_out.write """ +{{{t}}}# Java getter: {{{java_class.class_type.extern_equivalent}}}.{{{java_id}}} +{{{ct}}}fun {{{nit_id}}}: {{{nit_type}}} in "Java" `{ +{{{ct}}} return {{{recv}}}.{{{java_id}}}; +{{{ct}}}`} + +{{{t}}}# Java setter: {{{java_class.class_type.extern_equivalent}}}.{{{java_id}}} +{{{ct}}}fun {{{nit_id}}}=(value: {{{nit_type}}}) in "Java" `{ +{{{ct}}} {{{recv}}}.{{{java_id}}} = value; +{{{ct}}}`} + +""" + end + + # Generate getter and setter to access an attribute, of field + private fun generate_constructor(java_class: JavaClass, constructor: JavaConstructor, name: String) + do + var c = "" + var nit_params_s = "" + var java_params_s = "" + + if constructor.params.not_empty then + var nit_params = new Array[String] + var java_params = new Array[String] + var param_id = 'a' + for java_type in constructor.params do + + java_params.add "{java_type.param_cast}{param_id}" + + var nit_type = model.java_to_nit_type(java_type) + nit_params.add "{param_id}: {nit_type}" + param_id = param_id.successor(1) + + if not nit_type.is_known and comment_unknown_types then c = "#" + if java_type.is_vararg then c = "#" end - nit_signature.add ": {return_type} " + nit_params_s = "(" + nit_params.join(", ") + ")" + java_params_s = java_params.join(", ") end - var temp = new Array[String] + file_out.write """ + # Java constructor: {{{java_class.class_type.extern_equivalent}}} +{{{c}}} new {{{name}}}{{{nit_params_s}}} in "Java" `{ +{{{c}}} return new {{{java_class.class_type.java_full_name}}}({{{java_params_s}}}); +{{{c}}} `} - temp.add(comment + nit_signature.join("")) +""" + end - # FIXME : This huge `if` block is only necessary to copy primitive arrays as long as there's no better way to do it - if comment == "#" then - temp.add(" in \"Java\" `\{\n{comment}\t\trecv.{jmethod_id}({java_params});\n{comment}\t`\}\n") - # Methods with return type - else if return_type != null then - temp.add(" in \"Java\" `\{\n{comment}\t\treturn {jreturn_type.return_cast}recv.{jmethod_id}({java_params});\n{comment}\t`\}\n") - # Methods without return type - else if jreturn_type.is_void then - temp.add(" in \"Java\" `\{\n{comment}\t\trecv.{jmethod_id}({java_params});\n{comment}\t`\}\n") - # No copy - else - temp.add(" in \"Java\" `\{\n{comment}\t\trecv.{jmethod_id}({java_params});\n{comment}\t`\}\n") - end + private fun generate_primitive_array(java_class: JavaClass, dimensions: Int) + do + var base_java_type = java_class.class_type + var java_type = base_java_type.clone + java_type.array_dimension = dimensions + + var base_nit_type = model.java_to_nit_type(base_java_type) + var nit_type = model.java_to_nit_type(java_type) + + file_out.write """ +# Java primitive array: {{{java_type.extern_equivalent}}} +extern class {{{nit_type}}} in "Java" `{ {{{java_type.extern_equivalent}}} `} + super AbstractJavaArray[{{{base_nit_type}}}] + + # Get a new array of the given `size` + new(size: Int) in "Java" `{ return new {{{base_java_type}}}[(int)size]; `} + + redef fun [](i) in "Java" `{ return self[(int)i]; `} + + redef fun []=(i, e) in "Java" `{ self[(int)i] = e; `} + + redef fun length in "Java" `{ return self.length; `} + +""" + generate_jni_services(java_type) + file_out.write """ +end + +""" + end + + # Generate JNI related services + # + # For now, mostly avoid issue #845, but more services could be generated as needed. + private fun generate_jni_services(java_type: JavaType) + do + var nit_type = model.java_to_nit_type(java_type) + + file_out.write """ + redef fun new_global_ref import sys, Sys.jni_env `{ + Sys sys = {{{nit_type}}}_sys(self); + JNIEnv *env = Sys_jni_env(sys); + return (*env)->NewGlobalRef(env, self); + `} + + redef fun pop_from_local_frame_with_env(jni_env) `{ + return (*jni_env)->PopLocalFrame(jni_env, self); + `} +""" + end +end - return temp.join("") +redef class Sys + # List of Nit keywords + # + # These may also be keywords in Java, but there they would be used capitalized. + private var nit_keywords: Set[String] is lazy do + var set = new HashSet[String] + set.add_all keywords + set.add_all methods_in_pointer + return set end + + # Name of methods used at the top-level + # + # Used by `JavaClass::nit_name_for` with static properties. + private var top_level_used_names = new HashSet[String] + + # Option to _not_ generate properties (static or from classes) + var opt_no_properties = new OptionBool("Do not wrap properties, only classes and basic services", "-n", "--no-properties") + + # Should the model be serialized to a file? + var opt_save_model = new OptionBool("Save the model next to the generated Nit module", "-s", "--save-model") end redef class String + # Convert the Java method name `self` to the Nit style # # * Converts to snake case @@ -248,12 +448,70 @@ redef class String fun to_nit_method_name: String do var name = self.to_snake_case - if name.has_prefix("get_") then - name = name.substring_from(4) - else if name.has_prefix("set_") then - name = name.substring_from(4) + "=" + + # Strip the '_' prefix + while name.has_prefix("_") do name = name.substring(1, name.length-1) + + # Escape Nit keywords + if nit_keywords.has(name) then name += "_" + + # If the name starts by something other than a letter, prefix with `java_` + if not name.chars.first.is_letter then name = "java_" + name + + name = name.replace("$", "_") + + return name + end +end + +redef class JavaClass + # Property names used in this class + private var used_names = new HashSet[String] is serialize + + # Get an available property name for the Java property with `name` and parameters + # + # If `use_parameters_name` then expect that there will be conflicts, + # so use the types of `parameters` to build the name. + private fun nit_name_for(name: String, parameters: Array[JavaType], use_parameters_name: Bool, is_static: Bool, local_only: nullable Bool): String + do + # Append the name of each parameter + if use_parameters_name then + for param in parameters do + var id = param.id + id += "Array"*param.array_dimension + name += "_" + id + end + end + + # Set of sets of property names, local or top-level + var local_used_names + var used_names + if is_static then + # Top-level methods + local_used_names = sys.top_level_used_names + used_names = sys.top_level_used_names + else if local_only == true then + # Local only: constructors + local_used_names = self.used_names + used_names = self.used_names + else + # Avoid conflicts with all super classes + local_used_names = self.used_names + used_names = new HashSet[String] + for sup in in_hierarchy.greaters do + used_names.add_all sup.used_names + end + end + + # As a last resort, append numbers to the name + var base_name = name + var count = 1 + while used_names.has(name) do + name = base_name + count.to_s + count += 1 end + local_used_names.add name return name end end