diff --git a/.gitignore b/.gitignore index e07d7f703..464a3ac3a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,12 @@ node_modules .idea .vscode .todo + +# Build artifacts of the :codegen subproject +/codegen/.gradle +/codegen/build +/codegen/bin + +# Generated by the generateApiBindings task (see codegen/, build.gradle) +/src/main/java/com/zhekasmirnov/innercore/api/gen +/src/test/java/com/zhekasmirnov/innercore/api/gen \ No newline at end of file diff --git a/build.gradle b/build.gradle index abf29e891..38dcffa9d 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,14 @@ repositories { } } +configurations { + // Isolated classpath for the forked codegen JVM (the :codegen subproject + JavaParser). + codegenClasspath +} + dependencies { + codegenClasspath project(':codegen') + // https://repo.lanink.cn/#browse/browse:maven-public:cn/nukkit/Nukkit/MOT-SNAPSHOT /*implementation (group: 'cn.nukkit', name: 'Nukkit', version: 'MOT-20240213.074103-139') { exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' @@ -42,6 +49,15 @@ dependencies { implementation fileTree (dir: 'iclibs', include: '*.jar') implementation fileTree (dir: 'iclibs/dex2jar', include: '*.jar') + + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + dependsOn 'generateApiBindingsTest' + useJUnitPlatform() } def targetJavaVersion = 17 @@ -67,6 +83,52 @@ run { standardInput System.in } +// Generates JS-binding base classes (com.zhekasmirnov.innercore.api.gen.*BindGenBase) +// from @ApiBinding-annotated sources, before compilation (the hand-written module classes +// extend the generated bases). The generator lives in the :codegen subproject and runs in a +// forked JVM so its JavaParser dependency stays isolated from Gradle's bundled copy. +def apiGenSourceRoot = file('src/main/java/org/innercore') +def apiGenOutputDir = layout.projectDirectory.dir('src/main/java/com/zhekasmirnov/innercore/api/gen') + +def apiGenSourceTestRoot = file('src/test/java/org/innercore') +def apiGenOutputTestDir = layout.projectDirectory.dir('src/test/java/com/zhekasmirnov/innercore/api/gen') + +tasks.register('generateApiBindingsTest', JavaExec) { + group = 'codegen' + description = 'Generates JS binding base classes from @ApiBinding-annotated sources.' + javaLauncher = javaToolchains.launcherFor { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + if (JavaVersion.current() < javaVersion) { + languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } + } + classpath = configurations.codegenClasspath + mainClass = 'org.innercore.codegen.Codegen' + args 'com.zhekasmirnov.innercore.api.gen', apiGenOutputTestDir.asFile.absolutePath, apiGenSourceTestRoot.absolutePath + inputs.files(fileTree(apiGenSourceRoot) { include '**/*.java' }).withPropertyName('apiBindingSources') + outputs.dir(apiGenOutputDir).withPropertyName('apiBindingOutput') +} + +tasks.register('generateApiBindings', JavaExec) { + group = 'codegen' + description = 'Generates JS binding base classes from @ApiBinding-annotated sources.' + javaLauncher = javaToolchains.launcherFor { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + if (JavaVersion.current() < javaVersion) { + languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } + } + classpath = configurations.codegenClasspath + mainClass = 'org.innercore.codegen.Codegen' + args 'com.zhekasmirnov.innercore.api.gen', apiGenOutputDir.asFile.absolutePath, apiGenSourceRoot.absolutePath + inputs.files(fileTree(apiGenSourceRoot) { include '**/*.java' }).withPropertyName('apiBindingSources') + outputs.dir(apiGenOutputDir).withPropertyName('apiBindingOutput') +} + +tasks.named('compileJava') { + dependsOn 'generateApiBindings' +} + task rebuildAndroidStubs(type: JavaExec, dependsOn: classes) { javaLauncher = javaToolchains.launcherFor { def javaVersion = JavaVersion.toVersion(targetJavaVersion) diff --git a/codegen/build.gradle b/codegen/build.gradle new file mode 100644 index 000000000..861691315 --- /dev/null +++ b/codegen/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + // Java-source parser used by the API-binding code generator. + // 3.25.x is the last line targeting Java 8 bytecode (3.26+ requires Java 17). + implementation 'com.github.javaparser:javaparser-core:3.25.10' +} + +// Plain library, no gradleApi() — that is exactly why this lives in its own subproject +// rather than buildSrc: it keeps JavaParser 3.25.10 off any classpath shared with Gradle's +// bundled javaparser-core 3.17.0. The generator is launched in a forked JVM (see the root +// build's generateApiBindings task), so Java 8 bytecode runs fine on the Java 17 launcher. +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} diff --git a/codegen/src/main/java/org/innercore/codegen/BindingOverload.java b/codegen/src/main/java/org/innercore/codegen/BindingOverload.java new file mode 100644 index 000000000..7709e64e4 --- /dev/null +++ b/codegen/src/main/java/org/innercore/codegen/BindingOverload.java @@ -0,0 +1,20 @@ +package org.innercore.codegen; + +import java.util.ArrayList; +import java.util.List; + +/** A binding overload prepared for code generation. */ +public final class BindingOverload { + public String name = ""; + public String moduleName = ""; + public String mangledName = ""; + public String bindType = ""; + public String moduleClsName = ""; + public String fnType = ""; + public VarDecl ret = new VarDecl(Encoding.BASE_TYPE_VOID); + public List args = new ArrayList(); + public int argsCnt = 0; + public boolean needJavaWrap = false; + public String callSrc = ""; + public List attrs = new ArrayList(); +} diff --git a/codegen/src/main/java/org/innercore/codegen/BindingRecord.java b/codegen/src/main/java/org/innercore/codegen/BindingRecord.java new file mode 100644 index 000000000..66e0b85b1 --- /dev/null +++ b/codegen/src/main/java/org/innercore/codegen/BindingRecord.java @@ -0,0 +1,29 @@ +package org.innercore.codegen; + +import java.util.ArrayList; +import java.util.List; + +/** + * A single resolved binding, equivalent to one JSON line in the reference + * {@code gen_java_bindings.txt} store. Produced by a resolver (Java here; a C++ + * resolver may contribute records too) and consumed by {@link JavaBindingGenerator}. + */ +public final class BindingRecord { + /** "Module.name". */ + public String bind; + /** Fully-qualified hand-written class (Java bindings); null for native records. */ + public String cls; + /** Source member name. */ + public String src; + /** One of the {@code FN_TYPE_*} constants. */ + public String typ; + /** Whether a Java wrapper is needed (native pipeline); always false for Java. */ + public boolean wrap; + public List args = new ArrayList(); + public VarDecl ret; + public List attrs = new ArrayList(); + /** Mangled native name (native records only). */ + public String mgl; + /** Originating C++ class (native records only). */ + public String cppCls; +} diff --git a/codegen/src/main/java/org/innercore/codegen/BindingStore.java b/codegen/src/main/java/org/innercore/codegen/BindingStore.java new file mode 100644 index 000000000..a31d13a9b --- /dev/null +++ b/codegen/src/main/java/org/innercore/codegen/BindingStore.java @@ -0,0 +1,31 @@ +package org.innercore.codegen; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * In-memory replacement for the reference pipeline's intermediate text files + * ({@code gen_java_bindings.txt} + {@code gen_java_modules.txt}). It is the single + * sink that resolvers feed and the generator reads. + * + *

The Java resolver is the only producer today. A future C++ resolver (libclang) + * can append its native {@link BindingRecord}s here as well; the generator already + * understands native records, so generation would then "just work". If the C++ pass + * runs out-of-process, this class is the natural place to add JSON-lines + * (de)serialization. + */ +public final class BindingStore { + public final List records = new ArrayList(); + /** moduleName -> fully-qualified hand-written module class. */ + public final Map javaModules = new LinkedHashMap(); + + public void addRecord(BindingRecord record) { + records.add(record); + } + + public void addModule(String moduleName, String fullClassName) { + javaModules.put(moduleName, fullClassName); + } +} diff --git a/codegen/src/main/java/org/innercore/codegen/CodeWriter.java b/codegen/src/main/java/org/innercore/codegen/CodeWriter.java new file mode 100644 index 000000000..7c99ea526 --- /dev/null +++ b/codegen/src/main/java/org/innercore/codegen/CodeWriter.java @@ -0,0 +1,69 @@ +package org.innercore.codegen; + +/** + * A tiny indentation-aware text buffer, mirroring the reference {@code BaseGenerator} + * write/writeln helpers. {@code write} emits an optional leading indent then the + * given parts with no trailing newline; {@code writeln} emits each line with an + * optional indent and a trailing newline. + */ +public final class CodeWriter { + private final StringBuilder sb = new StringBuilder(); + private final String indentChars; + + public CodeWriter() { + this(" "); + } + + public CodeWriter(String indentChars) { + this.indentChars = indentChars; + } + + public String text() { + return sb.toString(); + } + + public void reset() { + sb.setLength(0); + } + + private void indent(int n) { + for (int i = 0; i < n; i++) { + sb.append(indentChars); + } + } + + public void write(String... parts) { + write(0, parts); + } + + public void write(int indent, String... parts) { + if (indent > 0) { + indent(indent); + } + for (String p : parts) { + sb.append(p); + } + } + + /** Emit a single blank line. */ + public void writeln() { + sb.append('\n'); + } + + public void writeln(String... lines) { + writeln(0, lines); + } + + public void writeln(int indent, String... lines) { + if (lines.length == 0) { + sb.append('\n'); + return; + } + for (String line : lines) { + if (indent > 0 && line.length() > 0) { + indent(indent); + } + sb.append(line).append('\n'); + } + } +} diff --git a/codegen/src/main/java/org/innercore/codegen/Codegen.java b/codegen/src/main/java/org/innercore/codegen/Codegen.java new file mode 100644 index 000000000..87adf237a --- /dev/null +++ b/codegen/src/main/java/org/innercore/codegen/Codegen.java @@ -0,0 +1,84 @@ +package org.innercore.codegen; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Orchestrates the API-binding codegen: clears the output package, resolves + * {@code @ApiBinding} sources into a {@link BindingStore}, then generates the + * {@code BindGenBase} + {@code ApiModuleMap} sources. + * + *

This is the single-process, Java-only entry point. The C++ (libclang) pass is + * out of scope; when added, it would contribute records to the same {@link BindingStore} + * before {@link JavaBindingGenerator#generate} runs. + */ +public final class Codegen { + private Codegen() {} + + /** Runs codegen over an explicit set of {@code .java} files (used by the Gradle task). */ + public static boolean run(List javaFiles, File outputDir, String outputPackage) { + prepareOutput(outputDir); + BindingStore store = new BindingStore(); + boolean resolveOk = new JavaBindingResolver(store).resolveFiles(javaFiles); + boolean generateOk = new JavaBindingGenerator(store).generate(outputDir, outputPackage); + return resolveOk && generateOk; + } + + /** Runs codegen by recursively scanning source roots (used by {@link #main}). */ + public static boolean runWithRoots(List sourceRoots, File outputDir, String outputPackage) { + prepareOutput(outputDir); + BindingStore store = new BindingStore(); + boolean resolveOk = new JavaBindingResolver(store).resolve(sourceRoots, outputDir); + boolean generateOk = new JavaBindingGenerator(store).generate(outputDir, outputPackage); + return resolveOk && generateOk; + } + + private static void prepareOutput(File outputDir) { + clearDirectory(outputDir); + if (!outputDir.exists() && !outputDir.mkdirs()) { + throw new RuntimeException("failed to create output directory " + outputDir); + } + } + + private static void clearDirectory(File dir) { + if (!dir.isDirectory()) { + return; + } + try (Stream walk = Files.walk(dir.toPath())) { + List children = walk + .filter(p -> !p.equals(dir.toPath())) + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + for (java.nio.file.Path child : children) { + Files.deleteIfExists(child); + } + } catch (IOException e) { + throw new RuntimeException("failed to clear output directory " + dir, e); + } + } + + /** CLI entry point for manual runs/debugging: {@code ...}. */ + public static void main(String[] args) { + if (args.length < 3) { + System.err.println("usage: Codegen ..."); + System.exit(2); + } + String outputPackage = args[0]; + File outputDir = new File(args[1]); + List roots = new ArrayList(); + for (int i = 2; i < args.length; i++) { + roots.add(new File(args[i])); + } + boolean ok = runWithRoots(roots, outputDir, outputPackage); + if (!ok) { + System.err.println("codegen finished with errors"); + System.exit(1); + } + } +} diff --git a/codegen/src/main/java/org/innercore/codegen/Encoding.java b/codegen/src/main/java/org/innercore/codegen/Encoding.java new file mode 100644 index 000000000..18b394b62 --- /dev/null +++ b/codegen/src/main/java/org/innercore/codegen/Encoding.java @@ -0,0 +1,80 @@ +package org.innercore.codegen; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Type-encoding constants and codegen feature flags, ported from the reference + * Python codegen. The internal type encoding is a small string DSL: + *

    + *
  • primitives: {@code void/boolean/int/long/float/double}
  • + *
  • {@code String}
  • + *
  • {@code @} for Java class types
  • + *
  • {@code [} prefix, one per array dimension
  • + *
  • {@code *} prefix for C++ pointers (reserved; unused in the Java-only pipeline)
  • + *
+ */ +public final class Encoding { + private Encoding() {} + + // Feature flags (mirror the reference module-level constants). + public static final boolean SUPPORT_CRIT_NATIVE = false; + public static final boolean ALLOW_CROSSTYPE_RESOLUTIONS = true; + public static final boolean ALLOW_COERCE_TO_NUMBER = true; + + public static final String TYPE_PREFIX_PTR = "*"; + public static final String TYPE_PREFIX_LIST = "["; + public static final String TYPE_PREFIX_JAVA_CLS = "@"; + + public static final String BASE_TYPE_VOID = "void"; + public static final String BASE_TYPE_BOOL = "boolean"; + public static final String BASE_TYPE_INT = "int"; + public static final String BASE_TYPE_LONG = "long"; + public static final String BASE_TYPE_FLOAT = "float"; + public static final String BASE_TYPE_DOUBLE = "double"; + public static final String BASE_TYPE_STRING = "String"; + + public static final List BASE_PRIMITIVE_TYPES = Arrays.asList( + BASE_TYPE_BOOL, BASE_TYPE_INT, BASE_TYPE_LONG, BASE_TYPE_FLOAT, BASE_TYPE_DOUBLE); + + // Binding "function" kinds. + public static final String FN_TYPE_STATIC_METHOD = "static-method"; + public static final String FN_TYPE_METHOD = "method"; + public static final String FN_TYPE_STATIC_FIELD = "static-field"; + public static final String FN_TYPE_FIELD = "field"; + public static final String FN_TYPE_CONSTRUCTOR = "constructor"; + public static final String FN_TYPE_REF_CNT = "refcnt"; + public static final String FN_TYPE_JAVA_GET_POINTER = "java-get-ptr"; + + /** Types treated as already-imported when resolving simple type names. */ + public static final Set DEFAULT_JAVA_IMPORTS = new LinkedHashSet(Arrays.asList( + "java.lang.Object", + "java.lang.Class", + "java.lang.String", + "java.lang.Integer", + "java.lang.Long", + "java.lang.Float", + "java.lang.Double", + "java.lang.Number", + "java.lang.Boolean", + "java.lang.Character", + "java.lang.Runnable", + "java.lang.CharSequence", + "org.mozilla.javascript.Scriptable", + "org.mozilla.javascript.ScriptableObject", + "org.mozilla.javascript.NativeArray")); + + public static boolean isFieldType(String fnType) { + return FN_TYPE_FIELD.equals(fnType) || FN_TYPE_STATIC_FIELD.equals(fnType); + } + + public static String repeat(String s, int n) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + sb.append(s); + } + return sb.toString(); + } +} diff --git a/codegen/src/main/java/org/innercore/codegen/JavaBindingGenerator.java b/codegen/src/main/java/org/innercore/codegen/JavaBindingGenerator.java new file mode 100644 index 000000000..966252e2b --- /dev/null +++ b/codegen/src/main/java/org/innercore/codegen/JavaBindingGenerator.java @@ -0,0 +1,1187 @@ +package org.innercore.codegen; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.innercore.codegen.Encoding.*; + +/** + * Consumes the {@link BindingStore} and emits, per module, a + * {@code BindGenBase.java} class plus a shared {@code ApiModuleMap.java}. + * + *

Faithful port of the reference {@code JavaBindingGenerator}. The native-record + * code paths are preserved so that, once a C++ resolver contributes native records, + * generation works unchanged; the Java-only pipeline simply never exercises them. + */ +public final class JavaBindingGenerator { + private static final String IND = " "; + + private final BindingStore store; + private final TypeResolver typeResolver = new TypeResolver(); + private final java.util.Set typesWithCustomPointerGetter = new java.util.HashSet(); + + private CodeWriter w = new CodeWriter(); + private boolean prepareOk = true; + private Map cppTypeToJava = new LinkedHashMap(); + + public JavaBindingGenerator(BindingStore store) { + this.store = store; + } + + // --- holders ------------------------------------------------------------- + + private static final class ModuleData { + final Map> bindings = new LinkedHashMap>(); + final Map cppCls = new LinkedHashMap(); + String cppClsResolved = null; + } + + private static final class Prepared { + final String className; + Map> bindings; + + Prepared(String className, Map> bindings) { + this.className = className; + this.bindings = bindings; + } + } + + private static final class JavaBindingClassRequirements { + boolean needsPointerField = false; + boolean needsRefCount = false; + boolean needsChangePointerMethod = false; + } + + /** (overload, argIdx, subResolutions); empty subs == leaf call. */ + private static final class Resolution { + final BindingOverload overload; + final int argIdx; + final List subs; + + Resolution(BindingOverload overload, int argIdx, List subs) { + this.overload = overload; + this.argIdx = argIdx; + this.subs = subs; + } + } + + private static final class ResolveResult { + final Resolution res; + final boolean ok; + + ResolveResult(Resolution res, boolean ok) { + this.res = res; + this.ok = ok; + } + } + + // --- entry point --------------------------------------------------------- + + public boolean generate(File outputDir, String outputPackage) { + Map modulesData = loadBindingsData(); + if (modulesData.isEmpty()) { + System.out.println("no binding data to generate java classes, skipping codegen"); + return true; + } + boolean allOk = true; + + // All module classes are themselves scriptable. + for (String moduleCls : store.javaModules.values()) { + typeResolver.javaScriptableClasses.add(moduleCls); + } + + Map prepared = prepareBindingOverloads(modulesData); + allOk = allOk && prepareOk; + + for (Prepared info : prepared.values()) { + info.bindings = generateDefaultedOverloads(info.bindings); + } + + Map moduleMap = new LinkedHashMap(); + for (Map.Entry entry : prepared.entrySet()) { + String moduleName = entry.getKey(); + Prepared info = entry.getValue(); + String genClassName = info.className; + + if (!store.javaModules.containsKey(moduleName)) { + allOk = false; + System.out.println("Module " + moduleName + " has no associated java class! " + + "You should create java class with @ApiBinding(\"" + moduleName + "\") to proceed."); + continue; + } + + w = new CodeWriter(); + try { + if (generateJavaClass(outputPackage, moduleName, genClassName, info.bindings)) { + System.out.println("codegen done for " + genClassName + ".java"); + } else { + System.out.println("codegen failed for " + genClassName + ".java, see errors above"); + allOk = false; + } + moduleMap.put(moduleName, store.javaModules.get(moduleName)); + writeFile(outputDir, genClassName + ".java"); + } catch (RuntimeException ex) { + throw new RuntimeException("codegen failed for " + genClassName + ".java", ex); + } + } + + if (!moduleMap.isEmpty()) { + w = new CodeWriter(); + generateModuleMap(outputPackage, moduleMap, !allOk); + writeFile(outputDir, "ApiModuleMap.java"); + } + + return allOk; + } + + private void writeFile(File outputDir, String name) { + File target = new File(outputDir, name); + try { + File parent = target.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + Files.write(target.toPath(), w.text().getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("failed to write " + target, e); + } + } + + // --- writer shims -------------------------------------------------------- + + private void writeln(String... lines) { + w.writeln(lines); + } + + private void writelnI(int indent, String... lines) { + w.writeln(indent, lines); + } + + private void write(String... parts) { + w.write(parts); + } + + private void writeI(int indent, String... parts) { + w.write(indent, parts); + } + + // --- data loading -------------------------------------------------------- + + private static String[] splitBind(String bind) { + int dot = bind.indexOf('.'); + return new String[]{bind.substring(0, dot), bind.substring(dot + 1)}; + } + + private Map loadBindingsData() { + Map modules = new LinkedHashMap(); + for (BindingRecord rec : store.records) { + String[] parts = splitBind(rec.bind); + String bindModule = parts[0]; + String bindName = parts[1]; + + ModuleData md = modules.get(bindModule); + if (md == null) { + md = new ModuleData(); + modules.put(bindModule, md); + } + if (rec.cppCls != null && !rec.cppCls.isEmpty()) { + md.cppCls.put(rec.cppCls, bindModule + "." + bindName); + } + List list = md.bindings.get(bindName); + if (list == null) { + list = new ArrayList(); + md.bindings.put(bindName, list); + } + list.add(rec); + } + + // Ensure every module declared via a class-level @ApiBinding gets a (possibly empty) + // base class, even when it has no bound members yet. The reference relied on the C++ + // pass to supply records for such native-driven modules (e.g. Game, Dimensions); in the + // Java-only pipeline an empty base is still required so the hand-written module class, + // which extends it, compiles. Once a C++ resolver contributes records, these fill in. + for (String moduleName : store.javaModules.keySet()) { + if (!modules.containsKey(moduleName)) { + modules.put(moduleName, new ModuleData()); + } + } + + for (Map.Entry e : modules.entrySet()) { + ModuleData md = e.getValue(); + if (md.cppCls.size() > 1) { + System.out.println("module '" + e.getKey() + "' bindings declare different CPP classes:"); + for (Map.Entry c : md.cppCls.entrySet()) { + System.out.println(" " + c.getValue() + " declared " + c.getKey()); + } + } + md.cppClsResolved = md.cppCls.isEmpty() ? null : md.cppCls.keySet().iterator().next(); + } + return modules; + } + + // --- overload preparation ------------------------------------------------ + + private VarDecl deserializeAndCvtType(VarDecl source) { + VarDecl decl = source.copy(); + if (decl.type.startsWith(TYPE_PREFIX_PTR)) { + String typ = decl.type.substring(1); + if (cppTypeToJava.containsKey(typ)) { + decl.type = TYPE_PREFIX_PTR + cppTypeToJava.get(typ); + } else { + System.out.println("failed to resolve CPP type: " + typ); + decl.type = TYPE_PREFIX_PTR + "CppTypeWasNotRecognized_" + typ; + prepareOk = false; + } + } + return decl; + } + + private Map prepareBindingOverloads(Map modules) { + prepareOk = true; + Map prepared = new LinkedHashMap(); + cppTypeToJava = new LinkedHashMap(); + for (Map.Entry e : modules.entrySet()) { + if (e.getValue().cppClsResolved != null) { + cppTypeToJava.put(e.getValue().cppClsResolved, store.javaModules.get(e.getKey())); + } + } + + for (Map.Entry me : modules.entrySet()) { + String moduleName = me.getKey(); + ModuleData module = me.getValue(); + String genClassName = moduleName + "BindGenBase"; + + Map> bindings = new LinkedHashMap>(); + for (Map.Entry> be : module.bindings.entrySet()) { + String name = be.getKey(); + List resolvedOverloads = new ArrayList(); + + for (BindingRecord oj : be.getValue()) { + BindingOverload o = new BindingOverload(); + o.name = name; + o.moduleName = moduleName; + o.bindType = "native"; + o.moduleClsName = store.javaModules.get(moduleName); + o.callSrc = name; + o.ret = deserializeAndCvtType(oj.ret); + o.fnType = oj.typ; + o.needJavaWrap = oj.wrap; + o.mangledName = oj.mgl != null ? oj.mgl : o.name; + + boolean mismatchedResolution = false; + for (BindingOverload included : resolvedOverloads) { + if (o.fnType.equals(included.fnType)) { + continue; + } + String prefixMessage = "found"; + boolean oField = isFieldType(o.fnType); + boolean incField = isFieldType(included.fnType); + if (oField != incField) { + prefixMessage = "failed to include"; + mismatchedResolution = true; + } + if (!ALLOW_CROSSTYPE_RESOLUTIONS || !prefixMessage.equals("found")) { + System.out.println(prefixMessage + " mismatched type resolution for " + + moduleName + "." + o.name + " " + o.bindType + " " + o.fnType + + " (got " + included.fnType + ", but here is " + o.fnType + ")"); + } + break; + } + if (mismatchedResolution) { + continue; + } + + if (!isFieldType(o.fnType)) { + o.args = new ArrayList(); + for (VarDecl arg : oj.args) { + o.args.add(deserializeAndCvtType(arg)); + } + o.argsCnt = o.args.size(); + } + if (oj.src != null) { + o.callSrc = oj.src; + } + if (oj.cls != null) { + o.bindType = "java"; + o.moduleClsName = oj.cls; + } + if (oj.attrs != null) { + o.attrs = oj.attrs; + } + if ((o.fnType.equals(FN_TYPE_STATIC_METHOD) || o.fnType.equals(FN_TYPE_STATIC_FIELD)) + && o.bindType.equals("java")) { + o.callSrc = oj.cls + "." + o.callSrc; + } + if (o.fnType.equals(FN_TYPE_JAVA_GET_POINTER)) { + typesWithCustomPointerGetter.add(store.javaModules.get(moduleName)); + } + resolvedOverloads.add(o); + } + bindings.put(name, resolvedOverloads); + } + prepared.put(moduleName, new Prepared(genClassName, bindings)); + } + return prepared; + } + + private Map> generateDefaultedOverloads( + Map> bindings) { + for (Map.Entry> e : bindings.entrySet()) { + String name = e.getKey(); + List snapshot = new ArrayList(e.getValue()); + for (BindingOverload o : snapshot) { + for (int i = 0; i < o.argsCnt; i++) { + if (o.args.get(o.args.size() - 1 - i).def.isEmpty()) { + break; + } + BindingOverload doo = new BindingOverload(); + doo.name = o.name; + doo.moduleClsName = o.moduleClsName; + doo.bindType = "defaulted-overload"; + doo.fnType = o.fnType; + doo.callSrc = o.callSrc; + doo.args = o.args; + doo.argsCnt = o.argsCnt - 1 - i; + doo.ret = o.ret; + doo.needJavaWrap = false; + doo.attrs = o.attrs; + bindings.get(name).add(doo); + } + } + } + return bindings; + } + + private JavaBindingClassRequirements analyzeClassRequirements( + Map> bindings) { + JavaBindingClassRequirements req = new JavaBindingClassRequirements(); + outer: + for (List allOverloads : bindings.values()) { + for (BindingOverload overload : allOverloads) { + if (!req.needsPointerField) { + if (overload.attrs.contains("native") && !overload.bindType.equals("defaulted-overload") + && (overload.fnType.equals(FN_TYPE_CONSTRUCTOR) + || overload.fnType.equals(FN_TYPE_REF_CNT) + || overload.fnType.equals(FN_TYPE_METHOD))) { + req.needsPointerField = true; + } + } + if (!req.needsRefCount) { + if (overload.fnType.equals(FN_TYPE_REF_CNT)) { + req.needsRefCount = true; + } + } + if (req.needsPointerField && req.needsRefCount) { + break outer; + } + } + } + req.needsChangePointerMethod = req.needsPointerField; + return req; + } + + // --- class skeleton ------------------------------------------------------ + + private void writeClassHeader(String pkg, String genClassName, JavaBindingClassRequirements req) { + writeln("package " + pkg + ";"); + writeln(); + writeln( + "import org.mozilla.javascript.*;", + "import com.zhekasmirnov.innercore.api.mod.util.ScriptableAccessorImpl;", + "import com.zhekasmirnov.innercore.api.mod.util.ScriptableFunctionImpl;", + "import com.zhekasmirnov.innercore.api.mod.util.ScriptableModuleImpl;", + "import com.zhekasmirnov.innercore.api.mod.util.ScriptableModuleInstanceImpl;", + "import com.zhekasmirnov.innercore.api.mod.ScriptableObjectHelper;"); + writeln(); + writeln("import dalvik.annotation.optimization.FastNative;"); + if (SUPPORT_CRIT_NATIVE) { + writeln("import dalvik.annotation.optimization.CriticalNative;"); + } + writeln(); + } + + private void writeClassDefinition(String moduleName, String genClassName, + JavaBindingClassRequirements req) { + writeln("public class " + genClassName + " extends ScriptableModuleInstanceImpl {"); + writelnI(1, + "@Override public String getClassName() { return \"" + moduleName + "\"; }", + "@Override public String toString() { return \"[object " + moduleName + "]\"; }", + "@Override public Object getDefaultValue(Class typeHint) { return typeHint == ScriptRuntime.StringClass ? toString() : this; }"); + } + + private void writeConstructorAndFields(String moduleName, String genClassName, + JavaBindingClassRequirements req) { + if (req.needsPointerField) { + writeln(); + writelnI(1, "public long pointer;"); + writelnI(1, "protected " + genClassName + " (long ptr, int refCntChange, Object stub) {"); + writelnI(2, "this.pointer = ptr;"); + writelnI(2, "this.setPrototype(JS_PROTO);"); + if (req.needsRefCount) { + writelnI(2, "if (refCntChange != 0) nativeChangeRefCnt(ptr, refCntChange);"); + } + writelnI(1, "}"); + writelnI(1, "public long getPointer() { return this.pointer; }"); + String javaModuleCls = store.javaModules.get(moduleName); + writelnI(1, "public static " + javaModuleCls + " fromNativePtrIncRef(long ptr) { return ptr != 0 ? new " + + javaModuleCls + "(ptr, 1, null) : null; }"); + writelnI(1, "public static " + javaModuleCls + " fromNativePtrNoIncRefUnsafe(long ptr) { return ptr != 0 ? new " + + javaModuleCls + "(ptr, 0, null) : null; }"); + + if (req.needsChangePointerMethod) { + writeI(1, "protected void setNewPointer(long ptr) {"); + write(req.needsRefCount ? "\n" : " "); + if (req.needsRefCount) { + writelnI(2, + "if (this.pointer == ptr) return;", + "if (this.pointer != 0) nativeChangeRefCnt(this.pointer, -1);"); + } + writeI(req.needsRefCount ? 2 : 0, "this.pointer = ptr;"); + write(req.needsRefCount ? "\n" : " "); + if (req.needsRefCount) { + writelnI(2, "if (this.pointer != 0) nativeChangeRefCnt(this.pointer, 1);"); + } + writelnI(req.needsRefCount ? 1 : 0, "}"); + } + } else { + writeln(); + writelnI(1, "protected " + genClassName + "() { super(); setPrototype(JS_PROTO); }"); + } + } + + // --- visibility / native helpers ----------------------------------------- + + private String getMethodVisibility(BindingOverload overload) { + for (String attribute : overload.attrs) { + if (attribute.equals("private") || attribute.equals("protected") || attribute.equals("public")) { + return attribute + " "; + } + if (attribute.equals("package-private")) { + return ""; + } + } + if (overload.bindType.equals("native")) { + if (overload.fnType.equals(FN_TYPE_CONSTRUCTOR)) { + return "private "; + } + if (overload.fnType.equals(FN_TYPE_REF_CNT)) { + return "protected "; + } + } + return "public "; + } + + private String getNativeAnnotation(BindingOverload overload) { + if (typeResolver.isCritNativeSignature(overload.ret, overload.args)) { + return "@CriticalNative"; + } + return "@FastNative"; + } + + private List getOverloadNativeCallArgs(BindingOverload overload) { + List callArgs = new ArrayList(); + for (VarDecl arg : overload.args) { + if (arg.type.startsWith(TYPE_PREFIX_PTR)) { + String getPtr = typesWithCustomPointerGetter.contains(arg.type.substring(1)) + ? "getPointer()" : "pointer"; + if (arg.nullable) { + callArgs.add(arg.name + " == null ? 0 : " + arg.name + "." + getPtr); + } else { + callArgs.add(arg.name + "." + getPtr); + } + } else { + callArgs.add(arg.name); + } + } + return callArgs; + } + + private void generateNativeMethod(BindingOverload overload) { + String visibility = getMethodVisibility(overload); + String nativeBindingName = overload.mangledName; + if (overload.needJavaWrap) { + visibility = "private "; + } + + List nativeArgs = overload.args; + List callArgs = getOverloadNativeCallArgs(overload); + if (overload.fnType.equals(FN_TYPE_METHOD) || overload.fnType.equals(FN_TYPE_REF_CNT)) { + List withSelf = new ArrayList(); + VarDecl ptr = new VarDecl(BASE_TYPE_LONG); + ptr.name = "pointer"; + withSelf.add(ptr); + withSelf.addAll(overload.args); + nativeArgs = withSelf; + callArgs.add(0, "this.pointer"); + } + + writeI(1, getNativeAnnotation(overload) + " " + visibility + "static native " + + typeResolver.getJavaToNativeType(overload.ret.type) + " " + nativeBindingName); + writeln("(" + joinArgs(nativeArgs, true) + ");"); + + if (overload.needJavaWrap) { + generateWrapMethod(overload, overload.name, nativeBindingName, callArgs); + } + } + + private void generateWrapMethod(BindingOverload overload, String wrapName, String callName, + List callArgs) { + String visibility = getMethodVisibility(overload); + if (overload.fnType.equals(FN_TYPE_METHOD)) { + writeI(1, visibility); + } else { + writeI(1, visibility + "static "); + } + write(typeResolver.getJavaType(overload.ret.type) + " " + wrapName); + write("(" + joinJavaArgs(overload.args.subList(0, overload.argsCnt)) + ")"); + write(" { "); + String callCode = callName + "(" + join(callArgs, ", ") + ")"; + if (!overload.ret.type.equals(BASE_TYPE_VOID)) { + write("return "); + } + if (overload.ret.type.startsWith(TYPE_PREFIX_PTR)) { + write(typeResolver.getJavaType(overload.ret.type) + ".fromNativePtrNoIncRefUnsafe(" + callCode + ");"); + } else { + write(callCode + ";"); + } + writeln(" }"); + } + + private void generateDefaultedOverloadMethod(BindingOverload overload) { + if (!overload.bindType.equals("defaulted-overload")) { + return; + } + List callArgs = new ArrayList(); + for (int i = 0; i < overload.args.size(); i++) { + if (i < overload.argsCnt) { + callArgs.add(overload.args.get(i).name); + } else { + callArgs.add(overload.args.get(i).def); + } + } + generateWrapMethod(overload, overload.name, overload.name, callArgs); + } + + private void generateConstructor(String genClassName, BindingOverload overload) { + if (!overload.fnType.equals(FN_TYPE_CONSTRUCTOR)) { + return; + } + List callArgs = getOverloadNativeCallArgs(overload); + writelnI(1, "protected " + genClassName + "(" + joinJavaArgs(overload.args) + ") {"); + writelnI(2, + "this.pointer = nativeConstruct(" + join(callArgs, ", ") + ");", + "setPrototype(JS_PROTO);"); + writelnI(1, "}"); + } + + private void generateFinalizer() { + writelnI(1, "@Override protected void finalize() throws Throwable {"); + writelnI(2, + "super.finalize();", + "if (this.pointer != 0) nativeChangeRefCnt(this.pointer, -1);"); + writelnI(1, "}"); + } + + // --- JS binding generation ------------------------------------------------ + + private boolean generateJsMethodBinding(String moduleName, String bindName, + List allOverloads) { + int indentCnt = 2; + writelnI(indentCnt, "if (allowOverrides || !curObj.has(\"" + bindName + "\", curObj)) {"); + indentCnt += 1; + writeI(indentCnt, "curObj.put(\"" + bindName + "\", curObj, "); + writeln("new ScriptableFunctionImpl() { @Override public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {"); + indentCnt += 1; + boolean ok = generateJsBindingCode(moduleName, bindName, allOverloads, indentCnt); + indentCnt -= 1; + writelnI(indentCnt, "}});"); + indentCnt -= 1; + writelnI(indentCnt, "}"); + return ok; + } + + private boolean generateJsConstructorBinding(String moduleName, String bindName, + List allOverloads) { + int indentCnt = 3; + writelnI(indentCnt, "@Override public Scriptable construct(Context cx, Scriptable scope, Object[] args) {"); + indentCnt += 1; + boolean ok = generateJsBindingCode(moduleName, bindName, allOverloads, indentCnt); + indentCnt -= 1; + writelnI(indentCnt, "}"); + return ok; + } + + private boolean generateJsBindingCode(String moduleName, String bindName, + List allOverloads, int indentCnt) { + boolean ok = true; + Map> overloadsByArgsCnt = new LinkedHashMap>(); + int maxArgsCnt = -1; + int minArgsCnt = 256; + int lastNullableArgPos = -1; + + for (BindingOverload o : allOverloads) { + minArgsCnt = Math.min(minArgsCnt, o.argsCnt); + maxArgsCnt = Math.max(maxArgsCnt, o.argsCnt); + List bucket = overloadsByArgsCnt.get(o.argsCnt); + if (bucket == null) { + bucket = new ArrayList(); + overloadsByArgsCnt.put(o.argsCnt, bucket); + } + for (int index = 0; index < o.args.size(); index++) { + if (o.args.get(index).nullable) { + lastNullableArgPos = Math.max(index, lastNullableArgPos); + } + } + bucket.add(o); + } + + boolean addReturnUndefined = overloadsByArgsCnt.isEmpty(); + for (List overloads : overloadsByArgsCnt.values()) { + for (BindingOverload o : overloads) { + if (o.ret.type.equals(BASE_TYPE_VOID)) { + addReturnUndefined = true; + break; + } + } + if (addReturnUndefined) { + break; + } + } + + String lengthAttribute = "args.length"; + int startStripNullArgsAt = Math.max(minArgsCnt, lastNullableArgPos + 1); + if (startStripNullArgsAt < maxArgsCnt) { + writelnI(indentCnt, + "int argCount = Math.min(" + lengthAttribute + ", " + maxArgsCnt + ");", + "for (; argCount - 1 >= " + startStripNullArgsAt + + " && (args[argCount - 1] == null || args[argCount - 1] == Undefined.instance); argCount--);"); + lengthAttribute = "argCount"; + } else if (minArgsCnt > 0) { + writeI(indentCnt, "if (" + lengthAttribute + " < " + minArgsCnt + ") "); + writeln("throw new IllegalArgumentException(\"" + moduleName + "." + bindName + + ": too few arguments (required=" + minArgsCnt + " got=\" + args.length + \")\");"); + } + + boolean afterElse = false; + for (int argCnt = maxArgsCnt; argCnt >= 0; argCnt--) { + if (!overloadsByArgsCnt.containsKey(argCnt)) { + continue; + } + List overloads = overloadsByArgsCnt.get(argCnt); + + if (!afterElse) { + writeI(indentCnt); + } + if (minArgsCnt != maxArgsCnt) { + write("if (" + lengthAttribute + " >= " + argCnt + ") "); + } + + ResolveResult resolved = resolveOverloads(overloads, argCnt, moduleName + "." + bindName); + if (!resolved.ok) { + ok = false; + } + + if (minArgsCnt != maxArgsCnt) { + writeln("{ /* overloads for " + argCnt + " args */"); + indentCnt += 1; + } + + if (argCnt > 0) { + if (minArgsCnt != maxArgsCnt) { + writeI(indentCnt, ""); + } + for (int argIdx = 0; argIdx < argCnt; argIdx++) { + write("Object argVal" + argIdx + " = args[" + argIdx + "];"); + } + write("\n"); + writeI(indentCnt, ""); + } + + if (!generateOverloadResolutionRecursive(resolved.res, moduleName + "." + bindName, indentCnt)) { + ok = false; + } + + if (minArgsCnt != maxArgsCnt) { + indentCnt -= 1; + writelnI(indentCnt, "}"); + } + + if (minArgsCnt != maxArgsCnt) { + writeI(indentCnt, "else "); + afterElse = true; + } + } + + if (afterElse) { + writeln("throw new IllegalArgumentException(\"" + moduleName + "." + bindName + + ": can't find overload with \" + args.length + \" arguments\");"); + } + if (addReturnUndefined) { + writelnI(indentCnt, "return Undefined.instance;"); + } + return ok; + } + + private boolean doOverloadArgTypesAlias(String t1, String t2) { + if (t1.equals(t2) || (t1.charAt(0) == '!' && t2.charAt(0) == '!')) { + return true; + } + if ((t1.charAt(0) == '!' && t1.substring(1).equals(t2)) + || (t2.charAt(0) == '!' && t2.substring(1).equals(t1))) { + return true; + } + return false; + } + + private ResolveResult resolveOverloadsRecursive(List overloads, + List> overloadsArgs, + List argIndices) { + if (overloads.size() == 1) { + return new ResolveResult(new Resolution(overloads.get(0), -1, new ArrayList()), true); + } + if (argIndices.isEmpty()) { + return new ResolveResult(new Resolution(overloads.get(0), -1, new ArrayList()), false); + } + + int lastIndex = argIndices.get(argIndices.size() - 1); + List> overloadUniqueArgs = new ArrayList>(); + for (int i = 0; i <= lastIndex; i++) { + overloadUniqueArgs.add(new ArrayList()); + } + for (int argIndex : argIndices) { + List uniqueTypes = overloadUniqueArgs.get(argIndex); + for (List args : overloadsArgs) { + String argType = args.get(argIndex); + boolean isUnique = true; + for (String uniqueArgType : uniqueTypes) { + if (doOverloadArgTypesAlias(argType, uniqueArgType)) { + isUnique = false; + break; + } + } + if (isUnique) { + uniqueTypes.add(argType); + } + } + } + + int bestArgIdx = argIndices.get(0); + for (int argIndex : argIndices) { + if (overloadUniqueArgs.get(bestArgIdx).size() < overloadUniqueArgs.get(argIndex).size()) { + bestArgIdx = argIndex; + } + } + + if (overloadUniqueArgs.get(bestArgIdx).size() == 1) { + System.out.println("resolution failed"); + System.out.println(" " + overloadsArgs); + System.out.println(" " + overloadUniqueArgs); + return new ResolveResult(new Resolution(overloads.get(0), -1, new ArrayList()), false); + } + + boolean allOk = true; + List overloadResolution = new ArrayList(); + List nextArgIndices = new ArrayList(); + for (int argIndex : argIndices) { + if (argIndex != bestArgIdx) { + nextArgIndices.add(argIndex); + } + } + for (String uniqueArg : overloadUniqueArgs.get(bestArgIdx)) { + List overloadsForType = new ArrayList(); + List> overloadsArgsForType = new ArrayList>(); + for (int i = 0; i < overloads.size(); i++) { + if (doOverloadArgTypesAlias(overloadsArgs.get(i).get(bestArgIdx), uniqueArg)) { + overloadsForType.add(overloads.get(i)); + overloadsArgsForType.add(overloadsArgs.get(i)); + } + } + ResolveResult resolutionForType = resolveOverloadsRecursive( + overloadsForType, overloadsArgsForType, nextArgIndices); + allOk = allOk && resolutionForType.ok; + overloadResolution.add(resolutionForType.res); + } + return new ResolveResult(new Resolution(overloads.get(0), bestArgIdx, overloadResolution), allOk); + } + + private ResolveResult resolveOverloads(List overloads, int argCnt, + String diagnosticsPrefix) { + if (overloads.size() <= 1) { + return new ResolveResult(new Resolution(overloads.get(0), -1, new ArrayList()), true); + } + List> overloadsArgs = new ArrayList>(); + for (BindingOverload overload : overloads) { + List row = new ArrayList(); + for (int i = 0; i < argCnt; i++) { + VarDecl arg = overload.args.get(i); + row.add((arg.nullable ? "!" : "") + typeResolver.getJavaJsArgType(arg.type)); + } + overloadsArgs.add(row); + } + List indices = new ArrayList(); + for (int i = 0; i < argCnt; i++) { + indices.add(i); + } + ResolveResult result = resolveOverloadsRecursive(overloads, overloadsArgs, indices); + if (!result.ok) { + System.out.println("failed to generate overload resolution for " + diagnosticsPrefix + + " with " + argCnt + " arguments:"); + for (BindingOverload resolution : overloads) { + List overloadArguments = new ArrayList(); + for (int i = 0; i < argCnt; i++) { + VarDecl arg = resolution.args.get(i); + overloadArguments.add((arg.nullable ? "@Nullable " : "") + + typeResolver.getJavaJsArgType(arg.type)); + } + List methodArguments = new ArrayList(); + for (VarDecl arg : resolution.args) { + methodArguments.add((arg.nullable ? "@Nullable " : "") + arg.type + " " + arg.name); + } + System.out.println(" (" + join(overloadArguments, ", ") + ") from " + + resolution.bindType + " " + resolution.fnType + + " (" + join(methodArguments, ", ") + ")"); + } + } + return result; + } + + private boolean generateOverloadResolutionRecursive(Resolution resolution, + String fullBindingName, int indentCnt) { + if (resolution.subs.isEmpty()) { + return generateSingleOverloadCall(resolution.overload, indentCnt); + } + boolean ok = true; + for (int k = 0; k < resolution.subs.size(); k++) { + Resolution subResolution = resolution.subs.get(k); + VarDecl resolutionArg = subResolution.overload.args.get(resolution.argIdx); + writeln("if (" + getArgumentTypeCheck(resolutionArg, "argVal" + resolution.argIdx) + ") {"); + writeI(indentCnt + 1, ""); + if (!generateOverloadResolutionRecursive(subResolution, fullBindingName, indentCnt + 1)) { + ok = false; + } + writeI(indentCnt, "}"); + if (k < resolution.subs.size() - 1) { + write(" else "); + } + } + write(" else "); + writeln("throw new IllegalArgumentException(\"" + fullBindingName + + ": can't find overload with \" + args.length + \" arguments\");"); + return ok; + } + + private boolean generateSingleOverloadCall(BindingOverload o, int identCnt) { + List argsCode = new ArrayList(); + for (int index = 0; index < o.argsCnt; index++) { + argsCode.add(convertScriptableToJava(o.args.get(index), "argVal" + index, o)); + } + + String callSrc = o.callSrc; + if (o.fnType.equals(FN_TYPE_METHOD)) { + callSrc = "((" + o.moduleClsName + ") thisObj)." + o.callSrc; + } else if (o.fnType.equals(FN_TYPE_CONSTRUCTOR)) { + callSrc = "new " + o.moduleClsName; + } + + List commented = new ArrayList(); + for (int i = 0; i < argsCode.size(); i++) { + commented.add("/* " + o.args.get(i).name + " */ " + argsCode.get(i)); + } + String callArgs = join(commented, ",\n" + repeat(IND, identCnt + 1)); + + boolean requiresNewline = argsCode.size() > 1; + if (requiresNewline) { + callSrc = callSrc + "(\n" + repeat(IND, identCnt + 1) + callArgs + "\n" + repeat(IND, identCnt) + ")"; + } else { + callSrc = callSrc + "(" + callArgs + ")"; + } + + if (!o.ret.type.equals(BASE_TYPE_VOID)) { + write("return "); + } + callSrc = convertJavaToScriptable(o.ret, callSrc, "scope", "cx"); + writeln(callSrc + ";"); + return true; + } + + private boolean generateJsFieldBinding(String moduleName, String bindName, + List allOverloads) { + boolean ok = true; + int indentCnt = 2; + writelnI(indentCnt, "if (allowOverrides || !curObj.has(\"" + bindName + "\", curObj)) {"); + indentCnt += 1; + writelnI(indentCnt, "new ScriptableAccessorImpl() {"); + indentCnt += 1; + + for (BindingOverload overload : allOverloads) { + writelnI(indentCnt, "@Override public boolean setValue(Object value, Scriptable owner, Scriptable start) {"); + indentCnt += 1; + if (overload.attrs.contains("final")) { + writelnI(indentCnt, "throw new UnsupportedOperationException(\"" + moduleName + "." + bindName + + " field can't be written to\");"); + } else { + writelnI(indentCnt, "((" + overload.moduleClsName + ") start)." + overload.callSrc + " = " + + convertScriptableToJava(overload.ret, "value", overload) + ";"); + writelnI(indentCnt, "return true;"); + } + indentCnt -= 1; + writelnI(indentCnt, "}"); + + writelnI(indentCnt, "@Override public Object getValue(Scriptable start) {"); + indentCnt += 1; + writelnI(indentCnt, "return " + convertJavaToScriptable(overload.ret, + "((" + overload.moduleClsName + ") start)." + overload.callSrc, + "ScriptableObjectHelper.getDefaultScope()", "null") + ";"); + indentCnt -= 1; + writelnI(indentCnt, "}"); + } + + indentCnt -= 1; + writelnI(indentCnt, "}.defineSelfAsProperty((ScriptableObject) curObj, \"" + bindName + "\", 0);"); + indentCnt -= 1; + writelnI(indentCnt, "}"); + return ok; + } + + // --- conversions --------------------------------------------------------- + + private String convertJavaToScriptable(VarDecl arg, String expr, String scope, String ctx) { + if (arg.type.startsWith(TYPE_PREFIX_JAVA_CLS) || arg.type.startsWith(TYPE_PREFIX_LIST)) { + if (!typeResolver.javaScriptableClasses.contains(typeResolver.getJavaType(arg.type))) { + expr = "Context.javaToJS(" + expr + ", " + scope + ", " + ctx + ")"; + } + } + return expr; + } + + private String convertScriptableToJava(VarDecl arg, String expr, BindingOverload overload) { + String nullCase = "null"; + if (!arg.nullable) { + nullCase = "ScriptableObjectHelper.throwNotNull(\"" + arg.name + " parameter of " + + overload.moduleName + "." + overload.name + "\")"; + } + if (arg.type.startsWith(TYPE_PREFIX_JAVA_CLS) || arg.type.startsWith(TYPE_PREFIX_LIST) + || arg.type.startsWith(TYPE_PREFIX_PTR)) { + String type = typeResolver.getJavaType(arg.type); + if (typeResolver.javaScriptableClasses.contains(type)) { + if (arg.nullable) { + return "(" + type + ") " + expr; + } else { + return "(" + type + ") (" + expr + " != null && " + expr + " != Undefined.instance ? " + + expr + " : " + nullCase + ")"; + } + } else { + if (arg.nullable) { + return "(" + type + ") Context.jsToJava(" + expr + ", " + type + ".class)"; + } else { + return "(" + type + ") Context.jsToJava(" + expr + " != null && " + expr + + " != Undefined.instance ? " + expr + " : " + nullCase + ", " + type + ".class)"; + } + } + } else if (arg.type.equals(BASE_TYPE_LONG)) { + return expr + " instanceof Wrapper ? (long) ((Wrapper) " + expr + ").unwrap() : ((Number) " + + expr + ").longValue()"; + } else if (arg.type.equals(BASE_TYPE_STRING)) { + return expr + " instanceof String ? " + expr + ".toString() : (" + expr + " != null && " + expr + + " != Undefined.instance ? ScriptRuntime.toString(" + expr + ") : " + nullCase + ")"; + } else if (arg.type.equals(BASE_TYPE_BOOL)) { + return expr + " instanceof Boolean ? (Boolean) " + expr + " : ScriptRuntime.toBoolean(" + expr + ")"; + } else { + if (ALLOW_COERCE_TO_NUMBER) { + String cap = Character.toUpperCase(arg.type.charAt(0)) + arg.type.substring(1); + return expr + " instanceof Number ? ((Number) " + expr + ")." + arg.type + "Value() : " + + "ScriptableObjectHelper.convertTo" + cap + "(" + expr + ")"; + } else { + return "((Number) " + expr + ")." + arg.type + "Value()"; + } + } + } + + private String getArgumentTypeCheck(VarDecl arg, String argStr) { + String nullableCheck = arg.nullable + ? " || " + argStr + " == null || " + argStr + " == Undefined.instance" : ""; + + if (arg.type.startsWith(TYPE_PREFIX_JAVA_CLS) || arg.type.startsWith(TYPE_PREFIX_LIST) + || arg.type.startsWith(TYPE_PREFIX_PTR)) { + String type = typeResolver.getJavaType(arg.type); + if (typeResolver.javaScriptableClasses.contains(type)) { + return argStr + " instanceof " + type + nullableCheck; + } else { + return "(" + argStr + " instanceof Wrapper ? ScriptableObjectHelper.unwrap(" + argStr + + ") instanceof " + type + " : " + argStr + " instanceof " + type + ")" + nullableCheck; + } + } else if (arg.type.equals(BASE_TYPE_LONG)) { + return argStr + " instanceof Wrapper || " + argStr + " instanceof Number"; + } else if (arg.type.equals(BASE_TYPE_STRING)) { + return argStr + " instanceof CharSequence" + nullableCheck; + } else if (arg.type.equals(BASE_TYPE_BOOL)) { + return argStr + " instanceof Boolean"; + } else { + return argStr + " instanceof Number"; + } + } + + // --- class assembly ------------------------------------------------------ + + private boolean generateJavaClass(String pkg, String moduleName, String genClassName, + Map> bindings) { + boolean ok = true; + JavaBindingClassRequirements req = analyzeClassRequirements(bindings); + + writeClassHeader(pkg, genClassName, req); + writeClassDefinition(moduleName, genClassName, req); + writeConstructorAndFields(moduleName, genClassName, req); + + writeln(); + writelnI(1, "// native:"); + + for (List allOverloads : bindings.values()) { + for (BindingOverload overload : allOverloads) { + if (overload.bindType.equals("native") || overload.bindType.equals("defaulted-overload")) { + if (overload.bindType.equals("native")) { + generateNativeMethod(overload); + } else { + generateDefaultedOverloadMethod(overload); + } + if (overload.fnType.equals(FN_TYPE_CONSTRUCTOR)) { + generateConstructor(genClassName, overload); + } + if (overload.fnType.equals(FN_TYPE_REF_CNT)) { + generateFinalizer(); + } + } + } + } + + Map> scriptableBindings = new LinkedHashMap>(); + for (Map.Entry> e : bindings.entrySet()) { + List filtered = new ArrayList(); + for (BindingOverload overload : e.getValue()) { + if (!overload.attrs.contains("scriptable-hidden")) { + filtered.add(overload); + } + } + if (!filtered.isEmpty()) { + scriptableBindings.put(e.getKey(), filtered); + } + } + + writeln(); + writelnI(1, "// module object:", "public static Scriptable createEmptyModuleObject() {"); + writelnI(2, "return new ScriptableModuleImpl() {"); + writelnI(3, "@Override public String getClassName() { return \"[ApiModule " + moduleName + "]\"; }"); + for (Map.Entry> e : scriptableBindings.entrySet()) { + List ctors = filterByFnType(e.getValue(), FN_TYPE_CONSTRUCTOR, false); + if (!ctors.isEmpty() && !generateJsConstructorBinding(moduleName, e.getKey(), ctors)) { + ok = false; + } + } + writelnI(2, "};"); + writelnI(1, "}"); + + writeln(); + writelnI(1, "// static:", "public static Scriptable injectJsStatic(Scriptable curObj, boolean allowOverrides) {"); + for (Map.Entry> e : scriptableBindings.entrySet()) { + List fields = filterByFnType(e.getValue(), FN_TYPE_STATIC_FIELD, false); + if (!fields.isEmpty() && !generateJsFieldBinding(moduleName, e.getKey(), fields)) { + ok = false; + } + } + for (Map.Entry> e : scriptableBindings.entrySet()) { + List methods = filterByFnType(e.getValue(), FN_TYPE_STATIC_METHOD, false); + if (!methods.isEmpty() && !generateJsMethodBinding(moduleName, e.getKey(), methods)) { + ok = false; + } + } + writelnI(2, "return curObj;"); + writelnI(1, "}"); + + writeln(); + writelnI(1, "public static final Scriptable JS_PROTO = injectJsProto(new ScriptableObject() { @Override public String getClassName() { return \"" + + moduleName + "Proto\"; }}, true);"); + writeln(); + writelnI(1, "// prototype:", "public static Scriptable injectJsProto(Scriptable curObj, boolean allowOverrides) {"); + for (Map.Entry> e : scriptableBindings.entrySet()) { + List fields = filterByFnType(e.getValue(), FN_TYPE_FIELD, false); + if (!fields.isEmpty() && !generateJsFieldBinding(moduleName, e.getKey(), fields)) { + ok = false; + } + } + for (Map.Entry> e : scriptableBindings.entrySet()) { + List methods = new ArrayList(); + for (BindingOverload overload : e.getValue()) { + if (overload.fnType.equals(FN_TYPE_METHOD) && !overload.bindType.equals("defaulted-overload")) { + methods.add(overload); + } + } + if (!methods.isEmpty() && !generateJsMethodBinding(moduleName, e.getKey(), methods)) { + ok = false; + } + } + writelnI(2, "return curObj;"); + writelnI(1, "}"); + writeln("}"); + + return ok; + } + + private void generateModuleMap(String pkg, Map moduleMap, boolean codegenFailed) { + writeln("package " + pkg + ";", "", "import java.util.HashMap;", "", "public class ApiModuleMap {"); + if (codegenFailed) { + writelnI(1, "// WARNING: codegen reported errors; this map may be incomplete."); + } + writelnI(1, "public static HashMap> MODULES = new HashMap<>();", "", "static {"); + for (Map.Entry e : moduleMap.entrySet()) { + writelnI(2, "MODULES.put(\"" + e.getKey() + "\", " + e.getValue() + ".class);"); + } + writelnI(1, "}"); + writeln("}"); + } + + // --- small utilities ----------------------------------------------------- + + private List filterByFnType(List overloads, String fnType, + boolean defaultedOnly) { + List result = new ArrayList(); + for (BindingOverload o : overloads) { + if (o.fnType.equals(fnType)) { + result.add(o); + } + } + return result; + } + + private String joinArgs(List args, boolean nativeType) { + List parts = new ArrayList(); + for (VarDecl arg : args) { + String type = nativeType ? typeResolver.getJavaToNativeType(arg.type) : typeResolver.getJavaType(arg.type); + parts.add(type + " " + arg.name); + } + return join(parts, ", "); + } + + private String joinJavaArgs(List args) { + List parts = new ArrayList(); + for (VarDecl arg : args) { + parts.add(typeResolver.getJavaType(arg.type) + " " + arg.name); + } + return join(parts, ", "); + } + + private static String join(List parts, String sep) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < parts.size(); i++) { + if (i > 0) { + sb.append(sep); + } + sb.append(parts.get(i)); + } + return sb.toString(); + } +} diff --git a/codegen/src/main/java/org/innercore/codegen/JavaBindingResolver.java b/codegen/src/main/java/org/innercore/codegen/JavaBindingResolver.java new file mode 100644 index 000000000..edfdfed5d --- /dev/null +++ b/codegen/src/main/java/org/innercore/codegen/JavaBindingResolver.java @@ -0,0 +1,421 @@ +package org.innercore.codegen; + +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.ImportDeclaration; +import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.MemberValuePair; +import com.github.javaparser.ast.expr.NormalAnnotationExpr; +import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; +import com.github.javaparser.ast.expr.StringLiteralExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.ast.type.Type; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.innercore.codegen.Encoding.*; + +/** + * Parses {@code @ApiBinding}-annotated Java sources (syntactically, via JavaParser) + * and emits normalized {@link BindingRecord}s into a {@link BindingStore}. + * + *

Port of the reference {@code JavaBindingResolver}. Unlike the reference, type + * resolution derives each class's package from its own {@code package} declaration + * and the file set is discovered recursively, rather than from a configured flat + * directory list. + */ +public final class JavaBindingResolver { + private final BindingStore store; + private boolean ok = true; + + public JavaBindingResolver(BindingStore store) { + this.store = store; + StaticJavaParser.getParserConfiguration() + .setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17); + } + + public boolean isOk() { + return ok; + } + + // --- annotation helpers -------------------------------------------------- + + private static String simpleAnnotationName(AnnotationExpr ann) { + return ann.getName().getIdentifier(); + } + + private static String unquote(String literalOrRaw) { + return literalOrRaw; + } + + /** + * Returns a map of the annotation's members (with implicit "value" key) if the + * given annotation is present, else null. Mirrors the reference get_java_annotation. + */ + private Map getJavaAnnotation(NodeWithAnnotations node, String annName) { + Map result = null; + for (AnnotationExpr ann : node.getAnnotations()) { + if (!simpleAnnotationName(ann).equals(annName)) { + continue; + } + if (result == null) { + result = new HashMap(); + result.put("value", ""); + } + if (ann instanceof SingleMemberAnnotationExpr) { + SingleMemberAnnotationExpr single = (SingleMemberAnnotationExpr) ann; + if (single.getMemberValue() instanceof StringLiteralExpr) { + result.put("value", ((StringLiteralExpr) single.getMemberValue()).asString()); + } + } else if (ann instanceof NormalAnnotationExpr) { + for (MemberValuePair pair : ((NormalAnnotationExpr) ann).getPairs()) { + String val; + if (pair.getValue() instanceof StringLiteralExpr) { + val = ((StringLiteralExpr) pair.getValue()).asString(); + } else { + val = unquote(pair.getValue().toString()); + } + result.put(pair.getNameAsString(), val); + } + } + // MarkerAnnotationExpr: nothing more to add. + } + return result; + } + + private boolean hasAnnotation(NodeWithAnnotations node, String annName) { + for (AnnotationExpr ann : node.getAnnotations()) { + if (simpleAnnotationName(ann).equals(annName)) { + return true; + } + } + return false; + } + + // --- type resolution ----------------------------------------------------- + + /** The textual (possibly dotted) name of a non-array type, ignoring generics. */ + private String resolveJavaTypeName(Type type) { + if (type.isClassOrInterfaceType()) { + ClassOrInterfaceType ct = type.asClassOrInterfaceType(); + String name = ct.getNameAsString(); + Optional scope = ct.getScope(); + while (scope.isPresent()) { + name = scope.get().getNameAsString() + "." + name; + scope = scope.get().getScope(); + } + return name; + } + if (type.isPrimitiveType()) { + return type.asPrimitiveType().asString(); + } + if (type.isVoidType()) { + return BASE_TYPE_VOID; + } + return type.asString(); + } + + private static String packageOf(CompilationUnit cu) { + return cu.getPackageDeclaration().map(p -> p.getNameAsString()).orElse(""); + } + + /** Port of stringify_java_type: encode a Java type into the internal type DSL. */ + private String stringifyJavaType(Type rawType, ClassOrInterfaceDeclaration cls, + CompilationUnit cu, String diagnosticsPrefix) { + Type base = rawType; + int dims = 0; + while (base.isArrayType()) { + dims++; + base = base.asArrayType().getComponentType(); + } + String prefix = repeat(TYPE_PREFIX_LIST, dims); + + String declName = resolveJavaTypeName(base); + if (declName.equals(BASE_TYPE_VOID) || BASE_PRIMITIVE_TYPES.contains(declName) + || declName.equals(BASE_TYPE_STRING)) { + return prefix + declName; + } + + for (ImportDeclaration imp : cu.getImports()) { + if (!imp.isAsterisk() && imp.getNameAsString().endsWith("." + declName)) { + return prefix + TYPE_PREFIX_JAVA_CLS + imp.getNameAsString(); + } + } + + String pkg = packageOf(cu); + String canonical = pkg + "." + cls.getNameAsString(); + if (declName.equals(cls.getNameAsString()) || declName.equals(canonical)) { + return prefix + TYPE_PREFIX_JAVA_CLS + canonical; + } + + for (String systemType : DEFAULT_JAVA_IMPORTS) { + String simple = systemType.contains(".") + ? systemType.substring(systemType.lastIndexOf('.') + 1) : systemType; + if (declName.equals(simple) || declName.equals(systemType)) { + return prefix + TYPE_PREFIX_JAVA_CLS + declName; + } + } + + for (BodyDeclaration member : cls.getMembers()) { + if (member instanceof TypeDeclaration + && ((TypeDeclaration) member).getNameAsString().equals(declName)) { + return prefix + TYPE_PREFIX_JAVA_CLS + pkg + "." + cls.getNameAsString() + "." + declName; + } + } + + System.out.println((diagnosticsPrefix != null && !diagnosticsPrefix.isEmpty() + ? diagnosticsPrefix : canonical) + + " has unresolved import " + declName + ", guessing type placed in same directory"); + return prefix + TYPE_PREFIX_JAVA_CLS + pkg + "." + declName; + } + + // --- declaration resolution ---------------------------------------------- + + private VarDecl resolveMethodReturn(MethodDeclaration method, ClassOrInterfaceDeclaration cls, + CompilationUnit cu, String diag) { + if (method.getType().isVoidType()) { + return new VarDecl(BASE_TYPE_VOID); + } + String typ = stringifyJavaType(method.getType(), cls, cu, diag); + if (typ == null) { + return null; + } + VarDecl d = new VarDecl(typ); + d.nullable = hasAnnotation(method, "Nullable"); + return d; + } + + private VarDecl resolveConstructorReturn(ConstructorDeclaration ctor, ClassOrInterfaceDeclaration cls, + CompilationUnit cu) { + VarDecl d = new VarDecl(TYPE_PREFIX_JAVA_CLS + packageOf(cu) + "." + cls.getNameAsString()); + d.nullable = hasAnnotation(ctor, "Nullable"); + return d; + } + + private VarDecl resolveParameter(Parameter param, ClassOrInterfaceDeclaration cls, + CompilationUnit cu, String diag) { + String typ = stringifyJavaType(param.getType(), cls, cu, diag); + if (typ == null) { + return null; + } + VarDecl d = new VarDecl(typ); + d.name = param.getNameAsString(); + d.nullable = hasAnnotation(param, "Nullable"); + return d; + } + + private VarDecl resolveField(FieldDeclaration field, ClassOrInterfaceDeclaration cls, + CompilationUnit cu, String diag) { + Type type = field.getVariable(0).getType(); + String typ = stringifyJavaType(type, cls, cu, diag); + if (typ == null) { + return null; + } + VarDecl d = new VarDecl(typ); + d.name = field.getVariable(0).getNameAsString(); + d.nullable = hasAnnotation(field, "Nullable"); + return d; + } + + // --- binding extraction -------------------------------------------------- + + private boolean processMethodBinding(Object decl, ClassOrInterfaceDeclaration cls, + String moduleName, String fullClsName, CompilationUnit cu) { + boolean isCtor = decl instanceof ConstructorDeclaration; + NodeWithAnnotations annotated = (NodeWithAnnotations) decl; + Map bindAnn = getJavaAnnotation(annotated, "ApiBinding"); + if (bindAnn == null) { + return true; + } + + String memberName = isCtor ? cls.getNameAsString() : ((MethodDeclaration) decl).getNameAsString(); + String bindName = bindAnn.get("value"); + if (bindName == null || bindName.isEmpty()) { + bindName = memberName; + } + if (isCtor) { + bindName = "nativeConstruct"; + } + + String diag = moduleName + "." + bindName; + BindingRecord record = new BindingRecord(); + boolean argsOk = true; + + List params = isCtor + ? ((ConstructorDeclaration) decl).getParameters() + : ((MethodDeclaration) decl).getParameters(); + for (Parameter param : params) { + VarDecl argDecl = resolveParameter(param, cls, cu, diag); + if (argDecl == null) { + argsOk = false; + continue; + } + record.args.add(argDecl); + } + + VarDecl resultType = isCtor + ? resolveConstructorReturn((ConstructorDeclaration) decl, cls, cu) + : resolveMethodReturn((MethodDeclaration) decl, cls, cu, diag); + if (resultType == null || !argsOk) { + return false; + } + + boolean isStatic = !isCtor && ((MethodDeclaration) decl).isStatic(); + String typ = isStatic ? FN_TYPE_STATIC_METHOD : FN_TYPE_METHOD; + if ("getPointer".equals(bindName)) { + typ = FN_TYPE_JAVA_GET_POINTER; + record.attrs.add("scriptable-hidden"); + } + if (isCtor) { + typ = FN_TYPE_CONSTRUCTOR; + } + + record.bind = moduleName + "." + bindName; + record.cls = fullClsName; + record.src = memberName; + record.typ = typ; + record.wrap = false; + record.ret = resultType; + store.addRecord(record); + return true; + } + + private boolean processFieldBinding(FieldDeclaration field, ClassOrInterfaceDeclaration cls, + String moduleName, String fullClsName, CompilationUnit cu) { + Map bindAnn = getJavaAnnotation(field, "ApiBinding"); + if (bindAnn == null) { + return true; + } + + String memberName = field.getVariable(0).getNameAsString(); + String bindName = bindAnn.get("value"); + if (bindName == null || bindName.isEmpty()) { + bindName = memberName; + } + + VarDecl resultType = resolveField(field, cls, cu, moduleName + "." + bindName); + if (resultType == null) { + return false; + } + + BindingRecord record = new BindingRecord(); + if (field.isFinal()) { + record.attrs.add("final"); + } + boolean isStatic = field.isStatic(); + record.bind = moduleName + "." + bindName; + record.cls = fullClsName; + record.src = memberName; + record.typ = isStatic ? FN_TYPE_STATIC_FIELD : FN_TYPE_FIELD; + record.wrap = false; + record.ret = resultType; + store.addRecord(record); + return true; + } + + private boolean processClassBinding(ClassOrInterfaceDeclaration cls, CompilationUnit cu) { + Map moduleAnn = getJavaAnnotation(cls, "ApiBinding"); + if (moduleAnn == null) { + return true; + } + + String moduleName = moduleAnn.get("value"); + if (moduleName == null || moduleName.isEmpty()) { + moduleName = cls.getNameAsString(); + } + String fullClsName = packageOf(cu) + "." + cls.getNameAsString(); + store.addModule(moduleName, fullClsName); + System.out.println("parsing bindings of " + fullClsName); + + boolean classOk = true; + for (BodyDeclaration member : cls.getMembers()) { + if (member instanceof MethodDeclaration || member instanceof ConstructorDeclaration) { + if (!processMethodBinding(member, cls, moduleName, fullClsName, cu)) { + classOk = false; + } + } else if (member instanceof FieldDeclaration) { + if (!processFieldBinding((FieldDeclaration) member, cls, moduleName, fullClsName, cu)) { + classOk = false; + } + } + } + return classOk; + } + + private void processFile(File file) { + CompilationUnit cu; + try { + cu = StaticJavaParser.parse(file); + } catch (Exception e) { + ok = false; + System.out.println("failed to parse " + file + ": " + e.getMessage()); + return; + } + for (TypeDeclaration type : cu.getTypes()) { + if (type instanceof ClassOrInterfaceDeclaration + && !((ClassOrInterfaceDeclaration) type).isInterface()) { + if (!processClassBinding((ClassOrInterfaceDeclaration) type, cu)) { + System.out.println("cannot resolve java file " + file + " bindings"); + ok = false; + } + } + } + } + + /** Resolves bindings from an explicit list of {@code .java} files (e.g. supplied by Gradle). */ + public boolean resolveFiles(List files) { + for (File file : files) { + if (file.isFile() && file.getName().endsWith(".java")) { + processFile(file); + } + } + return ok; + } + + /** Recursively scans the given roots for {@code .java} files and resolves bindings. */ + public boolean resolve(List roots, File excludeDir) { + for (File root : roots) { + if (!root.isDirectory()) { + System.out.println("non-existing codegen directory skipped: " + root); + continue; + } + List javaFiles; + try (Stream walk = Files.walk(root.toPath())) { + final String excludePath = excludeDir != null + ? excludeDir.getAbsolutePath() : null; + javaFiles = walk + .filter(Files::isRegularFile) + .map(java.nio.file.Path::toFile) + .filter(f -> f.getName().endsWith(".java")) + .filter(f -> excludePath == null + || !f.getAbsolutePath().startsWith(excludePath)) + .collect(Collectors.toList()); + } catch (IOException e) { + ok = false; + System.out.println("failed to walk " + root + ": " + e.getMessage()); + continue; + } + for (File file : javaFiles) { + processFile(file); + } + } + return ok; + } +} diff --git a/codegen/src/main/java/org/innercore/codegen/TypeResolver.java b/codegen/src/main/java/org/innercore/codegen/TypeResolver.java new file mode 100644 index 000000000..6f89c0254 --- /dev/null +++ b/codegen/src/main/java/org/innercore/codegen/TypeResolver.java @@ -0,0 +1,84 @@ +package org.innercore.codegen; + +import java.util.ArrayList; +import java.util.List; + +import static org.innercore.codegen.Encoding.*; + +/** + * Helpers that translate the internal type encoding into Java/JS forms. + * + *

Only the Java-relevant subset of the reference {@code TypeResolver} is ported. + * The JNI/C++ helpers (get_jni_type, get_mangled_type_name, crit-native checks, ...) + * are intentionally omitted; they belong with the future C++ resolver. + */ +public final class TypeResolver { + /** Rhino types that are passed through without a jsToJava/javaToJS conversion. */ + public final List javaScriptableClasses = new ArrayList(); + + public TypeResolver() { + javaScriptableClasses.add("org.mozilla.javascript.Scriptable"); + javaScriptableClasses.add("org.mozilla.javascript.ScriptableObject"); + javaScriptableClasses.add("org.mozilla.javascript.NativeArray"); + } + + /** Decode an internal type into a Java type literal (e.g. {@code @a.b.C} -> {@code a.b.C}, {@code [int} -> {@code int[]}). */ + public String getJavaType(String t) { + String suffix = ""; + while (t.startsWith(TYPE_PREFIX_LIST)) { + suffix += "[]"; + t = t.substring(1); + } + if (t.startsWith(TYPE_PREFIX_PTR)) { + t = t.substring(1); + } + if (t.startsWith(TYPE_PREFIX_JAVA_CLS)) { + t = t.substring(1); + } + return t + suffix; + } + + /** Pointers collapse to {@code long} on the Java side; everything else is unchanged. */ + public String getJavaToNativeType(String t) { + if (t.startsWith(TYPE_PREFIX_PTR)) { + return "long"; + } + return t; + } + + /** The coarse JS argument type used for overload discrimination. */ + public String getJavaJsArgType(String t) { + assert !t.startsWith(TYPE_PREFIX_PTR); + boolean isList = t.startsWith(TYPE_PREFIX_LIST); + while (t.startsWith(TYPE_PREFIX_LIST)) { + t = t.substring(1); + } + if (isList) { + return "NativeArray"; + } + if (t.startsWith(TYPE_PREFIX_JAVA_CLS)) { + t = t.substring(1); + } + if (t.equals(BASE_TYPE_INT) || t.equals(BASE_TYPE_LONG) + || t.equals(BASE_TYPE_FLOAT) || t.equals(BASE_TYPE_DOUBLE)) { + return "Number"; + } + return t; + } + + /** Whether the (result, args) signature qualifies for {@code @CriticalNative} (always false while disabled). */ + public boolean isCritNativeSignature(VarDecl resultType, List argsDecl) { + if (!SUPPORT_CRIT_NATIVE) { + return false; + } + if (resultType.type.equals(BASE_TYPE_STRING)) { + return false; + } + for (VarDecl arg : argsDecl) { + if (arg.type.equals(BASE_TYPE_STRING)) { + return false; + } + } + return true; + } +} diff --git a/codegen/src/main/java/org/innercore/codegen/VarDecl.java b/codegen/src/main/java/org/innercore/codegen/VarDecl.java new file mode 100644 index 000000000..a2aadd038 --- /dev/null +++ b/codegen/src/main/java/org/innercore/codegen/VarDecl.java @@ -0,0 +1,25 @@ +package org.innercore.codegen; + +/** A variable / argument / return declaration in the internal type encoding. */ +public final class VarDecl { + public String type = ""; + public String name = ""; + /** Default value expression (used by the C++ pipeline; always empty for Java). */ + public String def = ""; + public boolean nullable = false; + + public VarDecl() {} + + public VarDecl(String type) { + this.type = type; + } + + public VarDecl copy() { + VarDecl v = new VarDecl(); + v.type = this.type; + v.name = this.name; + v.def = this.def; + v.nullable = this.nullable; + return v; + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..433708101 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,6 @@ +rootProject.name = 'ZoteCoreLoader' + +// Build-time only: the @ApiBinding JavaScript-binding code generator. Kept as a +// separate subproject (not buildSrc) so its JavaParser dependency does not collide +// with the older copy bundled inside Gradle's own runtime. +include 'codegen' diff --git a/src/main/java/com/zhekasmirnov/innercore/api/mod/ScriptableObjectHelper.java b/src/main/java/com/zhekasmirnov/innercore/api/mod/ScriptableObjectHelper.java index 66c67ddee..84be3df8b 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/mod/ScriptableObjectHelper.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/mod/ScriptableObjectHelper.java @@ -1,15 +1,11 @@ package com.zhekasmirnov.innercore.api.mod; -import com.zhekasmirnov.horizon.runtime.logger.Logger; import com.zhekasmirnov.innercore.mod.executable.Compiler; -import com.zhekasmirnov.innercore.utils.UIUtils; import org.json.JSONException; import org.json.JSONObject; -import org.mozilla.javascript.Context; -import org.mozilla.javascript.NativeArray; -import org.mozilla.javascript.NativeJSON; -import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.*; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -18,14 +14,12 @@ */ public class ScriptableObjectHelper { - public static Object getProperty(ScriptableObject object, String key, Object fallback) { - if (object.has(key, object)) { - return object.get(key); - } - return fallback; + public static Object getProperty(Scriptable object, String key, Object fallback) { + Object prop = object.get(key, object); + return prop == Scriptable.NOT_FOUND ? fallback : prop; } - public static Object getJavaProperty(ScriptableObject object, String key, Class clazz, Object fallback) { + public static Object getJavaProperty(Scriptable object, String key, Class clazz, Object fallback) { try { return Context.jsToJava(getProperty(object, key, fallback), clazz); } catch (Exception e) { @@ -33,8 +27,15 @@ public static Object getJavaProperty(ScriptableObject object, String key, Class< } } - public static ScriptableObject getScriptableObjectProperty(ScriptableObject object, String key, - ScriptableObject fallback) { + public static Scriptable getScriptableProperty(Scriptable object, String key, Scriptable fallback) { + Object result = getProperty(object, key, fallback); + if (result instanceof Scriptable) { + return (Scriptable) result; + } + return fallback; + } + + public static ScriptableObject getScriptableObjectProperty(Scriptable object, String key, ScriptableObject fallback) { Object result = getProperty(object, key, fallback); if (result instanceof ScriptableObject) { return (ScriptableObject) result; @@ -42,7 +43,7 @@ public static ScriptableObject getScriptableObjectProperty(ScriptableObject obje return fallback; } - public static NativeArray getNativeArrayProperty(ScriptableObject object, String key, NativeArray fallback) { + public static NativeArray getNativeArrayProperty(Scriptable object, String key, NativeArray fallback) { Object result = getProperty(object, key, fallback); if (result instanceof NativeArray) { return (NativeArray) result; @@ -50,7 +51,7 @@ public static NativeArray getNativeArrayProperty(ScriptableObject object, String return fallback; } - public static String getStringProperty(ScriptableObject object, String key, CharSequence fallback) { + public static String getStringProperty(Scriptable object, String key, CharSequence fallback) { Object result = getProperty(object, key, fallback); if (result instanceof CharSequence) { return result.toString(); @@ -58,7 +59,7 @@ public static String getStringProperty(ScriptableObject object, String key, Char return fallback != null ? fallback.toString() : null; } - public static boolean getBooleanProperty(ScriptableObject object, String key, boolean fallback) { + public static boolean getBooleanProperty(Scriptable object, String key, boolean fallback) { Object result = getProperty(object, key, fallback); if (result instanceof Boolean) { return (boolean) result; @@ -66,7 +67,7 @@ public static boolean getBooleanProperty(ScriptableObject object, String key, bo return fallback; } - public static float getFloatProperty(ScriptableObject object, String key, float fallback) { + public static float getFloatProperty(Scriptable object, String key, float fallback) { Object result = getProperty(object, key, fallback); if (result instanceof Double) { return (float) (double) result; @@ -77,10 +78,44 @@ public static float getFloatProperty(ScriptableObject object, String key, float if (result instanceof Float) { return (float) result; } + return fallback; + } + + public static double getDoubleProperty(Scriptable object, String key, double fallback) { + Object result = getProperty(object, key, fallback); + if (result instanceof Number) { + return ((Number) result).doubleValue(); + } return fallback; } - public static int getIntProperty(ScriptableObject object, String key, int fallback) { + public static long[] scriptableToLongArray(NativeArray array) { + if (array != null) { + long[] longs = new long[array.size()]; + int offset = 0; + for (Object obj : array.toArray()) { + if (obj != null) { + longs[offset++] = ((Number) obj).longValue(); + } + } + if (offset != longs.length) { + return Arrays.copyOf(longs, offset); + } + return longs; + } + return new long[0]; + } + + public static NativeArray longToScriptableArray(long[] longs) { + Object[] array = new Object[longs.length]; + int offset = 0; + for (long obj : longs) { + array[offset++] = obj; + } + return ScriptableObjectHelper.createArray(array); + } + + public static int getIntProperty(Scriptable object, String key, int fallback) { Object result = getProperty(object, key, fallback); if (result instanceof Double) { return (int) (double) result; @@ -91,10 +126,10 @@ public static int getIntProperty(ScriptableObject object, String key, int fallba if (result instanceof Float) { return (int) (float) result; } - return fallback; + return fallback; } - public static int getLongProperty(ScriptableObject object, String key, int fallback) { + public static int getLongProperty(Scriptable object, String key, int fallback) { Object result = getProperty(object, key, fallback); if (result instanceof Double) { return (int) (double) result; @@ -105,44 +140,9 @@ public static int getLongProperty(ScriptableObject object, String key, int fallb if (result instanceof Float) { return (int) (float) result; } - return fallback; - } - - public static Object getPropByPath(ScriptableObject object, String path, Object fallback) { - int dotIndex = path.indexOf("."); - if (dotIndex == -1) { - String name = path; - if (object.has(name, object)) { - return object.get(name); - } - } else { - String name = path.substring(0, dotIndex); - if (object.has(name, object)) { - Object obj = object.get(name); - if (obj instanceof ScriptableObject) { - return getPropByPath((ScriptableObject) obj, path.substring(dotIndex + 1), fallback); - } - } else { - Logger.debug("INNERCORE", name + " not found"); - } - } - return fallback; + return fallback; } - public ScriptableObject createFromString(String str) { - try { - Context ctx = Compiler.assureContextForCurrentThread(); - return (ScriptableObject) ctx.evaluateString(new ScriptableObject() { - @Override - public String getClassName() { - return "Empty Scope"; - } - }, str, "Scriptable From String", 0, null); - } catch (Exception e) { - UIUtils.processError(e); - } - return null; - } private static ScriptableObject defaultScope = Context.enter().initStandardObjects(); @@ -154,7 +154,7 @@ public static ScriptableObject createEmpty() { return (ScriptableObject) Context.enter().newObject(defaultScope); } - public static ScriptableObject createFromMap(Map map) { + public static ScriptableObject createFromMap(Map map) { if (map == null) { return null; } @@ -177,6 +177,63 @@ public static NativeArray createArray(List arr) { return (NativeArray) Context.enter().newArray(defaultScope, arr.toArray()); } + + public static Object unwrap(Object obj) { + while (obj instanceof Wrapper) + obj = ((Wrapper) obj).unwrap(); + return obj; + } + + public static String throwNotNull(String msg) { + throw new NullPointerException(msg); + } + + public static float convertToFloat(Object obj) { + if (obj instanceof CharSequence) { + return Float.parseFloat(((CharSequence) obj).toString()); + } + if (obj instanceof Number) { + return ((Number) obj).floatValue(); + } + double val = ScriptRuntime.toNumber(obj); + if (Double.isNaN(val)) throw new IllegalArgumentException("Cannot convert " + obj + " to float"); + return (float) val; + } + + public static double convertToDouble(Object obj) { + if (obj instanceof CharSequence) { + return Double.parseDouble(((CharSequence) obj).toString()); + } + if (obj instanceof Number) { + return ((Number) obj).doubleValue(); + } + double val = ScriptRuntime.toNumber(obj); + if (Double.isNaN(val)) throw new IllegalArgumentException("Cannot convert " + obj + " to double"); + return val; + } + + public static int convertToInt(Object obj) { + if (obj instanceof CharSequence) { + return (int) Double.parseDouble(((CharSequence) obj).toString()); + } + if (obj instanceof Number) { + return ((Number) obj).intValue(); + } + double val = ScriptRuntime.toNumber(obj); + if (Double.isNaN(val)) throw new IllegalArgumentException("Cannot convert " + obj + " to double"); + return (int) val; + } + + public static boolean convertToBool(Object obj) { + if (obj instanceof Boolean) { + return (Boolean) obj; + } + if (obj == null || obj == Undefined.instance) + return false; + return ScriptRuntime.toBoolean(obj); + } + + public static JSONObject toJSON(ScriptableObject obj) { try { return toJSON(obj, new JSONObject()); @@ -196,9 +253,11 @@ private static JSONObject toJSON(ScriptableObject obj, JSONObject json) throws J Object val = obj.get(key); if (val.getClass().isPrimitive()) { json.put(key.toString(), val); - } else if (val instanceof CharSequence) { + } + else if (val instanceof CharSequence) { json.put(key.toString(), val.toString()); - } else if (val instanceof ScriptableObject) { + } + else if (val instanceof ScriptableObject) { json.put(key.toString(), toJSON((ScriptableObject) val, new JSONObject())); } } diff --git a/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableAccessorImpl.java b/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableAccessorImpl.java new file mode 100644 index 000000000..12865848a --- /dev/null +++ b/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableAccessorImpl.java @@ -0,0 +1,35 @@ +package com.zhekasmirnov.innercore.api.mod.util; + +import org.mozilla.javascript.*; + +public abstract class ScriptableAccessorImpl implements AccessorSlot.Getter, AccessorSlot.Setter { + private Function getterFunction = null; + @Override + public Function asGetterFunction(String name, Scriptable scope) { + if (getterFunction == null) + getterFunction = new ScriptableFunctionImpl() { + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return ScriptableAccessorImpl.this.getValue(thisObj); + } + }; + return getterFunction; + } + + private Function setterFunction = null; + @Override + public Function asSetterFunction(String name, Scriptable scope) { + if (setterFunction == null) + setterFunction = new ScriptableFunctionImpl() { + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return ScriptableAccessorImpl.this.setValue(args.length > 0 ? args[0] : Undefined.instance, thisObj, thisObj); + } + }; + return setterFunction; + } + + public void defineSelfAsProperty(ScriptableObject o, String name, int attributes) { + o.defineAccessorProperty(name, this, this, attributes); + } +} diff --git a/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleImpl.java b/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleImpl.java new file mode 100644 index 000000000..c24a1ebb4 --- /dev/null +++ b/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleImpl.java @@ -0,0 +1,18 @@ +package com.zhekasmirnov.innercore.api.mod.util; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Function; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; + +public abstract class ScriptableModuleImpl extends ScriptableObject implements Function { + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return construct(cx, scope, args); + } + + @Override + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { + throw new UnsupportedOperationException("No constructor for " + getClassName()); + } +} diff --git a/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleInstanceImpl.java b/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleInstanceImpl.java new file mode 100644 index 000000000..f47a2aa11 --- /dev/null +++ b/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleInstanceImpl.java @@ -0,0 +1,125 @@ +package com.zhekasmirnov.innercore.api.mod.util; + +import org.mozilla.javascript.AccessorSlot; +import org.mozilla.javascript.ScriptRuntime; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; + +import java.util.HashMap; +import java.util.Set; + +public abstract class ScriptableModuleInstanceImpl implements Scriptable { + public static final boolean VALIDATE_INVALID_WRITES = true; // turn off if mods begin to fail, allows writing extra fields to these objects + + private ScriptableObject prototype; + private Scriptable parentScope; + // slow path fallback to allow adding extra fields to an object, only enabled if VALIDATE_INVALID_WRITES = false + private HashMap extraFields; + + @Override + public Object get(String name, Scriptable start) { + if (extraFields != null && extraFields.containsKey(name)) + return extraFields.get(name); + return prototype.get(name, start); + } + + @Override + public Object get(int index, Scriptable start) { + if (extraFields != null && extraFields.containsKey(index)) + return extraFields.get(index); + return prototype.get(index, start); + } + + @Override + public boolean has(String name, Scriptable start) { + return prototype.has(name, start) || extraFields != null && extraFields.containsKey(name); + } + + @Override + public boolean has(int index, Scriptable start) { + return prototype.has(index, start) || extraFields != null && extraFields.containsKey(index); + } + + @Override + public void put(String name, Scriptable start, Object value) { + AccessorSlot.Setter setter = prototype.getRawSetter(name, 0); + if (setter != null) { + setter.setValue(value, prototype, start); + return; + } + // this automatically handles overriding methods: + // - if the guard is in place, it will throw an error + // - if the guard is absent, overridden method will be stored in extra fields and only this instance will have it + if (VALIDATE_INVALID_WRITES) throw new UnsupportedOperationException("writing missing field " + name + " to " + this); + if (extraFields == null) + extraFields = new HashMap<>(); + extraFields.put(name, value); + } + + @Override + public void put(int index, Scriptable start, Object value) { + AccessorSlot.Setter setter = prototype.getRawSetter(null, index); + if (setter != null) { + setter.setValue(value, prototype, start); + return; + } + if (VALIDATE_INVALID_WRITES) throw new UnsupportedOperationException("writing missing field " + index + " to " + this); + if (extraFields == null) + extraFields = new HashMap<>(); + extraFields.put(index, value); + } + + @Override + public void delete(String name) { + if (VALIDATE_INVALID_WRITES) throw new UnsupportedOperationException("deleting field " + name + " to " + this); + if (extraFields != null) + extraFields.remove(name); + } + + @Override + public void delete(int index) { + if (VALIDATE_INVALID_WRITES) throw new UnsupportedOperationException("deleting field " + index + " to " + this); + if (extraFields != null) + extraFields.remove(index); + } + + @Override + public Scriptable getPrototype() { + return prototype; + } + + @Override + public void setPrototype(Scriptable prototype) { + if (this.prototype != null) + throw new IllegalStateException("can't override prototype API module instance " + this); + this.prototype = (ScriptableObject) prototype; + } + + @Override + public Scriptable getParentScope() { + return parentScope; + } + + @Override + public void setParentScope(Scriptable parent) { + this.parentScope = parent; + } + + @Override + public Object[] getIds() { + if (extraFields == null) + return prototype.getIds(); + Object[] ids = prototype.getIds(); + if (extraFields.isEmpty()) + return ids; + Set keySet = extraFields.keySet(); + Object[] allIds = keySet.toArray(new Object[ids.length + keySet.size()]); + System.arraycopy(ids, 0, allIds, keySet.size(), ids.length); + return allIds; + } + + @Override + public boolean hasInstance(Scriptable instance) { + return ScriptRuntime.jsDelegatesTo(instance, this); + } +} diff --git a/src/main/java/dalvik/annotation/optimization/CriticalNative.java b/src/main/java/dalvik/annotation/optimization/CriticalNative.java new file mode 100644 index 000000000..47584e8d1 --- /dev/null +++ b/src/main/java/dalvik/annotation/optimization/CriticalNative.java @@ -0,0 +1,4 @@ +package dalvik.annotation.optimization; + +public @interface CriticalNative { +} diff --git a/src/main/java/dalvik/annotation/optimization/FastNative.java b/src/main/java/dalvik/annotation/optimization/FastNative.java new file mode 100644 index 000000000..ed0118571 --- /dev/null +++ b/src/main/java/dalvik/annotation/optimization/FastNative.java @@ -0,0 +1,4 @@ +package dalvik.annotation.optimization; + +public @interface FastNative { +} diff --git a/src/main/java/org/innercore/icstd/annotations/ApiBinding.java b/src/main/java/org/innercore/icstd/annotations/ApiBinding.java new file mode 100644 index 000000000..28713c382 --- /dev/null +++ b/src/main/java/org/innercore/icstd/annotations/ApiBinding.java @@ -0,0 +1,13 @@ +package org.innercore.icstd.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD}) +public @interface ApiBinding { + String value() default ""; + boolean noStrict() default true; +} diff --git a/src/main/java/org/innercore/icstd/annotations/ApiHidden.java b/src/main/java/org/innercore/icstd/annotations/ApiHidden.java new file mode 100644 index 000000000..86e85df12 --- /dev/null +++ b/src/main/java/org/innercore/icstd/annotations/ApiHidden.java @@ -0,0 +1,11 @@ +package org.innercore.icstd.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD}) +public @interface ApiHidden { +} \ No newline at end of file diff --git a/src/main/java/org/innercore/icstd/common/AABB.java b/src/main/java/org/innercore/icstd/common/AABB.java new file mode 100644 index 000000000..838dea0a2 --- /dev/null +++ b/src/main/java/org/innercore/icstd/common/AABB.java @@ -0,0 +1,15 @@ +package org.innercore.icstd.common; + +public class AABB { + public final float minX, minY, minZ; + public final float maxX, maxY, maxZ; + + public AABB(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + this.minX = minX; + this.minY = minY; + this.minZ = minZ; + this.maxX = maxX; + this.maxY = maxY; + this.maxZ = maxZ; + } +} diff --git a/src/main/java/org/innercore/icstd/common/BlockPos.java b/src/main/java/org/innercore/icstd/common/BlockPos.java new file mode 100644 index 000000000..6328aeaea --- /dev/null +++ b/src/main/java/org/innercore/icstd/common/BlockPos.java @@ -0,0 +1,66 @@ +package org.innercore.icstd.common; + +import com.zhekasmirnov.innercore.api.mod.ScriptableObjectHelper; +import org.innercore.icstd.common.script.GenericScriptPos3; +import org.mozilla.javascript.ScriptableObject; + +import java.util.Objects; + +public class BlockPos { + public static final BlockPos ZERO = new BlockPos(0, 0, 0); + public final int x, y, z; + + public BlockPos(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + public BlockPos(double x, double y, double z) { + this((int) Math.floor(x), (int) Math.floor(y), (int) Math.floor(z)); + } + + public BlockPos(Vec3f p) { + this(p.x, p.y, p.z); + } + + public BlockPos(GenericScriptPos3 pos) { + this(pos.x, pos.y, pos.z); + } + + public static BlockPos parse(Object o, boolean nullable) { + if (o instanceof BlockPos) { + return (BlockPos) o; + } + if (o instanceof GenericScriptPos3) { + return new BlockPos((GenericScriptPos3) o); + } + if (o instanceof ScriptableObject) { + ScriptableObject s = (ScriptableObject) o; + int x = ScriptableObjectHelper.getIntProperty(s, "x", 0); + int y = ScriptableObjectHelper.getIntProperty(s, "y", 0); + int z = ScriptableObjectHelper.getIntProperty(s, "z", 0); + return new BlockPos(x, y, z); + } + return nullable ? null : ZERO; + } + + public GenericScriptPos3 toScriptPos() { return new GenericScriptPos3(x, y, z); } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BlockPos)) return false; + BlockPos blockPos = (BlockPos) object; + return x == blockPos.x && y == blockPos.y && z == blockPos.z; + } + + @Override + public int hashCode() { + return Objects.hash(x, y, z); + } + + @Override + public String toString() { + return "BlockPos{" + x + ", " + y + ", " + z + '}'; + } +} diff --git a/src/main/java/org/innercore/icstd/common/BlockPosAndSide.java b/src/main/java/org/innercore/icstd/common/BlockPosAndSide.java new file mode 100644 index 000000000..b13138593 --- /dev/null +++ b/src/main/java/org/innercore/icstd/common/BlockPosAndSide.java @@ -0,0 +1,45 @@ +package org.innercore.icstd.common; + +import com.zhekasmirnov.innercore.api.mod.ScriptableObjectHelper; +import org.innercore.icstd.common.script.GenericScriptPos3; +import org.mozilla.javascript.ScriptableObject; + + +public class BlockPosAndSide extends BlockPos { + public static final BlockPosAndSide ZERO = new BlockPosAndSide(0, 0, 0, 0); + public final int side; + + public BlockPosAndSide(int x, int y, int z, int side) { + super(x, y, z); + this.side = side; + } + + public BlockPosAndSide(double x, double y, double z, int side) { + super(x, y, z); + this.side = side; + } + + public BlockPosAndSide(GenericScriptPos3 pos) { + this(pos.x, pos.y, pos.z, pos.side); + } + + public static BlockPosAndSide parse(Object o, boolean nullable) { + if (o instanceof BlockPosAndSide) { + return (BlockPosAndSide) o; + } + if (o instanceof GenericScriptPos3) { + return new BlockPosAndSide((GenericScriptPos3) o); + } + if (o instanceof ScriptableObject) { + ScriptableObject s = (ScriptableObject) o; + int x = ScriptableObjectHelper.getIntProperty(s, "x", 0); + int y = ScriptableObjectHelper.getIntProperty(s, "y", 0); + int z = ScriptableObjectHelper.getIntProperty(s, "z", 0); + int side = ScriptableObjectHelper.getIntProperty(s, "side", 0); + return new BlockPosAndSide(x, y, z, side); + } + return nullable ? null : ZERO; + } + + public GenericScriptPos3 toScriptPos() { return GenericScriptPos3.makeWithSideAndRelative(x, y, z, side); } +} diff --git a/src/main/java/org/innercore/icstd/common/ChunkAABB.java b/src/main/java/org/innercore/icstd/common/ChunkAABB.java new file mode 100644 index 000000000..1a6845b8a --- /dev/null +++ b/src/main/java/org/innercore/icstd/common/ChunkAABB.java @@ -0,0 +1,33 @@ +package org.innercore.icstd.common; + +import java.util.Objects; + +public class ChunkAABB { + public final int x1, z1, x2, z2; + + public ChunkAABB(int x1, int z1, int x2, int z2) { + this.x1 = x1; + this.z1 = z1; + this.x2 = x2; + this.z2 = z2; + } + + public boolean contains(int x, int z) { return x >= x1 && x < x2 && z >= z1 && z < z2; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + ChunkAABB chunkAABB = (ChunkAABB) o; + return x1 == chunkAABB.x1 && z1 == chunkAABB.z1 && x2 == chunkAABB.x2 && z2 == chunkAABB.z2; + } + + @Override + public int hashCode() { + return Objects.hash(x1, z1, x2, z2); + } + + @Override + public String toString() { + return "ChunkAABB{" + x1 + "," + z1 + " ; " + x2 + "," + z2 + "}"; + } +} diff --git a/src/main/java/org/innercore/icstd/common/ChunkPos.java b/src/main/java/org/innercore/icstd/common/ChunkPos.java new file mode 100644 index 000000000..3e8266ca7 --- /dev/null +++ b/src/main/java/org/innercore/icstd/common/ChunkPos.java @@ -0,0 +1,38 @@ +package org.innercore.icstd.common; + +import java.util.Objects; + +public class ChunkPos { + public final int x, z; + + public ChunkPos(int x, int z) { + this.x = x; + this.z = z; + } + + public ChunkPos(int blockX, int blockY, int blockZ) { + this.x = (blockX >= 0 ? blockX / 16 : (blockX - 15) / 16); + this.z = (blockZ >= 0 ? blockZ / 16 : (blockZ - 15) / 16); + } + + public ChunkPos(BlockPos pos) { + this(pos.x, pos.y, pos.z); + } + + @Override + public boolean equals(Object object) { + if (object == null || getClass() != object.getClass()) return false; + ChunkPos chunkPos = (ChunkPos) object; + return x == chunkPos.x && z == chunkPos.z; + } + + @Override + public int hashCode() { + return Objects.hash(x, z); + } + + @Override + public String toString() { + return "ChunkPos{" + x + ", " + z + '}'; + } +} diff --git a/src/main/java/org/innercore/icstd/common/Vec3f.java b/src/main/java/org/innercore/icstd/common/Vec3f.java new file mode 100644 index 000000000..9f8e4e08d --- /dev/null +++ b/src/main/java/org/innercore/icstd/common/Vec3f.java @@ -0,0 +1,43 @@ +package org.innercore.icstd.common; + +import com.zhekasmirnov.innercore.api.mod.ScriptableObjectHelper; +import org.innercore.icstd.common.script.GenericScriptPos3; +import org.mozilla.javascript.ScriptableObject; + +public class Vec3f { + public static Vec3f ZERO = new Vec3f(0, 0, 0); + public final float x, y, z; + + public Vec3f(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vec3f(Vec3i v) { this(v.x, v.y, v.z); } + + public Vec3f(GenericScriptPos3 pos) { + this((float) pos.x, (float) pos.y, (float) pos.z); + } + + public static Vec3f parse(Object o, boolean nullable) { + if (o instanceof Vec3f) { + return (Vec3f) o; + } + if (o instanceof Vec3i) { + return new Vec3f((Vec3i) o); + } + if (o instanceof GenericScriptPos3) { + return new Vec3f((GenericScriptPos3) o); + } + if (o instanceof ScriptableObject) { + ScriptableObject s = (ScriptableObject) o; + float x = ScriptableObjectHelper.getFloatProperty(s, "x", 0); + float y = ScriptableObjectHelper.getFloatProperty(s, "y", 0); + float z = ScriptableObjectHelper.getFloatProperty(s, "z", 0); + return new Vec3f(x, y, z); + } + return nullable ? null : ZERO; + } + +} diff --git a/src/main/java/org/innercore/icstd/common/Vec3i.java b/src/main/java/org/innercore/icstd/common/Vec3i.java new file mode 100644 index 000000000..2e30cd475 --- /dev/null +++ b/src/main/java/org/innercore/icstd/common/Vec3i.java @@ -0,0 +1,48 @@ +package org.innercore.icstd.common; + +import com.zhekasmirnov.innercore.api.mod.ScriptableObjectHelper; +import org.innercore.icstd.common.script.GenericScriptPos3; +import org.mozilla.javascript.ScriptableObject; + +public class Vec3i { + public static final Vec3i ZERO = new Vec3i(0, 0, 0); + public final int x, y, z; + + public Vec3i(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vec3i(double x, double y, double z) { + this((int) Math.floor(x), (int) Math.floor(y), (int) Math.floor(z)); + } + + public Vec3i(Vec3f v) { + this(v.x, v.y, v.z); + } + + public Vec3i(GenericScriptPos3 pos) { + this(pos.x, pos.y, pos.z); + } + + public static Vec3i parse(Object o, boolean nullable) { + if (o instanceof Vec3i) { + return (Vec3i) o; + } + if (o instanceof Vec3f) { + return new Vec3i((Vec3f) o); + } + if (o instanceof GenericScriptPos3) { + return new Vec3i((GenericScriptPos3) o); + } + if (o instanceof ScriptableObject) { + ScriptableObject s = (ScriptableObject) o; + int x = ScriptableObjectHelper.getIntProperty(s, "x", 0); + int y = ScriptableObjectHelper.getIntProperty(s, "y", 0); + int z = ScriptableObjectHelper.getIntProperty(s, "z", 0); + return new Vec3i(x, y, z); + } + return nullable ? null : ZERO; + } +} diff --git a/src/main/java/org/innercore/icstd/common/script/GenericScriptPos3.java b/src/main/java/org/innercore/icstd/common/script/GenericScriptPos3.java new file mode 100644 index 000000000..4ee7b4d40 --- /dev/null +++ b/src/main/java/org/innercore/icstd/common/script/GenericScriptPos3.java @@ -0,0 +1,127 @@ +package org.innercore.icstd.common.script; + +import org.innercore.icstd.annotations.ApiBinding; +import com.zhekasmirnov.innercore.api.gen.GenericPos3BindGenBase; +import com.zhekasmirnov.innercore.api.mod.ScriptableObjectHelper; +import org.innercore.icstd.common.BlockPos; +import org.innercore.icstd.common.BlockPosAndSide; +import org.innercore.icstd.common.Vec3f; +import org.innercore.icstd.common.Vec3i; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; + +@ApiBinding("GenericPos3") +public class GenericScriptPos3 extends GenericPos3BindGenBase { + @ApiBinding public double x; + @ApiBinding public double y; + @ApiBinding public double z; + @ApiBinding public int side; + + @ApiBinding public GenericScriptPos3 vec = null; + @ApiBinding public GenericScriptPos3 relative = null; + + @ApiBinding + public GenericScriptPos3(double x, double y, double z, int side) { + super(); + this.x = x; + this.y = y; + this.z = z; + this.side = side; + } + + @ApiBinding + public GenericScriptPos3(double x, double y, double z) { + this(x, y, z, -1); + } + + public GenericScriptPos3(Vec3f pos) { this(pos.x, pos.y, pos.z); } + public GenericScriptPos3(Vec3i pos) { this(pos.x, pos.y, pos.z); } + public GenericScriptPos3(BlockPos pos) { this(pos.x, pos.y, pos.z); } + public GenericScriptPos3(BlockPosAndSide pos) { this(pos.x, pos.y, pos.z, pos.side); } + + @ApiBinding + public GenericScriptPos3 makeRelativePos() { + double relX = x, relY = y, relZ = z; + switch(side) { + case 0: + relY--; + break; + case 1: + relY++; + break; + case 2: + relZ--; + break; + case 3: + relZ++; + break; + case 4: + relX--; + break; + case 5: + relX++; + break; + } + return new GenericScriptPos3(relX, relY, relZ); + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder("GenericPos{x=" + x + ", y=" + y + ", z=" + z); + if (side != -1) { + str.append(", side=").append(side); + } + if (relative != null) { + str.append(", relative=").append(relative); + } + if (vec != null) { + str.append(", vec=").append(vec); + } + str.append('}'); + return str.toString(); + } + + @ApiBinding + public static GenericScriptPos3 makeWithSideAndRelative(double x, double y, double z, int side) { + GenericScriptPos3 pos = new GenericScriptPos3(x, y, z, side); + pos.relative = pos.makeRelativePos(); + return pos; + } + + @ApiBinding + public static GenericScriptPos3 parse(Object o, boolean nullable) { + if (o instanceof GenericScriptPos3) { + return (GenericScriptPos3) o; + } + if (o instanceof Scriptable) { + Scriptable s = (Scriptable) o; + double x = ScriptableObjectHelper.getDoubleProperty(s, "x", 0); + double y = ScriptableObjectHelper.getDoubleProperty(s, "y", 0); + double z = ScriptableObjectHelper.getDoubleProperty(s, "z", 0); + int side = ScriptableObjectHelper.getIntProperty(s, "side", -1); + Scriptable rel = ScriptableObjectHelper.getScriptableProperty(s, "relative", null); + Scriptable vec = ScriptableObjectHelper.getScriptableProperty(s, "vec", null); + GenericScriptPos3 pos = new GenericScriptPos3(x, y, z, side); + pos.relative = rel != null ? new GenericScriptPos3(Vec3f.parse(rel, false)) : null; + pos.vec = vec != null ? new GenericScriptPos3(Vec3f.parse(vec, false)) : null; + return pos; + } + return nullable ? null : new GenericScriptPos3(0, 0, 0, -1); + } + + @ApiBinding + public Object toScriptableObject() { + ScriptableObject obj = ScriptableObjectHelper.createEmpty(); + obj.put("x", obj, x); + obj.put("y", obj, y); + obj.put("z", obj, z); + if (side >= 0) obj.put("side", obj, side); + if (relative != null) { + obj.put("relative", obj, relative.toScriptableObject()); + } + if (vec != null) { + obj.put("vec", obj, vec.toScriptableObject()); + } + return obj; + } +} diff --git a/src/main/java/org/mozilla/javascript/ScriptableObject.java b/src/main/java/org/mozilla/javascript/ScriptableObject.java index 291609baa..303a080e0 100644 --- a/src/main/java/org/mozilla/javascript/ScriptableObject.java +++ b/src/main/java/org/mozilla/javascript/ScriptableObject.java @@ -1684,6 +1684,16 @@ public void defineAccessorProperty(String name, AccessorSlot.Getter getter, Acce aSlot.setter = setter; setAttributes(name, attributes); } + + public AccessorSlot.Getter getRawGetter(String name, int index) { + Slot slot = slotMap.query(name, index); + return slot instanceof AccessorSlot ? ((AccessorSlot) slot).getter : null; + } + + public AccessorSlot.Setter getRawSetter(String name, int index) { + Slot slot = slotMap.query(name, index); + return slot instanceof AccessorSlot ? ((AccessorSlot) slot).setter : null; + } // # END protected void checkPropertyDefinition(ScriptableObject desc) { diff --git a/src/test/java/org/innercore/icstd/CodeGen.java b/src/test/java/org/innercore/icstd/CodeGen.java new file mode 100644 index 000000000..954a66752 --- /dev/null +++ b/src/test/java/org/innercore/icstd/CodeGen.java @@ -0,0 +1,40 @@ +package org.innercore.icstd; + +import com.zhekasmirnov.innercore.api.gen.CodeGenBindGenBase; +import org.innercore.icstd.annotations.ApiBinding; + +@ApiBinding +public class CodeGen extends CodeGenBindGenBase { + @ApiBinding public int test; + + @ApiBinding + public CodeGen() { + this.test = 5; + } + + @ApiBinding + public int sum(int a, int b) { + return a + b; + } + + @ApiBinding + public int sum(int a, int b, int c) { + return a + b + c; + } + + @ApiBinding + public String other(String a, int b, CodeGen c) { + return a + b + c.test; + } + + @ApiBinding + public static int staticSum(int a, int b) { + return a + b; + } + + @ApiBinding + public static int staticSum(int a, int b, int c) { + return a + b + c; + } + +} diff --git a/src/test/java/org/innercore/icstd/extension/TimingExtension.java b/src/test/java/org/innercore/icstd/extension/TimingExtension.java new file mode 100644 index 000000000..a5510b3fc --- /dev/null +++ b/src/test/java/org/innercore/icstd/extension/TimingExtension.java @@ -0,0 +1,22 @@ +package org.innercore.icstd.extension; + +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { + private static final String START_TIME = "start time"; + + @Override + public void beforeTestExecution(ExtensionContext context) throws Exception { + context.getStore(ExtensionContext.Namespace.GLOBAL) + .put(START_TIME, System.currentTimeMillis()); + } + + @Override + public void afterTestExecution(ExtensionContext context) throws Exception { + long startTime = context.getStore(ExtensionContext.Namespace.GLOBAL) + .remove(START_TIME, long.class); + System.out.printf("End test %s - %d ms%n", context.getDisplayName(), System.currentTimeMillis() - startTime); + } +} \ No newline at end of file diff --git a/src/test/java/org/innercore/icstd/test/CodeGenTest.java b/src/test/java/org/innercore/icstd/test/CodeGenTest.java new file mode 100644 index 000000000..aedc98528 --- /dev/null +++ b/src/test/java/org/innercore/icstd/test/CodeGenTest.java @@ -0,0 +1,44 @@ +package org.innercore.icstd.test; + +import com.zhekasmirnov.innercore.api.gen.CodeGenBindGenBase; +import org.innercore.icstd.extension.TimingExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mozilla.javascript.Function; +import org.mozilla.javascript.Scriptable; + +@ExtendWith(TimingExtension.class) +public class CodeGenTest { + @Test + public void test() { + Scriptable api = CodeGenBindGenBase.createEmptyModuleObject(); + CodeGenBindGenBase.injectJsStatic(api, true); + + Function staticSum = (Function) api.get("staticSum", api); + + assert ((Number) staticSum.call(null, null, null, new Object[]{5, 5, 5, 5})).intValue() == 15; + assert ((Number) staticSum.call(null, null, null, new Object[]{5, 5, 5})).intValue() == 15; + assert ((Number) staticSum.call(null, null, null, new Object[]{5, 5})).intValue() == 10; + assert ((Number) staticSum.call(null, null, null, new Object[]{5, null, 2f})).intValue() == 7; + assert ((Number) staticSum.call(null, null, null, new Object[]{"5", "5"})).intValue() == 10; + + Scriptable self = ((Function) api).construct(null, null, new Object[]{}); + + Function sum = (Function) self.get("sum", self); + + assert ((Number) sum.call(null, null, self, new Object[]{5, 5, 5, 5})).intValue() == 15; + assert ((Number) sum.call(null, null, self, new Object[]{5, 5, 5})).intValue() == 15; + assert ((Number) sum.call(null, null, self, new Object[]{5, 5})).intValue() == 10; + assert ((Number) sum.call(null, null, self, new Object[]{5, null, 2f})).intValue() == 7; + assert ((Number) sum.call(null, null, self, new Object[]{"5", "5"})).intValue() == 10; + + Function other = (Function) self.get("other", self); + + assert (other.call(null, null, self, new Object[]{"test", "5", self})).toString().equals("test55"); + + assert ((Number) self.get("test", self)).intValue() == 5; + self.put("test", self, 1); + + assert (other.call(null, null, self, new Object[]{"test", "5", self})).toString().equals("test51"); + } +}