From 47273749c8bc149a37ae327bbc92a70e421541be Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Mon, 7 May 2007 18:59:42 +0000 Subject: [PATCH] New plan! We now post-process our class files and add individual public reader and writer methods for private and protected fields. This will allow us to preserve the current read and write ordering and in the future when we "fix" read and write ordering, we won't be fucked by a bunch of generated methods that force the ordering. (Someone is using Presents on the CLR which turns out to take advantage of the loose specification of Class.getDeclaredFields() and not return them in declaration order, yay!). git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4692 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- build.xml | 7 +- .../tools/InstrumentStreamableTask.java | 282 ++++++++++++++++++ 2 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 src/java/com/threerings/presents/tools/InstrumentStreamableTask.java diff --git a/build.xml b/build.xml index ab930c3d0..3eb7dc410 100644 --- a/build.xml +++ b/build.xml @@ -88,9 +88,12 @@ + classname="com.threerings.presents.tools.InstrumentStreamableTask"/> - + + + + diff --git a/src/java/com/threerings/presents/tools/InstrumentStreamableTask.java b/src/java/com/threerings/presents/tools/InstrumentStreamableTask.java new file mode 100644 index 000000000..5eed6a58a --- /dev/null +++ b/src/java/com/threerings/presents/tools/InstrumentStreamableTask.java @@ -0,0 +1,282 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.presents.tools; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; + +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtField; +import javassist.CtMethod; +import javassist.CtNewMethod; +import javassist.Modifier; +import javassist.NotFoundException; +import javassist.CannotCompileException; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.FileSet; +import org.apache.tools.ant.types.Path; + +import com.samskivert.io.StreamUtil; +import com.threerings.io.Streamable; + +/** + * Instruments compiled {@link Streamable} classes with public methods that can be used to stream + * protected and private members when running in a sandboxed JVM. + */ +public class InstrumentStreamableTask extends Task +{ + /** + * Adds a nested <fileset> element which enumerates streamable class files. + */ + public void addFileset (FileSet set) + { + _filesets.add(set); + } + + /** + * Adds a <path> element which defines our classpath. + */ + public void addPath (Path path) + { + _paths.add(path); + } + + /** + * Configures the directory into which we write our instrumented class files. + */ + public void setOutdir (File outdir) + { + _outdir = outdir; + } + + /** + * Performs the actual work of the task. + */ + public void execute () throws BuildException + { + // configure our ClassPool with our classpath + for (Path path : _paths) { + for (String element : path.list()) { + try { + _pool.appendClassPath(element); + } catch (NotFoundException nfe) { + System.err.println("Invalid classpath entry [path=" + element + "]: " + nfe); + } + } + } + + // now process the files + for (FileSet fs : _filesets) { + DirectoryScanner ds = fs.getDirectoryScanner(getProject()); + File fromDir = fs.getDir(getProject()); + String[] srcFiles = ds.getIncludedFiles(); + for (int f = 0; f < srcFiles.length; f++) { + processClass(new File(fromDir, srcFiles[f])); + } + } + } + + /** + * Processes a class file. + */ + protected void processClass (File source) + { + CtClass clazz; + InputStream in = null; + try { + clazz = _pool.makeClass(in = new BufferedInputStream(new FileInputStream(source))); + } catch (IOException ioe) { + System.err.println("Failed to load " + source + ": " + ioe); + return; + } finally { + StreamUtil.close(in); + } + + try { + if (isStreamable(clazz)) { + processStreamable(source, clazz); + } + } catch (NotFoundException nfe) { + System.err.println("Error processing class [class=" + clazz.getName() + + ", error=" + nfe + "]."); + } + } + + /** + * Returns true if the supplied class or any of its parents implements {@link Streamable}. + */ + protected boolean isStreamable (CtClass clazz) + throws NotFoundException + { + if (clazz == null) { + return false; + } + + for (CtClass iface : clazz.getInterfaces()) { + if (iface.getName().equals(Streamable.class.getName())) { + return true; + } + } + return isStreamable(clazz.getSuperclass()); + } + + /** + * Instruments the supplied {@link Streamable} implementing class. + */ + protected void processStreamable (File source, CtClass clazz) + throws NotFoundException + { + // System.err.println("Processing " + clazz.getName() + "..."); + + ArrayList fields = new ArrayList(); + for (CtField field : clazz.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers) || + !(Modifier.isProtected(modifiers) || Modifier.isPrivate(modifiers))) { + continue; + } + fields.add(field); + } + + HashSet methods = new HashSet(); + for (CtMethod method : clazz.getMethods()) { + methods.add(method.getName()); + } + + int added = 0; + for (CtField field : fields) { + String rname = "readField_" + field.getName(); + if (!methods.contains(rname)) { + String reader = + "public void " + rname + " (com.threerings.io.ObjectInputStream ins) {\n" + + " " + field.getName() + " = " + getFieldReader(field) + ";\n" + + "}"; + // System.err.println("Adding reader " + clazz.getName() + "." + rname); + try { + clazz.addMethod(CtNewMethod.make(reader, clazz)); + added++; + } catch (CannotCompileException cce) { + System.err.println("Unable to compile reader [class=" + clazz.getName() + + ", error=" + cce + "]:"); + System.err.println(reader); + } + } + String wname = "writeField_" + field.getName(); + if (!methods.contains(wname)) { + // System.err.println("Adding writer " + clazz.getName() + "." + wname); + String writer = + "public void " + wname + " (com.threerings.io.ObjectOutputStream out) {\n" + + " out." + getFieldWriter(field) + ";\n" + + "}"; + try { + clazz.addMethod(CtNewMethod.make(writer, clazz)); + added++; + } catch (CannotCompileException cce) { + System.err.println("Unable to compile writer [class=" + clazz.getName() + + ", error=" + cce + "]:"); + System.err.println(writer); + } + } + } + + if (added > 0) { + try { + clazz.writeFile(_outdir.getPath()); + } catch (Exception e) { + System.err.println("Failed to write instrumented class [class=" + clazz + + ", outdir=" + _outdir + "]: " + e); + } + } + } + + protected String getFieldReader (CtField field) + throws NotFoundException + { + CtClass type = field.getType(); + if (type.getName().equals("java.lang.String")) { + return "ins.readUTF()"; + } else if (type.equals(CtClass.booleanType) || type.getName().equals("java.lang.Boolean")) { + return "ins.readBoolean()"; + } else if (type.equals(CtClass.byteType) || type.getName().equals("java.lang.Byte")) { + return "ins.readByte()"; + } else if (type.equals(CtClass.shortType) || type.getName().equals("java.lang.Short")) { + return "ins.readShort()"; + } else if (type.equals(CtClass.intType) || type.getName().equals("java.lang.Integer")) { + return "ins.readInt()"; + } else if (type.equals(CtClass.longType) || type.getName().equals("java.lang.Long")) { + return "ins.readLong()"; + } else if (type.equals(CtClass.floatType) || type.getName().equals("java.lang.Float")) { + return "ins.readFloat()"; + } else if (type.equals(CtClass.doubleType) || type.getName().equals("java.lang.Double")) { + return "ins.readDouble()"; + } else { + return "(" + type.getSimpleName() + ")ins.readObject()"; + } + } + + protected String getFieldWriter (CtField field) + throws NotFoundException + { + CtClass type = field.getType(); + String name = field.getName(); + if (type.equals(CtClass.booleanType) || type.getName().equals("java.lang.Boolean")) { + return "writeBoolean(" + name + ")"; + } else if (type.equals(CtClass.byteType) || type.getName().equals("java.lang.Byte")) { + return "writeByte(" + name + ")"; + } else if (type.equals(CtClass.shortType) || type.getName().equals("java.lang.Short")) { + return "writeShort(" + name + ")"; + } else if (type.equals(CtClass.intType) || type.getName().equals("java.lang.Integer")) { + return "writeInt(" + name + ")"; + } else if (type.equals(CtClass.longType) || type.getName().equals("java.lang.Long")) { + return "writeLong(" + name + ")"; + } else if (type.equals(CtClass.floatType) || type.getName().equals("java.lang.Float")) { + return "writeFloat(" + name + ")"; + } else if (type.equals(CtClass.doubleType) || type.getName().equals("java.lang.Double")) { + return "writeDouble(" + name + ")"; + } else if (type.getName().equals("java.lang.String")) { + return "writeUTF(" + name + ")"; + } else { + return "writeObject(" + name + ")"; + } + } + + /** A list of filesets that contain Streamable class files. */ + protected ArrayList _filesets = new ArrayList(); + + /** A list of paths that make up our classpath. */ + protected ArrayList _paths = new ArrayList(); + + /** The directory to which we write our instrumented class files. */ + protected File _outdir; + + /** Used to instrument class files. */ + protected ClassPool _pool = ClassPool.getDefault(); +}