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() {
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();

View File

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

View File

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

View File

@ -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) {

View File

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

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

View File

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

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

View File

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

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.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();
}