Chapter 4 of 10
uvm_object and Transactions
The real relationship between uvm_object and uvm_component (not just their differences), the uvm_sequence_item base every transaction actually extends, and writing do_copy/do_compare/convert2string by hand instead of field macros.
Chapter 1 already drew a quick contrast: uvm_component has phases and needs a parent, uvm_object doesn't and generally only needs a name. True as far as it goes, but it makes them sound like two unrelated categories — they aren't. This chapter covers the actual relationship, and what uvm_object looks like when you write one yourself: a transaction.
The real relationship: uvm_component is built on uvm_object
uvm_component isn't a sibling of uvm_object — it's built on top of it:
uvm_object
└── uvm_report_object
└── uvm_component
Every uvm_component you wrote in chapters 2-3 already inherits everything uvm_object provides — for free. Spelled out, that's: create() (via the factory, chapters 3/6), clone()/copy() (this chapter), compare(), print(), and a name. One more worth knowing exists even though this track doesn't use it: record(), which feeds a transaction into a recording database that waveform-viewer tools can display alongside signals — a debug/tooling feature that belongs to dv-methodology's scope, not this track's. uvm_report_object (the layer in between) is what actually gives uvm_component its `uvm_info/`uvm_error/etc. reporting ability from chapter 2. uvm_component then adds the structural pieces on top: a parent handle, and a fixed slot in the phase-driven simulation lifecycle.
So the real question isn't "which one has more features" — uvm_component always has more, because it's built on uvm_object. The real question is what a permanent slot in the component tree and a phase-driven lifecycle actually cost, and when that cost isn't worth paying.
Two concrete differences the relationship implies
This isn't just an abstract inheritance chain — it shows up in two places you'll actually run into:
print()walks the tree for a component, but not for an object.print()is inherited fromuvm_object, so everyuvm_componenthas it too — but call it on a component and it does more than show that one object's fields. Because auvm_componenttracks its children (that's what theparentbookkeeping from chapter 2 is for), the built-in printer recurses into the whole subtree underneath whatever you calledprint()on.env.print()shows the driver's fields, the monitor's fields, and the scoreboard's fields all at once — not justenv's own, which usually has none of its own to show. Callprint()on amux_transaction, and you only ever see that one transaction'ssel/a/bfields, because auvm_objecthas no children to walk in the first place.- The registration macro decides
create()'s signature.`uvm_object_utils-registered types getSomeType::type_id::create(name)— noparentargument, because auvm_objectdoesn't have one.`uvm_component_utils-registered types getSomeType::type_id::create(name, parent), matching the two-argument constructor chapter 3 already relied on. Using the wrong macro for a class (say,`uvm_object_utilson something meant to be auvm_component) doesn't fail with an explanation — it shows up as a confusing compile error about a missing or unexpected argument somewhere else in the code, which is worth recognizing for what it actually is.
When plain uvm_object is the right base — and why it's a hard rule, not just a cost
In UVM's own common vocabulary, this split has a name: uvm_component is static — built once, with a fixed identity and tree position for the rest of the run — while uvm_object is dynamic (or transient) — created and destroyed as often as needed, with no fixed position anywhere. Those are the terms you'll see used for this exact distinction elsewhere (Verification Academy, most UVM references); the rest of this section is why that's true, not just a definition to memorize.
A uvm_component's place in the tree isn't just bookkeeping — the phase engine actually depends on that tree being fixed once elaboration finishes. UVM's phasing assumes the entire hierarchy is complete by the end of build_phase/connect_phase: printing the testbench's topology, and the bottom-up ordering connect_phase relies on (chapter 2), both assume a tree that doesn't change shape afterward. Constructing a new uvm_component in the middle of run_phase — one per transaction, say — isn't just wasteful, it works against an assumption the phase engine is actually built on. This is a real UVM rule, not a style preference: components get constructed during build_phase, and nowhere else.
That's the sharper version of "a permanent tree slot is wasteful": it's not only wasteful, a uvm_component built outside build_phase is doing something the methodology doesn't support in the first place. A plain uvm_object has no such restriction — a stimulus transaction generated fresh for every item driven, a sequence, a small configuration object passed around briefly, can all be created and discarded at any point during run_phase, as often as needed, precisely because none of them ever join the fixed tree to begin with.
Transactions specifically: uvm_sequence_item, not plain uvm_object
A stimulus class isn't written as extends uvm_object directly — it goes one level more specific:
uvm_object
└── uvm_transaction
└── uvm_sequence_item
uvm_sequence_item is uvm_object plus the bookkeeping a sequencer needs to track an item as it moves through the sequencer/driver handshake (chapter 7 uses this directly). Naming this chain here means chapter 7 doesn't need to introduce a new base class mid-chapter — by then, uvm_sequence_item is already familiar.
Writing a transaction: mux_transaction
class mux_transaction extends uvm_sequence_item;
`uvm_object_utils(mux_transaction)
rand bit sel;
rand bit a;
rand bit b;
function new(string name = "mux_transaction");
super.new(name);
endfunction
function void do_copy(uvm_object rhs);
mux_transaction rhs_;
if (!$cast(rhs_, rhs))
`uvm_fatal("DO_COPY", "rhs is not a mux_transaction")
super.do_copy(rhs);
sel = rhs_.sel;
a = rhs_.a;
b = rhs_.b;
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
mux_transaction rhs_;
if (!$cast(rhs_, rhs)) return 0;
return super.do_compare(rhs, comparer) &&
(sel == rhs_.sel) && (a == rhs_.a) && (b == rhs_.b);
endfunction
function string convert2string();
return $sformatf("sel=%0b a=%0b b=%0b", sel, a, b);
endfunction
endclassA few things worth connecting to material you already have:
uvm_object's constructor only takesname— noparent, unlikeuvm_component. That's the one-line proof of the difference chapter 1 already mentioned.do_copy/do_compareuse$castto downcast the genericuvm_object rhsargument tomux_transactionbefore touching its fields — the exact same$castpattern SV ch11 (oop-inheritance-polymorphism) taught, applied to UVM's own base classes.do_copyis the standardized version of SV ch10's hand-writtencopy(). SV ch10 taught writing acopy()method thatnew()s a fresh object and copies each field across — and said outright that "UVM'suvm_object::copy()/clone()are the standardized version of exactly this idea." This is that payoff: you writedo_copy()(just the field-copying part), anduvm_object's owncopy()— inherited, not written by you — handles the rest. To get a copy the way SV ch10'scopy()did (allocate a new object and copy into it in one step), callclone()and$castthe result:
mux_transaction tr2;
$cast(tr2, tr1.clone());convert2string()replaces hand-rolled$sformatfcalls scattered through driver code — write the formatting once, then calltr.convert2string()anywhere you need to print a transaction, including inside`uvm_info.
Field macros vs. hand-written methods: which one this track uses
You'll see transaction classes elsewhere written with field macros instead:
`uvm_object_utils_begin(mux_transaction)
`uvm_field_int(sel, UVM_ALL_ON)
`uvm_field_int(a, UVM_ALL_ON)
`uvm_field_int(b, UVM_ALL_ON)
`uvm_object_utils_endThis generates do_copy, do_compare, convert2string, and more, all from one field list, instead of writing each method by hand. It's common in existing/legacy UVM code, and worth recognizing when you run into it — but it's also less explicit (the generated convert2string() output format isn't something you chose), and its use has fallen out of favor in newer codebases in favor of hand-written methods. This track uses hand-written do_copy/do_compare/convert2string, for the same reason SV ch10 taught a hand-written copy() instead of relying on a shortcut: it's what makes each method's exact behavior visible in the code, not generated from a macro list. (Same shape of decision as SV ch2's `timescale vs. timeunit/timeprecision — name the older, still-common style, then use the more explicit one going forward.)
Putting it to use: the driver drives transactions, not raw signals
Chapter 3's driver assigned vif.sel/vif.a/vif.b directly from hardcoded literals. Swap that for mux_transaction objects:
task run_phase(uvm_phase phase);
mux_transaction tr;
phase.raise_objection(this);
tr = new();
tr.sel = 0; tr.a = 1; tr.b = 0;
drive(tr);
tr = new();
tr.sel = 1; tr.a = 1; tr.b = 0;
drive(tr);
phase.drop_objection(this);
endtask
task drive(mux_transaction tr);
vif.sel = tr.sel;
vif.a = tr.a;
vif.b = tr.b;
#1;
`uvm_info("DRV", $sformatf("%s -> y=%0b", tr.convert2string(), vif.y), UVM_MEDIUM)
endtaskStill the same two hardcoded combinations chapter 3 drove — nothing about the stimulus changed yet, only how it's represented. That distinction matters: once stimulus is a structured object instead of loose local variables, it's ready to be the thing a sequence produces and hands to the driver (chapter 7) instead of something the driver invents itself.
Summary
uvm_componentisn't a sibling ofuvm_object— it's built on top of it (uvm_object→uvm_report_object→uvm_component), inheriting naming,print()/copy()/compare(), and factory registration for free, then adding aparenthandle and a place in the phase-driven lifecycle.print()recurses into an entire subtree for auvm_component(because it tracks children) but only ever shows one object's own fields for a plainuvm_object; the registration macro (`uvm_object_utilsvs.`uvm_component_utils) decides whethertype_id::create()takes just anameor anameand aparent.uvm_componentis static (built once, fixed identity and tree position) anduvm_objectis dynamic/transient (created and destroyed as often as needed) — standard UVM vocabulary for a rule with real teeth: components must be constructed duringbuild_phase, and only there, because the phase engine's guarantees (likeconnect_phase's bottom-up ordering) depend on the tree being fixed once elaboration finishes.- A transaction's real base is
uvm_sequence_item(uvm_object→uvm_transaction→uvm_sequence_item), which adds the bookkeeping the sequencer/driver handshake (chapter 7) needs. do_copy/do_compareuse$cast(SV ch11) to downcast the genericuvm_objectargument before touching fields;do_copyis the standardized version of SV ch10's hand-writtencopy(), andclone()+$castis the closest match to what thatcopy()method did in one step.- This track writes
do_copy/do_compare/convert2stringby hand rather than using field macros (`uvm_field_intand friends) — common in existing code, but less explicit than writing each method out.
Which of these best describes the actual relationship between uvm_object and uvm_component?
Calling env.print() (env a uvm_component) shows fields from the driver, monitor, and scoreboard underneath it too, not just env's own. Why doesn't tr.print() (tr a mux_transaction) do anything similar?
Why does UVM insist that uvm_components only be constructed during build_phase, rather than, say, once per transaction inside run_phase?
Why is a transaction class written as extends uvm_sequence_item rather than extends uvm_object directly?
Inside do_copy(uvm_object rhs), why is $cast needed before accessing rhs's fields?
Which method call gives you a brand-new, independent copy of a transaction in one step (the closest analog to SV chapter 10's hand-written copy() method)? (lowercase, no parentheses)