From 978ee8db795125b6b0d4717506e6242a6c188918 Mon Sep 17 00:00:00 2001 From: TopchetoEU <36534413+TopchetoEU@users.noreply.github.com> Date: Thu, 28 Dec 2023 16:55:57 +0200 Subject: [PATCH] feat: make better native wrapper API --- src/me/topchetoeu/jscript/Main.java | 10 +- .../jscript/engine/Environment.java | 28 +- .../jscript/engine/scope/GlobalScope.java | 4 +- .../jscript/engine/values/NativeFunction.java | 5 +- .../jscript/engine/values/Values.java | 15 +- .../topchetoeu/jscript/interop/Arguments.java | 95 ++++++ .../{NativeSetter.java => Expose.java} | 5 +- ...onstructor.java => ExposeConstructor.java} | 5 +- .../jscript/interop/ExposeTarget.java | 28 ++ .../jscript/interop/ExposeType.java | 9 + .../topchetoeu/jscript/interop/InitType.java | 7 - src/me/topchetoeu/jscript/interop/Native.java | 13 - .../interop/NativeWrapperProvider.java | 295 +++++++++--------- .../{NativeInit.java => OnWrapperInit.java} | 4 +- .../topchetoeu/jscript/interop/Overload.java | 70 ----- .../jscript/interop/OverloadFunction.java | 127 -------- .../{NativeGetter.java => WrapperName.java} | 7 +- 17 files changed, 323 insertions(+), 404 deletions(-) create mode 100644 src/me/topchetoeu/jscript/interop/Arguments.java rename src/me/topchetoeu/jscript/interop/{NativeSetter.java => Expose.java} (68%) rename src/me/topchetoeu/jscript/interop/{NativeConstructor.java => ExposeConstructor.java} (76%) create mode 100644 src/me/topchetoeu/jscript/interop/ExposeTarget.java create mode 100644 src/me/topchetoeu/jscript/interop/ExposeType.java delete mode 100644 src/me/topchetoeu/jscript/interop/InitType.java delete mode 100644 src/me/topchetoeu/jscript/interop/Native.java rename src/me/topchetoeu/jscript/interop/{NativeInit.java => OnWrapperInit.java} (83%) delete mode 100644 src/me/topchetoeu/jscript/interop/Overload.java delete mode 100644 src/me/topchetoeu/jscript/interop/OverloadFunction.java rename src/me/topchetoeu/jscript/interop/{NativeGetter.java => WrapperName.java} (62%) diff --git a/src/me/topchetoeu/jscript/Main.java b/src/me/topchetoeu/jscript/Main.java index a1aa22b..cfa303b 100644 --- a/src/me/topchetoeu/jscript/Main.java +++ b/src/me/topchetoeu/jscript/Main.java @@ -94,15 +94,15 @@ public class Main { private static void initEnv() { environment = Internals.apply(environment); - environment.global.define(false, new NativeFunction("exit", (_ctx, th, args) -> { + environment.global.define(false, new NativeFunction("exit", args -> { exited = true; throw new InterruptException(); })); - environment.global.define(false, new NativeFunction("go", (_ctx, th, args) -> { + environment.global.define(false, new NativeFunction("go", args -> { try { var f = Path.of("do.js"); - var func = _ctx.compile(new Filename("do", "do/" + j++ + ".js"), new String(Files.readAllBytes(f))); - return func.call(_ctx); + var func = args.ctx.compile(new Filename("do", "do/" + j++ + ".js"), new String(Files.readAllBytes(f))); + return func.call(args.ctx); } catch (IOException e) { throw new EngineException("Couldn't open do.js"); @@ -119,7 +119,7 @@ public class Main { } private static void initEngine() { var ctx = new DebugContext(); - // engine.globalEnvironment.add(DebugContext.ENV_KEY, ctx); + engine.globalEnvironment.add(DebugContext.ENV_KEY, ctx); debugServer.targets.put("target", (ws, req) -> new SimpleDebugger(ws).attach(ctx)); engineTask = engine.start(); diff --git a/src/me/topchetoeu/jscript/engine/Environment.java b/src/me/topchetoeu/jscript/engine/Environment.java index 4a0c0a5..a8c9cf5 100644 --- a/src/me/topchetoeu/jscript/engine/Environment.java +++ b/src/me/topchetoeu/jscript/engine/Environment.java @@ -5,6 +5,7 @@ import java.util.stream.Collectors; import me.topchetoeu.jscript.Filename; import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.engine.debug.DebugContext; import me.topchetoeu.jscript.engine.scope.GlobalScope; import me.topchetoeu.jscript.engine.values.ArrayValue; import me.topchetoeu.jscript.engine.values.FunctionValue; @@ -16,7 +17,6 @@ import me.topchetoeu.jscript.exceptions.EngineException; import me.topchetoeu.jscript.interop.NativeWrapperProvider; import me.topchetoeu.jscript.parsing.Parsing; -// TODO: Remove hardcoded extensions form environment @SuppressWarnings("unchecked") public class Environment implements Extensions { @@ -64,31 +64,31 @@ public class Environment implements Extensions { } public static FunctionValue compileFunc(Extensions ext) { - return ext.init(COMPILE_FUNC, new NativeFunction("compile", (ctx, thisArg, args) -> { - var source = Values.toString(ctx, args[0]); - var filename = Values.toString(ctx, args[1]); - var isDebug = Values.toBoolean(args[2]); - - var env = Values.wrapper(Values.getMember(ctx, args[2], Symbol.get("env")), Environment.class); + return ext.init(COMPILE_FUNC, new NativeFunction("compile", args -> { + var source = args.getString(0); + var filename = args.getString(1); + var env = Values.wrapper(args.get(2, ObjectValue.class).getMember(args.ctx, Symbol.get("env")), Environment.class); + var isDebug = args.ctx.has(DebugContext.ENV_KEY); var res = new ObjectValue(); var target = Parsing.compile(env, Filename.parse(filename), source); Engine.functions.putAll(target.functions); Engine.functions.remove(0l); - res.defineProperty(ctx, "function", target.func(env)); - res.defineProperty(ctx, "mapChain", new ArrayValue()); + res.defineProperty(args.ctx, "function", target.func(env)); + res.defineProperty(args.ctx, "mapChain", new ArrayValue()); - if (isDebug) { - res.defineProperty(ctx, "breakpoints", ArrayValue.of(ctx, target.breakpoints.stream().map(Location::toString).collect(Collectors.toList()))); - } + if (isDebug) res.defineProperty( + args.ctx, "breakpoints", + ArrayValue.of(args.ctx, target.breakpoints.stream().map(Location::toString).collect(Collectors.toList())) + ); return res; })); } public static FunctionValue regexConstructor(Extensions ext) { - return ext.init(COMPILE_FUNC, new NativeFunction("RegExp", (ctx, thisArg, args) -> { - throw EngineException.ofError("Regular expressions not supported.").setCtx(ctx.environment, ctx.engine); + return ext.init(COMPILE_FUNC, new NativeFunction("RegExp", args -> { + throw EngineException.ofError("Regular expressions not supported.").setCtx(args.ctx.environment, args.ctx.engine); })); } diff --git a/src/me/topchetoeu/jscript/engine/scope/GlobalScope.java b/src/me/topchetoeu/jscript/engine/scope/GlobalScope.java index 3590362..4d78e3a 100644 --- a/src/me/topchetoeu/jscript/engine/scope/GlobalScope.java +++ b/src/me/topchetoeu/jscript/engine/scope/GlobalScope.java @@ -35,8 +35,8 @@ public class GlobalScope implements ScopeRecord { } public void define(String name, Variable val) { obj.defineProperty(null, name, - new NativeFunction("get " + name, (ctx, th, a) -> val.get(ctx)), - new NativeFunction("set " + name, (ctx, th, args) -> { val.set(ctx, args.length > 0 ? args[0] : null); return null; }), + new NativeFunction("get " + name, args -> val.get(args.ctx)), + new NativeFunction("set " + name, args -> { val.set(args.ctx, args.get(0)); return null; }), true, true ); } diff --git a/src/me/topchetoeu/jscript/engine/values/NativeFunction.java b/src/me/topchetoeu/jscript/engine/values/NativeFunction.java index 64bd502..f2cf2a1 100644 --- a/src/me/topchetoeu/jscript/engine/values/NativeFunction.java +++ b/src/me/topchetoeu/jscript/engine/values/NativeFunction.java @@ -1,17 +1,18 @@ package me.topchetoeu.jscript.engine.values; import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.interop.Arguments; public class NativeFunction extends FunctionValue { public static interface NativeFunctionRunner { - Object run(Context ctx, Object thisArg, Object[] args); + Object run(Arguments args); } public final NativeFunctionRunner action; @Override public Object call(Context ctx, Object thisArg, Object ...args) { - return action.run(ctx, thisArg, args); + return action.run(new Arguments(ctx, thisArg, args)); } public NativeFunction(String name, NativeFunctionRunner action) { diff --git a/src/me/topchetoeu/jscript/engine/values/Values.java b/src/me/topchetoeu/jscript/engine/values/Values.java index 7107513..4e94819 100644 --- a/src/me/topchetoeu/jscript/engine/values/Values.java +++ b/src/me/topchetoeu/jscript/engine/values/Values.java @@ -523,6 +523,9 @@ public class Values { ); return (T)res; } + if (clazz.isAssignableFrom(NativeWrapper.class)) { + return (T)new NativeWrapper(obj); + } if (clazz == String.class) return (T)toString(ctx, obj); if (clazz == Boolean.class || clazz == Boolean.TYPE) return (T)(Boolean)toBoolean(obj); @@ -606,15 +609,15 @@ public class Values { try { var key = getMember(ctx, getMember(ctx, ctx.get(Environment.SYMBOL_PROTO), "constructor"), "iterator"); - res.defineProperty(ctx, key, new NativeFunction("", (_ctx, thisArg, args) -> thisArg)); + res.defineProperty(ctx, key, new NativeFunction("", args -> args.thisArg)); } catch (IllegalArgumentException | NullPointerException e) { } - res.defineProperty(ctx, "next", new NativeFunction("", (_ctx, _th, _args) -> { + res.defineProperty(ctx, "next", new NativeFunction("", args -> { if (!it.hasNext()) return new ObjectValue(ctx, Map.of("done", true)); else { var obj = new ObjectValue(); - obj.defineProperty(_ctx, "value", it.next()); + obj.defineProperty(args.ctx, "value", it.next()); return obj; } })); @@ -631,16 +634,16 @@ public class Values { try { var key = getMemberPath(ctx, ctx.get(Environment.SYMBOL_PROTO), "constructor", "asyncIterator"); - res.defineProperty(ctx, key, new NativeFunction("", (_ctx, thisArg, args) -> thisArg)); + res.defineProperty(ctx, key, new NativeFunction("", args -> args.thisArg)); } catch (IllegalArgumentException | NullPointerException e) { } - res.defineProperty(ctx, "next", new NativeFunction("", (_ctx, _th, _args) -> { + res.defineProperty(ctx, "next", new NativeFunction("", args -> { return PromiseLib.await(ctx, () -> { if (!it.hasNext()) return new ObjectValue(ctx, Map.of("done", true)); else { var obj = new ObjectValue(); - obj.defineProperty(_ctx, "value", it.next()); + obj.defineProperty(args.ctx, "value", it.next()); return obj; } }); diff --git a/src/me/topchetoeu/jscript/interop/Arguments.java b/src/me/topchetoeu/jscript/interop/Arguments.java new file mode 100644 index 0000000..796e96a --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/Arguments.java @@ -0,0 +1,95 @@ +package me.topchetoeu.jscript.interop; + +import java.lang.reflect.Array; + +import me.topchetoeu.jscript.engine.Context; +import me.topchetoeu.jscript.engine.values.NativeWrapper; +import me.topchetoeu.jscript.engine.values.Values; + +public class Arguments { + public final Object thisArg; + public final Object[] args; + public final Context ctx; + + public T get(int i, Class type) { + return Values.convert(ctx, get(i), type); + } + public Object get(int i, boolean unwrap) { + Object res = null; + + if (i == -1) res = thisArg; + if (i >= 0 && i < args.length) res = args[i]; + if (unwrap && res instanceof NativeWrapper) res = ((NativeWrapper)res).wrapped; + + return res; + } + public Object get(int i) { + return get(i, false); + } + + @SuppressWarnings("unchecked") + public T[] slice(int i, Class type) { + var res = Array.newInstance(type, Math.max(0, args.length - i)); + for (; i < args.length; i++) Array.set(res, i - args.length, get(i, type)); + return ((T[])res); + } + public Object slice(int i, boolean unwrap) { + var res = new Object[Math.max(0, args.length - i)]; + for (; i < args.length; i++) res[i - args.length] = get(i, unwrap); + return res; + } + public Object slice(int i) { + return slice(i, false); + } + + public int[] sliceInt(int i) { + var res = new int[Math.max(0, args.length - i)]; + for (; i < args.length; i++) res[i - args.length] = get(i, Integer.class); + return res; + } + public long[] sliceLong(int i) { + var res = new long[Math.max(0, args.length - i)]; + for (; i < args.length; i++) res[i - args.length] = get(i, Long.class); + return res; + } + public short[] sliceShort(int i) { + var res = new short[Math.max(0, args.length - i)]; + for (; i < args.length; i++) res[i - args.length] = get(i, Short.class); + return res; + } + public float[] sliceFloat(int i) { + var res = new float[Math.max(0, args.length - i)]; + for (; i < args.length; i++) res[i - args.length] = get(i, Float.class); + return res; + } + public double[] sliceDouble(int i) { + var res = new double[Math.max(0, args.length - i)]; + for (; i < args.length; i++) res[i - args.length] = get(i, Double.class); + return res; + } + public byte[] sliceByte(int i) { + var res = new byte[Math.max(0, args.length - i)]; + for (; i < args.length; i++) res[i - args.length] = get(i, Byte.class); + return res; + } + public char[] sliceChar(int i) { + var res = new char[Math.max(0, args.length - i)]; + for (; i < args.length; i++) res[i - args.length] = get(i, Character.class); + return res; + } + public boolean[] sliceBool(int i) { + var res = new boolean[Math.max(0, args.length - i)]; + for (; i < args.length; i++) res[i - args.length] = get(i, Boolean.class); + return res; + } + + public String getString(int i) { return Values.toString(ctx, get(i)); } + public boolean getBoolean(int i) { return Values.toBoolean(get(i)); } + public int getInt(int i) { return (int)Values.toNumber(ctx, get(i)); } + + public Arguments(Context ctx, Object thisArg, Object... args) { + this.ctx = ctx; + this.args = args; + this.thisArg = thisArg; + } +} diff --git a/src/me/topchetoeu/jscript/interop/NativeSetter.java b/src/me/topchetoeu/jscript/interop/Expose.java similarity index 68% rename from src/me/topchetoeu/jscript/interop/NativeSetter.java rename to src/me/topchetoeu/jscript/interop/Expose.java index f74c7a7..c9cf82b 100644 --- a/src/me/topchetoeu/jscript/interop/NativeSetter.java +++ b/src/me/topchetoeu/jscript/interop/Expose.java @@ -7,7 +7,8 @@ import java.lang.annotation.Target; @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) -public @interface NativeSetter { +public @interface Expose { public String value() default ""; - public boolean thisArg() default false; + public ExposeType type() default ExposeType.METHOD; + public ExposeTarget target() default ExposeTarget.MEMBER; } diff --git a/src/me/topchetoeu/jscript/interop/NativeConstructor.java b/src/me/topchetoeu/jscript/interop/ExposeConstructor.java similarity index 76% rename from src/me/topchetoeu/jscript/interop/NativeConstructor.java rename to src/me/topchetoeu/jscript/interop/ExposeConstructor.java index e9aa462..8c9d8fa 100644 --- a/src/me/topchetoeu/jscript/interop/NativeConstructor.java +++ b/src/me/topchetoeu/jscript/interop/ExposeConstructor.java @@ -7,7 +7,4 @@ import java.lang.annotation.Target; @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) -public @interface NativeConstructor { - public boolean thisArg() default false; -} - +public @interface ExposeConstructor { } diff --git a/src/me/topchetoeu/jscript/interop/ExposeTarget.java b/src/me/topchetoeu/jscript/interop/ExposeTarget.java new file mode 100644 index 0000000..66f2e32 --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/ExposeTarget.java @@ -0,0 +1,28 @@ +package me.topchetoeu.jscript.interop; + +public enum ExposeTarget { + STATIC(true, true, false), + MEMBER(false, false, true), + NAMESPACE(false, true, false), + CONSTRUCTOR(true, false, false), + PROTOTYPE(false, false, true), + ALL(true, true, true); + + public final boolean constructor; + public final boolean namespace; + public final boolean prototype; + + public boolean shouldApply(ExposeTarget other) { + if (other.constructor && !constructor) return false; + if (other.namespace && !namespace) return false; + if (other.prototype && !prototype) return false; + + return true; + } + + private ExposeTarget(boolean constructor, boolean namespace, boolean prototype) { + this.constructor = constructor; + this.namespace = namespace; + this.prototype = prototype; + } +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/interop/ExposeType.java b/src/me/topchetoeu/jscript/interop/ExposeType.java new file mode 100644 index 0000000..1b48f6a --- /dev/null +++ b/src/me/topchetoeu/jscript/interop/ExposeType.java @@ -0,0 +1,9 @@ +package me.topchetoeu.jscript.interop; + +public enum ExposeType { + INIT, + METHOD, + FIELD, + GETTER, + SETTER, +} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/interop/InitType.java b/src/me/topchetoeu/jscript/interop/InitType.java deleted file mode 100644 index 89793f8..0000000 --- a/src/me/topchetoeu/jscript/interop/InitType.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.topchetoeu.jscript.interop; - -public enum InitType { - CONSTRUCTOR, - PROTOTYPE, - NAMESPACE, -} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/interop/Native.java b/src/me/topchetoeu/jscript/interop/Native.java deleted file mode 100644 index 0448c2d..0000000 --- a/src/me/topchetoeu/jscript/interop/Native.java +++ /dev/null @@ -1,13 +0,0 @@ -package me.topchetoeu.jscript.interop; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -public @interface Native { - public String value() default ""; - public boolean thisArg() default false; -} diff --git a/src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java b/src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java index 4aa409f..7544adf 100644 --- a/src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java +++ b/src/me/topchetoeu/jscript/interop/NativeWrapperProvider.java @@ -1,16 +1,23 @@ package me.topchetoeu.jscript.interop; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; +import me.topchetoeu.jscript.Location; +import me.topchetoeu.jscript.engine.Context; import me.topchetoeu.jscript.engine.Environment; import me.topchetoeu.jscript.engine.WrappersProvider; import me.topchetoeu.jscript.engine.values.FunctionValue; import me.topchetoeu.jscript.engine.values.NativeFunction; import me.topchetoeu.jscript.engine.values.ObjectValue; import me.topchetoeu.jscript.engine.values.Symbol; +import me.topchetoeu.jscript.engine.values.Values; import me.topchetoeu.jscript.exceptions.EngineException; -import me.topchetoeu.jscript.exceptions.UncheckedException; +import me.topchetoeu.jscript.exceptions.InterruptException; public class NativeWrapperProvider implements WrappersProvider { private final HashMap, FunctionValue> constructors = new HashMap<>(); @@ -18,125 +25,153 @@ public class NativeWrapperProvider implements WrappersProvider { private final HashMap, ObjectValue> namespaces = new HashMap<>(); private final Environment env; - private static void applyMethods(Environment env, boolean member, ObjectValue target, Class clazz) { - for (var method : clazz.getDeclaredMethods()) { - var nat = method.getAnnotation(Native.class); - var get = method.getAnnotation(NativeGetter.class); - var set = method.getAnnotation(NativeSetter.class); - var memberMatch = !Modifier.isStatic(method.getModifiers()) == member; - - if (nat != null) { - if (nat.thisArg() && !member || !nat.thisArg() && !memberMatch) continue; - - Object name = nat.value(); - if (name.toString().startsWith("@@")) name = Symbol.get(name.toString().substring(2)); - else if (name.equals("")) name = method.getName(); - - var val = target.values.get(name); - - if (!(val instanceof OverloadFunction)) target.defineProperty(null, name, val = new OverloadFunction(name.toString()), true, true, false); - - ((OverloadFunction)val).add(Overload.fromMethod(method, nat.thisArg())); + private static Object call(Context ctx, String name, Method method, Object thisArg, Object... args) { + try { + var realArgs = new Object[method.getParameterCount()]; + System.arraycopy(args, 0, realArgs, 0, realArgs.length); + if (Modifier.isStatic(method.getModifiers())) thisArg = null; + return Values.normalize(ctx, method.invoke(Values.convert(ctx, thisArg, method.getDeclaringClass()), realArgs)); + } + catch (InvocationTargetException e) { + if (e.getTargetException() instanceof EngineException) { + throw ((EngineException)e.getTargetException()).add(ctx, name, Location.INTERNAL); } - else { - if (get != null) { - if (get.thisArg() && !member || !get.thisArg() && !memberMatch) continue; - - Object name = get.value(); - if (name.toString().startsWith("@@")) name = Symbol.get(name.toString().substring(2)); - else if (name.equals("")) name = method.getName(); - - var prop = target.properties.get(name); - OverloadFunction getter = null; - var setter = prop == null ? null : prop.setter; - - if (prop != null && prop.getter instanceof OverloadFunction) getter = (OverloadFunction)prop.getter; - else getter = new OverloadFunction("get " + name); - - getter.add(Overload.fromMethod(method, get.thisArg())); - target.defineProperty(null, name, getter, setter, true, false); - } - if (set != null) { - if (set.thisArg() && !member || !set.thisArg() && !memberMatch) continue; - - Object name = set.value(); - if (name.toString().startsWith("@@")) name = Symbol.get(name.toString().substring(2)); - else if (name.equals("")) name = method.getName(); - - var prop = target.properties.get(name); - var getter = prop == null ? null : prop.getter; - OverloadFunction setter = null; - - if (prop != null && prop.setter instanceof OverloadFunction) setter = (OverloadFunction)prop.setter; - else setter = new OverloadFunction("set " + name); - - setter.add(Overload.fromMethod(method, set.thisArg())); - target.defineProperty(null, name, getter, setter, true, false); - } + else if (e.getTargetException() instanceof NullPointerException) { + e.printStackTrace(); + throw EngineException.ofType("Unexpected value of 'undefined'.").add(ctx, name, Location.INTERNAL); } + else if (e.getTargetException() instanceof InterruptException || e.getTargetException() instanceof InterruptedException) { + throw new InterruptException(); + } + else throw new EngineException(e.getTargetException()).add(ctx, name, Location.INTERNAL); + } + catch (ReflectiveOperationException e) { + throw EngineException.ofError(e.getMessage()).add(ctx, name, Location.INTERNAL); } } - private static void applyFields(Environment env, boolean member, ObjectValue target, Class clazz) { - for (var field : clazz.getDeclaredFields()) { - if (!Modifier.isStatic(field.getModifiers()) != member) continue; - var nat = field.getAnnotation(Native.class); + private static FunctionValue create(String name, Method method) { + return new NativeFunction(name, args -> call(args.ctx, name, method, args.thisArg, args)); + } + private static void checkSignature(Method method, boolean forceStatic, Class ...params) { + if (forceStatic && !Modifier.isStatic(method.getModifiers())) throw new IllegalArgumentException(String.format( + "Method %s must be static.", + method.getDeclaringClass().getName() + "." + method.getName() + )); - if (nat != null) { - Object name = nat.value(); - if (name.toString().startsWith("@@")) name = Symbol.get(name.toString().substring(2)); - else if (name.equals("")) name = field.getName(); - - var getter = OverloadFunction.of("get " + name, Overload.getterFromField(field)); - var setter = OverloadFunction.of("set " + name, Overload.setterFromField(field)); - target.defineProperty(null, name, getter, setter, true, false); + var actual = method.getParameterTypes(); + + boolean failed = actual.length > params.length; + + if (!failed) for (var i = 0; i < actual.length; i++) { + if (!actual[i].isAssignableFrom(params[i])) { + failed = true; + break; } } + + if (failed) throw new IllegalArgumentException(String.format( + "Method %s was expected to have a signature of '%s', found '%s' instead.", + method.getDeclaringClass().getName() + "." + method.getName(), + String.join(", ", Arrays.stream(params).map(v -> v.getName()).toList()), + String.join(", ", Arrays.stream(actual).map(v -> v.getName()).toList()) + )); } - private static void applyClasses(Environment env, boolean member, ObjectValue target, Class clazz) { - for (var cl : clazz.getDeclaredClasses()) { - if (!Modifier.isStatic(cl.getModifiers()) != member) continue; - var nat = cl.getAnnotation(Native.class); - - if (nat != null) { - Object name = nat.value(); - if (name.toString().startsWith("@@")) name = Symbol.get(name.toString().substring(2)); - else if (name.equals("")) name = cl.getName(); - - var getter = new NativeFunction("get " + name, (ctx, thisArg, args) -> cl); - - target.defineProperty(null, name, getter, null, true, false); - } - } - } - - public static String getName(Class clazz) { - var classNat = clazz.getAnnotation(Native.class); + private static String getName(Class clazz) { + var classNat = clazz.getAnnotation(WrapperName.class); if (classNat != null && !classNat.value().trim().equals("")) return classNat.value().trim(); else return clazz.getSimpleName(); } + private static void apply(ObjectValue obj, Environment env, ExposeTarget target, Class clazz) { + var getters = new HashMap(); + var setters = new HashMap(); + var props = new HashSet(); + var nonProps = new HashSet(); + + for (var method : clazz.getDeclaredMethods()) { + for (var annotation : method.getAnnotationsByType(Expose.class)) { + if (!annotation.target().shouldApply(target)) continue; + + Object key = annotation.value(); + if (key.toString().startsWith("@@")) key = Symbol.get(key.toString().substring(2)); + else if (key.equals("")) key = method.getName(); + var name = key.toString(); + + var repeat = false; + + switch (annotation.type()) { + case INIT: + checkSignature(method, true, + target == ExposeTarget.CONSTRUCTOR ? FunctionValue.class : ObjectValue.class, + Environment.class + ); + call(null, null, method, obj, null, env); + break; + case FIELD: + if (props.contains(key) || nonProps.contains(key)) repeat = true; + else { + checkSignature(method, true, Environment.class); + obj.defineProperty(null, key, call(new Context(null, env), name, method, null, env)); + nonProps.add(key); + } + break; + case METHOD: + if (props.contains(key) || nonProps.contains(key)) repeat = true; + else { + checkSignature(method, false, Arguments.class); + obj.defineProperty(null, key, create(name, method)); + nonProps.add(key); + } + break; + case GETTER: + if (nonProps.contains(key) || getters.containsKey(key)) repeat = true; + else { + checkSignature(method, false, Arguments.class); + getters.put(key, create(name, method)); + props.add(key); + } + break; + case SETTER: + if (nonProps.contains(key) || setters.containsKey(key)) repeat = true; + else { + checkSignature(method, false, Arguments.class); + setters.put(key, create(name, method)); + props.add(key); + } + break; + } + + if (repeat) + throw new IllegalArgumentException(String.format( + "A member '%s' in the wrapper for '%s' of type '%s' is already present.", + name, clazz.getName(), target.toString() + )); + } + } + + for (var key : props) obj.defineProperty(null, key, getters.get(key), setters.get(key), true, true); + } + + private static Method getConstructor(Environment env, Class clazz) { + for (var method : clazz.getDeclaredMethods()) { + if (!method.isAnnotationPresent(ExposeConstructor.class)) continue; + checkSignature(method, true, Arguments.class); + return method; + } + + return null; + } + /** * Generates a prototype for the given class. * The returned object will have appropriate wrappers for all instance members. * All accessors and methods will expect the this argument to be a native wrapper of the given class type. * @param clazz The class for which a prototype should be generated */ - public static ObjectValue makeProto(Environment ctx, Class clazz) { + public static ObjectValue makeProto(Environment env, Class clazz) { var res = new ObjectValue(); - res.defineProperty(null, Symbol.get("Symbol.typeName"), getName(clazz)); - - for (var overload : clazz.getDeclaredMethods()) { - var init = overload.getAnnotation(NativeInit.class); - if (init == null || init.value() != InitType.PROTOTYPE) continue; - try { overload.invoke(null, ctx, res); } - catch (Throwable e) { throw new UncheckedException(e); } - } - - applyMethods(ctx, true, res, clazz); - applyFields(ctx, true, res, clazz); - applyClasses(ctx, true, res, clazz); - + apply(res, env, ExposeTarget.PROTOTYPE, clazz); return res; } /** @@ -146,36 +181,15 @@ public class NativeWrapperProvider implements WrappersProvider { * @param clazz The class for which a constructor should be generated */ public static FunctionValue makeConstructor(Environment ctx, Class clazz) { - FunctionValue func = new OverloadFunction(getName(clazz)); + var constr = getConstructor(ctx, clazz); - for (var overload : clazz.getDeclaredConstructors()) { - var nat = overload.getAnnotation(Native.class); - if (nat == null) continue; - ((OverloadFunction)func).add(Overload.fromConstructor(overload, nat.thisArg())); - } - for (var overload : clazz.getDeclaredMethods()) { - var constr = overload.getAnnotation(NativeConstructor.class); - if (constr == null) continue; - ((OverloadFunction)func).add(Overload.fromMethod(overload, constr.thisArg())); - } - for (var overload : clazz.getDeclaredMethods()) { - var init = overload.getAnnotation(NativeInit.class); - if (init == null || init.value() != InitType.CONSTRUCTOR) continue; - try { overload.invoke(null, ctx, func); } - catch (Throwable e) { throw new UncheckedException(e); } - } + FunctionValue res = constr == null ? + new NativeFunction(getName(clazz), args -> { throw EngineException.ofError("This constructor is not invokable."); }) : + create(getName(clazz), constr); - if (((OverloadFunction)func).overloads.size() == 0) { - func = new NativeFunction(getName(clazz), (a, b, c) -> { throw EngineException.ofError("This constructor is not invokable."); }); - } + apply(res, ctx, ExposeTarget.CONSTRUCTOR, clazz); - applyMethods(ctx, false, func, clazz); - applyFields(ctx, false, func, clazz); - applyClasses(ctx, false, func, clazz); - - func.special = true; - - return func; + return res; } /** * Generates a namespace for the given class. @@ -184,19 +198,9 @@ public class NativeWrapperProvider implements WrappersProvider { * @param clazz The class for which a constructor should be generated */ public static ObjectValue makeNamespace(Environment ctx, Class clazz) { - ObjectValue res = new ObjectValue(); - - for (var overload : clazz.getDeclaredMethods()) { - var init = overload.getAnnotation(NativeInit.class); - if (init == null || init.value() != InitType.NAMESPACE) continue; - try { overload.invoke(null, ctx, res); } - catch (Throwable e) { throw new UncheckedException(e); } - } - - applyMethods(ctx, false, res, clazz); - applyFields(ctx, false, res, clazz); - applyClasses(ctx, false, res, clazz); - + var res = new ObjectValue(); + res.defineProperty(null, Symbol.get("Symbol.typeName"), getName(clazz)); + apply(res, ctx, ExposeTarget.NAMESPACE, clazz); return res; } @@ -249,8 +253,7 @@ public class NativeWrapperProvider implements WrappersProvider { return constructors.get(clazz); } - @Override - public WrappersProvider fork(Environment env) { + @Override public WrappersProvider fork(Environment env) { return new NativeWrapperProvider(env); } @@ -263,12 +266,12 @@ public class NativeWrapperProvider implements WrappersProvider { private void initError() { var proto = new ObjectValue(); - proto.defineProperty(null, "message", new NativeFunction("message", (ctx, thisArg, args) -> { - if (thisArg instanceof Throwable) return ((Throwable)thisArg).getMessage(); + proto.defineProperty(null, "message", new NativeFunction("message", args -> { + if (args.thisArg instanceof Throwable) return ((Throwable)args.thisArg).getMessage(); else return null; })); - proto.defineProperty(null, "name", new NativeFunction("name", (ctx, thisArg, args) -> getName(thisArg.getClass()))); - proto.defineProperty(null, "toString", new NativeFunction("toString", (ctx, thisArg, args) -> thisArg.toString())); + proto.defineProperty(null, "name", new NativeFunction("name", args -> getName(args.thisArg.getClass()))); + proto.defineProperty(null, "toString", new NativeFunction("toString", args -> args.thisArg.toString())); var constr = makeConstructor(null, Throwable.class); proto.defineProperty(null, "constructor", constr, true, false, false); diff --git a/src/me/topchetoeu/jscript/interop/NativeInit.java b/src/me/topchetoeu/jscript/interop/OnWrapperInit.java similarity index 83% rename from src/me/topchetoeu/jscript/interop/NativeInit.java rename to src/me/topchetoeu/jscript/interop/OnWrapperInit.java index 66d13fe..ab75db3 100644 --- a/src/me/topchetoeu/jscript/interop/NativeInit.java +++ b/src/me/topchetoeu/jscript/interop/OnWrapperInit.java @@ -7,6 +7,6 @@ import java.lang.annotation.Target; @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) -public @interface NativeInit { - InitType value(); +public @interface OnWrapperInit { + } diff --git a/src/me/topchetoeu/jscript/interop/Overload.java b/src/me/topchetoeu/jscript/interop/Overload.java deleted file mode 100644 index 34871e0..0000000 --- a/src/me/topchetoeu/jscript/interop/Overload.java +++ /dev/null @@ -1,70 +0,0 @@ -package me.topchetoeu.jscript.interop; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -import me.topchetoeu.jscript.engine.Context; - -public class Overload { - public static interface OverloadRunner { - Object run(Context ctx, Object thisArg, Object[] args) throws ReflectiveOperationException, IllegalArgumentException; - } - - public final OverloadRunner runner; - public final boolean variadic; - public final boolean passThis; - public final Class thisArg; - public final Class[] params; - - public static Overload fromMethod(Method method, boolean passThis) { - return new Overload( - (ctx, th, args) -> method.invoke(th, args), - method.isVarArgs(), passThis, - Modifier.isStatic(method.getModifiers()) ? null : method.getDeclaringClass(), - method.getParameterTypes() - ); - } - public static Overload fromConstructor(Constructor method, boolean passThis) { - return new Overload( - (ctx, th, args) -> method.newInstance(args), - method.isVarArgs(), passThis, - null, - method.getParameterTypes() - ); - } - public static Overload getterFromField(Field field) { - return new Overload( - (ctx, th, args) -> field.get(th), false, false, - Modifier.isStatic(field.getModifiers()) ? null : field.getDeclaringClass(), - new Class[0] - ); - } - public static Overload setterFromField(Field field) { - if (Modifier.isFinal(field.getModifiers())) return null; - return new Overload( - (ctx, th, args) -> { - field.set(th, args[0]); return null; - }, false, false, - Modifier.isStatic(field.getModifiers()) ? null : field.getDeclaringClass(), - new Class[] { field.getType() } - ); - } - - public static Overload getter(Class thisArg, OverloadRunner runner, boolean passThis) { - return new Overload( - (ctx, th, args) -> runner.run(ctx, th, args), false, passThis, - thisArg, - new Class[0] - ); - } - - public Overload(OverloadRunner runner, boolean variadic, boolean passThis, Class thisArg, Class args[]) { - this.runner = runner; - this.variadic = variadic; - this.passThis = passThis; - this.thisArg = thisArg; - this.params = args; - } -} \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/interop/OverloadFunction.java b/src/me/topchetoeu/jscript/interop/OverloadFunction.java deleted file mode 100644 index c896d1b..0000000 --- a/src/me/topchetoeu/jscript/interop/OverloadFunction.java +++ /dev/null @@ -1,127 +0,0 @@ -package me.topchetoeu.jscript.interop; - -import java.lang.reflect.Array; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; - -import me.topchetoeu.jscript.Location; -import me.topchetoeu.jscript.engine.Context; -import me.topchetoeu.jscript.engine.values.FunctionValue; -import me.topchetoeu.jscript.engine.values.NativeWrapper; -import me.topchetoeu.jscript.engine.values.Values; -import me.topchetoeu.jscript.exceptions.ConvertException; -import me.topchetoeu.jscript.exceptions.EngineException; -import me.topchetoeu.jscript.exceptions.InterruptException; - -public class OverloadFunction extends FunctionValue { - public final List overloads = new ArrayList<>(); - - public Object call(Context ctx, Object thisArg, Object ...args) { - loop: for (var overload : overloads) { - Object[] newArgs = new Object[overload.params.length]; - - boolean consumesEngine = overload.params.length > 0 && overload.params[0] == Context.class; - int start = (consumesEngine ? 1 : 0) + (overload.passThis ? 1 : 0); - int end = overload.params.length - (overload.variadic ? 1 : 0); - - for (var i = start; i < end; i++) { - Object val; - - if (i - start >= args.length) val = null; - else val = args[i - start]; - - try { - newArgs[i] = Values.convert(ctx, val, overload.params[i]); - } - catch (ConvertException e) { - if (overloads.size() > 1) continue loop; - else throw EngineException.ofType(String.format("Argument %d can't be converted from %s to %s", i - start, e.source, e.target)); - } - } - - if (overload.variadic) { - var type = overload.params[overload.params.length - 1].getComponentType(); - var n = Math.max(args.length - end + start, 0); - Object varArg = Array.newInstance(type, n); - - for (var i = 0; i < n; i++) { - try { - Array.set(varArg, i, Values.convert(ctx, args[i + end - start], type)); - } - catch (ConvertException e) { - if (overloads.size() > 1) continue loop; - else throw EngineException.ofType(String.format("Element in variadic argument can't be converted from %s to %s", e.source, e.target)); - } - } - - newArgs[newArgs.length - 1] = varArg; - } - - var thisArgType = overload.passThis ? overload.params[consumesEngine ? 1 : 0] : overload.thisArg; - Object _this; - - try { - _this = thisArgType == null ? null : Values.convert(ctx, thisArg, thisArgType); - } - catch (ConvertException e) { - if (overloads.size() > 1) continue loop; - else throw EngineException.ofType(String.format("This argument can't be converted from %s to %s", e.source, e.target)); - } - - if (consumesEngine) newArgs[0] = ctx; - if (overload.passThis) { - newArgs[consumesEngine ? 1 : 0] = _this; - _this = null; - } - - try { - return Values.normalize(ctx, overload.runner.run(ctx, _this, newArgs)); - } - catch (InstantiationException e) { throw EngineException.ofError("The class may not be instantiated."); } - catch (IllegalAccessException | IllegalArgumentException e) { continue; } - catch (InvocationTargetException e) { - var loc = Location.INTERNAL; - if (e.getTargetException() instanceof EngineException) { - throw ((EngineException)e.getTargetException()).add(ctx, name, loc); - } - else if (e.getTargetException() instanceof NullPointerException) { - e.printStackTrace(); - throw EngineException.ofType("Unexpected value of 'undefined'.").add(ctx, name, loc); - } - else if (e.getTargetException() instanceof InterruptException || e.getTargetException() instanceof InterruptedException) { - throw new InterruptException(); - } - else { - var target = e.getTargetException(); - var targetClass = target.getClass(); - var err = new NativeWrapper(e.getTargetException()); - - err.defineProperty(ctx, "message", target.getMessage()); - err.defineProperty(ctx, "name", NativeWrapperProvider.getName(targetClass)); - - throw new EngineException(err).add(ctx, name, loc); - } - } - catch (ReflectiveOperationException e) { - throw EngineException.ofError(e.getMessage()).add(ctx, name, Location.INTERNAL); - } - } - - throw EngineException.ofType("No overload found for native method."); - } - - public OverloadFunction add(Overload overload) { - this.overloads.add(overload); - return this; - } - - public OverloadFunction(String name) { - super(name, 0); - } - - public static OverloadFunction of(String name, Overload overload) { - if (overload == null) return null; - else return new OverloadFunction(name).add(overload); - } -} diff --git a/src/me/topchetoeu/jscript/interop/NativeGetter.java b/src/me/topchetoeu/jscript/interop/WrapperName.java similarity index 62% rename from src/me/topchetoeu/jscript/interop/NativeGetter.java rename to src/me/topchetoeu/jscript/interop/WrapperName.java index d7704c2..8898105 100644 --- a/src/me/topchetoeu/jscript/interop/NativeGetter.java +++ b/src/me/topchetoeu/jscript/interop/WrapperName.java @@ -5,9 +5,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@Target({ ElementType.METHOD }) +@Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) -public @interface NativeGetter { - public String value() default ""; - public boolean thisArg() default false; +public @interface WrapperName { + String value(); }