Compare commits
12 Commits
a942d5d3dc
...
cedc0473a4
| Author | SHA1 | Date | |
|---|---|---|---|
| cedc0473a4 | |||
| ec230e71c9 | |||
| f12080af5b | |||
| 67119d0be8 | |||
| 0f1626ecef | |||
| 1cf16721d6 | |||
| 7cbac80172 | |||
| 9ea7ecf9ee | |||
| 12e65f059f | |||
| 3266a9c163 | |||
| cbff83badd | |||
| cb6e321e5f |
@@ -0,0 +1,53 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
runs-on: ubuntu-latest
|
||||
# pull-requests: write is required so `gh pr comment` and the inline-comment
|
||||
# tool can post the review back onto the PR via GITHUB_TOKEN.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
# Authenticates as the Claude subscription, so review usage is covered
|
||||
# by the plan (no metered API charge). Set the CLAUDE_CODE_OAUTH_TOKEN
|
||||
# secret on this repo (Settings > Secrets and variables > Actions).
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Review this pull request. narya is a Java distributed-object /
|
||||
networking library (the Presents framework: DObjects, invocation
|
||||
services/marshallers, the io streaming layer). Focus on:
|
||||
- Correctness bugs and broken edge cases
|
||||
- Wire-protocol / streaming compatibility (field order, Streamable
|
||||
read/writeObject, backwards compatibility of serialized forms)
|
||||
- Thread-safety and concurrency
|
||||
- Security implications
|
||||
- Code quality and existing project conventions
|
||||
|
||||
The PR branch is already checked out in the working directory.
|
||||
Use `gh pr comment` for a short top-level summary.
|
||||
Use `mcp__github_inline_comment__create_inline_comment` (with
|
||||
`confirmed: true`) to flag specific lines.
|
||||
Only post GitHub comments - do not return the review as a message.
|
||||
If you find nothing worth flagging, post a brief "no issues found"
|
||||
summary comment.
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
# Set the CLAUDE_CODE_OAUTH_TOKEN secret on this repo (Settings >
|
||||
# Secrets and variables > Actions) for this to authenticate.
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>com.threerings</groupId>
|
||||
<artifactId>narya-parent</artifactId>
|
||||
<version>1.19</version>
|
||||
<version>1.22</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>narya</artifactId>
|
||||
|
||||
@@ -32,4 +32,22 @@ public class SystemMessage extends ChatMessage
|
||||
super(message, bundle);
|
||||
this.attentionLevel = attentionLevel;
|
||||
}
|
||||
|
||||
// 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.crowd.chat.data.ChatMessage.class, "message", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.chat.data.ChatMessage.class, "bundle", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.chat.data.SystemMessage.class, "attentionLevel", this, ins);
|
||||
}
|
||||
|
||||
// from interface Streamable
|
||||
public void writeObject (com.threerings.io.ObjectOutputStream out)
|
||||
throws java.io.IOException
|
||||
{
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.chat.data.ChatMessage.class, "message", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.chat.data.ChatMessage.class, "bundle", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.chat.data.SystemMessage.class, "attentionLevel", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,20 @@ public class TellFeedbackMessage extends UserMessage
|
||||
}
|
||||
|
||||
protected boolean _failure;
|
||||
|
||||
// from interface Streamable
|
||||
public void readObject (com.threerings.io.ObjectInputStream ins)
|
||||
throws java.io.IOException, java.lang.ClassNotFoundException
|
||||
{
|
||||
super.readObject(ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.chat.data.TellFeedbackMessage.class, "_failure", this, ins);
|
||||
}
|
||||
|
||||
// from interface Streamable
|
||||
public void writeObject (com.threerings.io.ObjectOutputStream out)
|
||||
throws java.io.IOException
|
||||
{
|
||||
super.writeObject(out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.chat.data.TellFeedbackMessage.class, "_failure", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +62,24 @@ public class UserMessage extends ChatMessage
|
||||
}
|
||||
return "m.speak_format";
|
||||
}
|
||||
|
||||
// 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.crowd.chat.data.ChatMessage.class, "message", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.chat.data.ChatMessage.class, "bundle", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.chat.data.UserMessage.class, "speaker", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.chat.data.UserMessage.class, "mode", this, ins);
|
||||
}
|
||||
|
||||
// from interface Streamable
|
||||
public void writeObject (com.threerings.io.ObjectOutputStream out)
|
||||
throws java.io.IOException
|
||||
{
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.chat.data.ChatMessage.class, "message", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.chat.data.ChatMessage.class, "bundle", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.chat.data.UserMessage.class, "speaker", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.chat.data.UserMessage.class, "mode", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,20 @@ public class UserSystemMessage extends SystemMessage
|
||||
super(message, bundle, attentionLevel);
|
||||
this.speaker = sender;
|
||||
}
|
||||
|
||||
// from interface Streamable
|
||||
public void readObject (com.threerings.io.ObjectInputStream ins)
|
||||
throws java.io.IOException, java.lang.ClassNotFoundException
|
||||
{
|
||||
super.readObject(ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.chat.data.UserSystemMessage.class, "speaker", this, ins);
|
||||
}
|
||||
|
||||
// from interface Streamable
|
||||
public void writeObject (com.threerings.io.ObjectOutputStream out)
|
||||
throws java.io.IOException
|
||||
{
|
||||
super.writeObject(out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.chat.data.UserSystemMessage.class, "speaker", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,4 +237,28 @@ public class BodyObject extends ClientObject
|
||||
|
||||
/** The default (no tokens) access control. */
|
||||
protected static final TokenRing EMPTY_TOKENS = new TokenRing(0);
|
||||
|
||||
// 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.DObject.class, "_oid", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.presents.data.ClientObject.class, "username", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.presents.data.ClientObject.class, "receivers", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.data.BodyObject.class, "location", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.data.BodyObject.class, "status", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.data.BodyObject.class, "awayMessage", 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.DObject.class, "_oid", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.data.ClientObject.class, "username", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.data.ClientObject.class, "receivers", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.BodyObject.class, "location", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.BodyObject.class, "status", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.BodyObject.class, "awayMessage", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,4 +117,22 @@ public class OccupantInfo extends SimpleStreamableObject
|
||||
throw new AssertionError(cnse);
|
||||
}
|
||||
}
|
||||
|
||||
// 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.crowd.data.OccupantInfo.class, "bodyOid", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.data.OccupantInfo.class, "username", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.data.OccupantInfo.class, "status", this, ins);
|
||||
}
|
||||
|
||||
// from interface Streamable
|
||||
public void writeObject (com.threerings.io.ObjectOutputStream out)
|
||||
throws java.io.IOException
|
||||
{
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.OccupantInfo.class, "bodyOid", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.OccupantInfo.class, "username", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.OccupantInfo.class, "status", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +37,18 @@ public class Place extends SimpleStreamableObject
|
||||
{
|
||||
return placeOid;
|
||||
}
|
||||
|
||||
// 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.crowd.data.Place.class, "placeOid", this, ins);
|
||||
}
|
||||
|
||||
// from interface Streamable
|
||||
public void writeObject (com.threerings.io.ObjectOutputStream out)
|
||||
throws java.io.IOException
|
||||
{
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.Place.class, "placeOid", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,4 +236,24 @@ public class PlaceObject extends DObject
|
||||
this.speakService = value;
|
||||
}
|
||||
// AUTO-GENERATED: METHODS END
|
||||
|
||||
// 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.DObject.class, "_oid", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.data.PlaceObject.class, "occupants", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.data.PlaceObject.class, "occupantInfo", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.data.PlaceObject.class, "speakService", 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.DObject.class, "_oid", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.PlaceObject.class, "occupants", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.PlaceObject.class, "occupantInfo", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.PlaceObject.class, "speakService", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +89,18 @@ public class TokenRing extends SimpleStreamableObject
|
||||
|
||||
/** The tokens contained in this ring (composed together bitwise). */
|
||||
protected int _tokens;
|
||||
|
||||
// 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.crowd.data.TokenRing.class, "_tokens", this, ins);
|
||||
}
|
||||
|
||||
// from interface Streamable
|
||||
public void writeObject (com.threerings.io.ObjectOutputStream out)
|
||||
throws java.io.IOException
|
||||
{
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.data.TokenRing.class, "_tokens", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,20 @@ public class CrowdClientInfo extends ClientInfo
|
||||
{
|
||||
/** The client's visible name, which is used for chatting. */
|
||||
public Name visibleName;
|
||||
|
||||
// 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.peer.data.ClientInfo.class, "username", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.peer.data.CrowdClientInfo.class, "visibleName", 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.peer.data.ClientInfo.class, "username", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.peer.data.CrowdClientInfo.class, "visibleName", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,4 +101,38 @@ public class CrowdNodeObject extends NodeObject
|
||||
this.hostedChannels = clone;
|
||||
}
|
||||
// AUTO-GENERATED: METHODS END
|
||||
|
||||
// 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.DObject.class, "_oid", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.presents.peer.data.NodeObject.class, "nodeName", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.presents.peer.data.NodeObject.class, "bootStamp", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.presents.peer.data.NodeObject.class, "peerService", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.presents.peer.data.NodeObject.class, "clients", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.presents.peer.data.NodeObject.class, "locks", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.presents.peer.data.NodeObject.class, "acquiringLock", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.presents.peer.data.NodeObject.class, "releasingLock", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.presents.peer.data.NodeObject.class, "cacheData", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.peer.data.CrowdNodeObject.class, "crowdPeerService", this, ins);
|
||||
com.threerings.io.GenStreamUtil.readField(com.threerings.crowd.peer.data.CrowdNodeObject.class, "hostedChannels", 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.DObject.class, "_oid", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.peer.data.NodeObject.class, "nodeName", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.peer.data.NodeObject.class, "bootStamp", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.peer.data.NodeObject.class, "peerService", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.peer.data.NodeObject.class, "clients", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.peer.data.NodeObject.class, "locks", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.peer.data.NodeObject.class, "acquiringLock", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.peer.data.NodeObject.class, "releasingLock", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.presents.peer.data.NodeObject.class, "cacheData", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.peer.data.CrowdNodeObject.class, "crowdPeerService", this, out);
|
||||
com.threerings.io.GenStreamUtil.writeField(com.threerings.crowd.peer.data.CrowdNodeObject.class, "hostedChannels", this, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>The default reflective {@link Streamer} streams a class's fields in {@link
|
||||
* com.samskivert.util.ClassUtil#getFields} order, which is <em>superclass-first</em> but relies on
|
||||
* {@link Class#getDeclaredFields} <em>within</em> 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<String, Entry> _cache = new ConcurrentHashMap<String, Entry>();
|
||||
|
||||
private GenStreamUtil () {}
|
||||
}
|
||||
@@ -412,5 +412,5 @@ public class ObjectInputStream extends DataInputStream
|
||||
protected static volatile List<String> _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");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ package com.threerings.presents.data;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.threerings.io.GenStreamUtil;
|
||||
import com.threerings.io.ObjectInputStream;
|
||||
import com.threerings.io.ObjectOutputStream;
|
||||
import com.threerings.io.Streamable;
|
||||
|
||||
import com.threerings.presents.client.Client;
|
||||
@@ -244,17 +246,37 @@ public class InvocationMarshaller<T extends ClientObject>
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom reader method for {@link Streamable}.
|
||||
* A custom reader method for {@link Streamable}. Reads {@code _invOid}/{@code _invCode}
|
||||
* explicitly (by name, in declaration order) rather than via {@code defaultReadObject()} so the
|
||||
* field order is deterministic across ART and HotSpot (Epic A1 Phase 5, approach b) — byte
|
||||
* identical to the old reflective read. Marshaller SUBCLASSES intentionally have NO generated
|
||||
* read/writeObject (they are field-free) so they inherit this method, which also performs the
|
||||
* side-effect the generated flatten-mode streamers would otherwise skip: binding {@link #_invdir}
|
||||
* from the stream's client. That binding is the whole reason this is a hand-written custom
|
||||
* reader; a subclass overriding it would leave {@code _invdir} null and NPE on the first request.
|
||||
*/
|
||||
public void readObject (ObjectInputStream in)
|
||||
throws IOException, ClassNotFoundException
|
||||
{
|
||||
in.defaultReadObject();
|
||||
GenStreamUtil.readField(InvocationMarshaller.class, "_invOid", this, in);
|
||||
GenStreamUtil.readField(InvocationMarshaller.class, "_invCode", this, in);
|
||||
if (in instanceof ClientObjectInputStream) {
|
||||
_invdir = ((ClientObjectInputStream)in).client.getInvocationDirector();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom writer method for {@link Streamable}; mirrors {@link #readObject} (explicit,
|
||||
* deterministic field order) so field-free marshaller subclasses inherit it. Byte identical to
|
||||
* the previous reflective default write.
|
||||
*/
|
||||
public void writeObject (ObjectOutputStream out)
|
||||
throws IOException
|
||||
{
|
||||
GenStreamUtil.writeField(InvocationMarshaller.class, "_invOid", this, out);
|
||||
GenStreamUtil.writeField(InvocationMarshaller.class, "_invCode", this, out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -173,4 +173,22 @@ public class CompoundEvent extends DEvent
|
||||
|
||||
/** A list of the events associated with this compound event. */
|
||||
protected StreamableArrayList<DEvent> _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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -107,4 +107,24 @@ public class EntryAddedEvent<T extends DSet.Entry> extends EntryEvent<T>
|
||||
/** 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
|
||||
}
|
||||
|
||||
@@ -109,4 +109,24 @@ public class EntryRemovedEvent<T extends DSet.Entry> extends EntryEvent<T>
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
@@ -112,4 +112,24 @@ public class EntryUpdatedEvent<T extends DSet.Entry> extends EntryEvent<T>
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<groupId>com.threerings</groupId>
|
||||
<artifactId>narya-parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.19</version>
|
||||
<version>1.22</version>
|
||||
|
||||
<name>Narya Parent</name>
|
||||
<description>Facilities for making networked multiplayer games.</description>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>com.threerings</groupId>
|
||||
<artifactId>narya-parent</artifactId>
|
||||
<version>1.19</version>
|
||||
<version>1.22</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>narya-tools</artifactId>
|
||||
|
||||
@@ -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,17 +44,44 @@ public class GenStreamableTask extends GenTask
|
||||
_filesets.add(set);
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, generate <em>self-contained</em> 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 ()
|
||||
{
|
||||
// pre-scan: collect every top-level class name covered by the filesets, so the
|
||||
// post-regen hierarchy model (willDeclare) knows which ancestors this run can generate
|
||||
// methods for
|
||||
List<File> sources = Lists.newArrayList();
|
||||
for (FileSet fs : _filesets) {
|
||||
DirectoryScanner ds = fs.getDirectoryScanner(getProject());
|
||||
File fromDir = fs.getDir(getProject());
|
||||
String[] srcFiles = ds.getIncludedFiles();
|
||||
for (String srcFile : srcFiles) {
|
||||
processClass(new File(fromDir, srcFile));
|
||||
for (String srcFile : ds.getIncludedFiles()) {
|
||||
File source = new File(fromDir, srcFile);
|
||||
sources.add(source);
|
||||
try {
|
||||
_filesetClasses.add(GenUtil.readClassName(source));
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to parse " + source + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (File source : sources) {
|
||||
processClass(source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,6 +121,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 +184,439 @@ 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.
|
||||
* The top-level class AND every named member class (recursively) are processed — a nested
|
||||
* Streamable (e.g. {@code BangConfig.Round}) streams reflectively just like a top-level one,
|
||||
* so it needs the same deterministic methods on ART. All detection/substitution/insertion is
|
||||
* scoped to each class's own body region so an instrumented outer class never masks (or
|
||||
* poisons) a nested one and vice versa.
|
||||
*/
|
||||
protected void processFlatten (File source, Class<?> sclass)
|
||||
throws IOException
|
||||
{
|
||||
String src = new String(Files.readAllBytes(source.toPath()));
|
||||
String out = processFlattenTree(src, sclass);
|
||||
if (!out.equals(src)) {
|
||||
writeFile(source.getAbsolutePath(), out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes {@code sclass} then recurses into its named member classes. Recursion is NOT gated
|
||||
* on the enclosing class's eligibility: an abstract (or non-Streamable) outer class can still
|
||||
* declare concrete nested Streamables that need instrumenting.
|
||||
*/
|
||||
protected String processFlattenTree (String src, Class<?> sclass)
|
||||
{
|
||||
src = processFlattenClass(src, sclass);
|
||||
for (Class<?> nested : sclass.getDeclaredClasses()) {
|
||||
src = processFlattenTree(src, nested);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments a single class (top-level or nested) within {@code src}, returning the updated
|
||||
* source text (or {@code src} unchanged if the class is ineligible or already instrumented).
|
||||
*/
|
||||
protected String processFlattenClass (String src, Class<?> sclass)
|
||||
{
|
||||
if (!Streamable.class.isAssignableFrom(sclass) || sclass.isInterface() || sclass.isEnum()) {
|
||||
return src;
|
||||
}
|
||||
// ABSTRACT classes get SUBSTITUTION ONLY: an existing default*Object() pass-through is
|
||||
// replaced with the class's own flattened prefix (so the method becomes a safe super-call
|
||||
// target for generated subclass methods — reflective defaultReadObject reads the full
|
||||
// DYNAMIC-class set, which would double-read the subclass tail), but never whole generated
|
||||
// methods (those would "poison" any concrete subclass that didn't get its own — silently
|
||||
// dropping its fields from the stream)
|
||||
boolean isAbstract = Modifier.isAbstract(sclass.getModifiers());
|
||||
|
||||
// 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<Field> fields = streamedFields(sclass);
|
||||
|
||||
// detection and substitution look only at THIS class's body, with any nested member class
|
||||
// bodies blanked out (space-padded, so offsets in the masked text map 1:1 onto src)
|
||||
int[] region = findClassRegion(src, sclass);
|
||||
String masked = maskNestedClasses(src, region, sclass);
|
||||
|
||||
boolean hasRead = masked.contains("public void readObject");
|
||||
boolean hasWrite = masked.contains("public void writeObject");
|
||||
boolean hasReadDefault = DEFAULT_READ.matcher(masked).find();
|
||||
boolean hasWriteDefault = DEFAULT_WRITE.matcher(masked).find();
|
||||
|
||||
// a field-less class has nothing to generate, but an existing pass-through must STILL be
|
||||
// substituted (to an empty read/write — the reflective call would stream the full
|
||||
// dynamic-class set when reached via a subclass's super-call)
|
||||
if (fields.isEmpty() && !hasReadDefault && !hasWriteDefault) {
|
||||
return src;
|
||||
}
|
||||
|
||||
// a hand-written custom method (present, but NOT a defaultXObject pass-through) already
|
||||
// manages that DIRECTION's streaming deterministically — leave it alone. The skip is
|
||||
// per-side, not per-class: Prop has a custom readObject but no writeObject at all, and
|
||||
// its write side still needs the generated method (both because legacy reflective writes
|
||||
// must become explicit, and because willDeclare models sides independently — subclasses
|
||||
// emit super.writeObject against it)
|
||||
|
||||
// an ancestor that declares its own read/writeObject is the class's streaming "base": a
|
||||
// generated method here must super-call it (preserving its side effects — Prop.init(),
|
||||
// InvocationMarshaller's _invdir rebind, Item's _nondb extras) and then stream only the
|
||||
// fields declared BELOW it. A self-contained full-set method would silently drop those
|
||||
// side effects for every subclass that previously just inherited the ancestor's method
|
||||
// (the Phase 5(b) Prop._vscale server NPE / Item._itemId wire bug). Byte layout on the
|
||||
// stream is unchanged either way: the ancestor's method covers the flattened prefix.
|
||||
Class<?> readAncestor = findStreamMethodAncestor(
|
||||
sclass, "readObject", com.threerings.io.ObjectInputStream.class);
|
||||
Class<?> writeAncestor = findStreamMethodAncestor(
|
||||
sclass, "writeObject", com.threerings.io.ObjectOutputStream.class);
|
||||
List<Field> readFields = fieldsBelow(fields, readAncestor);
|
||||
List<Field> writeFields = fieldsBelow(fields, writeAncestor);
|
||||
|
||||
// methods are indented one level deeper than the class declaration's nesting depth
|
||||
int depth = 0;
|
||||
for (Class<?> c = sclass; c.getEnclosingClass() != null; c = c.getEnclosingClass()) {
|
||||
depth++;
|
||||
}
|
||||
String ind = indent(depth + 1);
|
||||
|
||||
// build whole methods for any streaming direction that has no method at all — but when an
|
||||
// ancestor declares the method and this class adds no fields of its own, plain inheritance
|
||||
// is already complete and deterministic: generate nothing (the field-free-marshaller case)
|
||||
StringBuilder methods = new StringBuilder();
|
||||
if (!hasRead && !isAbstract && !readFields.isEmpty()) {
|
||||
methods.append(ind).append("// from interface Streamable\n")
|
||||
.append(ind).append("public void readObject (com.threerings.io.ObjectInputStream ins)\n")
|
||||
.append(ind).append(" throws java.io.IOException, java.lang.ClassNotFoundException\n")
|
||||
.append(ind).append("{\n");
|
||||
if (readAncestor != null) {
|
||||
methods.append(ind).append(" super.readObject(ins);\n");
|
||||
}
|
||||
for (Field field : readFields) {
|
||||
methods.append(ind).append(" ").append(flatReadStatement(field, "ins")).append("\n");
|
||||
}
|
||||
methods.append(ind).append("}\n");
|
||||
}
|
||||
if (!hasWrite && !isAbstract && !writeFields.isEmpty()) {
|
||||
if (methods.length() > 0) {
|
||||
methods.append("\n");
|
||||
}
|
||||
methods.append(ind).append("// from interface Streamable\n")
|
||||
.append(ind).append("public void writeObject (com.threerings.io.ObjectOutputStream out)\n")
|
||||
.append(ind).append(" throws java.io.IOException\n")
|
||||
.append(ind).append("{\n");
|
||||
if (writeAncestor != null) {
|
||||
methods.append(ind).append(" super.writeObject(out);\n");
|
||||
}
|
||||
for (Field field : writeFields) {
|
||||
methods.append(ind).append(" ").append(flatWriteStatement(field, "out")).append("\n");
|
||||
}
|
||||
methods.append(ind).append("}\n");
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
// replace pass-through default read/write calls in place, preserving surrounding logic
|
||||
// (matched against the masked body so a nested class's pass-through is never rewritten
|
||||
// with THIS class's fields — it gets its own substitution when its class is processed)
|
||||
if (hasReadDefault || hasWriteDefault) {
|
||||
src = substituteDefaults(src, masked, region, fields);
|
||||
changed = true;
|
||||
}
|
||||
// insert whole methods just before this class's 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) {
|
||||
int closeLineStart = src.lastIndexOf('\n', region[1]) + 1;
|
||||
src = src.substring(0, closeLineStart) + "\n" + methods + src.substring(closeLineStart);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
System.err.println("Converting (flatten) " + sclass.getName() + "...");
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the body of {@code sclass}'s declaration in {@code src}, searching only within the
|
||||
* enclosing class's own region for nested classes (so same-simple-named classes elsewhere in
|
||||
* the file can't be confused). Returns {@code {bodyStart, closeBrace}}: the offset just after
|
||||
* the opening brace and the offset of the matching closing brace.
|
||||
*/
|
||||
protected int[] findClassRegion (String src, Class<?> sclass)
|
||||
{
|
||||
int searchStart = 0, searchEnd = src.length();
|
||||
Class<?> outer = sclass.getEnclosingClass();
|
||||
if (outer != null) {
|
||||
int[] outerRegion = findClassRegion(src, outer);
|
||||
searchStart = outerRegion[0];
|
||||
searchEnd = outerRegion[1];
|
||||
}
|
||||
Pattern decl = Pattern.compile(
|
||||
"\\b(?:class|interface|enum)\\s+" + Pattern.quote(sclass.getSimpleName()) + "\\b");
|
||||
Matcher m = decl.matcher(src);
|
||||
m.region(searchStart, searchEnd);
|
||||
if (!m.find()) {
|
||||
throw new RuntimeException("Cannot locate declaration of " + sclass.getName());
|
||||
}
|
||||
int open = src.indexOf('{', m.end());
|
||||
if (open < 0 || open >= searchEnd) {
|
||||
throw new RuntimeException("Cannot locate body of " + sclass.getName());
|
||||
}
|
||||
return new int[] { open + 1, matchBrace(src, open) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of the brace closing the one at {@code open}, skipping braces inside
|
||||
* string/char literals and comments.
|
||||
*/
|
||||
protected int matchBrace (String src, int open)
|
||||
{
|
||||
int braces = 0;
|
||||
for (int ii = open; ii < src.length(); ii++) {
|
||||
char c = src.charAt(ii);
|
||||
switch (c) {
|
||||
case '{':
|
||||
braces++;
|
||||
break;
|
||||
case '}':
|
||||
if (--braces == 0) {
|
||||
return ii;
|
||||
}
|
||||
break;
|
||||
case '"':
|
||||
case '\'':
|
||||
char quote = c;
|
||||
for (ii++; ii < src.length(); ii++) {
|
||||
char d = src.charAt(ii);
|
||||
if (d == '\\') {
|
||||
ii++;
|
||||
} else if (d == quote) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '/':
|
||||
if (ii + 1 < src.length()) {
|
||||
char d = src.charAt(ii + 1);
|
||||
if (d == '/') {
|
||||
int nl = src.indexOf('\n', ii);
|
||||
ii = (nl < 0) ? src.length() : nl;
|
||||
} else if (d == '*') {
|
||||
int end = src.indexOf("*/", ii + 2);
|
||||
if (end < 0) {
|
||||
throw new RuntimeException("Unterminated block comment");
|
||||
}
|
||||
ii = end + 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Unbalanced braces scanning class body");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code src} with everything outside {@code region} AND every named member class body
|
||||
* within it replaced by spaces — same length as {@code src}, so match offsets in the masked
|
||||
* text index directly into the original.
|
||||
*/
|
||||
protected String maskNestedClasses (String src, int[] region, Class<?> sclass)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(src);
|
||||
for (int ii = 0; ii < region[0]; ii++) {
|
||||
blank(sb, ii);
|
||||
}
|
||||
for (int ii = region[1]; ii < sb.length(); ii++) {
|
||||
blank(sb, ii);
|
||||
}
|
||||
for (Class<?> nested : sclass.getDeclaredClasses()) {
|
||||
int[] nregion;
|
||||
try {
|
||||
nregion = findClassRegion(src, nested);
|
||||
} catch (RuntimeException e) {
|
||||
continue; // e.g. synthetic member with no source declaration
|
||||
}
|
||||
for (int ii = nregion[0]; ii < nregion[1]; ii++) {
|
||||
blank(sb, ii);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
protected static void blank (StringBuilder sb, int ii)
|
||||
{
|
||||
if (sb.charAt(ii) != '\n') {
|
||||
sb.setCharAt(ii, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces every pass-through {@code recv.defaultReadObject()} / {@code recv.defaultWriteObject()}
|
||||
* statement found in {@code masked} with an explicit, same-indentation block that streams every
|
||||
* field via {@link com.threerings.io.GenStreamUtil}, reusing the receiver variable name.
|
||||
* Matching runs on the masked text (this class's body only); edits apply to the real source.
|
||||
*/
|
||||
protected String substituteDefaults (String src, String masked, int[] region, List<Field> fields)
|
||||
{
|
||||
// collect matches from both patterns, then apply in reverse offset order so earlier
|
||||
// replacements don't shift later match positions
|
||||
List<int[]> matches = Lists.newArrayList(); // {start, end, isRead}
|
||||
for (Pattern pat : new Pattern[] { DEFAULT_READ, DEFAULT_WRITE }) {
|
||||
Matcher m = pat.matcher(masked);
|
||||
while (m.find()) {
|
||||
matches.add(new int[] { m.start(), m.end(), (pat == DEFAULT_READ) ? 1 : 0 });
|
||||
}
|
||||
}
|
||||
matches.sort((a, b) -> b[0] - a[0]);
|
||||
|
||||
StringBuilder sb = new StringBuilder(src);
|
||||
for (int[] match : matches) {
|
||||
Matcher m = ((match[2] == 1) ? DEFAULT_READ : DEFAULT_WRITE).matcher(masked);
|
||||
m.region(match[0], match[1]);
|
||||
if (!m.find()) {
|
||||
throw new RuntimeException("Lost a default*Object match on re-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((match[2] == 1)
|
||||
? flatReadStatement(field, recv) : flatWriteStatement(field, recv));
|
||||
}
|
||||
sb.replace(match[0], match[1], block.toString());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
protected static String indent (int levels)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int ii = 0; ii < levels; ii++) {
|
||||
sb.append(" ");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the nearest strict ancestor of {@code sclass} that will declare the given streaming
|
||||
* method AFTER this run's regeneration, or null. This must model the POST-regen hierarchy —
|
||||
* not the classes being reflected on (which were compiled from stripped sources): javac binds
|
||||
* the emitted {@code super.readObject(ins)} to the nearest declaration in the FINAL sources,
|
||||
* so computing the tail against a farther ancestor double-streams every field an intermediate
|
||||
* generated method covers (found the hard way: CargoTank extends Counter extends Prop —
|
||||
* Counter regenerates a method covering {@code count}, so a CargoTank tail computed against
|
||||
* Prop read {@code count} twice and desynced every persisted .board file).
|
||||
*
|
||||
* An ancestor declares the method post-regen iff (a) it declares it now (hand-written custom,
|
||||
* or a pass-through frame that substitution rewrites in place — abstract classes included), or
|
||||
* (b) it is a concrete in-fileset Streamable whose own post-regen tail is nonempty, i.e. this
|
||||
* run generates one for it (recursive, memoized). Ancestors OUTSIDE the task's filesets never
|
||||
* have methods generated, so (b) additionally requires fileset membership. Any such ancestor
|
||||
* is a safe super-call target: its method streams exactly the ancestor's own flattened prefix.
|
||||
*/
|
||||
protected Class<?> findStreamMethodAncestor (Class<?> sclass, String name, Class<?> arg)
|
||||
{
|
||||
for (Class<?> c = sclass.getSuperclass(); c != null; c = c.getSuperclass()) {
|
||||
if (willDeclare(c, name, arg)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether {@code c} will declare the given streaming method once regeneration completes. */
|
||||
protected boolean willDeclare (Class<?> c, String name, Class<?> arg)
|
||||
{
|
||||
String key = c.getName() + "#" + name;
|
||||
Boolean cached = _willDeclare.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
boolean result;
|
||||
try {
|
||||
c.getDeclaredMethod(name, arg);
|
||||
result = true; // declares it in (stripped) source or a dependency jar
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
if (!Streamable.class.isAssignableFrom(c) || c.isInterface() || c.isEnum() ||
|
||||
Modifier.isAbstract(c.getModifiers()) || !inFilesets(c)) {
|
||||
result = false; // never generated for these
|
||||
} else {
|
||||
// will get a generated method iff its own tail (below ITS post-regen ancestor,
|
||||
// recursively) is nonempty
|
||||
result = !fieldsBelow(streamedFields(c),
|
||||
findStreamMethodAncestor(c, name, arg)).isEmpty();
|
||||
}
|
||||
}
|
||||
_willDeclare.put(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Whether {@code c}'s source is covered by this task's filesets (nested classes count via
|
||||
* their outermost enclosing class's file). */
|
||||
protected boolean inFilesets (Class<?> c)
|
||||
{
|
||||
Class<?> outer = c;
|
||||
while (outer.getEnclosingClass() != null) {
|
||||
outer = outer.getEnclosingClass();
|
||||
}
|
||||
return _filesetClasses.contains(outer.getName());
|
||||
}
|
||||
|
||||
/** Gathers the streamed fields of {@code sclass} in reflective-streamer order (superclass
|
||||
* first), skipping {@code @NotStreamable} and closure refs. */
|
||||
protected static List<Field> streamedFields (Class<?> sclass)
|
||||
{
|
||||
List<Field> 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);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
/** Returns the subset of {@code fields} declared strictly below {@code ancestor} (all of
|
||||
* them when {@code ancestor} is null). */
|
||||
protected static List<Field> fieldsBelow (List<Field> fields, Class<?> ancestor)
|
||||
{
|
||||
if (ancestor == null) {
|
||||
return fields;
|
||||
}
|
||||
List<Field> below = Lists.newArrayList();
|
||||
for (Field field : fields) {
|
||||
Class<?> declaring = field.getDeclaringClass();
|
||||
if (declaring != ancestor && ancestor.isAssignableFrom(declaring)) {
|
||||
below.add(field);
|
||||
}
|
||||
}
|
||||
return below;
|
||||
}
|
||||
|
||||
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 +669,15 @@ public class GenStreamableTask extends GenTask
|
||||
/** A list of filesets that contain tile images. */
|
||||
protected ArrayList<FileSet> _filesets = Lists.newArrayList();
|
||||
|
||||
/** Whether to generate self-contained flattened methods (Android/ART support). */
|
||||
protected boolean _flatten;
|
||||
|
||||
/** Top-level class names covered by the filesets (see {@link #inFilesets}). */
|
||||
protected java.util.Set<String> _filesetClasses = new java.util.HashSet<String>();
|
||||
|
||||
/** Memo for {@link #willDeclare} ("classname#methodname" → will it declare post-regen). */
|
||||
protected java.util.Map<String, Boolean> _willDeclare = new java.util.HashMap<String, Boolean>();
|
||||
|
||||
protected static final String READ_OPEN =
|
||||
" // from interface Streamable\n" +
|
||||
" public void readObject (ObjectInputStream ins)\n" +
|
||||
@@ -209,4 +691,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]*$");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user