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}