feat: make better native wrapper API
This commit is contained in:
parent
e372941e99
commit
978ee8db79
@ -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();
|
||||
|
@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
95
src/me/topchetoeu/jscript/interop/Arguments.java
Normal file
95
src/me/topchetoeu/jscript/interop/Arguments.java
Normal file
@ -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> T get(int i, Class<T> 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> T[] slice(int i, Class<T> 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 { }
|
28
src/me/topchetoeu/jscript/interop/ExposeTarget.java
Normal file
28
src/me/topchetoeu/jscript/interop/ExposeTarget.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
9
src/me/topchetoeu/jscript/interop/ExposeType.java
Normal file
9
src/me/topchetoeu/jscript/interop/ExposeType.java
Normal file
@ -0,0 +1,9 @@
|
||||
package me.topchetoeu.jscript.interop;
|
||||
|
||||
public enum ExposeType {
|
||||
INIT,
|
||||
METHOD,
|
||||
FIELD,
|
||||
GETTER,
|
||||
SETTER,
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package me.topchetoeu.jscript.interop;
|
||||
|
||||
public enum InitType {
|
||||
CONSTRUCTOR,
|
||||
PROTOTYPE,
|
||||
NAMESPACE,
|
||||
}
|
@ -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;
|
||||
}
|
@ -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<Class<?>, FunctionValue> constructors = new HashMap<>();
|
||||
@ -18,125 +25,153 @@ public class NativeWrapperProvider implements WrappersProvider {
|
||||
private final HashMap<Class<?>, 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 actual = method.getParameterTypes();
|
||||
|
||||
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);
|
||||
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<Object, FunctionValue>();
|
||||
var setters = new HashMap<Object, FunctionValue>();
|
||||
var props = new HashSet<Object>();
|
||||
var nonProps = new HashSet<Object>();
|
||||
|
||||
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);
|
||||
|
@ -7,6 +7,6 @@ import java.lang.annotation.Target;
|
||||
|
||||
@Target({ ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NativeInit {
|
||||
InitType value();
|
||||
public @interface OnWrapperInit {
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<Overload> 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
Loading…
Reference in New Issue
Block a user