Every UVM object can be copied with copy() or duplicated with clone(), two methods inherited from uvm_object. copy() fills an existing object with another object's field values, while clone() creates a brand new object and copies into it in one step. Both rely on the same hook, do_copy(), which either the field macros or your own code provides.
Copying matters more in a testbench than it first appears. A monitor that sends the same transaction handle to a scoreboard, then reuses that handle for the next bus cycle, silently corrupts the scoreboard's data. Copying or cloning the transaction before handing it on is how you avoid that.
8 min read | Intermediate Level
What You'll Learn
- Explain the difference between handle assignment,
copy()andclone() - Copy objects automatically using UVM field macros
- Write a correct
do_copy()implementation by hand - Use
clone()with$castto duplicate an object in one call
Syntax
All three methods are declared in uvm_object, so every transaction, sequence and configuration object has them.
function void copy (uvm_object rhs); // copy rhs's fields into this object
virtual function void do_copy (uvm_object rhs); // hook called by copy(); you override this
virtual function uvm_object clone (); // create a new object, copy this one into it
copy()is not virtual and should never be overridden. It callsdo_copy(), which is where a class adds its own fields.do_copy()receives the source as a baseuvm_objecthandle, so it has to cast it to the real type before reading any fields.clone()callscreate()to allocate a new object of the same type, then callscopy()on it. It returns auvm_objecthandle.
Handle Assignment vs copy() vs clone()
The three operations look similar in code but produce very different results in memory.
A plain assignment such as obj2 = obj1; copies only the handle. Both names now refer to the same object, so any change made through one is visible through the other. That is rarely what a testbench wants when it passes data between components.
obj2.copy(obj1) needs two separate objects to already exist. It overwrites the fields of obj2 with the values from obj1, and afterwards the two objects are independent. clone() does the allocation for you, which makes it the natural choice when the destination does not exist yet.
Copying with Field Macros
The quickest way to get a working do_copy() is to register each field with a field macro between `uvm_object_utils_begin and `uvm_object_utils_end. The macros generate copy, compare, print and pack support for every registered field.
In this example, class Packet has a single field, and class Object holds several fields of different types, including a Packet handle. Each type needs its matching macro - for example, `uvm_field_string for a string and `uvm_field_object for a class handle.
typedef enum {FALSE, TRUE} e_bool;
class Packet extends uvm_object;
rand bit[15:0] m_addr;
// Automation macros
`uvm_object_utils_begin(Packet)
`uvm_field_int(m_addr, UVM_DEFAULT)
`uvm_object_utils_end
function new(string name = "Packet");
super.new(name);
endfunction
endclass
class Object extends uvm_object;
rand e_bool m_bool;
rand bit[3:0] m_mode;
rand byte m_data[4];
rand shortint m_queue[$];
string m_name;
rand Packet m_pkt;
constraint c_queue { m_queue.size() == 3; }
function new(string name = "Object");
super.new(name);
m_name = name;
m_pkt = Packet::type_id::create("m_pkt");
m_pkt.randomize();
endfunction
`uvm_object_utils_begin(Object)
`uvm_field_enum(e_bool, m_bool, UVM_DEFAULT)
`uvm_field_int (m_mode, UVM_DEFAULT)
`uvm_field_sarray_int(m_data, UVM_DEFAULT)
`uvm_field_queue_int(m_queue, UVM_DEFAULT)
`uvm_field_string(m_name, UVM_DEFAULT)
`uvm_field_object(m_pkt, UVM_DEFAULT)
`uvm_object_utils_end
endclass
The test creates two objects, randomizes and prints both, then copies obj1 into obj2 and prints obj2 again.
class base_test extends uvm_test;
`uvm_component_utils(base_test)
function new(string name = "base_test", uvm_component parent=null);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
Object obj1 = Object::type_id::create("obj1");
Object obj2 = Object::type_id::create("obj2");
obj1.randomize();
obj1.print();
obj2.randomize();
obj2.print();
obj2.copy(obj1);
`uvm_info("TEST", "After copy", UVM_LOW)
obj2.print();
endfunction
endclass
module tb;
initial begin
run_test("base_test");
end
endmodule
The first table shows obj1 after randomization, the second shows obj2, and the third shows obj2 after the copy.
ncsim> run UVM_INFO @ 0: reporter [RNTST] Running test base_test... -------------------------------------- Name Type Size Value -------------------------------------- obj1 Object - @1903 m_bool e_bool 32 TRUE m_mode integral 4 'he m_data sa(integral) 4 - [0] integral 8 'hf4 [1] integral 8 'he [2] integral 8 'h58 [3] integral 8 'hbd m_queue da(integral) 3 - [0] integral 16 'h9ae9 [1] integral 16 'hd31d [2] integral 16 'ha96c m_name string 4 obj1 m_pkt Packet - @1906 m_addr integral 16 'h3cb6 -------------------------------------- -------------------------------------- Name Type Size Value -------------------------------------- obj2 Object - @1908 m_bool e_bool 32 FALSE m_mode integral 4 'he m_data sa(integral) 4 - [0] integral 8 'h60 [1] integral 8 'h24 [2] integral 8 'h27 [3] integral 8 'hb5 m_queue da(integral) 3 - [0] integral 16 'he17f [1] integral 16 'h98e6 [2] integral 16 'h5a41 m_name string 4 obj2 m_pkt Packet - @1910 m_addr integral 16 'h64c1 -------------------------------------- UVM_INFO testbench.sv(60) @ 0: uvm_test_top [TEST] After copy -------------------------------------- Name Type Size Value -------------------------------------- obj2 Object - @1908 m_bool e_bool 32 TRUE m_mode integral 4 'he m_data sa(integral) 4 - [0] integral 8 'hf4 [1] integral 8 'he [2] integral 8 'h58 [3] integral 8 'hbd m_queue da(integral) 3 - [0] integral 16 'h9ae9 [1] integral 16 'hd31d [2] integral 16 'ha96c m_name string 4 obj1 m_pkt Packet - @1909 m_addr integral 16 'h3cb6 -------------------------------------- UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER] --- UVM Report Summary ---
Two details in that output are worth noticing. obj2 keeps its own instance number, @1908, because copy() changes field values but never replaces the object itself. Its m_pkt field, however, now points to a new object, @1909, holding the same m_addr as obj1's packet.
That is a deep copy: for a `uvm_field_object field, the macro duplicates the nested object instead of sharing its handle. Adding the UVM_REFERENCE flag changes this so that only the handle is copied, and both objects then share one Packet.
Field macros are convenient but generate a lot of code behind the scenes, and they run slower than hand-written methods. Many teams prefer writing do_copy() themselves. The trade-off is covered in UVM Utility & Field Macros.
Writing do_copy() by Hand
Without field macros, you override do_copy() and copy each field yourself. Only the plain `uvm_object_utils macro is used here, which registers the class with the factory but does not register any fields. Because the automatic print() output would therefore be empty, each class also implements convert2string() to show its contents.
typedef enum {FALSE, TRUE} e_bool;
class Packet extends uvm_object;
rand bit[15:0] m_addr;
// Function is used to return contents of this class in a
// string format
virtual function string convert2string();
string contents = $sformatf("m_addr=0x%0h", m_addr);
return contents;
endfunction
`uvm_object_utils(Packet)
// Implementation of "do_copy". A generic uvm_object called "rhs"
// is received and type casted into Packet called "_pkt". Then
// m_addr is copied from _pkt to the variable in current class
virtual function void do_copy(uvm_object rhs);
Packet _pkt;
super.do_copy(rhs);
$cast(_pkt, rhs);
m_addr = _pkt.m_addr;
`uvm_info(get_name(), "In Packet::do_copy()", UVM_LOW)
endfunction
function new(string name = "Packet");
super.new(name);
endfunction
endclass
class Object extends uvm_object;
rand e_bool m_bool;
rand bit[3:0] m_mode;
rand byte m_data[4];
rand shortint m_queue[$];
string m_name;
rand Packet m_pkt;
constraint c_queue { m_queue.size() == 3; }
function new(string name = "Object");
super.new(name);
m_name = name;
m_pkt = Packet::type_id::create("m_pkt");
m_pkt.randomize();
endfunction
// Function used to return contents of this class in a
// string format
virtual function string convert2string();
string contents = "";
$sformat(contents, "%s m_name=%s", contents, m_name);
$sformat(contents, "%s m_bool=%s", contents, m_bool.name());
$sformat(contents, "%s m_mode=0x%0h", contents, m_mode);
foreach(m_data[i]) begin
$sformat(contents, "%s m_data[%0d]=0x%0h", contents, i, m_data[i]);
end
return contents;
endfunction
`uvm_object_utils(Object)
// "rhs" does not contain m_bool, m_mode, etc since its a parent
// handle. So cast into child data type and access using child handle
// Copy each field from the casted handle into local variables
virtual function void do_copy(uvm_object rhs);
Object _obj;
super.do_copy(rhs);
$cast(_obj, rhs);
m_bool = _obj.m_bool;
m_mode = _obj.m_mode;
m_data = _obj.m_data;
m_queue = _obj.m_queue;
m_name = _obj.m_name;
m_pkt.copy(_obj.m_pkt);
`uvm_info(get_name(), "In Object::do_copy()", UVM_LOW)
endfunction
endclass
Every do_copy() follows the same three steps. It calls super.do_copy(rhs) so that any fields declared in a parent class are copied too. It casts rhs to its own type, because a base uvm_object handle cannot see fields such as m_mode. Then it assigns each field.
Notice how m_pkt is handled. Writing m_pkt = _obj.m_pkt; would share one Packet between the two objects. Calling m_pkt.copy(_obj.m_pkt) instead copies the values into this object's own packet, which keeps the two objects fully independent.
The test is the same as before, except that it prints with convert2string().
class base_test extends uvm_test;
`uvm_component_utils(base_test)
function new(string name = "base_test", uvm_component parent=null);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
Object obj1 = Object::type_id::create("obj1");
Object obj2 = Object::type_id::create("obj2");
obj1.randomize();
`uvm_info("TEST", $sformatf("Obj1.print: %s", obj1.convert2string()), UVM_LOW)
obj2.randomize();
`uvm_info("TEST", $sformatf("Obj2.print: %s", obj2.convert2string()), UVM_LOW)
obj2.copy(obj1);
`uvm_info("TEST", "After copy", UVM_LOW)
`uvm_info("TEST", $sformatf("Obj2.print: %s", obj2.convert2string()), UVM_LOW)
endfunction
endclass
module tb;
initial begin
run_test("base_test");
end
endmodule
ncsim> run UVM_INFO @ 0: reporter [RNTST] Running test base_test... UVM_INFO testbench.sv(93) @ 0: uvm_test_top [TEST] Obj1.print: m_name=obj1 m_bool=TRUE m_mode=0xe m_data[0]=0xf4 m_data[1]=0xe m_data[2]=0x58 m_data[3]=0xbd UVM_INFO testbench.sv(95) @ 0: uvm_test_top [TEST] Obj2.print: m_name=obj2 m_bool=FALSE m_mode=0xe m_data[0]=0x60 m_data[1]=0x24 m_data[2]=0x27 m_data[3]=0xb5 UVM_INFO testbench.sv(26) @ 0: reporter [m_pkt] In Packet::do_copy() UVM_INFO testbench.sv(79) @ 0: reporter [obj2] In Object::do_copy() UVM_INFO testbench.sv(98) @ 0: uvm_test_top [TEST] After copy UVM_INFO testbench.sv(99) @ 0: uvm_test_top [TEST] Obj2.print: m_name=obj1 m_bool=TRUE m_mode=0xe m_data[0]=0xf4 m_data[1]=0xe m_data[2]=0x58 m_data[3]=0xbd UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER] --- UVM Report Summary ---
The message from Packet's do_copy() appears before the one from Object's, because Object's implementation copies m_pkt before it reaches its own `uvm_info call.
Cloning an Object
clone() saves you from creating the destination first. It builds a new object of the same type, copies the original into it, and returns the result. Because the return type is uvm_object, you need $cast to assign it to a handle of your own class.
class base_test extends uvm_test;
`uvm_component_utils(base_test)
function new(string name = "base_test", uvm_component parent=null);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
// Create obj1, but only declare handle for obj2
Object obj2;
Object obj1 = Object::type_id::create("obj1");
obj1.randomize();
`uvm_info("TEST", $sformatf("Obj1.print: %s", obj1.convert2string()), UVM_LOW)
// Use $cast to clone obj1 into obj2
$cast(obj2, obj1.clone());
`uvm_info("TEST", "After clone", UVM_LOW)
`uvm_info("TEST", $sformatf("Obj2.print: %s", obj2.convert2string()), UVM_LOW)
endfunction
endclass
module tb;
initial begin
run_test("base_test");
end
endmodule
ncsim> run UVM_INFO @ 0: reporter [RNTST] Running test base_test... UVM_INFO testbench.sv(105) @ 0: uvm_test_top [TEST] Obj1.print: m_name=obj1 m_bool=TRUE m_mode=0xe m_data[0]=0xf4 m_data[1]=0xe m_data[2]=0x58 m_data[3]=0xbd UVM_INFO testbench.sv(20) @ 0: reporter [m_pkt] In Packet::do_copy() UVM_INFO testbench.sv(86) @ 0: reporter [obj1] In Object::do_copy() UVM_INFO testbench.sv(108) @ 0: uvm_test_top [TEST] After clone UVM_INFO testbench.sv(109) @ 0: uvm_test_top [TEST] Obj2.print: m_name=obj1 m_bool=TRUE m_mode=0xe m_data[0]=0xf4 m_data[1]=0xe m_data[2]=0x58 m_data[3]=0xbd UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER]
The do_copy() messages show that clone() really is create() followed by copy(). The Object message reports the name obj1, because the new object was created with the original's name and then filled in.
clone() depends on create(), which the `uvm_object_utils macro generates. A class that skips factory registration has no working create(), so cloning it fails even if its do_copy() is correct.
Quick Check
Q: A monitor captures a transaction into tr, writes it to an analysis port, then reuses tr for the next bus cycle. What problem can this cause, and how do you fix it?
Show Answer
Every subscriber receives the same handle, so when the monitor overwrites tr for the next cycle, any subscriber still holding the earlier transaction sees its fields change. Write a clone instead, for example $cast(tr_copy, tr.clone());, or create a fresh transaction for each cycle.
Common Mistakes
Mistake #1: Calling copy() on a handle that was never created
Wrong: Object obj2; obj2.copy(obj1);
Right: $cast(obj2, obj1.clone());
Why? copy() writes into an existing object. If obj2 is still null, the call dereferences a null handle and the simulation stops with a runtime error. Use clone() when the destination does not exist yet.
Mistake #2: Assigning the result of clone() without $cast
Wrong: obj2 = obj1.clone();
Right: $cast(obj2, obj1.clone());
Why? clone() returns a uvm_object handle. Assigning a base class handle to a derived class variable is a compile error, so the result must be cast.
Mistake #3: Leaving out super.do_copy() or the $cast
A do_copy() without super.do_copy(rhs) silently skips every field declared in the parent class. Without the cast, the code cannot even reference the derived fields, because rhs is only a uvm_object.
Mistake #4: Overriding copy() instead of do_copy()
copy() is not virtual. An override in a derived class is skipped whenever the object is copied through a base class handle, which is exactly how clone() and the field macros call it. Put your field copying in do_copy().
Summary
In this article, you learned:
- Handle assignment shares one object, while
copy()andclone()produce independent objects copy()fills an existing object, andclone()creates a new one and copies into it- Field macros generate
do_copy()automatically and deep-copy nested objects unlessUVM_REFERENCEis set - A hand-written
do_copy()callssuper.do_copy(), castsrhs, then copies each field - The result of
clone()must be assigned with$cast, and the class must be registered with the factory