From b846ee7ca1a9980f6455631c5745bbb495b07a39 Mon Sep 17 00:00:00 2001 From: Eugene Smirnov Date: Mon, 22 Jun 2026 00:45:38 +0300 Subject: [PATCH 1/6] codegen rewritten on java and wired into gradle build (vibe-coded slop) --- .gitignore | 8 + build.gradle | 34 + codegen/build.gradle | 22 + .../innercore/codegen/BindingOverload.java | 20 + .../org/innercore/codegen/BindingRecord.java | 29 + .../org/innercore/codegen/BindingStore.java | 31 + .../org/innercore/codegen/CodeWriter.java | 69 + .../java/org/innercore/codegen/Codegen.java | 84 ++ .../java/org/innercore/codegen/Encoding.java | 80 ++ .../codegen/JavaBindingGenerator.java | 1187 +++++++++++++++++ .../codegen/JavaBindingResolver.java | 421 ++++++ .../org/innercore/codegen/TypeResolver.java | 84 ++ .../java/org/innercore/codegen/VarDecl.java | 25 + settings.gradle | 6 + .../api/mod/util/ScriptableAccessorImpl.java | 35 + .../api/mod/util/ScriptableModuleImpl.java | 18 + .../util/ScriptableModuleInstanceImpl.java | 101 ++ .../optimization/CriticalNative.java | 4 + .../annotation/optimization/FastNative.java | 4 + .../icstd/annotations/ApiBinding.java | 13 + .../icstd/annotations/ApiHidden.java | 11 + 21 files changed, 2286 insertions(+) create mode 100644 codegen/build.gradle create mode 100644 codegen/src/main/java/org/innercore/codegen/BindingOverload.java create mode 100644 codegen/src/main/java/org/innercore/codegen/BindingRecord.java create mode 100644 codegen/src/main/java/org/innercore/codegen/BindingStore.java create mode 100644 codegen/src/main/java/org/innercore/codegen/CodeWriter.java create mode 100644 codegen/src/main/java/org/innercore/codegen/Codegen.java create mode 100644 codegen/src/main/java/org/innercore/codegen/Encoding.java create mode 100644 codegen/src/main/java/org/innercore/codegen/JavaBindingGenerator.java create mode 100644 codegen/src/main/java/org/innercore/codegen/JavaBindingResolver.java create mode 100644 codegen/src/main/java/org/innercore/codegen/TypeResolver.java create mode 100644 codegen/src/main/java/org/innercore/codegen/VarDecl.java create mode 100644 settings.gradle create mode 100644 src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableAccessorImpl.java create mode 100644 src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleImpl.java create mode 100644 src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleInstanceImpl.java create mode 100644 src/main/java/dalvik/annotation/optimization/CriticalNative.java create mode 100644 src/main/java/dalvik/annotation/optimization/FastNative.java create mode 100644 src/main/java/org/innercore/icstd/annotations/ApiBinding.java create mode 100644 src/main/java/org/innercore/icstd/annotations/ApiHidden.java diff --git a/.gitignore b/.gitignore index e07d7f703..0994fe6e4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,11 @@ 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 diff --git a/build.gradle b/build.gradle index abf29e891..0e6737547 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' @@ -67,6 +74,33 @@ 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') + +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/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..b30cfa69f --- /dev/null +++ b/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleInstanceImpl.java @@ -0,0 +1,101 @@ +package com.zhekasmirnov.innercore.api.mod.util; + +import org.mozilla.javascript.ScriptRuntime; +import org.mozilla.javascript.Scriptable; + +import java.util.HashMap; + +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 Scriptable 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) { + Object o = prototype.get(name, start); + if (o == Scriptable.NOT_FOUND && extraFields != null && extraFields.containsKey(name)) + o = extraFields.get(name); + return o; + } + + @Override + public Object get(int index, Scriptable start) { + Object o = prototype.get(index, start); + if (o == Scriptable.NOT_FOUND && extraFields != null && extraFields.containsKey(index)) + o = extraFields.get(index); + return o; + } + + @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) { + 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) { + 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) { + this.prototype = prototype; + } + + @Override + public Scriptable getParentScope() { + return parentScope; + } + + @Override + public void setParentScope(Scriptable parent) { + this.parentScope = parent; + } + + @Override + public Object[] getIds() { + return prototype.getIds(); + } + + @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 From 2140db4c91849d1f16a5e65cf07d960955eb68ba Mon Sep 17 00:00:00 2001 From: reider745 Date: Mon, 22 Jun 2026 12:05:42 +0300 Subject: [PATCH 2/6] fix: code gen --- .../api/mod/ScriptableObjectHelper.java | 179 ++++++++++++------ 1 file changed, 119 insertions(+), 60 deletions(-) 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())); } } From 0c3626826a135af6828e9f69eefc5276ab263a64 Mon Sep 17 00:00:00 2001 From: reider745 Date: Mon, 22 Jun 2026 12:06:23 +0300 Subject: [PATCH 3/6] feat: test code gen --- .gitignore | 1 + build.gradle | 28 ++++++++++++ .../java/org/innercore/icstd/CodeGen.java | 40 +++++++++++++++++ .../icstd/extension/TimingExtension.java | 22 ++++++++++ .../org/innercore/icstd/test/CodeGenTest.java | 44 +++++++++++++++++++ 5 files changed, 135 insertions(+) create mode 100644 src/test/java/org/innercore/icstd/CodeGen.java create mode 100644 src/test/java/org/innercore/icstd/extension/TimingExtension.java create mode 100644 src/test/java/org/innercore/icstd/test/CodeGenTest.java diff --git a/.gitignore b/.gitignore index 0994fe6e4..464a3ac3a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ node_modules # 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 0e6737547..38dcffa9d 100644 --- a/build.gradle +++ b/build.gradle @@ -49,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 @@ -81,6 +90,25 @@ run { 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.' 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"); + } +} From 4009da9e031b95d4e049226b04ae2df4cd243df3 Mon Sep 17 00:00:00 2001 From: reider745 Date: Mon, 22 Jun 2026 12:06:34 +0300 Subject: [PATCH 4/6] feat: icstd.common --- .../java/org/innercore/icstd/common/AABB.java | 15 +++ .../org/innercore/icstd/common/BlockPos.java | 66 +++++++++ .../icstd/common/BlockPosAndSide.java | 45 +++++++ .../org/innercore/icstd/common/ChunkAABB.java | 33 +++++ .../org/innercore/icstd/common/ChunkPos.java | 38 ++++++ .../org/innercore/icstd/common/Vec3f.java | 43 ++++++ .../org/innercore/icstd/common/Vec3i.java | 48 +++++++ .../common/script/GenericScriptPos3.java | 127 ++++++++++++++++++ 8 files changed, 415 insertions(+) create mode 100644 src/main/java/org/innercore/icstd/common/AABB.java create mode 100644 src/main/java/org/innercore/icstd/common/BlockPos.java create mode 100644 src/main/java/org/innercore/icstd/common/BlockPosAndSide.java create mode 100644 src/main/java/org/innercore/icstd/common/ChunkAABB.java create mode 100644 src/main/java/org/innercore/icstd/common/ChunkPos.java create mode 100644 src/main/java/org/innercore/icstd/common/Vec3f.java create mode 100644 src/main/java/org/innercore/icstd/common/Vec3i.java create mode 100644 src/main/java/org/innercore/icstd/common/script/GenericScriptPos3.java 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; + } +} From 7b3cfef9b92826145a6701270f007e62290d30cd Mon Sep 17 00:00:00 2001 From: Eugene Smirnov Date: Mon, 22 Jun 2026 18:59:24 +0300 Subject: [PATCH 5/6] ScriptableModuleInstanceImpl: support direct .put calls also implement getIds() --- .../util/ScriptableModuleInstanceImpl.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) 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 index b30cfa69f..7d17a3986 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleInstanceImpl.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleInstanceImpl.java @@ -2,8 +2,12 @@ import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; +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 @@ -31,16 +35,20 @@ public Object get(int index, Scriptable start) { @Override public boolean has(String name, Scriptable start) { - return prototype.has(name, start) || (extraFields != null && extraFields.containsKey(name)); + return extraFields != null && extraFields.containsKey(name); } @Override public boolean has(int index, Scriptable start) { - return prototype.has(index, start) || (extraFields != null && extraFields.containsKey(index)); + return extraFields != null && extraFields.containsKey(index); } @Override public void put(String name, Scriptable start, Object value) { + if (prototype.has(name, start)) { // in case it is being set via "put" + ScriptableObject.putProperty(this, name, value); + return; + } if (VALIDATE_INVALID_WRITES) throw new UnsupportedOperationException("writing missing field " + name + " to " + this); if (extraFields == null) extraFields = new HashMap<>(); @@ -49,6 +57,10 @@ public void put(String name, Scriptable start, Object value) { @Override public void put(int index, Scriptable start, Object value) { + if (prototype.has(index, start)) { // in case it is being set via "put" + ScriptableObject.putProperty(this, index, value); + return; + } if (VALIDATE_INVALID_WRITES) throw new UnsupportedOperationException("writing missing field " + index + " to " + this); if (extraFields == null) extraFields = new HashMap<>(); @@ -91,7 +103,15 @@ public void setParentScope(Scriptable parent) { @Override public Object[] getIds() { - return prototype.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 From 5b7d7fc2a622086cf7115e094feeab4c41f5794d Mon Sep 17 00:00:00 2001 From: Eugene Smirnov Date: Mon, 22 Jun 2026 20:05:36 +0300 Subject: [PATCH 6/6] ScriptableModuleInstanceImpl: reworked .get/put/has() to be fully consistent: Now get/put/has and ScriptableObject.get/put/hasProperty on the ScriptableModuleInstanceImpl object will be equivalent to each other 1. has() will return true for bound methods, fields and extra fields, added in prototype 2. get() will return methods or fields from prototype, but if they are overridden in this object, will prioritize those 3. put() will - write into java field bindings directly via new ScriptableObject.getRawSetter - override java method bindings for this object only (if VALIDATE_INVALID_WRITES = false) - add/override extra fields (if VALIDATE_INVALID_WRITES = false) --- .../util/ScriptableModuleInstanceImpl.java | 40 ++++++++++--------- .../mozilla/javascript/ScriptableObject.java | 10 +++++ 2 files changed, 32 insertions(+), 18 deletions(-) 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 index 7d17a3986..f47a2aa11 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleInstanceImpl.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/mod/util/ScriptableModuleInstanceImpl.java @@ -1,54 +1,55 @@ 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.Arrays; import java.util.HashMap; -import java.util.HashSet; 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 Scriptable prototype; + 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) { - Object o = prototype.get(name, start); - if (o == Scriptable.NOT_FOUND && extraFields != null && extraFields.containsKey(name)) - o = extraFields.get(name); - return o; + if (extraFields != null && extraFields.containsKey(name)) + return extraFields.get(name); + return prototype.get(name, start); } @Override public Object get(int index, Scriptable start) { - Object o = prototype.get(index, start); - if (o == Scriptable.NOT_FOUND && extraFields != null && extraFields.containsKey(index)) - o = extraFields.get(index); - return o; + if (extraFields != null && extraFields.containsKey(index)) + return extraFields.get(index); + return prototype.get(index, start); } @Override public boolean has(String name, Scriptable start) { - return extraFields != null && extraFields.containsKey(name); + return prototype.has(name, start) || extraFields != null && extraFields.containsKey(name); } @Override public boolean has(int index, Scriptable start) { - return extraFields != null && extraFields.containsKey(index); + return prototype.has(index, start) || extraFields != null && extraFields.containsKey(index); } @Override public void put(String name, Scriptable start, Object value) { - if (prototype.has(name, start)) { // in case it is being set via "put" - ScriptableObject.putProperty(this, name, 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<>(); @@ -57,8 +58,9 @@ public void put(String name, Scriptable start, Object value) { @Override public void put(int index, Scriptable start, Object value) { - if (prototype.has(index, start)) { // in case it is being set via "put" - ScriptableObject.putProperty(this, index, 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); @@ -88,7 +90,9 @@ public Scriptable getPrototype() { @Override public void setPrototype(Scriptable prototype) { - this.prototype = prototype; + if (this.prototype != null) + throw new IllegalStateException("can't override prototype API module instance " + this); + this.prototype = (ScriptableObject) prototype; } @Override 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) {