Skip to content

Honor a JavaType's own RpcCodec instead of flattening it - #8743

Open
knutwannheden wants to merge 2 commits into
mainfrom
split-rpc-codec-commits-out-to-main
Open

Honor a JavaType's own RpcCodec instead of flattening it#8743
knutwannheden wants to merge 2 commits into
mainfrom
split-rpc-codec-commits-out-to-main

Conversation

@knutwannheden

@knutwannheden knutwannheden commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

A JavaType that carries its own RpcCodec never reaches the wire as itself:

sent as     JavaType$Class   + the 10 structural fields of JavaType.Class
should be   StubProxy        + the 2 values its own codec emits

Two branches in RpcSendQueue report a superclass name instead of the real class — one for a JavaType subclass declared outside org.openrewrite.java.tree, one for any tree subclass generated outside org.openrewrite. Both exist to keep a synthetic proxy the remote has no factory for off the wire, but they fire unconditionally, so they also erase the name of a subclass that does declare a wire form.

JavaTypeSender.visitClass then walks those ten fields regardless of the instance, among them getSupertype(), getInterfaces(), getMembers() and getMethods(). For a body-less type that resolves lazily from a type table, that is exactly the resolution it exists to avoid.

What this does

Implementing RpcCodec is the signal that a type describes its own wire form, so that is what the three changed branches key on:

  • RpcSendQueue — both flattening branches skip a type that is its own codec, so the real class name survives as the discriminator
  • JavaTypeSender.visitClass — delegates to the instance's rpcSend
  • JavaTypeReceiver.visitClass — delegates to the instance's rpcReceive

No built-in JavaType implements RpcCodec — the hierarchy does not reference it at all — so all three are inert for them and their serialization is unchanged. Only the send half needed opening; receiving was already registry-driven through Class.forName, objenesis and RpcCodec.forInstance.

Python gets the symmetric treatment in RpcSendQueue._get_value_type and PythonRpcSender._visit_type, gated on a registered send codec — the registry being Python's equivalent of "the instance is its own codec". Two supporting changes ride along:

  • JavaType.Class gains an __init__ and read accessors so a body-less shell is fully formed, the list-valued ones returning [] to match the Java getters
  • Tree/Marker/Markers return None from .id for an unset _id, because the RPC receiver reads .id on an all-None placeholder before filling it in

Verification

JavaTypeProxyRpcSendTest uses a StubProxy whose body getters throw, so a structural send fails loudly rather than over-resolving silently. One test pins the discriminator and the exact payload; the other that the round trip rebuilds a StubProxy rather than a desynced plain Class. tests/python/test_rpc_type_proxy.py covers the Python side including the negatives — a plain JavaType.Class with no registered codec still flattens and still takes the hardcoded path.

Why now

Extracted from the boundary-aware type-attribution branch, where these were the only generic RPC plumbing among five commits and carried no boundary concept, so they can be judged on their own and the boundary PR stays boundary work.

…ss subclass

Verifying the RPC codec registries are open to a host (e.g. a moderne-cli addon)
registering codecs for its own type-table proxy value_type surfaced two gaps on
the Python *send* side. The receive side was already registry-driven
(`_new_obj(value_type)` -> registered factory, `_get_codec` by class name), and
needs no change; but the send side flattened any custom `JavaType.Class`
subclass to the built-in `JavaType$Class` and never consulted the registered
send codec, so a host could register codecs that never took effect.

Two guarded hooks close that, both gated on "a send codec is registered for this
object" — the Python analogue of the JVM's "instance is its own RpcCodec" signal.
Built-in JavaTypes have no registered send codec, so both hooks are inert for
them (verified: no built-in JavaType subclass has a registered send codec) and
their serialization is unchanged:

- RpcSendQueue._get_value_type: when the object has a registered send codec,
  honor its registered value_type discriminator instead of flattening it to a
  built-in JavaType name.
- PythonRpcSender._visit_type: when a send codec is registered for the exact
  type, defer to it instead of the hardcoded isinstance serialization.

Adds a pure-Python round-trip test proving a custom JavaType.Class subclass with
registered codecs survives send->receive with its value_type preserved on the
wire and both codecs invoked.

Note: the rewrite-core (JVM) send side has the symmetric flattening
(RpcSendQueue value-type computation collapses foreign-package JavaType
subclasses to their superclass name); that cross-language change is deferred to a
follow-up. The JVM *receive* side is already open (Class.forName + objenesis,
RpcCodec.forInstance via DynamicDispatchRpcCodec ServiceLoader or the instance
implementing RpcCodec).

(cherry picked from commit 67762ce)
The previous commit opened the Python send side to a host-registered codec for
its own JavaType.Class subclass and deferred the symmetric JVM change. This is
that change, so a body-less type-table proxy survives the Java -> Python
direction too.

- RpcSendQueue: a JavaType subclass that implements RpcCodec keeps its real
  class name. Both "flatten a foreign-package subclass to its superclass"
  branches skip it, so the discriminator the remote has a factory registered for
  reaches the wire intact.
- JavaTypeSender/JavaTypeReceiver: visitClass delegates to the instance's own
  codec rather than the structural ten-field walk, which would resolve a proxy's
  body — the thing a proxy exists to avoid.
- JavaTypeProxyRpcSendTest covers the round trip.

On the Python side, JavaType.Class gains an __init__ and read accessors so a
body-less shell is fully formed; the list-valued accessors return [] for an
unpopulated field, matching the Java getters, so iterating a shell never raises.

Unrelated to the boundary work: Tree, Marker and Markers return None from `.id`
when `_id` is unset, since the RPC receiver reads `.id` on an all-None
placeholder before filling it in.

(cherry picked from commit 94d9e87)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant