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() and clone()
  • Copy objects automatically using UVM field macros
  • Write a correct do_copy() implementation by hand
  • Use clone() with $cast to 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 calls do_copy(), which is where a class adds its own fields.
  • do_copy() receives the source as a base uvm_object handle, so it has to cast it to the real type before reading any fields.
  • clone() calls create() to allocate a new object of the same type, then calls copy() on it. It returns a uvm_object handle.

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.

Output
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
Output
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
Output
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() and clone() produce independent objects
  • copy() fills an existing object, and clone() creates a new one and copies into it
  • Field macros generate do_copy() automatically and deep-copy nested objects unless UVM_REFERENCE is set
  • A hand-written do_copy() calls super.do_copy(), casts rhs, then copies each field
  • The result of clone() must be assigned with $cast, and the class must be registered with the factory
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5
Cron Job Starts