Skip to main content

Object Utility Methods

Every uvm_object — every transaction, every config object — comes with four capabilities built into the base class: printing itself for a log, copying its fields into another instance, comparing itself against another instance, and serializing itself to and from a flat bit stream. None of these work automatically the moment a class is written; each one has a hook method that either gets implemented by hand or generated by the field macros covered on the next page. This page covers what each capability actually does and how the hand-written path works, so the macro-generated version on the next page isn't a black box.

Printing: print(), sprint(), and do_print()​

class bus_txn extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data;
`uvm_object_utils(bus_txn)

function new(string name = "bus_txn");
super.new(name);
endfunction

function void do_print(uvm_printer printer);
super.do_print(printer);
printer.print_field_int("addr", addr, $bits(addr), UVM_HEX);
printer.print_field_int("data", data, $bits(data), UVM_HEX);
endfunction
endclass
  • print() writes a formatted table straight to the log — name, type, size, and value for every field registered with the printer.
  • sprint() produces the exact same table, but as a returned string instead of direct output — the natural choice when the formatted table needs to be embedded inside a `uvm_info call rather than printed on its own.
  • do_print(uvm_printer printer) is the hook that actually populates the table, called internally by both print() and sprint(). printer.print_field_int() takes the field's name, value, bit width ($bits() reads this directly off the variable, avoiding a hardcoded number that could drift out of sync), and a radix (UVM_HEX, UVM_DEC, etc.) controlling how it's displayed.
  • Arrays and queues print with each entry auto-indexed (addr[0], addr[1], …) without any extra code.

super.do_print(printer) at the top matters here for the same reason super.build_phase(phase) matters in every phase method — it lets the base class (and anything the macros generate, on the next page) contribute to the same table instead of being silently skipped.

Copying and cloning: copy(), clone(), and do_copy()​

function void bus_txn::do_copy(uvm_object rhs);
bus_txn rhs_;
if (!$cast(rhs_, rhs))
`uvm_fatal("COPY", "Cast failed — rhs is not a bus_txn")
super.do_copy(rhs);
addr = rhs_.addr;
data = rhs_.data;
endfunction
  • copy(rhs) copies rhs's fields into an already-existing object.
  • clone() does one step more: it constructs a brand-new object and copies into it, in one call — useful anywhere a fresh, independent snapshot is needed without a separate new() beforehand.
  • do_copy(uvm_object rhs) is the hook both of them call under the hood. Notice rhs arrives typed as a generic uvm_object — the base class has no way to know it's actually a bus_txn — so $cast() is required before any field on it can be accessed. This is the same reason do_compare() below needs an identical cast: neither hook can assume the type of what it's been handed.

clone() has one sharp edge worth knowing up front:

bus_txn copy_of_it;
$cast(copy_of_it, original.clone()); // cast required — clone() returns uvm_object, not bus_txn

Because clone() is declared once on uvm_object and shared by every subclass, its return type is fixed at uvm_object even when called on a bus_txn — so the caller has to $cast() the result back down to the real type, every time.

Comparing: compare() and do_compare()​

function bit bus_txn::do_compare(uvm_object rhs, uvm_comparer comparer);
bus_txn rhs_;
bit result;
if (!$cast(rhs_, rhs)) return 0;
result = super.do_compare(rhs, comparer);
result &= comparer.compare_field_int("addr", addr, rhs_.addr, $bits(addr));
result &= comparer.compare_field_int("data", data, rhs_.data, $bits(data));
return result;
endfunction

compare() returns a single bit — equal or not — and, when using macro-generated comparison, automatically logs a "MISCMP" message identifying exactly which field(s) disagreed. Writing do_compare() by hand, as above, means adding that visibility manually with `uvm_info calls if it's wanted, since the framework only generates it for the macro path. For an object composed of nested objects, each nested comparison's result gets folded into the running total with a bitwise AND (&=), so any single field mismatch anywhere in the structure fails the whole comparison. The uvm_comparer argument is a configurable policy object (severity, how many mismatches to report, and more), covered in depth in a later page on printer and comparer policies.

Serializing: pack() and unpack()​

Pack/unpack turn an object into a flat stream of bits and back — the tool for anything that has to cross a boundary UVM's class hierarchy doesn't reach on its own: a DPI call into C, a file written to disk, a socket to another simulator in a co-simulation setup.

There are three pack variants, differing only in what shape the output takes:

  • pack(bits[]) — a raw, unpadded bit array.
  • pack_bytes(bytes[]) — byte-aligned; pads up to 7 bits so the result lands on a byte boundary.
  • pack_ints(ints[]) — word-aligned; pads up to 31 bits so the result lands on a 32-bit boundary.

unpack() (and its _bytes/_ints counterparts) reverse the process — with one detail that catches people off guard the first time:

unpack()'s return value is a bit count, not data

unpack() returns how many bits it consumed, not any field value — it's easy to assume otherwise, especially coming from compare()'s bit-for-equal convention. Field order also has to match exactly between the pack() call that produced the stream and the unpack() call reading it back — a mismatched order silently misreads every field after the first discrepancy, with no error to flag it.

Like printing, copying, and comparing, pack/unpack have manual hooks too — do_pack(uvm_packer packer) / do_unpack(uvm_packer packer) — using packer.pack_field_int() / packer.unpack_field_int() in the same shape as the printing and comparing examples above.

Hand-written hooks vs. automation​

Every hook on this page (do_print, do_copy, do_compare, do_pack/do_unpack) can be written by hand, as shown, or generated automatically by wrapping fields in field macros — covered next. Hand-written hooks give full control over exactly what's included and how it's formatted, and avoid the modest simulation overhead the generic macro-driven path carries; macros are far less typing and stay in sync automatically as fields are added or removed. Neither is "wrong" — which one to reach for is mostly a question of how many fields a class has and how much that overhead actually matters for the component in question.

What's next​

Utility & Field Macros covers the macro-driven alternative to everything on this page — including the factory-registration macros every class in this section has already been using without full explanation.