private static T convertToInterface(Class type, final Object jsObject) { List names = new ArrayList<>(); for (Method m : type.getMethods()) { if (m.getDeclaringClass() == Object.class) { continue; } names.add(m.getName()); } String[] required = names.toArray(new String[names.size()]); Object[] found = findProps(jsObject, required); if (required.length != found.length) { throw new IllegalArgumentException("Expecting " + type + " but found only " + Arrays.toString(found) + " functions"); } return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, new PH(jsObject)); } @JavaScriptBody(args = { "obj", "propNames" }, body = "var found = new Array();\n" + "for (var i = 0; i < propNames.length; i++) {\n" + " var p = obj[propNames[i]];\n" + " if (typeof p !== 'undefined') {\n" + " found.push(propNames[i]);\n" + " }\n" + "\n" + "}\n" + "return found;\n" + "\n" ) private static native Object[] findProps(Object obj, String[] propNames); @JavaScriptBody(args = { "jsObject", "name", "args" }, body = "var p = jsObject[name];\n" + "var t = typeof p;\n" + "if (t === 'function') {\n" + " return jsObject[name].apply(jsObject, args);\n" + "} else if (t !== 'undefined') {\n" + " return p;\n" + "}\n" + "return null;\n" + "\n" ) private static native Object callJS(Object jsObject, String name, Object[] args); private static class PH implements InvocationHandler { private final Object jsObject; public PH(Object jsObject) { this.jsObject = jsObject; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object ret = callJS(jsObject, method.getName(), args); Class rt = method.getReturnType(); if (!rt.isInstance(ret) && rt.isInterface()) { return convertToInterface(rt, ret); } return ret; } }