feat: make better native wrapper API

This commit is contained in:
TopchetoEU 2023-12-28 16:55:57 +02:00
parent e372941e99
commit 978ee8db79
Signed by: topchetoeu
GPG Key ID: 6531B8583E5F6ED4
17 changed files with 323 additions and 404 deletions

View File

@ -94,15 +94,15 @@ public class Main {
private static void initEnv() { private static void initEnv() {
environment = Internals.apply(environment); environment = Internals.apply(environment);
environment.global.define(false, new NativeFunction("exit", (_ctx, th, args) -> { environment.global.define(false, new NativeFunction("exit", args -> {
exited = true; exited = true;
throw new InterruptException(); throw new InterruptException();
})); }));
environment.global.define(false, new NativeFunction("go", (_ctx, th, args) -> { environment.global.define(false, new NativeFunction("go", args -> {
try { try {
var f = Path.of("do.js"); var f = Path.of("do.js");
var func = _ctx.compile(new Filename("do", "do/" + j++ + ".js"), new String(Files.readAllBytes(f))); var func = args.ctx.compile(new Filename("do", "do/" + j++ + ".js"), new String(Files.readAllBytes(f)));
return func.call(_ctx); return func.call(args.ctx);
} }
catch (IOException e) { catch (IOException e) {
throw new EngineException("Couldn't open do.js"); throw new EngineException("Couldn't open do.js");
@ -119,7 +119,7 @@ public class Main {
} }
private static void initEngine() { private static void initEngine() {
var ctx = new DebugContext(); 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)); debugServer.targets.put("target", (ws, req) -> new SimpleDebugger(ws).attach(ctx));
engineTask = engine.start(); engineTask = engine.start();

View File

@ -5,6 +5,7 @@ import java.util.stream.Collectors;
import me.topchetoeu.jscript.Filename; import me.topchetoeu.jscript.Filename;
import me.topchetoeu.jscript.Location; import me.topchetoeu.jscript.Location;
import me.topchetoeu.jscript.engine.debug.DebugContext;
import me.topchetoeu.jscript.engine.scope.GlobalScope; import me.topchetoeu.jscript.engine.scope.GlobalScope;
import me.topchetoeu.jscript.engine.values.ArrayValue; import me.topchetoeu.jscript.engine.values.ArrayValue;
import me.topchetoeu.jscript.engine.values.FunctionValue; 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.interop.NativeWrapperProvider;
import me.topchetoeu.jscript.parsing.Parsing; import me.topchetoeu.jscript.parsing.Parsing;
// TODO: Remove hardcoded extensions form environment
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class Environment implements Extensions { public class Environment implements Extensions {
@ -64,31 +64,31 @@ public class Environment implements Extensions {
} }
public static FunctionValue compileFunc(Extensions ext) { public static FunctionValue compileFunc(Extensions ext) {
return ext.init(COMPILE_FUNC, new NativeFunction("compile", (ctx, thisArg, args) -> { return ext.init(COMPILE_FUNC, new NativeFunction("compile", args -> {
var source = Values.toString(ctx, args[0]); var source = args.getString(0);
var filename = Values.toString(ctx, args[1]); var filename = args.getString(1);
var isDebug = Values.toBoolean(args[2]); 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 env = Values.wrapper(Values.getMember(ctx, args[2], Symbol.get("env")), Environment.class);
var res = new ObjectValue(); var res = new ObjectValue();
var target = Parsing.compile(env, Filename.parse(filename), source); var target = Parsing.compile(env, Filename.parse(filename), source);
Engine.functions.putAll(target.functions); Engine.functions.putAll(target.functions);
Engine.functions.remove(0l); Engine.functions.remove(0l);
res.defineProperty(ctx, "function", target.func(env)); res.defineProperty(args.ctx, "function", target.func(env));
res.defineProperty(ctx, "mapChain", new ArrayValue()); res.defineProperty(args.ctx, "mapChain", new ArrayValue());
if (isDebug) { if (isDebug) res.defineProperty(
res.defineProperty(ctx, "breakpoints", ArrayValue.of(ctx, target.breakpoints.stream().map(Location::toString).collect(Collectors.toList()))); args.ctx, "breakpoints",
} ArrayValue.of(args.ctx, target.breakpoints.stream().map(Location::toString).collect(Collectors.toList()))
);
return res; return res;
})); }));
} }
public static FunctionValue regexConstructor(Extensions ext) { public static FunctionValue regexConstructor(Extensions ext) {
return ext.init(COMPILE_FUNC, new NativeFunction("RegExp", (ctx, thisArg, args) -> { return ext.init(COMPILE_FUNC, new NativeFunction("RegExp", args -> {
throw EngineException.ofError("Regular expressions not supported.").setCtx(ctx.environment, ctx.engine); throw EngineException.ofError("Regular expressions not supported.").setCtx(args.ctx.environment, args.ctx.engine);
})); }));
} }

View File

@ -35,8 +35,8 @@ public class GlobalScope implements ScopeRecord {
} }
public void define(String name, Variable val) { public void define(String name, Variable val) {
obj.defineProperty(null, name, obj.defineProperty(null, name,
new NativeFunction("get " + name, (ctx, th, a) -> val.get(ctx)), new NativeFunction("get " + name, args -> val.get(args.ctx)),
new NativeFunction("set " + name, (ctx, th, args) -> { val.set(ctx, args.length > 0 ? args[0] : null); return null; }), new NativeFunction("set " + name, args -> { val.set(args.ctx, args.get(0)); return null; }),
true, true true, true
); );
} }

View File

@ -1,17 +1,18 @@
package me.topchetoeu.jscript.engine.values; package me.topchetoeu.jscript.engine.values;
import me.topchetoeu.jscript.engine.Context; import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.interop.Arguments;
public class NativeFunction extends FunctionValue { public class NativeFunction extends FunctionValue {
public static interface NativeFunctionRunner { public static interface NativeFunctionRunner {
Object run(Context ctx, Object thisArg, Object[] args); Object run(Arguments args);
} }
public final NativeFunctionRunner action; public final NativeFunctionRunner action;
@Override @Override
public Object call(Context ctx, Object thisArg, Object ...args) { 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) { public NativeFunction(String name, NativeFunctionRunner action) {

View File

@ -523,6 +523,9 @@ public class Values {
); );
return (T)res; return (T)res;
} }
if (clazz.isAssignableFrom(NativeWrapper.class)) {
return (T)new NativeWrapper(obj);
}
if (clazz == String.class) return (T)toString(ctx, obj); if (clazz == String.class) return (T)toString(ctx, obj);
if (clazz == Boolean.class || clazz == Boolean.TYPE) return (T)(Boolean)toBoolean(obj); if (clazz == Boolean.class || clazz == Boolean.TYPE) return (T)(Boolean)toBoolean(obj);
@ -606,15 +609,15 @@ public class Values {
try { try {
var key = getMember(ctx, getMember(ctx, ctx.get(Environment.SYMBOL_PROTO), "constructor"), "iterator"); 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) { } 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)); if (!it.hasNext()) return new ObjectValue(ctx, Map.of("done", true));
else { else {
var obj = new ObjectValue(); var obj = new ObjectValue();
obj.defineProperty(_ctx, "value", it.next()); obj.defineProperty(args.ctx, "value", it.next());
return obj; return obj;
} }
})); }));
@ -631,16 +634,16 @@ public class Values {
try { try {
var key = getMemberPath(ctx, ctx.get(Environment.SYMBOL_PROTO), "constructor", "asyncIterator"); 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) { } catch (IllegalArgumentException | NullPointerException e) { }
res.defineProperty(ctx, "next", new NativeFunction("", (_ctx, _th, _args) -> { res.defineProperty(ctx, "next", new NativeFunction("", args -> {
return PromiseLib.await(ctx, () -> { return PromiseLib.await(ctx, () -> {
if (!it.hasNext()) return new ObjectValue(ctx, Map.of("done", true)); if (!it.hasNext()) return new ObjectValue(ctx, Map.of("done", true));
else { else {
var obj = new ObjectValue(); var obj = new ObjectValue();
obj.defineProperty(_ctx, "value", it.next()); obj.defineProperty(args.ctx, "value", it.next());
return obj; return obj;
} }
}); });

View 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;
}
}

View File

@ -7,7 +7,8 @@ import java.lang.annotation.Target;
@Target({ ElementType.METHOD }) @Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface NativeSetter { public @interface Expose {
public String value() default ""; public String value() default "";
public boolean thisArg() default false; public ExposeType type() default ExposeType.METHOD;
public ExposeTarget target() default ExposeTarget.MEMBER;
} }

View File

@ -7,7 +7,4 @@ import java.lang.annotation.Target;
@Target({ ElementType.METHOD }) @Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface NativeConstructor { public @interface ExposeConstructor { }
public boolean thisArg() default false;
}

View 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;
}
}

View File

@ -0,0 +1,9 @@
package me.topchetoeu.jscript.interop;
public enum ExposeType {
INIT,
METHOD,
FIELD,
GETTER,
SETTER,
}

View File

@ -1,7 +0,0 @@
package me.topchetoeu.jscript.interop;
public enum InitType {
CONSTRUCTOR,
PROTOTYPE,
NAMESPACE,
}

View File

@ -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;
}

View File

@ -1,16 +1,23 @@
package me.topchetoeu.jscript.interop; package me.topchetoeu.jscript.interop;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap; 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.Environment;
import me.topchetoeu.jscript.engine.WrappersProvider; import me.topchetoeu.jscript.engine.WrappersProvider;
import me.topchetoeu.jscript.engine.values.FunctionValue; import me.topchetoeu.jscript.engine.values.FunctionValue;
import me.topchetoeu.jscript.engine.values.NativeFunction; import me.topchetoeu.jscript.engine.values.NativeFunction;
import me.topchetoeu.jscript.engine.values.ObjectValue; import me.topchetoeu.jscript.engine.values.ObjectValue;
import me.topchetoeu.jscript.engine.values.Symbol; 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.EngineException;
import me.topchetoeu.jscript.exceptions.UncheckedException; import me.topchetoeu.jscript.exceptions.InterruptException;
public class NativeWrapperProvider implements WrappersProvider { public class NativeWrapperProvider implements WrappersProvider {
private final HashMap<Class<?>, FunctionValue> constructors = new HashMap<>(); 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 HashMap<Class<?>, ObjectValue> namespaces = new HashMap<>();
private final Environment env; private final Environment env;
private static void applyMethods(Environment env, boolean member, ObjectValue target, Class<?> clazz) { private static Object call(Context ctx, String name, Method method, Object thisArg, Object... args) {
for (var method : clazz.getDeclaredMethods()) { try {
var nat = method.getAnnotation(Native.class); var realArgs = new Object[method.getParameterCount()];
var get = method.getAnnotation(NativeGetter.class); System.arraycopy(args, 0, realArgs, 0, realArgs.length);
var set = method.getAnnotation(NativeSetter.class); if (Modifier.isStatic(method.getModifiers())) thisArg = null;
var memberMatch = !Modifier.isStatic(method.getModifiers()) == member; return Values.normalize(ctx, method.invoke(Values.convert(ctx, thisArg, method.getDeclaringClass()), realArgs));
}
if (nat != null) { catch (InvocationTargetException e) {
if (nat.thisArg() && !member || !nat.thisArg() && !memberMatch) continue; if (e.getTargetException() instanceof EngineException) {
throw ((EngineException)e.getTargetException()).add(ctx, name, Location.INTERNAL);
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()));
} }
else { else if (e.getTargetException() instanceof NullPointerException) {
if (get != null) { e.printStackTrace();
if (get.thisArg() && !member || !get.thisArg() && !memberMatch) continue; throw EngineException.ofType("Unexpected value of 'undefined'.").add(ctx, name, Location.INTERNAL);
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 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) { private static FunctionValue create(String name, Method method) {
for (var field : clazz.getDeclaredFields()) { return new NativeFunction(name, args -> call(args.ctx, name, method, args.thisArg, args));
if (!Modifier.isStatic(field.getModifiers()) != member) continue; }
var nat = field.getAnnotation(Native.class); 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) { var actual = method.getParameterTypes();
Object name = nat.value();
if (name.toString().startsWith("@@")) name = Symbol.get(name.toString().substring(2)); boolean failed = actual.length > params.length;
else if (name.equals("")) name = field.getName();
if (!failed) for (var i = 0; i < actual.length; i++) {
var getter = OverloadFunction.of("get " + name, Overload.getterFromField(field)); if (!actual[i].isAssignableFrom(params[i])) {
var setter = OverloadFunction.of("set " + name, Overload.setterFromField(field)); failed = true;
target.defineProperty(null, name, getter, setter, true, false); 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) { private static String getName(Class<?> clazz) {
for (var cl : clazz.getDeclaredClasses()) { var classNat = clazz.getAnnotation(WrapperName.class);
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);
if (classNat != null && !classNat.value().trim().equals("")) return classNat.value().trim(); if (classNat != null && !classNat.value().trim().equals("")) return classNat.value().trim();
else return clazz.getSimpleName(); 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. * Generates a prototype for the given class.
* The returned object will have appropriate wrappers for all instance members. * 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. * 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 * @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(); var res = new ObjectValue();
res.defineProperty(null, Symbol.get("Symbol.typeName"), getName(clazz)); res.defineProperty(null, Symbol.get("Symbol.typeName"), getName(clazz));
apply(res, env, ExposeTarget.PROTOTYPE, 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);
return res; return res;
} }
/** /**
@ -146,36 +181,15 @@ public class NativeWrapperProvider implements WrappersProvider {
* @param clazz The class for which a constructor should be generated * @param clazz The class for which a constructor should be generated
*/ */
public static FunctionValue makeConstructor(Environment ctx, Class<?> clazz) { public static FunctionValue makeConstructor(Environment ctx, Class<?> clazz) {
FunctionValue func = new OverloadFunction(getName(clazz)); var constr = getConstructor(ctx, clazz);
for (var overload : clazz.getDeclaredConstructors()) { FunctionValue res = constr == null ?
var nat = overload.getAnnotation(Native.class); new NativeFunction(getName(clazz), args -> { throw EngineException.ofError("This constructor is not invokable."); }) :
if (nat == null) continue; create(getName(clazz), constr);
((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); }
}
if (((OverloadFunction)func).overloads.size() == 0) { apply(res, ctx, ExposeTarget.CONSTRUCTOR, clazz);
func = new NativeFunction(getName(clazz), (a, b, c) -> { throw EngineException.ofError("This constructor is not invokable."); });
}
applyMethods(ctx, false, func, clazz); return res;
applyFields(ctx, false, func, clazz);
applyClasses(ctx, false, func, clazz);
func.special = true;
return func;
} }
/** /**
* Generates a namespace for the given class. * 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 * @param clazz The class for which a constructor should be generated
*/ */
public static ObjectValue makeNamespace(Environment ctx, Class<?> clazz) { public static ObjectValue makeNamespace(Environment ctx, Class<?> clazz) {
ObjectValue res = new ObjectValue(); var res = new ObjectValue();
res.defineProperty(null, Symbol.get("Symbol.typeName"), getName(clazz));
for (var overload : clazz.getDeclaredMethods()) { apply(res, ctx, ExposeTarget.NAMESPACE, clazz);
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);
return res; return res;
} }
@ -249,8 +253,7 @@ public class NativeWrapperProvider implements WrappersProvider {
return constructors.get(clazz); return constructors.get(clazz);
} }
@Override @Override public WrappersProvider fork(Environment env) {
public WrappersProvider fork(Environment env) {
return new NativeWrapperProvider(env); return new NativeWrapperProvider(env);
} }
@ -263,12 +266,12 @@ public class NativeWrapperProvider implements WrappersProvider {
private void initError() { private void initError() {
var proto = new ObjectValue(); var proto = new ObjectValue();
proto.defineProperty(null, "message", new NativeFunction("message", (ctx, thisArg, args) -> { proto.defineProperty(null, "message", new NativeFunction("message", args -> {
if (thisArg instanceof Throwable) return ((Throwable)thisArg).getMessage(); if (args.thisArg instanceof Throwable) return ((Throwable)args.thisArg).getMessage();
else return null; else return null;
})); }));
proto.defineProperty(null, "name", new NativeFunction("name", (ctx, thisArg, args) -> getName(thisArg.getClass()))); proto.defineProperty(null, "name", new NativeFunction("name", args -> getName(args.thisArg.getClass())));
proto.defineProperty(null, "toString", new NativeFunction("toString", (ctx, thisArg, args) -> thisArg.toString())); proto.defineProperty(null, "toString", new NativeFunction("toString", args -> args.thisArg.toString()));
var constr = makeConstructor(null, Throwable.class); var constr = makeConstructor(null, Throwable.class);
proto.defineProperty(null, "constructor", constr, true, false, false); proto.defineProperty(null, "constructor", constr, true, false, false);

View File

@ -7,6 +7,6 @@ import java.lang.annotation.Target;
@Target({ ElementType.METHOD }) @Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface NativeInit { public @interface OnWrapperInit {
InitType value();
} }

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -5,9 +5,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
@Target({ ElementType.METHOD }) @Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface NativeGetter { public @interface WrapperName {
public String value() default ""; String value();
public boolean thisArg() default false;
} }