RPNXCortado
C++23 JVM class-file, bytecode, validation, and JAR toolkit
Loading...
Searching...
No Matches
Verification, stack maps, and debug metadata

Cortado's symbolic builder performs method-level analysis by default. The analysis is part of code_builder::finish, which class_file_builder::add_method calls with the containing class, method descriptor, flags, and version.

Automatic method limits

The builder derives the initial locals from the method descriptor and access flags. It then follows normal and exceptional control-flow edges to calculate:

  • the maximum operand-stack depth in JVM slots;
  • the highest occupied local slot, including category-2 long and double values;
  • compatible stack heights at every merge; and
  • the verifier states used for StackMapTable generation.

Explicit code_build_options::max_stack or max_locals values act as requested limits. They may be larger than the analyzed requirement, but a smaller value is rejected.

StackMapTable generation

For class-file major versions 50 and later, frame generation is enabled by default. Straight-line methods may need no explicit entries. Branch targets, handlers, and blocks after unconditional transfers can require entries even when source-level control flow appears simple.

Primitive verifier states merge without external information. Merging two different reference types may require the common superclass. Supply a non-owning class_hierarchy_resolver_ref backed by your compiler's type model or class path:

rpnx::cortado::code_builder select_collection;
rpnx::cortado::label const use_map = select_collection.new_label();
rpnx::cortado::label const collection_selected = select_collection.new_label();
select_collection.iload({0})
.branch< rpnx::cortado::opcode::ifeq >(use_map)
.aload({1})
.branch< rpnx::cortado::opcode::goto_ >(collection_selected)
.bind(use_map)
.aload({2})
.bind(collection_selected)
.append< rpnx::cortado::opcode::areturn >();
static_cast< void >(builder.add_method(
"selectCollection",
"(ZLjava/util/ArrayList;Ljava/util/HashMap;)Ljava/lang/Object;",
rpnx::cortado::method_access_flags::is_public |
rpnx::cortado::method_access_flags::is_static,
select_collection,
{},

The referenced resolver must outlive the add_method or finish call. It is queried only when two distinct reference types actually need hierarchy knowledge. The tutorial resolver conservatively returns java/lang/Object for ArrayList and HashMap; a production resolver should return the real least common superclass for every input pair it accepts.

If no resolver is supplied and a merge needs one, finalization throws unsupported_feature_error rather than inventing a type.

Source and local-variable metadata

Line numbers and local scopes use the same labels as branches, so layout changes cannot stale their offsets. The sumTo example attaches three source lines and three local scopes before adding the method:

rpnx::cortado::label const sum_entry = sum_to.new_label();
rpnx::cortado::label const sum_loop = sum_to.new_label();
rpnx::cortado::label const sum_done = sum_to.new_label();
rpnx::cortado::label const sum_end = sum_to.new_label();
sum_to.bind(sum_entry)
.append< rpnx::cortado::opcode::iconst_0 >()
.istore({1})
.append< rpnx::cortado::opcode::iconst_1 >()
.istore({2})
.bind(sum_loop)
.iload({2})
.iload({0})
.branch< rpnx::cortado::opcode::if_icmpgt >(sum_done)
.iload({1})
.iload({2})
.append< rpnx::cortado::opcode::iadd >()
.istore({1})
.iinc({2}, 1)
.branch< rpnx::cortado::opcode::goto_ >(sum_loop)
.bind(sum_done)
.iload({1})
.append< rpnx::cortado::opcode::ireturn >()
.bind(sum_end);
sum_to.add_line_number(sum_entry, 10)
.add_line_number(sum_loop, 12)
.add_line_number(sum_done, 16)
.add_local_variable(sum_entry, sum_end, "limit", "I", {0})
.add_local_variable(sum_entry, sum_end, "sum", "I", {1})
.add_local_variable(sum_entry, sum_end, "index", "I", {2});
static_cast< void >(builder.add_method(
"sumTo",
"(I)I",
rpnx::cortado::method_access_flags::is_public |
rpnx::cortado::method_access_flags::is_static,
sum_to));

An add_line_number label must identify an instruction. A local-variable scope uses an inclusive start and exclusive end; the end label may be bound directly after the final instruction. add_local_variable_type has the same shape but stores a generic field signature for LocalVariableTypeTable.

Exact/manual metadata

set_stack_map_table installs a complete caller-provided table and bypasses automatic frame generation. Cortado replaces its name_index with the shared pool's interned StackMapTable name but otherwise retains the frames.

Automatic dataflow can be disabled only when both method limits are explicit:

options.max_stack = 2;
options.max_locals = 3;
options.perform_dataflow_analysis = false;
options.generate_stack_map_table = false;
Controls automatic Code metadata generation.
Definition builders.hpp:326
std::optional< std::uint16_t > max_stack
Exact requested maximum, or no value to use the analyzed requirement.
Definition builders.hpp:327
bool generate_stack_map_table
Emit analyzed frames unless an exact table was supplied.
Definition builders.hpp:330
bool perform_dataflow_analysis
Compute stack/local requirements and verifier states.
Definition builders.hpp:329
std::optional< std::uint16_t > max_locals
Exact requested maximum, or no value to use the analyzed requirement.
Definition builders.hpp:328

This is an expert escape hatch for externally analyzed code and legacy jsr/ret subroutines. For modern branched methods, disabling stack-map generation can produce a class the JVM rejects even when structural validation succeeds. Prefer the generic analysis path or provide an exact table.

Next: Exact instructions and the constant pool.