mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 14:22:01 +00:00
eb9dcf8b50
* Accept patch from Andrew Wason <aw@softcom.com>: Subject: Re: partial interface problem Date: Wed, 04 Aug 1999 13:04:37 -0400 From: Andrew Wason <aw@softcom.com> To: norris@netscape.com CC: Howard Lin <howard@softcom.com> >I'm having a problem implementing a Java interface in JS where I don't >implement all the methods, and one of the methods I don't define returns >non-void. I have a patch for this. I generate bytecode in JavaAdapter.generateReturnResult to check the return type on the stack from JavaAdapter.callMethod. If it is Undefined, return null. I'm not positive this is the right way to fix this - maybe it should be fixed closer to the source (e.g. prevent callMethod from returning Undefined to begin with) Andrew -- Andrew Wason SoftCom, Inc. aw@softcom.com
526 lines
21 KiB
Java
526 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)
|
|
{
|
|
Context cx = Context.enter();
|
|
try {
|
|
return (object.hasProperty(methodId)
|
|
? object.callMethod(methodId, args)
|
|
: Context.getUndefinedValue());
|
|
} catch (Exception ex) {
|
|
// TODO: wouldn't it be better to let the exception propagate
|
|
// up so that it could be dealt with by the calling code?
|
|
ex.printStackTrace(System.err);
|
|
throw new Error(ex.getMessage());
|
|
} finally {
|
|
Context.exit();
|
|
}
|
|
}
|
|
|
|
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;");
|
|
|
|
// If Undefined, return null
|
|
cfw.add(ByteCode.DUP);
|
|
cfw.add(ByteCode.INSTANCEOF, "org/mozilla/javascript/Undefined");
|
|
// skip 3 for IFEQ, 1 for ACONST_NULL, 1 for ARETURN
|
|
cfw.add(ByteCode.IFEQ, 5);
|
|
cfw.add(ByteCode.ACONST_NULL);
|
|
cfw.add(ByteCode.ARETURN);
|
|
|
|
// 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);
|
|
|
|
// Get argument Class
|
|
cfw.addLoadConstant(parms[i].getName());
|
|
cfw.add(ByteCode.INVOKESTATIC,
|
|
"java/lang/Class",
|
|
"forName",
|
|
"(Ljava/lang/String;)",
|
|
"Ljava/lang/Class;");
|
|
|
|
cfw.add(ByteCode.INVOKESTATIC,
|
|
"org/mozilla/javascript/Context",
|
|
"toObject",
|
|
"(Ljava/lang/Object;" +
|
|
"Lorg/mozilla/javascript/Scriptable;" +
|
|
"Ljava/lang/Class;)",
|
|
"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;
|
|
}
|