Skip to main content

jdwp_client/
reftype.rs

1// ReferenceType command implementations
2//
3// Commands for working with classes, interfaces, and arrays
4
5use crate::commands::{command_sets, reference_type_commands};
6use crate::connection::JdwpConnection;
7use crate::protocol::{CommandPacket, JdwpResult, ERR_ABSENT_INFORMATION, ERR_NOT_IMPLEMENTED};
8use crate::reader::{read_i32, read_string, read_u64, some_if_present};
9use crate::types::{FieldId, MethodId, ObjectId, ReferenceTypeId};
10use bytes::BufMut;
11use serde::{Deserialize, Serialize};
12
13/// Method information
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct MethodInfo {
16    pub method_id: MethodId,
17    pub name: String,
18    pub signature: String,
19    /// The **generic** signature from the class file's `Signature` attribute, when it carries one
20    /// (DISC-12, #95).
21    ///
22    /// `None` is the ordinary answer, not a degraded one: the attribute is optional, absent for code
23    /// compiled without it and for synthetic members whose types were erased. JDWP's generic commands
24    /// answer with an EMPTY STRING in that case rather than an error, and an empty string is normalised to
25    /// `None` here so that no caller can render a blank type.
26    pub generic_signature: Option<String>,
27    pub mod_bits: i32,
28}
29
30/// Field information
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct FieldInfo {
33    pub field_id: FieldId,
34    pub name: String,
35    pub signature: String,
36    /// The **generic** signature — see [`MethodInfo::generic_signature`] for what `None` means.
37    pub generic_signature: Option<String>,
38    pub mod_bits: i32,
39}
40
41impl JdwpConnection {
42    /// Get methods for a reference type (ReferenceType.Methods command)
43    ///
44    /// # Errors
45    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
46    pub async fn get_methods(&mut self, ref_type_id: ReferenceTypeId) -> JdwpResult<Vec<MethodInfo>> {
47        // Declared methods are fixed for a loaded type. Overload scoring walks this list once per
48        // candidate class per call, so the hit rate here is high.
49        if let Some(hit) = self.types().methods(ref_type_id) {
50            return Ok(hit);
51        }
52        // `MethodsWithGeneric` rather than `Methods` (DISC-12, #95): same cost, one extra string per
53        // entry, and it is the only place a *use-site* type argument can come from. Falls back to the plain
54        // command if a VM does not implement it — the generic variants are JDWP 1.5 and every supported JDK
55        // has them, so the fallback is for a non-HotSpot VM rather than for an old JDK.
56        let methods = match self.read_methods(ref_type_id, true).await {
57            Ok(m) => m,
58            Err(crate::JdwpError::JdwpErrorCode(code, _)) if code == ERR_NOT_IMPLEMENTED => {
59                self.read_methods(ref_type_id, false).await?
60            }
61            Err(e) => return Err(e),
62        };
63        self.types().put_methods(ref_type_id, &methods);
64        Ok(methods)
65    }
66
67    /// One read of a type's declared methods, with or without the generic signature column.
68    ///
69    /// **The two replies have a different layout and cannot share a reader by accident**, which is the
70    /// second risk #95 names: `MethodsWithGeneric` inserts one string per entry between the signature and
71    /// the modifier bits. Reading a generic reply with the plain loop would take the generic signature as
72    /// the mod bits and then desynchronise for every remaining method — so the layout is decided by the
73    /// same flag that chose the command, in one function, rather than by two loops that must be kept in
74    /// step.
75    async fn read_methods(
76        &mut self,
77        ref_type_id: ReferenceTypeId,
78        with_generic: bool,
79    ) -> JdwpResult<Vec<MethodInfo>> {
80        let command = if with_generic {
81            reference_type_commands::METHODS_WITH_GENERIC
82        } else {
83            reference_type_commands::METHODS
84        };
85        let id = self.next_id();
86        let mut packet = CommandPacket::new(id, command_sets::REFERENCE_TYPE, command);
87        packet.data.put_u64(ref_type_id);
88
89        let reply = self.send_command(packet).await?;
90        reply.check_error()?;
91
92        let mut data = reply.data();
93        let methods_count = read_i32(&mut data)?;
94        let mut methods = Vec::with_capacity(usize::try_from(methods_count).unwrap_or(0));
95
96        for _ in 0..methods_count {
97            let method_id = read_u64(&mut data)?;
98            let name = read_string(&mut data)?;
99            let signature = read_string(&mut data)?;
100            let generic_signature =
101                if with_generic { some_if_present(read_string(&mut data)?) } else { None };
102            let mod_bits = read_i32(&mut data)?;
103
104            methods.push(MethodInfo { method_id, name, signature, generic_signature, mod_bits });
105        }
106        Ok(methods)
107    }
108
109    /// `ReferenceType.ClassLoader` — which classloader defined this type (BP-5, #79).
110    ///
111    /// The answer that makes "the class is loaded twice" a statement a caller can act on rather than a
112    /// warning they can only nod at. `classes_by_signature` returns one entry per classloader that has
113    /// loaded a name, and on an app server that is the ordinary case, not the exotic one: `WildFly` gives
114    /// every deployment its own module classloader, and a library packed into each war's `WEB-INF/lib`
115    /// is a genuinely different reference type per deployment — different `public static` state,
116    /// different endpoint URLs, different mute flags.
117    ///
118    /// **`Ok(None)` means the bootstrap classloader**, which is what JDWP's null `objectID` encodes and
119    /// is a real answer (`java.lang.String` has no loader object). It is not a failure, and rendering
120    /// it as one would make every JDK type look broken.
121    ///
122    /// Returns the loader's raw `objectID` and nothing else on purpose. Naming it means reading its own
123    /// type — [`Self::get_object_reference_type`](crate::JdwpConnection::get_object_reference_type)
124    /// plus a signature — which the caller can do when it has a caller to answer; calling `toString()`
125    /// on it would need a suspended thread and is exactly the implicit invocation ADR-0001's posture
126    /// rules out.
127    ///
128    /// # Errors
129    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
130    pub async fn get_class_loader(&mut self, ref_type_id: ReferenceTypeId) -> JdwpResult<Option<ObjectId>> {
131        let id = self.next_id();
132        let mut packet =
133            CommandPacket::new(id, command_sets::REFERENCE_TYPE, reference_type_commands::CLASS_LOADER);
134        packet.data.put_u64(ref_type_id);
135
136        let reply = self.send_command(packet).await?;
137        reply.check_error()?;
138
139        let mut data = reply.data();
140        let loader = read_u64(&mut data)?;
141        Ok((loader != 0).then_some(loader))
142    }
143
144    /// `ReferenceType.Interfaces` — the interfaces this type declares **directly**.
145    ///
146    /// Direct only, per the JDWP spec: `class A implements Runnable` reports `Runnable`, and a class
147    /// whose *superclass* implements it reports nothing. Use [`Self::implements_interface`] for the
148    /// question callers actually have.
149    ///
150    /// # Errors
151    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
152    pub async fn get_interfaces(&mut self, ref_type_id: ReferenceTypeId) -> JdwpResult<Vec<ReferenceTypeId>> {
153        if let Some(hit) = self.types().interfaces(ref_type_id) {
154            return Ok(hit);
155        }
156        let id = self.next_id();
157        let mut packet =
158            CommandPacket::new(id, command_sets::REFERENCE_TYPE, reference_type_commands::INTERFACES);
159        packet.data.put_u64(ref_type_id);
160
161        let reply = self.send_command(packet).await?;
162        reply.check_error()?;
163
164        let mut data = reply.data();
165        let count = read_i32(&mut data)?;
166        let mut ifaces = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
167        for _ in 0..count {
168            ifaces.push(read_u64(&mut data)?);
169        }
170        self.types().put_interfaces(ref_type_id, &ifaces);
171        Ok(ifaces)
172    }
173
174    /// `ReferenceType.Modifiers` — the class-level access flags the JVM holds for this type.
175    ///
176    /// The same `u16` the class file's `access_flags` carries, widened to `i32` by the wire format, so it
177    /// is directly comparable with a parsed `.class` — which is what DISC-13 needs it for: `HotSpot`
178    /// refuses a redefinition whose class modifiers changed (`CLASS_MODIFIERS_CHANGE_NOT_IMPLEMENTED`),
179    /// and that is decidable before the attempt.
180    ///
181    /// Not cached. It is one packet, asked once per forecast, and a type's modifiers are the sort of
182    /// thing a redefinition is *about* — a cache here would be a way to answer from before the swap.
183    ///
184    /// # Errors
185    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
186    pub async fn get_modifiers(&mut self, ref_type_id: ReferenceTypeId) -> JdwpResult<i32> {
187        let id = self.next_id();
188        let mut packet =
189            CommandPacket::new(id, command_sets::REFERENCE_TYPE, reference_type_commands::MODIFIERS);
190        packet.data.put_u64(ref_type_id);
191
192        let reply = self.send_command(packet).await?;
193        reply.check_error()?;
194
195        read_i32(&mut reply.data())
196    }
197
198    /// Whether `type_id` implements the interface whose JNI signature is `wanted` (e.g.
199    /// `"Ljava/lang/Runnable;"`), the way `instanceof` would answer it.
200    ///
201    /// Walks the whole lattice, because JDWP only reports *direct* superinterfaces: up the superclass
202    /// chain (a parent's interfaces are inherited) and across each type's interfaces transitively (an
203    /// interface extends interfaces). Every step reads through the type cache, so a repeat question
204    /// about the same class costs nothing.
205    ///
206    /// # Errors
207    /// Returns a [`JdwpError`](crate::JdwpError) if a JDWP request fails or a reply cannot be parsed.
208    pub async fn implements_interface(&mut self, type_id: ReferenceTypeId, wanted: &str) -> JdwpResult<bool> {
209        // Breadth-first over (superclasses × interfaces), with `seen` guarding the diamonds that make
210        // interface graphs a lattice rather than a tree — without it, `Collection` is visited once per
211        // path that reaches it.
212        let mut seen = std::collections::HashSet::new();
213        let mut queue = vec![type_id];
214        // A bound on pathological/cyclic input, matching the superclass walks elsewhere in the crate.
215        let mut steps = 0;
216        while let Some(current) = queue.pop() {
217            steps += 1;
218            if steps > 500 {
219                break;
220            }
221            if !seen.insert(current) {
222                continue;
223            }
224            if self.get_signature(current).await.is_ok_and(|s| s == wanted) {
225                return Ok(true);
226            }
227            queue.extend(self.get_interfaces(current).await.unwrap_or_default());
228            if let Some(parent) = self.get_superclass(current).await.unwrap_or(None) {
229                queue.push(parent);
230            }
231        }
232        Ok(false)
233    }
234
235    /// `ReferenceType.SourceFile` — the file this type was compiled from, e.g. `OrderService.java`.
236    ///
237    /// A **bare file name, never a path**: the `SourceFile` class-file attribute records the name of
238    /// the compilation unit and nothing about where it lived, so a caller wanting a path has to get
239    /// the directory part from the type's own package. An inner or local type reports its *enclosing*
240    /// file (`Order.java` for `Order$Line`), because it has no compilation unit of its own — which is
241    /// exactly why resolving source by class name rather than by this cannot work.
242    ///
243    /// Deliberately uncached, unlike [`Self::get_methods`] / `get_signature`: those are read once per
244    /// frame in a loop, this is read once per `debug.source` call.
245    ///
246    /// # Errors
247    /// Returns [`JdwpError::JdwpErrorCode`](crate::JdwpError::JdwpErrorCode) carrying
248    /// [`ERR_ABSENT_INFORMATION`] for a class compiled without the attribute (`javac -g:none`, or a
249    /// synthetic class the JVM generated). That is an answer about the class, not a transport
250    /// failure, and callers are expected to report it as one.
251    pub async fn get_source_file(&mut self, ref_type_id: ReferenceTypeId) -> JdwpResult<String> {
252        let id = self.next_id();
253        let mut packet =
254            CommandPacket::new(id, command_sets::REFERENCE_TYPE, reference_type_commands::SOURCE_FILE);
255        packet.data.put_u64(ref_type_id);
256
257        let reply = self.send_command(packet).await?;
258        reply.check_error()?;
259
260        let mut data = reply.data();
261        read_string(&mut data)
262    }
263
264    /// `ReferenceType.SourceDebugExtension` — the JSR-45 SMAP that says which *original* file the
265    /// bytecode came from when that is not the `.java` in [`Self::get_source_file`]: a JSP, a Kotlin
266    /// or Groovy unit, anything run through a translating compiler.
267    ///
268    /// `Ok(None)` is the ordinary answer, not a degraded one, so two error codes are absorbed rather
269    /// than propagated: [`ERR_ABSENT_INFORMATION`] for a class with no SMAP — which is nearly every
270    /// class — and [`ERR_NOT_IMPLEMENTED`] for a VM that lacks the optional
271    /// `canGetSourceDebugExtension` capability. Reporting either as an error would make the common
272    /// case look broken.
273    ///
274    /// # Errors
275    /// Returns a [`JdwpError`](crate::JdwpError) only for a genuine failure — a transport error, some
276    /// other JDWP error code, or a reply that will not parse.
277    pub async fn get_source_debug_extension(
278        &mut self,
279        ref_type_id: ReferenceTypeId,
280    ) -> JdwpResult<Option<String>> {
281        let id = self.next_id();
282        let mut packet = CommandPacket::new(
283            id,
284            command_sets::REFERENCE_TYPE,
285            reference_type_commands::SOURCE_DEBUG_EXTENSION,
286        );
287        packet.data.put_u64(ref_type_id);
288
289        let reply = self.send_command(packet).await?;
290        if matches!(reply.error_code, ERR_ABSENT_INFORMATION | ERR_NOT_IMPLEMENTED) {
291            return Ok(None);
292        }
293        reply.check_error()?;
294
295        let mut data = reply.data();
296        read_string(&mut data).map(Some)
297    }
298
299    /// Get fields for a reference type (ReferenceType.Fields command)
300    ///
301    /// # Arguments
302    /// * `ref_type_id` - The `ReferenceTypeId` to get fields for
303    ///
304    /// # Returns
305    /// Vector of `FieldInfo` containing field IDs, names, signatures, and modifiers
306    ///
307    /// # Example
308    /// ```no_run
309    /// # use jdwp_client::types::ReferenceTypeId;
310    /// # async fn demo(mut connection: jdwp_client::JdwpConnection, class_id: ReferenceTypeId)
311    /// #     -> jdwp_client::JdwpResult<()> {
312    /// let fields = connection.get_fields(class_id).await?;
313    /// for field in fields {
314    ///     println!("Field: {} ({})", field.name, field.signature);
315    /// }
316    /// # Ok(()) }
317    /// ```
318    ///
319    /// # Errors
320    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed.
321    pub async fn get_fields(&mut self, ref_type_id: ReferenceTypeId) -> JdwpResult<Vec<FieldInfo>> {
322        // Declared fields are fixed for a loaded type. Expanding N objects of the same class used to ask
323        // the JVM for this list N times.
324        if let Some(hit) = self.types().fields(ref_type_id) {
325            return Ok(hit);
326        }
327        // `FieldsWithGeneric`, for the reasons on `get_methods` above.
328        let fields = match self.read_fields(ref_type_id, true).await {
329            Ok(f) => f,
330            Err(crate::JdwpError::JdwpErrorCode(code, _)) if code == ERR_NOT_IMPLEMENTED => {
331                self.read_fields(ref_type_id, false).await?
332            }
333            Err(e) => return Err(e),
334        };
335        self.types().put_fields(ref_type_id, &fields);
336        Ok(fields)
337    }
338
339    /// One read of a type's declared fields, with or without the generic signature column — see
340    /// [`Self::read_methods`] for why the layout and the command are chosen together.
341    async fn read_fields(
342        &mut self,
343        ref_type_id: ReferenceTypeId,
344        with_generic: bool,
345    ) -> JdwpResult<Vec<FieldInfo>> {
346        let command = if with_generic {
347            reference_type_commands::FIELDS_WITH_GENERIC
348        } else {
349            reference_type_commands::FIELDS
350        };
351        let id = self.next_id();
352        let mut packet = CommandPacket::new(id, command_sets::REFERENCE_TYPE, command);
353        packet.data.put_u64(ref_type_id);
354
355        let reply = self.send_command(packet).await?;
356        reply.check_error()?;
357
358        let mut data = reply.data();
359        let fields_count = read_i32(&mut data)?;
360        let mut fields = Vec::with_capacity(usize::try_from(fields_count).unwrap_or(0));
361
362        for _ in 0..fields_count {
363            let field_id = read_u64(&mut data)?;
364            let name = read_string(&mut data)?;
365            let signature = read_string(&mut data)?;
366            let generic_signature =
367                if with_generic { some_if_present(read_string(&mut data)?) } else { None };
368            let mod_bits = read_i32(&mut data)?;
369
370            fields.push(FieldInfo { field_id, name, signature, generic_signature, mod_bits });
371        }
372        Ok(fields)
373    }
374
375    /// The live instances of one type (`ReferenceType.Instances`, command 16).
376    ///
377    /// **This stops the world, and JDWP never says so.** No suspend is required and this client issues
378    /// none, yet the JVM holds every application thread for a full live-heap walk. Measured against
379    /// Temurin 17.0.20: **522 ms of held application threads on a 2,000,000-object heap** to answer with
380    /// 7 objects, against 54 ms on a 20,000-object heap for **the same 7 objects**. The cost tracks the
381    /// live heap, not the result. Full method and wire notes in `docs/heap-query-measurements.md`.
382    ///
383    /// **Exact type, not subtype-inclusive.** `Widget` answers 7 with two live `SubWidget`s in the heap,
384    /// not 9. On a CDI or EJB codebase the name a caller reaches for is usually the interface or the
385    /// base class, so this is the semantic most likely to produce a confident `0` about a type with
386    /// hundreds of live objects — the `Loaded` trap from `CONTEXT.md` in a new costume. Anything built
387    /// on this has to say so rather than let it be discovered.
388    ///
389    /// Only **strongly reachable** objects are reported. `max_instances` `0` means all, a positive value
390    /// clamps, and a negative one is `ILLEGAL_ARGUMENT` (103) — rejected here before the round trip,
391    /// since a wire error is a poor way to report an argument this crate can see is wrong.
392    ///
393    /// Each returned [`Value`](crate::types::Value) carries the JVM's own tag, so a String, an array, a thread and a class
394    /// object are distinguishable without a follow-up round trip.
395    ///
396    /// # Errors
397    /// Returns a [`JdwpError`](crate::JdwpError) if the JDWP request fails or the reply cannot be parsed. `NOT_IMPLEMENTED`
398    /// (99) when the JVM lacks `canGetInstanceInfo`, and `INVALID_OBJECT` (20) for a bogus type id — ask
399    /// [`capabilities_new`](JdwpConnection::capabilities_new) first, so a refusal reads as "this JVM
400    /// cannot answer that".
401    pub async fn instances(
402        &mut self,
403        ref_type_id: ReferenceTypeId,
404        max_instances: i32,
405    ) -> JdwpResult<Vec<crate::types::Value>> {
406        if max_instances < 0 {
407            return Err(crate::protocol::JdwpError::Protocol(format!(
408                "max_instances must be 0 (all) or positive, got {max_instances}"
409            )));
410        }
411        let id = self.next_id();
412        let mut packet =
413            CommandPacket::new(id, command_sets::REFERENCE_TYPE, reference_type_commands::INSTANCES);
414        packet.data.put_u64(ref_type_id);
415        packet.data.put_i32(max_instances);
416
417        let reply = self.send_command(packet).await?;
418        reply.check_error()?;
419
420        let mut data = reply.data();
421        let count = read_i32(&mut data)?;
422        let mut out = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
423        for _ in 0..count {
424            let tag = crate::reader::read_u8(&mut data)?;
425            let value_data = crate::reader::read_value_by_tag(tag, &mut data)?;
426            out.push(crate::types::Value { tag, data: value_data });
427        }
428        Ok(out)
429    }
430}