package utils; import haxe.crypto.BaseCode; import haxe.io.Bytes; import haxe.io.Input; import haxe.io.Output; import haxe.io.Path; import haxe.zip.Reader; import lime.tools.helpers.PathHelper; import lime.tools.helpers.ProcessHelper; import sys.io.File; import sys.io.Process; import sys.FileSystem; import neko.Lib; import neko.zip.Compress; import lime.project.HXProject; class JavaExternGenerator { private static inline var ACC_PUBLIC = 0x0001; private static inline var ACC_PRIVATE = 0x0002; private static inline var ACC_PROTECTED = 0x0004; private static inline var ACC_STATIC = 0x0008; private static inline var ACC_FINAL = 0x0010; private static inline var ACC_SUPER = 0x0020; private static inline var ACC_INTERFACE = 0x0200; private static inline var ACC_ABSTRACT = 0x0400; private static inline var dollars = "___"; private static var base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; private static var fmatch = ~/^\((.*)\)(.*)/; private var config:HXProject; private var externPath:String; private var extractedAndroidClasses:Bool; private var extractedAndroidPaths:Array; private var javaPath:String; private var mConstants:Array; private var mProcessed:Map; private var mStack:Array; private var mOutput:Output; private var mCurrentType:String; private var mExactTypes:Map; private var parsedTypes:Array; private var parsedIsObj:Array; private var retType:JNIType; public function new(config:HXProject, javaPath:String, externPath:String) { this.config = config; this.javaPath = javaPath; this.externPath = externPath; mProcessed = new Map(); mExactTypes = new Map(); mProcessed.set("java/lang/Object",true); var paths = new Array (); if (FileSystem.isDirectory (javaPath)) { this.javaPath += "/"; getPaths(javaPath, "", paths); } else { var path = Path.withoutExtension (javaPath); if (Path.extension (javaPath) == "jar") { this.javaPath = path + "/"; PathHelper.mkdir (path); ProcessHelper.runCommand (path, "jar", [ "-xvf", FileSystem.fullPath (javaPath) ], false); getPaths(path, "", paths); } else { this.javaPath = ""; paths = [ path ]; } } for (path in paths) { var type = Path.withoutExtension (path); mProcessed.set(type,true); mExactTypes.set(type,true); } mStack = paths; while(mStack.length > 0) { var clazz = mStack.pop(); var members = new Map(); generate(clazz, members); } for (path in extractedAndroidPaths) { if (FileSystem.exists (path)) { removeRecursive (path); } } } private function addType(inName:String, inJavaType:String, inArrayCount:Int) { parsedTypes.push( { name: inName, java: inJavaType, arrayCount: inArrayCount } ); } private static function debug(s:String) { } private function extractAndroidClasses() { if (!extractedAndroidClasses) { extractedAndroidPaths = []; var platformsDirectory = config.environment.get("ANDROID_SDK") + "/platforms"; if (FileSystem.exists (platformsDirectory)) { for (path in FileSystem.readDirectory (platformsDirectory)) { var directory = platformsDirectory + "/" + path; if (path.indexOf ("android-") > -1 && FileSystem.isDirectory (directory)) { var androidJAR = directory + "/android.jar"; if (FileSystem.exists(androidJAR)) { PathHelper.mkdir (path); extractedAndroidPaths.push (path); ProcessHelper.runCommand (path, "jar", [ "-xvf", androidJAR ], false); } } } } else { throw "Could not find Android SDK directory. Check that ANDROID_SDK is defined in ~/.lime/config.xml"; } } extractedAndroidClasses = true; } private function generate(inClass:String, inMembers:Map) { Lib.println(inClass); var parts = Path.withoutExtension (inClass).split("/"); var old_type = mCurrentType; mCurrentType = parts.join("."); mExactTypes.set(mCurrentType,true); var dir_parts = parts.slice(0,parts.length-1); var outputBase = externPath; var dir = outputBase; PathHelper.mkdir(dir); for(d in dir_parts) { dir += "/" + d; PathHelper.mkdir(dir); } var filename = javaPath + inClass + ".class"; if (!FileSystem.exists(filename)) { extractAndroidClasses(); var foundFile = false; for (path in extractedAndroidPaths) { if (!foundFile) { filename = path + "/" + inClass + ".class"; if (FileSystem.exists (filename)) { foundFile = true; } } } if (!foundFile) { throw "Could not find class file: \"" + inClass + "\""; } } var source = File.read(filename, true); var class_name = parts[parts.length - 1].split("$").join(dollars); var old_output = mOutput; mOutput = File.write(dir + "/" + class_name +".hx",true); var old_constants = mConstants; mConstants = new Array(); parse(source, inMembers); source.close(); mOutput.close(); mOutput = old_output; mCurrentType = old_type; mConstants = old_constants; } public static function getHaxelib(library:String):String { var proc = new Process("haxelib", ["path", library ]); var result = ""; try { while(true) { var line = proc.stdout.readLine(); if (line.substr(0, 1) != "-") result = line; } } catch (e:Dynamic) { }; proc.close(); if (result == "") throw ("Could not find haxelib path " + library + " - perhaps you need to install it?"); return result; } private function getPaths(basePath:String, source:String, paths:Array ) { var files = FileSystem.readDirectory(basePath + "/" + source); for (file in files) { if (file.substr(0, 1) != ".") { var itemSource:String = source + "/" + file; if (source == "") { itemSource = file; } if (FileSystem.isDirectory(basePath + "/" + itemSource)) { getPaths(basePath, itemSource, paths); } else { if (Path.extension(itemSource) == "class") { paths.push(Path.withoutExtension(itemSource)); } } } } } private function isJavaObject(inType) { if (inType.arrayCount > 0) return false; return switch(inType.name) { case "Int", "Void", "Bool", "Float" : false; default: true; } } private function isPOD(inName:String) { switch(inName) { case "Int", "Void", "Bool", "Float", "String" : return true; } return false; } private function javaType(inType) { var result = inType.java; result = StringTools.replace (result, "/", "."); for(i in 0...inType.arrayCount) result += "[]"; return result; } private function mkdir(inName:String) { if (!FileSystem.exists(inName)) FileSystem.createDirectory(inName); } private function nmeCallType(inType) { if (isJavaObject(inType)) return "callObjectFunction"; return "callNumericFunction"; } private function output(str:String) { mOutput.writeString(str); } private function outputClass(cid:Int, lastOnly:Bool) { var name:String = mConstants[mConstants[cid]]; //pushClass(name); name = name.split("$").join(dollars); var parts = name.split("/"); if (lastOnly) output(parts.pop()); else output(parts.join(".")); } private function outputFunctionArgs() { output("("); for(i in 0...parsedTypes.length) { if (i > 0) output(", "); output("arg" + i + ":"); outputType(parsedTypes[i]); } output(")"); } private function outputPackage(cid:Int) { var name = (mConstants[mConstants[cid]]); var parts = name.split("/"); parts.pop(); output("package " + parts.join(".") + ";\n\n\n"); } private function outputType(inType:JNIType) { for(i in 0...inType.arrayCount) output("Array<"); output(inType.name); for(i in 0...inType.arrayCount) output(">"); } private function parse(src:Input, inMembers:Map) { src.bigEndian = true; var m0 = src.readByte(); var m1 = src.readByte(); var m2 = src.readByte(); var m3 = src.readByte(); debug(StringTools.hex(m0, 2) + StringTools.hex(m1, 2) + StringTools.hex(m2, 2) + StringTools.hex(m3, 2)); debug("Version (min):" + src.readUInt16()); debug("Version (maj):" + StringTools.hex(src.readUInt16(), 4)); var ccount = src.readUInt16(); debug("mConstants : " + ccount); var cid = 1; while (cid < ccount) { var tag = src.readByte(); switch(tag) { case 1: var len = src.readUInt16(); var str = src.readString(len); //debug("Str:"+str); mConstants[cid] = str; case 3: var i = src.readInt32(); //debug("Int32:"+i); mConstants[cid] = i; case 4: var f = src.readFloat(); //debug("Float32:"+f); mConstants[cid] = f; case 5: var hi = src.readInt32(); var lo = src.readInt32(); //debug("Long - ignore"); mConstants[cid] = { lo: lo, hi: hi }; cid++; case 6: var f = src.readDouble(); //debug("Float64:"+f); mConstants[cid] = f; cid++; case 7: var cref = src.readUInt16(); //debug("Class ref:" + cref); mConstants[cid] = cref; case 8: var sref = src.readUInt16(); //debug("String ref:" + sref); mConstants[cid] = sref; case 9,10,11,12: var cref = src.readUInt16(); var type = src.readUInt16(); //debug("Member ref:" + cref + "," + type); mConstants[cid] = { cref: cref, type: type }; default: throw("Unknown constant tag:"+tag); } cid++; } var access = src.readUInt16(); debug("Access: " + access); var is_interface = (access & ACC_INTERFACE) > 0; var java_out:Output = null; var this_ref = src.readUInt16(); debug("This : " + mConstants[mConstants[this_ref]]); outputPackage(this_ref); output("class "); outputClass(this_ref, true); var super_ref = src.readUInt16(); if (super_ref > 0) { debug("Super : " + mConstants[mConstants[super_ref]]); var name = mConstants[mConstants[super_ref]]; if (name == "java/lang/Object") { debug(" -> ignore super"); super_ref = 0; } else { output(" extends "); outputClass(super_ref, false); } } else debug("Super : None."); if (super_ref > 0) generate(mConstants[mConstants[super_ref]], inMembers); var intf_count = src.readUInt16(); debug("Interfaces:" + intf_count); for(i in 0...intf_count) { var i_ref = src.readUInt16(); /* No need to expose these to haxe? if (i>0 || super_ref>0) output(","); output(" implements "); outputClass(i_ref,false); debug("Implements : " + mConstants[mConstants[i_ref]]); */ } output("\n{\n"); if (super_ref == 0) output(" var __jobject:Dynamic;\n \n"); var java_name = ""; if (is_interface) { var dir = "stubs"; var parts = mCurrentType.split("."); var dir_parts = parts.slice(0, parts.length - 1); PathHelper.mkdir(dir); for(d in dir_parts) { dir += "/" + d; PathHelper.mkdir(dir); } var interface_name = parts[parts.length - 1]; var impl_name = "Haxe" + parts[parts.length - 1].split("$").join(""); java_name = dir_parts.join("/") + "/" + impl_name +".java"; java_out = File.write("stubs/" + java_name, true); java_out.writeString("package " + dir_parts.join(".") + ";\n"); java_out.writeString("import org.haxe.lime.Value;\n"); java_out.writeString("import org.haxe.lime.Lime;\n\n"); java_out.writeString("import " + StringTools.replace (parts.join ("."), "$", ".") + ";\n\n"); java_out.writeString("class " + impl_name + " implements " + interface_name.split("$").join(".") + " {\n"); java_out.writeString(" long __haxeHandle;\n"); java_out.writeString(" public " + impl_name + "(long inHandle) { __haxeHandle=inHandle; }\n"); output(" public function new() { __jobject = openfl.utils.JNI.createInterface(this,\"" + dir_parts.join(".") + "." + impl_name + "\", classDef ); }\n \n" ); } var field_count = src.readUInt16(); debug("Fields:" + field_count); var seen = new Map(); for (i in 0...field_count) { var access = src.readUInt16(); var name_ref = src.readUInt16(); debug(" field : " + mConstants[name_ref]); var desc_ref = src.readUInt16(); debug(" desc : " + mConstants[desc_ref]); var expose = access == (ACC_PUBLIC|ACC_FINAL|ACC_STATIC); var as_string = false; if (expose) { var type = toHaxeType(mConstants[desc_ref]).name; if (isPOD(type)) { output(" static inline public var " + mConstants[name_ref] + ":" + type ); as_string = type == "String"; } else expose = false; } var att_count = src.readUInt16(); for (a in 0...att_count) { readAttribute(src, expose, as_string); } if (expose) output(";\n"); } var method_count = src.readUInt16(); debug("Method:" + method_count); output(" \n"); var constructed = false; for (i in 0...method_count) { var access = src.readUInt16(); var expose = (access&ACC_PUBLIC)>0; var is_static = (access&ACC_STATIC)>0; var name_ref = src.readUInt16(); debug(" method: " + mConstants[name_ref]); var desc_ref = src.readUInt16(); var func_name = mConstants[name_ref]; var constructor = func_name == ""; if (expose) { debug(" desc : " + mConstants[desc_ref]); splitFunctionType(mConstants[desc_ref]); var func_key = func_name + " " + mConstants[desc_ref]; if (constructor) { func_name = "_create"; } // Method overloading ... var uniq_name = func_name; var do_override = ""; if (inMembers.exists(func_key)) { uniq_name = inMembers.get(func_key); if (!constructor && !is_static) do_override = "override "; seen.set(uniq_name, true); } else { if (seen.exists(func_name)) { for (i in 1...100000) { uniq_name = func_name + i; if (!seen.exists(uniq_name)) break; } } seen.set(uniq_name,true); inMembers.set(func_key, uniq_name); } if (constructor) is_static = true; var ret_full_class = constructor || ((mExactTypes.exists(retType.name) || isPOD(retType.name)) && retType.arrayCount == 0); if (constructor) retType = { name: mCurrentType, java: mCurrentType, arrayCount: 0 }; var ret_void = (retType.name == "Void" && retType.arrayCount == 0); if (is_interface) { java_out.writeString(" @Override public " + javaType(retType) + " " + func_name + "("); for (i in 0...parsedTypes.length) { if (i > 0) java_out.writeString(","); java_out.writeString(javaType(parsedTypes[i]) + " arg" + i); } java_out.writeString(") {\n"); if (parsedTypes.length > 0) java_out.writeString(" Object [] args = new Object[" + parsedTypes.length + "];\n"); else java_out.writeString(" Object [] args = null;\n"); for (i in 0...parsedTypes.length) { if (isJavaObject(parsedTypes[i])) java_out.writeString(" args["+ i +"] = arg" + i + ";\n"); else java_out.writeString(" args["+ i +"] = new Value(arg" + i + ");\n"); } if (!ret_void) java_out.writeString(" return (" + javaType(retType) + ")" ); java_out.writeString(" Lime." + nmeCallType(retType) + "(__haxeHandle,\"" + uniq_name + "\",args)"); if (!ret_void) java_out.writeString(")"); java_out.writeString(";\n }\n"); output(" public function " + uniq_name); outputFunctionArgs(); output(":"); if (ret_full_class) outputType(retType); else output("Dynamic"); output("\n {\n return null;\n }\n \n"); } else { output(" private static var _" + uniq_name + "_func:Dynamic;\n\n"); output(" public "); if (is_static || constructor) output("static "); output(do_override + "function " + uniq_name); outputFunctionArgs(); output(":"); if (ret_full_class) outputType(retType); else output("Dynamic"); output("\n"); output(" {\n"); func_name = "_" + uniq_name + "_func"; output(" if (" + func_name + " == null)\n"); output(" " + func_name + " = openfl.utils.JNI." + (is_static?"createStaticMethod":"createMemberMethod")); output("(\"" + StringTools.replace (mCurrentType, ".", "/") + "\", \"" + mConstants[name_ref] + "\", \"" + mConstants[desc_ref] + "\", true);\n"); output(" var a = new Array();\n"); if (!is_static) output(" a.push (__jobject);\n"); for (i in 0...parsedTypes.length) output(" a.push(arg" + i + ");\n"); if (ret_void) output(" "); else if (ret_full_class && !isPOD(retType.name)) output(" return new " + retType.name + "("); else output(" return "); output(func_name + "(a)"); if (ret_full_class && !isPOD(retType.name)) output(");\n"); else output(";\n"); output(" }\n \n \n"); } } if (constructor && !constructed) { constructed = true; output(" public function new(handle:Dynamic)\n {\n"); if (super_ref > 0) output(" super(handle);"); else output(" __jobject = handle;"); output("\n }\n \n \n"); } var att_count = src.readUInt16(); for (a in 0...att_count) readAttribute(src,false,false); } if (java_out != null) { java_out.writeString("}\n"); java_out.close(); PathHelper.mkdir("compiled"); var nme_path = getHaxelib("openfl") + "/backends/native/templates/android/template/src"; ProcessHelper.runCommand ("", "javac", [ "-classpath", "\"classes/android.jar\";\"" + javaPath.substr (0, javaPath.length -1) + "\"", "-sourcepath", nme_path, "-d", "compiled", "stubs/" + java_name ], true, true, true); //Sys.setCwd("compiled"); var class_name = java_name.substr(0, java_name.length - 4) + "class"; var dx = Sys.getEnv("ANDROID_SDK") + "/platforms/" + extractedAndroidPaths[0] + "/tools/dx"; ProcessHelper.runCommand ("compiled", dx, [ "--dex", "--output=classes.jar", class_name ], true, true, true); if (FileSystem.exists ("classes.jar")) { var class_def = File.getBytes("classes.jar"); class_def = Compress.run(class_def, 9); var class_str = BaseCode.encode(class_def.toString(), base64); output("\n static var classDef = \"" + class_str + "\";\n"); } } output("}"); } private function parseTypes(type:String, inArrayCount:Int) { if (type == "") return; var is_obj = false; switch(type.substr(0, 1)) { case "[": parseTypes(type.substr(1), inArrayCount + 1); case "I" : addType("Int", "int", inArrayCount); case "C" : addType("Int", "char", inArrayCount); case "S" : addType("Int", "short", inArrayCount); case "B" : addType("Int", "byte", inArrayCount); case "V" : addType("Void", "void", inArrayCount); case "Z" : addType("Bool", "boolean", inArrayCount); case "J" : addType("Float", "long", inArrayCount); case "F" : addType("Float", "float", inArrayCount); case "D" : addType("Float", "double", inArrayCount); case "L": is_obj = true; var end = type.indexOf(";"); if (end < 1) throw("Bad object string: " + type); var name = type.substr(1, end - 1); addType(processObjectArg(name.split("/").join("."), inArrayCount), name, inArrayCount); type = type.substr(end); default: throw("Unknown java type: " + type); } parsedIsObj.push(is_obj); if (type.length > 1) parseTypes(type.substr(1), 0); } private function processObjectArg(inObj:String, inArrayCount:Int) { if (inObj == "java.lang.CharSequence" || inObj == "java.lang.String") return "String"; if (mExactTypes.exists(inObj) && inArrayCount == 0) return inObj; return "Dynamic /*" + inObj + "*/"; } private function pushClass(inName:String) { if (!mProcessed.get(inName)) { mProcessed.set(inName, true); mStack.push(inName); } } private function readAttribute(src:Input, inOutputConst:Bool, asString:Bool) { var name_ref = src.readUInt16(); debug(" attr:" + mConstants[name_ref]); var len = src.readInt32(); var bytes = Bytes.alloc(len); src.readBytes(bytes, 0, len); if (inOutputConst && mConstants[name_ref]=="ConstantValue") { var ref = (bytes.get(0) << 8) + bytes.get(1); if (asString) output(" = \"" + mConstants[mConstants[ref]] + "\""); else output(" = " + mConstants[ref] ); } } private function removeRecursive(file) { if (!FileSystem.isDirectory(file)) { FileSystem.deleteFile(file); return; } for (f in FileSystem.readDirectory (file)) removeRecursive(file + "/" + f); FileSystem.deleteDirectory(file); } private function splitFunctionType(type:String) { if (!fmatch.match(type)) throw("Not a function : " + type); var args = fmatch.matched(1); retType = toHaxeType(fmatch.matched(2)); parsedTypes = []; parsedIsObj = []; parseTypes(args, 0); } private function toHaxeType(inStr:String) { parsedTypes = []; parsedIsObj = []; parseTypes(inStr, 0); return parsedTypes[0]; } /*public static function main() { var args = Sys.args(); debug(args.toString()); new JavaExternGenerator(args[0], "gen"); }*/ } typedef JNIType = { name:String, java:String, arrayCount:Int };