From 9ea7ecf9ee23a90959431b402ffefd409e8270bd Mon Sep 17 00:00:00 2001 From: Tim Claridge Date: Sun, 5 Jul 2026 19:56:40 +1200 Subject: [PATCH] WIP checkpoint: Phase 5(b) narya streamer codegen for ART field-order Pre-crash safety checkpoint (box rebooted mid-implementation; NOT verified complete). GenStreamableTask flatten-mode + GenStreamUtil runtime helper + ObjectInputStream changes; explicit read/writeObject instrumentation across presents net + dobj event classes so ART matches HotSpot's ClassUtil.getFields order without the global sort flag (no data migration). narya 1.21. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut --- core/pom.xml | 2 +- .../java/com/threerings/io/GenStreamUtil.java | 107 +++++++++ .../com/threerings/io/ObjectInputStream.java | 2 +- .../presents/dobj/AttributeChangedEvent.java | 20 ++ .../presents/dobj/CompoundEvent.java | 18 ++ .../presents/dobj/ElementUpdatedEvent.java | 22 ++ .../presents/dobj/EntryAddedEvent.java | 20 ++ .../presents/dobj/EntryRemovedEvent.java | 20 ++ .../presents/dobj/EntryUpdatedEvent.java | 20 ++ .../dobj/InvocationNotificationEvent.java | 22 ++ .../presents/dobj/InvocationRequestEvent.java | 22 ++ .../dobj/InvocationResponseEvent.java | 22 ++ .../presents/dobj/MessageEvent.java | 20 ++ .../presents/dobj/ObjectAddedEvent.java | 20 ++ .../presents/dobj/ObjectDestroyedEvent.java | 16 ++ .../presents/dobj/ObjectRemovedEvent.java | 20 ++ .../presents/dobj/ReleaseLockEvent.java | 18 ++ .../presents/dobj/ServerMessageEvent.java | 20 ++ .../presents/net/AESAuthRequest.java | 12 +- .../threerings/presents/net/AuthRequest.java | 19 +- .../presents/net/FailureResponse.java | 20 ++ .../threerings/presents/net/PingRequest.java | 4 +- .../threerings/presents/net/PongResponse.java | 8 +- pom.xml | 2 +- tools/pom.xml | 2 +- .../presents/tools/GenStreamableTask.java | 208 ++++++++++++++++++ .../com/threerings/presents/tools.properties | 1 + 27 files changed, 676 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/com/threerings/io/GenStreamUtil.java diff --git a/core/pom.xml b/core/pom.xml index ef588ddfc..261add1a4 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -4,7 +4,7 @@ com.threerings narya-parent - 1.20 + 1.21 narya diff --git a/core/src/main/java/com/threerings/io/GenStreamUtil.java b/core/src/main/java/com/threerings/io/GenStreamUtil.java new file mode 100644 index 000000000..ad3b3fb46 --- /dev/null +++ b/core/src/main/java/com/threerings/io/GenStreamUtil.java @@ -0,0 +1,107 @@ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2025 Three Rings Design, Inc., All Rights Reserved +// https://github.com/threerings/narya/blob/master/LICENSE + +package com.threerings.io; + +import java.lang.reflect.Field; +import java.util.concurrent.ConcurrentHashMap; + +import java.io.IOException; + +/** + * Runtime support for the explicit {@code readObject}/{@code writeObject} methods emitted by {@link + * com.threerings.presents.tools.GenStreamableTask} in "flatten" mode. + * + *

The default reflective {@link Streamer} streams a class's fields in {@link + * com.samskivert.util.ClassUtil#getFields} order, which is superclass-first but relies on + * {@link Class#getDeclaredFields} within each class. That per-class order is not defined by + * the JVM spec and differs between HotSpot (server/desktop) and ART (Android) — so a default + * streamed object written by one and read by the other misaligns. The generated methods stream each + * field explicitly, in an order captured from HotSpot at build time, by calling {@link #readField}/ + * {@link #writeField} here. Because those methods delegate to the very same {@link FieldMarshaller} + * the reflective streamer uses, the produced bytes are identical to the legacy reflective form on + * HotSpot (so existing persisted boards and DB blobs read unchanged, and desktop↔server is + * unaffected) while being deterministic on ART. + */ +public final class GenStreamUtil +{ + /** + * Reads the named field of {@code target} (declared by {@code declaringClass}) from the stream, + * using the same {@link FieldMarshaller} the reflective streamer would use. + */ + public static void readField ( + Class declaringClass, String name, Object target, ObjectInputStream in) + throws IOException, ClassNotFoundException + { + Entry entry = entry(declaringClass, name); + try { + entry.marshaller.readField(entry.field, target, in); + } catch (IOException ioe) { + throw ioe; + } catch (ClassNotFoundException cnfe) { + throw cnfe; + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new IOException("Failure reading field [class=" + declaringClass.getName() + + ", field=" + name + "]", e); + } + } + + /** + * Writes the named field of {@code source} (declared by {@code declaringClass}) to the stream, + * using the same {@link FieldMarshaller} the reflective streamer would use. + */ + public static void writeField ( + Class declaringClass, String name, Object source, ObjectOutputStream out) + throws IOException + { + Entry entry = entry(declaringClass, name); + try { + entry.marshaller.writeField(entry.field, source, out); + } catch (IOException ioe) { + throw ioe; + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new IOException("Failure writing field [class=" + declaringClass.getName() + + ", field=" + name + "]", e); + } + } + + protected static Entry entry (Class declaringClass, String name) + { + String key = declaringClass.getName() + "#" + name; + Entry entry = _cache.get(key); + if (entry == null) { + Field field; + try { + field = declaringClass.getDeclaredField(name); + } catch (NoSuchFieldException nsfe) { + throw new RuntimeException("Missing streamed field [class=" + + declaringClass.getName() + ", field=" + name + "]", nsfe); + } + field.setAccessible(true); + entry = new Entry(field, FieldMarshaller.getFieldMarshaller(field)); + _cache.put(key, entry); + } + return entry; + } + + protected static final class Entry + { + public final Field field; + public final FieldMarshaller marshaller; + public Entry (Field field, FieldMarshaller marshaller) { + this.field = field; + this.marshaller = marshaller; + } + } + + /** Caches the resolved (accessible) field + its marshaller, keyed by declaring class + name. */ + protected static final ConcurrentHashMap _cache = new ConcurrentHashMap(); + + private GenStreamUtil () {} +} diff --git a/core/src/main/java/com/threerings/io/ObjectInputStream.java b/core/src/main/java/com/threerings/io/ObjectInputStream.java index 6f02c8037..7847b96f4 100644 --- a/core/src/main/java/com/threerings/io/ObjectInputStream.java +++ b/core/src/main/java/com/threerings/io/ObjectInputStream.java @@ -412,5 +412,5 @@ public class ObjectInputStream extends DataInputStream protected static volatile List _allowedPrefixes; /** Used to activate verbose debug logging. */ - protected static final boolean STREAM_DEBUG = false; + protected static final boolean STREAM_DEBUG = Boolean.getBoolean("com.threerings.io.streamDebug"); } diff --git a/core/src/main/java/com/threerings/presents/dobj/AttributeChangedEvent.java b/core/src/main/java/com/threerings/presents/dobj/AttributeChangedEvent.java index 55018da9f..5be7ce87b 100644 --- a/core/src/main/java/com/threerings/presents/dobj/AttributeChangedEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/AttributeChangedEvent.java @@ -165,4 +165,24 @@ public class AttributeChangedEvent extends NamedEvent protected Object _value; protected transient Object _oldValue = UNSET_OLD_VALUE; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.AttributeChangedEvent.class, "_value", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.AttributeChangedEvent.class, "_value", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/CompoundEvent.java b/core/src/main/java/com/threerings/presents/dobj/CompoundEvent.java index 68095279d..27c6ec633 100644 --- a/core/src/main/java/com/threerings/presents/dobj/CompoundEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/CompoundEvent.java @@ -173,4 +173,22 @@ public class CompoundEvent extends DEvent /** A list of the events associated with this compound event. */ protected StreamableArrayList _events; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.CompoundEvent.class, "_events", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.CompoundEvent.class, "_events", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/ElementUpdatedEvent.java b/core/src/main/java/com/threerings/presents/dobj/ElementUpdatedEvent.java index 92eebc00b..0a83caf84 100644 --- a/core/src/main/java/com/threerings/presents/dobj/ElementUpdatedEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/ElementUpdatedEvent.java @@ -178,4 +178,26 @@ public class ElementUpdatedEvent extends NamedEvent protected Object _value; protected int _index; protected transient Object _oldValue = UNSET_OLD_VALUE; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.ElementUpdatedEvent.class, "_value", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.ElementUpdatedEvent.class, "_index", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.ElementUpdatedEvent.class, "_value", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.ElementUpdatedEvent.class, "_index", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/EntryAddedEvent.java b/core/src/main/java/com/threerings/presents/dobj/EntryAddedEvent.java index 73a3e1488..34f355eca 100644 --- a/core/src/main/java/com/threerings/presents/dobj/EntryAddedEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/EntryAddedEvent.java @@ -107,4 +107,24 @@ public class EntryAddedEvent extends EntryEvent /** Used when this event is generated on the authoritative server where object changes are made * immediately. This lets us know not to apply ourselves when we're actually dispatched. */ protected transient boolean _alreadyApplied; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.EntryAddedEvent.class, "_entry", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.EntryAddedEvent.class, "_entry", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/EntryRemovedEvent.java b/core/src/main/java/com/threerings/presents/dobj/EntryRemovedEvent.java index 100add072..44752b5ea 100644 --- a/core/src/main/java/com/threerings/presents/dobj/EntryRemovedEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/EntryRemovedEvent.java @@ -109,4 +109,24 @@ public class EntryRemovedEvent extends EntryEvent @SuppressWarnings("unchecked") protected transient T _oldEntry = (T)UNSET_OLD_ENTRY; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.EntryRemovedEvent.class, "_key", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.EntryRemovedEvent.class, "_key", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/EntryUpdatedEvent.java b/core/src/main/java/com/threerings/presents/dobj/EntryUpdatedEvent.java index 9f7a11ddb..95025b552 100644 --- a/core/src/main/java/com/threerings/presents/dobj/EntryUpdatedEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/EntryUpdatedEvent.java @@ -112,4 +112,24 @@ public class EntryUpdatedEvent extends EntryEvent @SuppressWarnings("unchecked") protected transient T _oldEntry = (T)UNSET_OLD_ENTRY; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.EntryUpdatedEvent.class, "_entry", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.EntryUpdatedEvent.class, "_entry", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/InvocationNotificationEvent.java b/core/src/main/java/com/threerings/presents/dobj/InvocationNotificationEvent.java index 314d3a0f8..6e7f97f39 100644 --- a/core/src/main/java/com/threerings/presents/dobj/InvocationNotificationEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/InvocationNotificationEvent.java @@ -89,4 +89,26 @@ public class InvocationNotificationEvent extends DEvent /** The arguments to the receiver method being invoked. */ protected Object[] _args; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.InvocationNotificationEvent.class, "_receiverId", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.InvocationNotificationEvent.class, "_methodId", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.InvocationNotificationEvent.class, "_args", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.InvocationNotificationEvent.class, "_receiverId", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.InvocationNotificationEvent.class, "_methodId", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.InvocationNotificationEvent.class, "_args", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/InvocationRequestEvent.java b/core/src/main/java/com/threerings/presents/dobj/InvocationRequestEvent.java index 88ff249ac..c2a74ace9 100644 --- a/core/src/main/java/com/threerings/presents/dobj/InvocationRequestEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/InvocationRequestEvent.java @@ -89,4 +89,26 @@ public class InvocationRequestEvent extends DEvent /** The arguments to the method being invoked. */ protected Object[] _args; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.InvocationRequestEvent.class, "_invCode", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.InvocationRequestEvent.class, "_methodId", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.InvocationRequestEvent.class, "_args", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.InvocationRequestEvent.class, "_invCode", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.InvocationRequestEvent.class, "_methodId", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.InvocationRequestEvent.class, "_args", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/InvocationResponseEvent.java b/core/src/main/java/com/threerings/presents/dobj/InvocationResponseEvent.java index 43b38b836..aa7dd0c31 100644 --- a/core/src/main/java/com/threerings/presents/dobj/InvocationResponseEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/InvocationResponseEvent.java @@ -88,4 +88,26 @@ public class InvocationResponseEvent extends DEvent /** The arguments to the method being invoked. */ protected Object[] _args; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.InvocationResponseEvent.class, "_requestId", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.InvocationResponseEvent.class, "_methodId", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.InvocationResponseEvent.class, "_args", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.InvocationResponseEvent.class, "_requestId", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.InvocationResponseEvent.class, "_methodId", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.InvocationResponseEvent.class, "_args", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/MessageEvent.java b/core/src/main/java/com/threerings/presents/dobj/MessageEvent.java index 219d6e764..b68342652 100644 --- a/core/src/main/java/com/threerings/presents/dobj/MessageEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/MessageEvent.java @@ -74,4 +74,24 @@ public class MessageEvent extends NamedEvent } protected Object[] _args; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.MessageEvent.class, "_args", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.MessageEvent.class, "_args", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/ObjectAddedEvent.java b/core/src/main/java/com/threerings/presents/dobj/ObjectAddedEvent.java index 4ef8a31f2..c13912087 100644 --- a/core/src/main/java/com/threerings/presents/dobj/ObjectAddedEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/ObjectAddedEvent.java @@ -78,4 +78,24 @@ public class ObjectAddedEvent extends NamedEvent protected int _oid; protected transient boolean _alreadyApplied; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.ObjectAddedEvent.class, "_oid", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.ObjectAddedEvent.class, "_oid", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/ObjectDestroyedEvent.java b/core/src/main/java/com/threerings/presents/dobj/ObjectDestroyedEvent.java index 947abb0d5..00148497f 100644 --- a/core/src/main/java/com/threerings/presents/dobj/ObjectDestroyedEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/ObjectDestroyedEvent.java @@ -47,4 +47,20 @@ public class ObjectDestroyedEvent extends DEvent buf.append("DESTROY:"); super.toString(buf); } + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/ObjectRemovedEvent.java b/core/src/main/java/com/threerings/presents/dobj/ObjectRemovedEvent.java index 62e294033..cc05d72f2 100644 --- a/core/src/main/java/com/threerings/presents/dobj/ObjectRemovedEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/ObjectRemovedEvent.java @@ -78,4 +78,24 @@ public class ObjectRemovedEvent extends NamedEvent protected int _oid; protected transient boolean _alreadyApplied; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.ObjectRemovedEvent.class, "_oid", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.ObjectRemovedEvent.class, "_oid", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/ReleaseLockEvent.java b/core/src/main/java/com/threerings/presents/dobj/ReleaseLockEvent.java index 3e557f859..dd76121ee 100644 --- a/core/src/main/java/com/threerings/presents/dobj/ReleaseLockEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/ReleaseLockEvent.java @@ -55,4 +55,22 @@ public class ReleaseLockEvent extends NamedEvent buf.append("UNLOCK:"); super.toString(buf); } + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/dobj/ServerMessageEvent.java b/core/src/main/java/com/threerings/presents/dobj/ServerMessageEvent.java index 2e9b6cc4a..956c99720 100644 --- a/core/src/main/java/com/threerings/presents/dobj/ServerMessageEvent.java +++ b/core/src/main/java/com/threerings/presents/dobj/ServerMessageEvent.java @@ -31,4 +31,24 @@ public class ServerMessageEvent extends MessageEvent // this is what makes us server-only return true; } + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.DEvent.class, "_toid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.dobj.MessageEvent.class, "_args", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.DEvent.class, "_toid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.NamedEvent.class, "_name", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.dobj.MessageEvent.class, "_args", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/net/AESAuthRequest.java b/core/src/main/java/com/threerings/presents/net/AESAuthRequest.java index aaae81d2f..82edc9fc7 100644 --- a/core/src/main/java/com/threerings/presents/net/AESAuthRequest.java +++ b/core/src/main/java/com/threerings/presents/net/AESAuthRequest.java @@ -113,7 +113,11 @@ public class AESAuthRequest extends AuthRequest public void writeObject (ObjectOutputStream out) throws IOException { - out.defaultWriteObject(); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.UpstreamMessage.class, "messageId", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.AuthRequest.class, "_creds", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.AuthRequest.class, "_version", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.AuthRequest.class, "_zone", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.AuthRequest.class, "_bootGroups", this, out); ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); ObjectOutputStream oOut = new ObjectOutputStream(byteOut); oOut.writeObject(_clearCreds); @@ -136,7 +140,11 @@ public class AESAuthRequest extends AuthRequest public void readObject (ObjectInputStream in) throws IOException, ClassNotFoundException { - in.defaultReadObject(); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.UpstreamMessage.class, "messageId", this, in); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.AuthRequest.class, "_creds", this, in); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.AuthRequest.class, "_version", this, in); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.AuthRequest.class, "_zone", this, in); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.AuthRequest.class, "_bootGroups", this, in); _contents = new byte[in.readInt()]; in.read(_contents); } diff --git a/core/src/main/java/com/threerings/presents/net/AuthRequest.java b/core/src/main/java/com/threerings/presents/net/AuthRequest.java index e7dac1cb1..3f3c63862 100644 --- a/core/src/main/java/com/threerings/presents/net/AuthRequest.java +++ b/core/src/main/java/com/threerings/presents/net/AuthRequest.java @@ -89,7 +89,11 @@ public class AuthRequest extends UpstreamMessage throws IOException, ClassNotFoundException { try { - in.defaultReadObject(); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.UpstreamMessage.class, "messageId", this, in); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.AuthRequest.class, "_creds", this, in); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.AuthRequest.class, "_version", this, in); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.AuthRequest.class, "_zone", this, in); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.AuthRequest.class, "_bootGroups", this, in); } catch (IOException ioe) { // if we fail here because the client is old, leave ourselves with a partially // initialized set of credentials, which the server will generally cope with by telling @@ -108,4 +112,17 @@ public class AuthRequest extends UpstreamMessage /** The set of bootstrap service groups this client is interested in. */ protected String[] _bootGroups; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.UpstreamMessage.class, "messageId", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.AuthRequest.class, "_creds", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.AuthRequest.class, "_version", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.AuthRequest.class, "_zone", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.AuthRequest.class, "_bootGroups", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/net/FailureResponse.java b/core/src/main/java/com/threerings/presents/net/FailureResponse.java index 4702bcc33..f357add24 100644 --- a/core/src/main/java/com/threerings/presents/net/FailureResponse.java +++ b/core/src/main/java/com/threerings/presents/net/FailureResponse.java @@ -45,4 +45,24 @@ public class FailureResponse extends DownstreamMessage protected int _oid; protected String _message; + + // AUTO-GENERATED: METHODS START + // from interface Streamable + public void readObject (com.threerings.io.ObjectInputStream ins) + throws java.io.IOException, java.lang.ClassNotFoundException + { + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.DownstreamMessage.class, "messageId", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.FailureResponse.class, "_oid", this, ins); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.FailureResponse.class, "_message", this, ins); + } + + // from interface Streamable + public void writeObject (com.threerings.io.ObjectOutputStream out) + throws java.io.IOException + { + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.DownstreamMessage.class, "messageId", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.FailureResponse.class, "_oid", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.FailureResponse.class, "_message", this, out); + } + // AUTO-GENERATED: METHODS END } diff --git a/core/src/main/java/com/threerings/presents/net/PingRequest.java b/core/src/main/java/com/threerings/presents/net/PingRequest.java index 77b207bb4..3a4b78447 100644 --- a/core/src/main/java/com/threerings/presents/net/PingRequest.java +++ b/core/src/main/java/com/threerings/presents/net/PingRequest.java @@ -63,7 +63,7 @@ public class PingRequest extends UpstreamMessage // network _packStamp = System.currentTimeMillis(); - out.defaultWriteObject(); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.UpstreamMessage.class, "messageId", this, out); } /** @@ -76,7 +76,7 @@ public class PingRequest extends UpstreamMessage // the network _unpackStamp = System.currentTimeMillis(); - in.defaultReadObject(); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.UpstreamMessage.class, "messageId", this, in); } @Override diff --git a/core/src/main/java/com/threerings/presents/net/PongResponse.java b/core/src/main/java/com/threerings/presents/net/PongResponse.java index cca1ac6d3..6ed7a45fa 100644 --- a/core/src/main/java/com/threerings/presents/net/PongResponse.java +++ b/core/src/main/java/com/threerings/presents/net/PongResponse.java @@ -83,7 +83,9 @@ public class PongResponse extends DownstreamMessage _processDelay = (int)(_packStamp - _pingStamp); } - out.defaultWriteObject(); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.DownstreamMessage.class, "messageId", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.PongResponse.class, "_packStamp", this, out); + com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.net.PongResponse.class, "_processDelay", this, out); } /** @@ -96,7 +98,9 @@ public class PongResponse extends DownstreamMessage // the network _unpackStamp = System.currentTimeMillis(); - in.defaultReadObject(); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.DownstreamMessage.class, "messageId", this, in); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.PongResponse.class, "_packStamp", this, in); + com.threerings.io.GenStreamUtil.readField(com.threerings.presents.net.PongResponse.class, "_processDelay", this, in); } @Override diff --git a/pom.xml b/pom.xml index a81c4cb73..a027437b0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.threerings narya-parent pom - 1.20 + 1.21 Narya Parent Facilities for making networked multiplayer games. diff --git a/tools/pom.xml b/tools/pom.xml index cf8b22459..b4f1e55ad 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -4,7 +4,7 @@ com.threerings narya-parent - 1.20 + 1.21 narya-tools diff --git a/tools/src/main/java/com/threerings/presents/tools/GenStreamableTask.java b/tools/src/main/java/com/threerings/presents/tools/GenStreamableTask.java index 84f765667..bd70c4a8b 100644 --- a/tools/src/main/java/com/threerings/presents/tools/GenStreamableTask.java +++ b/tools/src/main/java/com/threerings/presents/tools/GenStreamableTask.java @@ -6,16 +6,24 @@ package com.threerings.presents.tools; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.types.FileSet; import com.google.common.collect.Lists; +import com.samskivert.util.ClassUtil; + +import com.threerings.io.NotStreamable; import com.threerings.io.Streamable; import com.threerings.presents.dobj.DObject; @@ -36,6 +44,21 @@ public class GenStreamableTask extends GenTask _filesets.add(set); } + /** + * If set, generate self-contained declaration-ordered {@code read/writeObject} methods + * that stream the entire flattened field set (in {@link ClassUtil#getFields} order, captured on + * HotSpot at build time) via {@link com.threerings.io.GenStreamUtil}, rather than the legacy + * per-class {@code super.read/writeObject(...)} + local-fields form. In this mode {@link DObject} + * subclasses are processed too, and a pass-through {@code readObject}/{@code writeObject} (one + * that calls {@code defaultReadObject}/{@code defaultWriteObject}) has that call replaced in + * place with the explicit field streaming (preserving any surrounding logic). This is what makes + * Streamables deterministic on ART (Android) while staying byte-identical on HotSpot. See {@link + * com.threerings.io.GenStreamUtil}. + */ + public void setFlatten (boolean flatten) { + _flatten = flatten; + } + @Override public void execute () { @@ -86,6 +109,11 @@ public class GenStreamableTask extends GenTask protected void processClass (File source, Class sclass) throws IOException { + if (_flatten) { + processFlatten(source, sclass); + return; + } + StreamableClassRequirements reqs = new StreamableClassRequirements(sclass); // we must implement Streamable, not be a DObject and have some fields that need to be // streamed @@ -144,6 +172,164 @@ public class GenStreamableTask extends GenTask writeFile(source.getAbsolutePath(), sfile.generate(null, methods.toString())); } + /** + * "Flatten" mode: emit self-contained declaration-ordered {@code read/writeObject} that stream + * the whole {@link ClassUtil#getFields} field set via {@link com.threerings.io.GenStreamUtil}. + * Handles {@link DObject}s and replaces pass-through {@code default*Object()} calls in place. + */ + protected void processFlatten (File source, Class sclass) + throws IOException + { + // must be a concrete Streamable; never instrument abstract bases (their concrete subclasses + // stream the full flattened field set themselves, so an abstract base carrying a read/write + // method would "poison" any subclass that didn't get its own — silently dropping fields) + if (!Streamable.class.isAssignableFrom(sclass) || sclass.isInterface() || + sclass.isEnum() || Modifier.isAbstract(sclass.getModifiers())) { + return; + } + + // gather the streamed fields in the exact order the reflective streamer uses (superclass + // first, then this class's declared fields), skipping @NotStreamable and closure refs + List fields = Lists.newArrayList(); + for (Field field : ClassUtil.getFields(sclass)) { + if (field.getAnnotation(NotStreamable.class) != null) { + continue; + } + if (field.isSynthetic() && field.getName().startsWith("this$")) { + continue; + } + fields.add(field); + } + if (fields.isEmpty()) { + return; + } + + String src = new String(Files.readAllBytes(source.toPath())); + + boolean hasRead = src.contains("public void readObject"); + boolean hasWrite = src.contains("public void writeObject"); + boolean hasReadDefault = DEFAULT_READ.matcher(src).find(); + boolean hasWriteDefault = DEFAULT_WRITE.matcher(src).find(); + + // if either method is hand-written custom streaming (present, but NOT a defaultXObject + // pass-through), the class fully manages its own streaming and is already deterministic; + // leave it entirely alone rather than risk a generated/hand-written mismatch + if ((hasRead && !hasReadDefault) || (hasWrite && !hasWriteDefault)) { + return; + } + + // build whole methods for any streaming direction that has no method at all + StringBuilder methods = new StringBuilder(); + if (!hasRead) { + methods.append(FLAT_READ_OPEN); + for (Field field : fields) { + methods.append(" ").append(flatReadStatement(field, "ins")).append("\n"); + } + methods.append(READ_CLOSE); + } + if (!hasWrite) { + if (methods.length() > 0) { + methods.append("\n"); + } + methods.append(FLAT_WRITE_OPEN); + for (Field field : fields) { + methods.append(" ").append(flatWriteStatement(field, "out")).append("\n"); + } + methods.append(WRITE_CLOSE); + } + + boolean changed = false; + // replace a pass-through default read/write call in place, preserving surrounding logic + if (hasReadDefault) { + src = substituteDefault(src, DEFAULT_READ, fields, true); + changed = true; + } + if (hasWriteDefault) { + src = substituteDefault(src, DEFAULT_WRITE, fields, false); + changed = true; + } + // insert whole methods just before the class-closing brace, so we NEVER disturb the + // "// AUTO-GENERATED: METHODS" section (which holds the gendobj/genservice accessors) — + // SourceFile.generate() would replace that section wholesale and drop the accessors + if (methods.length() > 0) { + src = insertBeforeClassClose(src, methods.toString()); + changed = true; + } + if (changed) { + System.err.println("Converting (flatten) " + sclass.getName() + "..."); + writeFile(source.getAbsolutePath(), src); + } + } + + /** + * Inserts {@code methods} (already 4-space indented, newline-terminated) just before the final + * top-level class-closing brace of {@code src}. + */ + protected String insertBeforeClassClose (String src, String methods) + { + String[] lines = src.split("\n", -1); + int lastBrace = -1; + for (int ii = 0; ii < lines.length; ii++) { + if (lines[ii].trim().equals("}")) { + lastBrace = ii; + } + } + if (lastBrace < 0) { + throw new RuntimeException("No class-closing brace found"); + } + StringBuilder sb = new StringBuilder(); + for (int ii = 0; ii < lines.length; ii++) { + if (ii == lastBrace) { + sb.append("\n").append(methods); + } + sb.append(lines[ii]); + if (ii < lines.length - 1) { + sb.append("\n"); + } + } + return sb.toString(); + } + + /** + * Replaces the single {@code recv.defaultReadObject()} / {@code recv.defaultWriteObject()} + * statement matched by {@code pat} with an explicit, same-indentation block that streams every + * field via {@link com.threerings.io.GenStreamUtil}, reusing the receiver variable name. + */ + protected String substituteDefault (String src, Pattern pat, List fields, boolean read) + { + Matcher m = pat.matcher(src); + StringBuffer sb = new StringBuffer(); + while (m.find()) { + String indent = m.group(1), recv = m.group(2); + StringBuilder block = new StringBuilder(); + for (int ii = 0; ii < fields.size(); ii++) { + if (ii > 0) { + block.append("\n"); + } + Field field = fields.get(ii); + block.append(indent).append( + read ? flatReadStatement(field, recv) : flatWriteStatement(field, recv)); + } + m.appendReplacement(sb, Matcher.quoteReplacement(block.toString())); + } + m.appendTail(sb); + return sb.toString(); + } + + protected String flatReadStatement (Field field, String recv) + { + return "com.threerings.io.GenStreamUtil.readField(" + + field.getDeclaringClass().getCanonicalName() + ".class, \"" + field.getName() + + "\", this, " + recv + ");"; + } + + protected String flatWriteStatement (Field field, String recv) + { + return "com.threerings.io.GenStreamUtil.writeField(" + + field.getDeclaringClass().getCanonicalName() + ".class, \"" + field.getName() + + "\", this, " + recv + ");"; + } + protected String toReadObject (Field field) { Class type = field.getType(); @@ -196,6 +382,9 @@ public class GenStreamableTask extends GenTask /** A list of filesets that contain tile images. */ protected ArrayList _filesets = Lists.newArrayList(); + /** Whether to generate self-contained flattened methods (Android/ART support). */ + protected boolean _flatten; + protected static final String READ_OPEN = " // from interface Streamable\n" + " public void readObject (ObjectInputStream ins)\n" + @@ -209,4 +398,23 @@ public class GenStreamableTask extends GenTask " throws IOException\n" + " {\n"; protected static final String WRITE_CLOSE = " }\n"; + + // fully-qualified signatures for flatten mode so no imports are required in the target file + protected static final String FLAT_READ_OPEN = + " // from interface Streamable\n" + + " public void readObject (com.threerings.io.ObjectInputStream ins)\n" + + " throws java.io.IOException, java.lang.ClassNotFoundException\n" + + " {\n"; + protected static final String FLAT_WRITE_OPEN = + " // from interface Streamable\n" + + " public void writeObject (com.threerings.io.ObjectOutputStream out)\n" + + " throws java.io.IOException\n" + + " {\n"; + + /** Matches a pass-through {@code recv.defaultReadObject();} statement (captures indent, recv). */ + protected static final Pattern DEFAULT_READ = + Pattern.compile("(?m)^([ \\t]*)(\\w+)\\.defaultReadObject\\s*\\(\\s*\\)\\s*;[ \\t]*$"); + /** Matches a pass-through {@code recv.defaultWriteObject();} statement. */ + protected static final Pattern DEFAULT_WRITE = + Pattern.compile("(?m)^([ \\t]*)(\\w+)\\.defaultWriteObject\\s*\\(\\s*\\)\\s*;[ \\t]*$"); } diff --git a/tools/src/main/resources/com/threerings/presents/tools.properties b/tools/src/main/resources/com/threerings/presents/tools.properties index c5dddd3fa..d60189f44 100644 --- a/tools/src/main/resources/com/threerings/presents/tools.properties +++ b/tools/src/main/resources/com/threerings/presents/tools.properties @@ -1,4 +1,5 @@ gendobj=com.threerings.presents.tools.GenDObjectTask +genstreamable=com.threerings.presents.tools.GenStreamableTask genservice=com.threerings.presents.tools.GenServiceTask genreceiver=com.threerings.presents.tools.GenReceiverTask instream=com.threerings.presents.tools.InstrumentStreamableTask