mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 14:22:01 +00:00
1057414fef
Thanks go to Andrew Wason <aw@softcom.com> for finding this problem and providing the patch: If we implement a Java interface in JavaScript, and a method in that interface returns a Java object, we get a java.lang.IncompatibleClassChangeError: org/mozilla/javascript/Wrapper exception. We have attached a sample JavaScript file which duplicates the error. The problem is JavaAdapter is generating an INVOKEVIRTUAL bytecode to call Wrapper.unwrap, but Wrapper is an interface and so INVOKEINTERFACE should be used instead. As a result of this change, the IFEQ bytecode generated needs to jump more bytes. We have attached a patch that fixes the problem. Andrew -- Andrew Wason SoftCom, Inc. aw@softcom.com
515 lines
21 KiB
Java
515 lines
21 KiB
Java
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
|
*
|
|
* The contents of this file are subject to the Netscape Public License
|
|
* Version 1.0 (the "NPL"); you may not use this file except in
|
|
* compliance with the NPL. You may obtain a copy of the NPL at
|
|
* http://www.mozilla.org/NPL/
|
|
*
|
|
* Software distributed under the NPL is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
|
|
* for the specific language governing rights and limitations under the
|
|
* NPL.
|
|
*
|
|
* The Initial Developer of this code under the NPL is Netscape
|
|
* Communications Corporation. Portions created by Netscape are
|
|
* Copyright (C) 1997-1999 Netscape Communications Corporation. All Rights
|
|
* Reserved.
|
|
*/
|
|
|
|
package org.mozilla.javascript;
|
|
|
|
import org.mozilla.classfile.*;
|
|
import java.lang.reflect.*;
|
|
import java.io.*;
|
|
import java.util.*;
|
|
|
|
public class JavaAdapter extends ScriptableObject {
|
|
|
|
public String getClassName() {
|
|
return "JavaAdapter";
|
|
}
|
|
|
|
public static Object js_JavaAdapter(Context cx, Object[] args,
|
|
Function ctorObj, boolean inNewExpr)
|
|
throws InstantiationException, NoSuchMethodException,
|
|
IllegalAccessException, InvocationTargetException,
|
|
ClassNotFoundException
|
|
{
|
|
Class superClass = Object.class;
|
|
Class[] intfs = new Class[args.length-1];
|
|
int interfaceCount = 0;
|
|
for (int i=0; i < args.length-1; i++) {
|
|
if (!(args[i] instanceof NativeJavaClass)) {
|
|
// TODO: report error
|
|
throw new RuntimeException("expected java class object");
|
|
}
|
|
Class c = ((NativeJavaClass) args[i]).getClassObject();
|
|
if (!c.isInterface()) {
|
|
superClass = c;
|
|
break;
|
|
}
|
|
intfs[interfaceCount++] = c;
|
|
}
|
|
|
|
StringBuffer sb = new StringBuffer("adapter");
|
|
sb.append(serial++);
|
|
String genName = sb.toString();
|
|
ClassFileWriter cfw = new ClassFileWriter(genName,
|
|
superClass.getName(),
|
|
"<adapter>");
|
|
cfw.addField("o", "Lorg/mozilla/javascript/FlattenedObject;",
|
|
ClassFileWriter.ACC_PRIVATE);
|
|
cfw.addField("self", "Lorg/mozilla/javascript/Scriptable;",
|
|
(short) (ClassFileWriter.ACC_PUBLIC | ClassFileWriter.ACC_FINAL));
|
|
for (int i = 0; i < interfaceCount; i++) {
|
|
if (intfs[i] != null)
|
|
cfw.addInterface(intfs[i].getName());
|
|
}
|
|
|
|
generateCtor(cfw, genName, superClass);
|
|
|
|
Hashtable generatedOverrides = new Hashtable();
|
|
|
|
// if abstract class was specified, then generate appropriate overrides.
|
|
for (int i = 0; i < interfaceCount; i++) {
|
|
Method[] methods = intfs[i].getMethods();
|
|
for (int j = 0; j < methods.length; j++) {
|
|
Method method = methods[j];
|
|
int mods = method.getModifiers();
|
|
if (Modifier.isStatic(mods) || Modifier.isFinal(mods))
|
|
continue;
|
|
// make sure to generate only one instance of a particular
|
|
// method/signature.
|
|
String methodKey = getMethodSignature(method);
|
|
if (! generatedOverrides.containsKey(methodKey)) {
|
|
generateOverride(cfw, genName, method);
|
|
generatedOverrides.put(methodKey, methodKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now, go through the superclasses methods, checking for abstract methods
|
|
// or additional methods to override.
|
|
FlattenedObject obj = new FlattenedObject(
|
|
(Scriptable) args[args.length - 1]);
|
|
|
|
// generate any additional overrides that the object might contain.
|
|
Method[] methods = superClass.getMethods();
|
|
for (int j = 0; j < methods.length; j++) {
|
|
Method method = methods[j];
|
|
int mods = method.getModifiers();
|
|
if (Modifier.isStatic(mods) || Modifier.isFinal(mods))
|
|
continue;
|
|
// if a method is marked abstract, must implement it or the
|
|
// resulting class won't be instantiable. otherwise, if the object
|
|
// has a property of the same name, then an override is intended.
|
|
if (Modifier.isAbstract(mods) || obj.hasProperty(method.getName())) {
|
|
// make sure to generate only one instance of a particular
|
|
// method/signature.
|
|
String methodKey = getMethodSignature(method);
|
|
if (! generatedOverrides.containsKey(methodKey)) {
|
|
generateOverride(cfw, genName, method);
|
|
generatedOverrides.put(methodKey, method);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: generate Java methods, fields for remaining properties that
|
|
// are not overrides? Probably not necessary to generate methods
|
|
// that aren't overrides.
|
|
|
|
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
|
|
try {
|
|
cfw.write(out);
|
|
}
|
|
catch (IOException ioe) {
|
|
throw new RuntimeException("unexpected IOException");
|
|
}
|
|
byte[] bytes = out.toByteArray();
|
|
SecuritySupport ss = cx.getSecuritySupport();
|
|
Class c;
|
|
if (ss != null) {
|
|
Object securityDomain = cx.getSecurityDomainForStackDepth(-1);
|
|
c = ss.defineClass(genName, bytes, securityDomain);
|
|
} else {
|
|
if (classLoader == null)
|
|
classLoader = new MyClassLoader();
|
|
classLoader.defineClass(genName, bytes);
|
|
c = classLoader.loadClass(genName, true);
|
|
}
|
|
Class[] ctorParms = { FlattenedObject.class };
|
|
Object[] ctorArgs = { obj };
|
|
Object v = c.getConstructor(ctorParms).newInstance(ctorArgs);
|
|
return cx.toObject(v, ScriptableObject.getTopLevelScope(ctorObj));
|
|
}
|
|
|
|
/**
|
|
* Utility method, which dynamically binds a Context to the current thread,
|
|
* if none already exists.
|
|
*/
|
|
public static Object callMethod(FlattenedObject object, Object methodId,
|
|
Object[] args)
|
|
{
|
|
try {
|
|
// old way, bind a Context dynamically, unbind if it wasn't there before.
|
|
Context cx = Context.getCurrentContext();
|
|
if (cx == null) {
|
|
cx = new Context();
|
|
cx.enter();
|
|
try {
|
|
return (object.hasProperty(methodId)
|
|
? object.callMethod(methodId, args)
|
|
: Context.getUndefinedValue());
|
|
} finally {
|
|
cx.exit();
|
|
}
|
|
} else {
|
|
return (object.hasProperty(methodId)
|
|
? object.callMethod(methodId, args)
|
|
: Context.getUndefinedValue());
|
|
}
|
|
} catch (Exception ex) {
|
|
ex.printStackTrace(System.err);
|
|
throw new Error(ex.getMessage());
|
|
}
|
|
}
|
|
|
|
private static void generateCtor(ClassFileWriter cfw, String genName,
|
|
Class superClass)
|
|
{
|
|
cfw.startMethod("<init>",
|
|
"(Lorg/mozilla/javascript/FlattenedObject;)V",
|
|
ClassFileWriter.ACC_PUBLIC);
|
|
|
|
// Invoke base class constructor
|
|
cfw.add(ByteCode.ALOAD_0); // this
|
|
cfw.add(ByteCode.INVOKESPECIAL,
|
|
superClass.getName().replace('.', '/'),
|
|
"<init>", "()", "V");
|
|
|
|
// save parameter in instance variable
|
|
cfw.add(ByteCode.ALOAD_0); // this
|
|
cfw.add(ByteCode.ALOAD_1); // first arg
|
|
cfw.add(ByteCode.PUTFIELD, genName, "o",
|
|
"Lorg/mozilla/javascript/FlattenedObject;");
|
|
|
|
// store Scriptable object in "self" a public instance variable, so scripts can read it.
|
|
cfw.add(ByteCode.ALOAD_0); // this
|
|
cfw.add(ByteCode.ALOAD_1); // first arg
|
|
cfw.add(ByteCode.INVOKEVIRTUAL,
|
|
"org/mozilla/javascript/FlattenedObject",
|
|
"getObject", "()",
|
|
"Lorg/mozilla/javascript/Scriptable;");
|
|
cfw.add(ByteCode.PUTFIELD, genName, "self",
|
|
"Lorg/mozilla/javascript/Scriptable;");
|
|
|
|
cfw.add(ByteCode.RETURN);
|
|
cfw.stopMethod((short)20, null); // TODO: magic number "20"
|
|
}
|
|
|
|
/**
|
|
* Generates code to create a java.lang.Boolean, java.lang.Character or a
|
|
* java.lang.Double to wrap the specified primitive parameter. Leaves the
|
|
* wrapper object on the top of the stack.
|
|
*/
|
|
private static int generateWrapParam(ClassFileWriter cfw, int paramOffset,
|
|
Class paramType)
|
|
{
|
|
if (paramType.equals(Boolean.TYPE)) {
|
|
// wrap boolean values with java.lang.Boolean.
|
|
cfw.add(ByteCode.NEW, "java/lang/Boolean");
|
|
cfw.add(ByteCode.DUP);
|
|
cfw.add(ByteCode.ILOAD, paramOffset++);
|
|
cfw.add(ByteCode.INVOKESPECIAL, "java/lang/Boolean",
|
|
"<init>", "(Z)", "V");
|
|
} else
|
|
if (paramType.equals(Character.TYPE)) {
|
|
// Create a string of length 1 using the character parameter.
|
|
cfw.add(ByteCode.NEW, "java/lang/String");
|
|
cfw.add(ByteCode.DUP);
|
|
cfw.add(ByteCode.ICONST_1);
|
|
cfw.add(ByteCode.NEWARRAY, ByteCode.T_CHAR);
|
|
cfw.add(ByteCode.DUP);
|
|
cfw.add(ByteCode.ICONST_0);
|
|
cfw.add(ByteCode.ILOAD, paramOffset++);
|
|
cfw.add(ByteCode.CASTORE);
|
|
cfw.add(ByteCode.INVOKESPECIAL, "java/lang/String",
|
|
"<init>", "([C)", "V");
|
|
} else {
|
|
// convert all numeric values to java.lang.Double.
|
|
cfw.add(ByteCode.NEW, "java/lang/Double");
|
|
cfw.add(ByteCode.DUP);
|
|
String typeName = paramType.getName();
|
|
switch (typeName.charAt(0)) {
|
|
case 'b':
|
|
case 's':
|
|
case 'i':
|
|
// load an int value, convert to double.
|
|
cfw.add(ByteCode.ILOAD, paramOffset++);
|
|
cfw.add(ByteCode.I2D);
|
|
break;
|
|
case 'l':
|
|
// load a long, convert to double.
|
|
cfw.add(ByteCode.LLOAD, paramOffset);
|
|
cfw.add(ByteCode.L2D);
|
|
paramOffset += 2;
|
|
break;
|
|
case 'f':
|
|
// load a float, convert to double.
|
|
cfw.add(ByteCode.FLOAD, paramOffset++);
|
|
cfw.add(ByteCode.F2D);
|
|
break;
|
|
case 'd':
|
|
cfw.add(ByteCode.DLOAD, paramOffset);
|
|
paramOffset += 2;
|
|
break;
|
|
}
|
|
cfw.add(ByteCode.INVOKESPECIAL, "java/lang/Double",
|
|
"<init>", "(D)", "V");
|
|
}
|
|
return paramOffset;
|
|
}
|
|
|
|
/**
|
|
* Generates code to convert a wrapped value type to a primitive type.
|
|
* Handles unwrapping java.lang.Boolean, and java.lang.Number types.
|
|
* May need to map between char and java.lang.String as well.
|
|
* Generates the appropriate RETURN bytecode.
|
|
*/
|
|
private static void generateReturnResult(ClassFileWriter cfw,
|
|
Class retType)
|
|
{
|
|
// wrap boolean values with java.lang.Boolean, convert all other
|
|
// primitive values to java.lang.Double.
|
|
if (retType.equals(Boolean.TYPE)) {
|
|
cfw.add(ByteCode.INVOKESTATIC,
|
|
"org/mozilla/javascript/Context",
|
|
"toBoolean", "(Ljava/lang/Object;)",
|
|
"Z");
|
|
cfw.add(ByteCode.IRETURN);
|
|
} else if (retType.equals(Character.TYPE)) {
|
|
// characters are represented as strings in JavaScript.
|
|
// return the first character.
|
|
// first convert the value to a string if possible.
|
|
cfw.add(ByteCode.INVOKESTATIC,
|
|
"org/mozilla/javascript/Context",
|
|
"toString", "(Ljava/lang/Object;)",
|
|
"Ljava/lang/String;");
|
|
cfw.add(ByteCode.ICONST_0);
|
|
cfw.add(ByteCode.INVOKEVIRTUAL, "java/lang/String", "charAt",
|
|
"(I)", "C");
|
|
cfw.add(ByteCode.IRETURN);
|
|
} else if (retType.isPrimitive()) {
|
|
cfw.add(ByteCode.INVOKESTATIC,
|
|
"org/mozilla/javascript/Context",
|
|
"toNumber", "(Ljava/lang/Object;)",
|
|
"D");
|
|
String typeName = retType.getName();
|
|
switch (typeName.charAt(0)) {
|
|
case 'b':
|
|
case 's':
|
|
case 'i':
|
|
cfw.add(ByteCode.D2I);
|
|
cfw.add(ByteCode.IRETURN);
|
|
break;
|
|
case 'l':
|
|
cfw.add(ByteCode.D2L);
|
|
cfw.add(ByteCode.LRETURN);
|
|
break;
|
|
case 'f':
|
|
cfw.add(ByteCode.D2F);
|
|
cfw.add(ByteCode.FRETURN);
|
|
break;
|
|
case 'd':
|
|
cfw.add(ByteCode.DRETURN);
|
|
break;
|
|
default:
|
|
throw new RuntimeException("Unexpected return type " +
|
|
retType.toString());
|
|
}
|
|
} else if (retType.equals(String.class)) {
|
|
cfw.add(ByteCode.INVOKESTATIC,
|
|
"org/mozilla/javascript/Context",
|
|
"toString", "(Ljava/lang/Object;)",
|
|
"Ljava/lang/String;");
|
|
cfw.add(ByteCode.ARETURN);
|
|
} else if (retType.equals(Scriptable.class)) {
|
|
cfw.add(ByteCode.ALOAD_0); // load 'this' to find scope from
|
|
cfw.add(ByteCode.INVOKESTATIC,
|
|
"org/mozilla/javascript/Context",
|
|
"toObject",
|
|
"(Ljava/lang/Object;" +
|
|
"Lorg/mozilla/javascript/Scriptable;)",
|
|
"Lorg/mozilla/javascript/Scriptable;");
|
|
cfw.add(ByteCode.ARETURN);
|
|
} else {
|
|
// If it is a wrapped type, cast to Wrapper and call unwrap()
|
|
cfw.add(ByteCode.DUP);
|
|
cfw.add(ByteCode.INSTANCEOF, "org/mozilla/javascript/Wrapper");
|
|
// skip 3 for IFEQ, 3 for CHECKCAST, and 5 for INVOKEINTERFACE
|
|
cfw.add(ByteCode.IFEQ, 11);
|
|
cfw.add(ByteCode.CHECKCAST, "org/mozilla/javascript/Wrapper");
|
|
cfw.add(ByteCode.INVOKEINTERFACE,
|
|
"org/mozilla/javascript/Wrapper",
|
|
"unwrap", "()", "Ljava/lang/Object;");
|
|
// Now cast to return type
|
|
String retTypeStr = retType.getName().replace('.', '/');
|
|
cfw.add(ByteCode.CHECKCAST, retTypeStr);
|
|
cfw.add(ByteCode.ARETURN);
|
|
}
|
|
}
|
|
|
|
private static void generateOverride(ClassFileWriter cfw, String genName,
|
|
Method m)
|
|
{
|
|
Class[] parms = m.getParameterTypes();
|
|
StringBuffer sb = new StringBuffer();
|
|
sb.append('(');
|
|
short arrayLocal = 1; // includes this.
|
|
for (int i = 0; i < parms.length; i++) {
|
|
Class type = parms[i];
|
|
appendTypeString(sb, type);
|
|
if (type.equals(Long.TYPE) || type.equals(Double.TYPE))
|
|
arrayLocal += 2;
|
|
else
|
|
arrayLocal += 1;
|
|
}
|
|
sb.append(')');
|
|
appendTypeString(sb, m.getReturnType());
|
|
String methodSignature = sb.toString();
|
|
// System.out.println("generating " + m.getName() + methodSignature);
|
|
// System.out.flush();
|
|
cfw.startMethod(m.getName(), methodSignature,
|
|
ClassFileWriter.ACC_PUBLIC);
|
|
cfw.add(ByteCode.BIPUSH, (byte) parms.length); // > 255 parms?
|
|
cfw.add(ByteCode.ANEWARRAY, "java/lang/Object");
|
|
cfw.add(ByteCode.ASTORE, arrayLocal);
|
|
|
|
// allocate a local variable to store the scope used to wrap native objects.
|
|
short scopeLocal = (short) (arrayLocal + 1);
|
|
boolean loadedScope = false;
|
|
|
|
int paramOffset = 1;
|
|
for (int i = 0; i < parms.length; i++) {
|
|
cfw.add(ByteCode.ALOAD, arrayLocal);
|
|
cfw.add(ByteCode.BIPUSH, i);
|
|
if (parms[i].isPrimitive()) {
|
|
paramOffset = generateWrapParam(cfw, paramOffset, parms[i]);
|
|
} else {
|
|
// An arbitary Java object; call Context.toObject to wrap in
|
|
// a Scriptable object
|
|
cfw.add(ByteCode.ALOAD, paramOffset++);
|
|
if (! loadedScope) {
|
|
// load this.self into a local the first time it's needed.
|
|
// it will provide the scope needed by Context.toObject().
|
|
cfw.add(ByteCode.ALOAD_0);
|
|
cfw.add(ByteCode.GETFIELD, genName, "self",
|
|
"Lorg/mozilla/javascript/Scriptable;");
|
|
cfw.add(ByteCode.ASTORE, scopeLocal);
|
|
loadedScope = true;
|
|
}
|
|
cfw.add(ByteCode.ALOAD, scopeLocal);
|
|
cfw.add(ByteCode.INVOKESTATIC,
|
|
"org/mozilla/javascript/Context",
|
|
"toObject",
|
|
"(Ljava/lang/Object;" +
|
|
"Lorg/mozilla/javascript/Scriptable;)",
|
|
"Lorg/mozilla/javascript/Scriptable;");
|
|
}
|
|
cfw.add(ByteCode.AASTORE);
|
|
}
|
|
|
|
cfw.add(ByteCode.ALOAD_0); // this
|
|
cfw.add(ByteCode.GETFIELD, genName, "o",
|
|
"Lorg/mozilla/javascript/FlattenedObject;");
|
|
|
|
cfw.addLoadConstant(m.getName());
|
|
cfw.add(ByteCode.ALOAD, arrayLocal);
|
|
|
|
// go through utility method, which creates a Context to run the
|
|
// method in.
|
|
cfw.add(ByteCode.INVOKESTATIC,
|
|
"org/mozilla/javascript/JavaAdapter",
|
|
"callMethod",
|
|
"(Lorg/mozilla/javascript/FlattenedObject;" +
|
|
"Ljava/lang/Object;[Ljava/lang/Object;)",
|
|
"Ljava/lang/Object;");
|
|
|
|
Class retType = m.getReturnType();
|
|
if (retType.equals(Void.TYPE)) {
|
|
cfw.add(ByteCode.POP);
|
|
cfw.add(ByteCode.RETURN);
|
|
} else {
|
|
generateReturnResult(cfw, retType);
|
|
}
|
|
cfw.stopMethod((short)(scopeLocal + 1), null);
|
|
}
|
|
|
|
/**
|
|
* Returns a fully qualified method name concatenated with its signature.
|
|
*/
|
|
private static String getMethodSignature(Method method) {
|
|
Class[] parms = method.getParameterTypes();
|
|
StringBuffer sb = new StringBuffer();
|
|
sb.append(method.getName());
|
|
sb.append('(');
|
|
for (int i = 0; i < parms.length; i++) {
|
|
Class type = parms[i];
|
|
appendTypeString(sb, type);
|
|
}
|
|
sb.append(')');
|
|
appendTypeString(sb, method.getReturnType());
|
|
return sb.toString();
|
|
}
|
|
|
|
private static StringBuffer appendTypeString(StringBuffer sb, Class type)
|
|
{
|
|
while (type.isArray()) {
|
|
sb.append('[');
|
|
type = type.getComponentType();
|
|
}
|
|
if (type.isPrimitive()) {
|
|
if (type.equals(Boolean.TYPE)) {
|
|
sb.append('Z');
|
|
} else
|
|
if (type.equals(Long.TYPE)) {
|
|
sb.append('J');
|
|
} else {
|
|
String typeName = type.getName();
|
|
sb.append(Character.toUpperCase(typeName.charAt(0)));
|
|
}
|
|
} else {
|
|
sb.append('L');
|
|
sb.append(type.getName().replace('.', '/'));
|
|
sb.append(';');
|
|
}
|
|
return sb;
|
|
}
|
|
|
|
static final class MyClassLoader extends ClassLoader {
|
|
public Class defineClass(String name, byte data[]) {
|
|
return super.defineClass(name, data, 0, data.length);
|
|
}
|
|
|
|
protected Class loadClass(String name, boolean resolve)
|
|
throws ClassNotFoundException
|
|
{
|
|
Class clazz = findLoadedClass(name);
|
|
if (clazz == null) {
|
|
ClassLoader loader = getClass().getClassLoader();
|
|
if (loader != null)
|
|
return loader.loadClass(name);
|
|
clazz = findSystemClass(name);
|
|
}
|
|
if (resolve)
|
|
resolveClass(clazz);
|
|
return clazz;
|
|
}
|
|
|
|
private java.util.Hashtable loaded;
|
|
}
|
|
|
|
private static int serial;
|
|
private static MyClassLoader classLoader;
|
|
}
|