aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/grothoff/Runabout.java
blob: 2a1dbb00530bc11ef908d7f8ab33adc6cc12a624 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
/*
 * (C) 2002, 2003, 2004, 2005, 2006 Christian Grothoff
 *
 * The Runabout is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2, or (at your option) any later version. The
 * Runabout 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 General Public License for more details.
 * You should have received a copy of the GNU General Public License along with
 * the Runabout; see the file COPYING. If not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 * This software is also licensed under the Eclipse Public License v1.0 
 * available at http://www.eclipse.org/legal/epl-v10.html.
 */
package org.grothoff;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;

/**
 * Runabout is a fast implementation of the Walkabout which is a variant of the
 * Visitor Pattern that does not require an accept method and uses reflection
 * instead.
 * <p/>
 * An instance of Runabout is able to walk over an arbitrary object graph using
 * visit methods which take arguments of the specific type of the object to
 * visit. For each node in the object graph the Runabout invokes the most
 * appropriate visit method.
 * <p/>
 * Using the Runabout typically involves subclassing Runabout and adding a
 * couple of visit methods. The Runabout provides a 'visitAppropriate' method
 * which will invoke the most appropriate visit method of the current Runabout
 * instance. If no visit method is applicable, visitAppropriate calls
 * visitDefault() which, if not overridden, throws an exception.
 * <p/>
 * The elements of the object graph typically extend the Element class, which
 * provides a generic way to quickly invoke the Runabout on all the fields of
 * the Element.
 * <p/>
 * Note that the Runabout uses dynamic code generation and dynamic loading in
 * order to be quickly able to invoke the appropriate visit methods. To make the
 * dynamic code generation fast, the code inlines parts of Java class-files in
 * binary form (ugly!).<br>
 * A per-thread Cache is used to speed-up the creation of the Runabout by
 * caching reflection, code creation and dynamic loading operations.
 * <p/>
 * <bf>Restrictions:</bf> Java semantics require:
 * <ul>
 * <li>all subclasses must be public, sadly this also means that <strong>you
 * can not use an anonymous inner class</strong> (!)</li>
 * <li>the types_length to all arguments of visit methods must be public</li>
 * <li>all visit methods must be public (!)</li>
 * </ul>
 * Otherwise the visitor will die with an IllegalAccessError during execution.
 * <p/>
 *
 * @author Christian Grothoff
 * @version 5.0
 */
public class Runabout {

    /**
     * Singleton of the Runabout.Cache. We cache reflective information per VM;
     * this avoids the need for repeated reflection, code generation and
     * dispatching map updates.
     */
    private static final Runabout.Cache cache_ = new Runabout.Cache();

    /**
     * map_ contains a mapping from a class to the appropriate visit method.
     * Note that at the beginning, map_ only contains the explicit mappings as
     * given by the visit methods. Over time, map_ will be amended to also
     * contain direct mappings for subclasses to the appropriate visit methods
     * if they are used.
     */
    private final HashMap<Class, Runabout.Code> map_;

    /**
     * Code to invoke if no visitor is found (used to avoid scanning the
     * hierarchy again and again).
     */
    private final Code noCode = new NoCode();

    /**
     * Set when the subclass of the runabout is not public.
     */
    private final boolean isPublic;

    /**
     * Create a Runabout.
     */
    public Runabout() {
        this.isPublic = Modifier.isPublic(getClass().getModifiers());
        map_ = cache_.get(this);
    }

    /**
     * Call the appropriate visit method. Use this method if you are visiting a
     * graph of objects (no primitives).
     *
     * @param o the object to visit
     */
    public void visitAppropriate(Object o) {
        getAppropriateCodeInternal(o.getClass()).visit(this, o);
    }

    /**
     * Obtain the appropriate code to call for class c. The method either
     * obtains the code quickly from the code map (fast path) or by calling the
     * lookup method getAppropriateCode.
     *
     * @return the code, never returns null
     */
    private Code getAppropriateCodeInternal(Class c) {
        synchronized (cache_) {
            Code co = map_.get(c);
            if (co != null)
                return co;
            co = getAppropriateCode(c);
            if (co == null)
                co = noCode;
            map_.put(c, co);
            return co;
        }
    }

    /**
     * Find the appropriate Code to call in the map. If no code is found, return
     * null. This lookup strategy first attempts to find a visit method defined
     * for the parent classes of c. If no such method exists, it attempts to
     * find an unambiguous visit method matching any interface transitively
     * implemented by c. If that does not exist either, null is returned. If
     * only an ambiguous visit method exists, an exception is raised.
     *
     * @param c the class for which to find the code
     * @return the code to run, or null if no code was found
     * @throws RunaboutException if the lookup would be ambiguous
     */
    private Code getAppropriateCode(Class c) {
        if (c.isArray()) {
            // uh uh, array subtyping in action...
            int dims = 1;
            Class component = c.getComponentType();
            while (component.isArray()) {
                dims++;
                component = component.getComponentType();
            }
            Class superComp = component.getSuperclass();
            while (superComp != null) {
                Code co = map_.get(Array.newInstance(superComp, new int[dims]).getClass());
                if (co != null)
                    return co;
                superComp = superComp.getSuperclass();
            }
            // now try subtyping with multi-dimensional Object[]
            // (see crazy runabout test).
            Class objectClass = c.getSuperclass();
            while (dims > 1) {
                Code co = map_.get(Array.newInstance(objectClass, new int[dims]).getClass());
                if (co != null)
                    return co;
                dims--;
            }
        }
        Class cl = c.getSuperclass();
        while (cl != null) {
            Code co = map_.get(cl);
            if (co != null)
                return co;
            cl = cl.getSuperclass();
        }
        return getAppropriateCode_ifc(c, c);
    }

    /**
     * Find the appropriate Code to call in the map. If no code is found, return
     * null.
     *
     * @param c  the class for which to find the code
     * @param cl the class where to start looking from
     * @return the code to run, or null if no code was found
     */
    private Code getAppropriateCode_ifc(Class c, Class cl) {
        Code co = null;
        while (cl != null) {
            Class[] ifc = cl.getInterfaces();
            for (Class anIfc : ifc) {
                Code r = map_.get(anIfc);
                if (r != null) {
                    if ((co != null) && (r != co))
                        throw new RunaboutException("Ambiguous resolution for visit call to "
                                + c + " in " + this.getClass().getName());
                    co = r;
                }
            }
            for (Class anIfc : ifc) {
                Code r = getAppropriateCode_ifc(c, anIfc);
                if (r != null) {
                    if ((co != null) && (r != co))
                        throw new RunaboutException("Ambiguous resolution for visit call to "
                                + c + " in " + this.getClass().getName());
                    co = r;
                }
            }
            cl = cl.getSuperclass();
        }
        return co;
    }

    /**
     * Generate the initial version of the map that maps classes to Code to call
     * the appropriate visit method.
     */
    final HashMap<Class, Runabout.Code> makeMap() {
        // get number of methods
        int size = 0;
        Class me = this.getClass();
        while (me != null) {
            size += me.getDeclaredMethods().length;
            me = me.getSuperclass();
        }
        // create map with slight over-estimate
        HashMap<Class, Runabout.Code> result = new HashMap<Class, Runabout.Code>(size * 2);
        // for all methods - create call code, put
        me = this.getClass();
        while (me != null) {
            Method[] methods = me.getDeclaredMethods();
            for (final Method m : methods) {
                if ((m.getName().equals("visit"))
                        && (!Modifier.isStatic(m.getModifiers()))) {
                    Class[] args = m.getParameterTypes();
                    if (args.length != 1)
                        throw new RunaboutException("Invalid number of arguments for Runabout in method "
                                + m);
                    final Class viC = args[0];
                    if (result.get(viC) != null)
                        continue;
                    Code co;
                    if (isPublic) {
                        // invoke the visitor with generated code
                        co = makeCode(viC);
                        if (co == null) {
                            throw new RunaboutException("Could not create/load dynamic code!");
                        }
                    } else {
                        if (!m.isAccessible()) {
                            m.setAccessible(true);
                        }
                        // invoke the visitor with an anonymous inner class,
                        // allows for the runabout to be public as the method made accessible
                        // by the Method instance.
                        // For Java 7+ the performance of this could be improved by using a MethodHandle
                        co = new Code() {
                            @Override
                            public void visit(Runabout r, Object o) {
                                try {

                                    m.invoke(r, o);
                                } catch (IllegalAccessException e) {
                                    throw new RunaboutException(e.toString());
                                } catch (InvocationTargetException e) {
                                    System.err.println("stacktrace:");
                                    e.getCause().printStackTrace(System.err);
                                    throw new RunaboutException(e.getCause().toString());
                                }
                            }
                        };
                    }

                    result.put(viC, co);

                }
            }
            me = me.getSuperclass();
        }
        return result;
    }

    /**
     * Create the code to invoke a visit method.
     *
     * @param c the type of the argument to the visit method
     */
    private Code makeCode(Class c) {
        byte[] myName // Lovm/util/RunaboutExample; substitute
                = canonicalName(getClass(), false);
        final int myNameLen = myName.length;
        final int myNameLenM2 = myNameLen - 2;
        byte[] cName // Ljava/lang/String; substitute
                = canonicalName(c, false);
        byte[] cNameCast = canonicalName(c, true);
        final int cNameLen = cName.length;
        final int cNameLenCast = cNameCast.length - 2;
        byte[] code = new byte[genCodeTemplate.length - 62 + myNameLenM2
                + cNameLenCast + cNameLen];

        // Build code by substituting a few strings in genCodeTemplate.
        // 117-145: org/grothoff/RunaboutExample => myName
        // 148-164: java/lang/String => cName
        // 192-200: XXXXXXXX => number
        // 250-271: (Ljava/lang/String;)V => "("+cName+")V"

        System.arraycopy(genCodeTemplate, 0, code, 0, 115);
        code[115] = (byte) ((myNameLenM2) >> 8);
        code[116] = (byte) ((myNameLenM2) & 255);
        System.arraycopy(myName, 1, code, 117, myNameLenM2);
        code[117 + myNameLenM2] = 1; // tag for string
        code[118 + myNameLenM2] = (byte) ((cNameLenCast) >> 8);
        code[119 + myNameLenM2] = (byte) ((cNameLenCast) & 255);
        System.arraycopy(cNameCast, 1, code, 120 + myNameLenM2, cNameLenCast);
        System.arraycopy(genCodeTemplate, 164, code, 120 + myNameLenM2
                + cNameLenCast, 248 - 164);
        code[120 + myNameLenM2 + cNameLenCast + 248 - 164] = (byte) ((cNameLen + 3) >> 8);
        code[120 + myNameLenM2 + cNameLenCast + 249 - 164] = (byte) ((cNameLen + 3) & 255);
        code[120 + myNameLenM2 + cNameLenCast + 250 - 164] = (byte) '(';
        System.arraycopy(cName, 0, code, 120 + myNameLenM2 + cNameLenCast + 251
                - 164, cNameLen);
        code[120 + myNameLenM2 + cNameLenCast + 250 - 164 + cNameLen + 1] = (byte) ')';
        code[120 + myNameLenM2 + cNameLenCast + 250 - 164 + cNameLen + 2] = (byte) 'V';
        System.arraycopy(genCodeTemplate,
                271,
                code,
                120 + myNameLenM2 + cNameLenCast + 250 - 164
                        + cNameLen + 3,
                genCodeTemplate.length - 271);
        return cache_.loadCode(code, 120 + myNameLenM2 + cNameLenCast + 192
                - 164);
    }

    /**
     * Get the class name in canonical form.
     *
     * @param cls the class, may not be primitive
     * @return the ovm name, following the convention of
     *         {@code java.util.Class.forName} according to the JavaDoc
     *         specification (JDK 1.2.2/1.3/1.4) which differs from the actual
     *         implementation in both SUN and IBM VMs.
     */
    private static byte[] canonicalName(Class cls, boolean forCast) {
        String cname = cls.getName();
        try {
            byte[] utf = cname.getBytes("UTF-8");
            int len = utf.length; // may be > cname.length()!
            if ((cname.charAt(0) != '[') || (forCast)) {
                byte[] ret = new byte[len + 2];
                ret[0] = (byte) 'L';
                System.arraycopy(utf, 0, ret, 1, len);
                ret[len + 1] = (byte) ';';
                for (int i = len; i > 0; i--)
                    if (ret[i] == (byte) '.')
                        ret[i] = (byte) '/';
                return ret;
            }
            for (int i = len - 1; i >= 0; i--)
                if (utf[i] == (byte) '.')
                    utf[i] = (byte) '/';
            return utf;
        } catch (UnsupportedEncodingException uee) {
            throw new RunaboutException("UTF8 encoding not supported!?: " + uee);
        }
    }

    /**
     * The Runabout.Cache is essentially a per-class cache of the internal
     * constant state of a Runabout instance. It contains the generated code to
     * quicly invoke the appropriate visit methods.
     *
     * @author Christian Grothoff
     */
    static final class Cache {

        /**
         * ClassLoader to use to load the code.
         */
        private final ClassLoader loader_;

        /**
         * Last name used by the class loader.
         */
        private final byte[] lastName_;

        /**
         * Mapping of classes to Maps.
         */
        private final HashMap<Class, HashMap<Class, Runabout.Code>> cachemap_;

        /**
         * Code that the loader should use.
         */
        byte[] code;

        /**
         * Create the Cache.
         */
        Cache() {
            loader_ = new ClassLoader() {
                public Class<?> loadClass(String name)
                        throws ClassNotFoundException {
                    //noinspection StringEquality
                    if (name == "Code") // == works here, as both strings are guaranteed to be interned
                        return defineClass(null, code, 0, code.length);
                    return Thread.currentThread().getContextClassLoader().loadClass(name);
                }
            };
            cachemap_ = new HashMap<Class, HashMap<Class, Runabout.Code>>();
            lastName_ = new byte[8];
            for (int i = 0; i < 8; i++)
                lastName_[i] = (byte) '0';
        }

        /**
         * Create a class from the given bytecode. Since classes loaded by the
         * same Loader must have a unique name, this method patches the bytecode
         * at the given offset, changing the next 8 characters to a unique Java
         * classname.
         *
         * @param byteCode the bytecode of the class which must describe a class
         *                 of type 'Code'. The class must contain a sequence XXXXXXXX at
         *                 offset xIdx where the classname is to be patched
         * @param xIdx     the index of the XXXXXXXX sequence
         * @return an instance of the loaded class, null on error
         * @throws ArrayIndexOutOfBoundsException if more than 62<sup>8</sup>
         *                                        classes are loaded :-)
         * @throws RunaboutException              if there are problems with dynamic loading
         *                                        of the byteCode
         */
        Code loadCode(byte[] byteCode, int xIdx) {
            boolean overflow = true;
            int index = 7;
            while (overflow) {
                overflow = false;
                lastName_[index]++;
                if (lastName_[index] == (byte) ('9' + 1))
                    lastName_[index] = (byte) 'A';
                if (lastName_[index] == (byte) ('Z' + 1))
                    lastName_[index] = (byte) 'a';
                if (lastName_[index] == (byte) ('z' + 1)) {
                    lastName_[index] = (byte) '0';
                    overflow = true;
                    index--;
                }
            }
            System.arraycopy(lastName_, 0, byteCode, xIdx, 8);
            code = byteCode;

            Code co;
            try {
                co = (Code) loader_.loadClass("Code").newInstance();
            } catch (InstantiationException ie) {
                throw new RunaboutException(ie.toString());
            } catch (ClassNotFoundException cnfe) {
                throw new RunaboutException(cnfe.toString());
            } catch (IllegalArgumentException iae) {
                throw new RunaboutException(iae.toString());
            } catch (ClassFormatError cfe) {
                throw new RunaboutException(cfe.toString());
            } catch (IllegalAccessException iae) {
                throw new RunaboutException(iae.toString());
            }
            code = null; // help GC
            return co;
        }

        /**
         * Obtain a map from the cache.
         */
        synchronized HashMap<Class, Runabout.Code> get(Runabout r) {
            Class c = r.getClass();
            HashMap<Class, Runabout.Code> map = cachemap_.get(c);
            if (map == null) {
                map = r.makeMap();
                cachemap_.put(c, map);
            }
            return map;
        }

    } // end of Runabout.Cache

    /**
     * Code is the generic interface that all generated classes implement. It is
     * used to quickly map a given class to the appropriate visit method.
     *
     * @author Christian Grothoff
     */
    public static abstract class Code {
        public Code() {
        }

        public abstract void visit(Runabout r, Object o);

    } // end of Runabout.Code

    /**
     * Implementation of Code that is called if no visit method matches (calls
     * visitDefault).
     *
     * @author Christian Grothoff
     */
    static final class NoCode extends Code {
        public final void visit(Runabout r, Object o) {
            r.visitDefault(o);
        }
    } // end of Runabout.NoCode

    /**
     * Override this method to provide a default behavior when no other visit
     * matches. The Runabout semantics are to search for a visit(X) and if there
     * is no match, call visitDefault(). As usual with the Runabout, visit(X)
     * looks at classes before interfaces. By default, visitDefault throws an
     * exception.
     */
    protected void visitDefault(Object o) {
        throw new RunaboutException("No visit method defined in "
                + this.getClass() + " for " + o.getClass());
    }

    /**
     * Generic Exception for problems in the Runabout.
     *
     * @author Christian Grothoff
     */
    public static final class RunaboutException extends RuntimeException {
        RunaboutException(String s) {
            super(s);
        }
    }

    /**
     * Compile 'GenCodeXXXXXXXX.java' with the option '-g:none' to tell javac
     * not to include any debugging information. This is the generated class
     * file.
     */
    private final static byte genCodeTemplate[] = {-54, -2, -70, -66, 0, 0, 0,
            49, 0, 22, 10, 0, 6, 0, 12, 7, 0, 13, 7, 0, 14, 10, 0, 2, 0, 15, 7,
            0, 16, 7, 0, 18, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40,
            41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 5, 118, 105, 115, 105,
            116, 1, 0, 44, 40, 76, 111, 114, 103, 47, 103, 114, 111, 116, 104,
            111, 102, 102, 47, 82, 117, 110, 97, 98, 111, 117, 116, 59, 76,
            106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99,
            116, 59, 41, 86, 12, 0, 7, 0, 8, 1, 0, 28, 111, 114, 103, 47, 103,
            114, 111, 116, 104, 111, 102, 102, 47, 82, 117, 110, 97, 98, 111,
            117, 116, 69, 120, 97, 109, 112, 108, 101, 1, 0, 16, 106, 97, 118,
            97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 12, 0,
            10, 0, 20, 1, 0, 28, 111, 114, 103, 47, 103, 114, 111, 116, 104,
            111, 102, 102, 47, 71, 101, 110, 67, 111, 100, 101, 88, 88, 88, 88,
            88, 88, 88, 88, 7, 0, 21, 1, 0, 26, 111, 114, 103, 47, 103, 114,
            111, 116, 104, 111, 102, 102, 47, 82, 117, 110, 97, 98, 111, 117,
            116, 36, 67, 111, 100, 101, 1, 0, 12, 73, 110, 110, 101, 114, 67,
            108, 97, 115, 115, 101, 115, 1, 0, 21, 40, 76, 106, 97, 118, 97,
            47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86,
            1, 0, 21, 111, 114, 103, 47, 103, 114, 111, 116, 104, 111, 102,
            102, 47, 82, 117, 110, 97, 98, 111, 117, 116, 0, 33, 0, 5, 0, 6, 0,
            0, 0, 0, 0, 2, 0, 1, 0, 7, 0, 8, 0, 1, 0, 9, 0, 0, 0, 17, 0, 1, 0,
            1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 0, 0, 1, 0, 10, 0, 11,
            0, 1, 0, 9, 0, 0, 0, 24, 0, 2, 0, 3, 0, 0, 0, 12, 43, -64, 0, 2,
            44, -64, 0, 3, -74, 0, 4, -79, 0, 0, 0, 0, 0, 1, 0, 19, 0, 0, 0,
            10, 0, 1, 0, 6, 0, 17, 0, 9, 4, 9}; // GenCodeXXXXXXXX.class

    public static void main(String[] args) {
        Runabout r = new Runabout() {
            public void visit(String s) {
                System.out.println("hi!!");
            }
        };
        r.visitAppropriate("foo");
    }

} // end of Runabout