A callback lets you hand a function to someone else's program. When that program reaches a certain point, it "calls back" your function and runs it, without needing to know exactly what your function does — a way for different parts of a system to work together more flexibly, without being tightly connected.
In SystemVerilog, a class method is written to call placeholder methods. A user can later extend the class and implement those placeholder methods — the placeholders are the callback methods, and the calls to them are the callback hooks. The major benefit is decoupling: a verification environment can register behavior to run during simulation without ever modifying the original code that invokes it.
8 min read | Intermediate Level
What You'll Learn
- What a callback is, and why it decouples components from each other
- The four steps to building a callback in SystemVerilog: define, register, implement, invoke
- How a callback differs from calling a regular method directly
- The lighter-weight "empty placeholder method" pattern used throughout real verification environments
Building a Callback: 4 Steps
1. Define the Callback Class
Callback functionality is usually implemented in a class with a virtual method that can be overridden or extended by other classes:
class MyCallback;
virtual function void callback_function();
// Default implementation (optional)
endfunction
endclass
2. Register the Callback
The class that will invoke the callback needs a way to store a reference to it, provided later at run time:
class MyTest;
MyCallback cb;
// 2. Registration of callback
function void register_callback(MyCallback callback);
cb = callback;
endfunction
function void execute();
if (cb != null) begin
cb.callback_function();
end
endfunction
endclass
3. Implement the Callback
The actual behavior lives in a class that extends the callback class and overrides its method:
// 3. Callback implementation
class UserCallback extends MyCallback;
function void callback_function();
$display("User-defined callback called!");
endfunction
endclass
4. Invoke the Callback
The invoking class doesn't know (or need to know) what the callback actually does — it just calls whatever was registered:
module tb;
initial begin
MyTest test;
UserCallback user_cb;
test = new;
user_cb = new;
// Register user-defined callback
test.register_callback(user_cb);
// 4. Execute which will invoke the callback
test.execute();
end
endmodule
Output
xcelium> run User-defined callback called! xmsim: *W,RNQUIE: Simulation is complete. xcelium> exit
test.cb is a MyCallback-typed handle, but it actually points to a UserCallback object. Because callback_function() is declared virtual, calling it through cb correctly dispatches to UserCallback's overridden version — the same polymorphism mechanism covered earlier in this tutorial is what makes the whole callback pattern work.
Quick Check
Q: What would happen if test.execute() were called without ever calling register_callback() first?
Show Answer
Nothing would happen — silently. cb defaults to null until something assigns it, and execute()'s if (cb != null) check means the callback is simply skipped rather than causing an error.
How Is a Callback Different From a Regular Method Call?
| Regular method call | Callback | |
|---|---|---|
| Who decides what runs | Fixed at the call site, by name | Whatever object was registered at run time |
| Coupling | Tight — the caller knows the exact implementation | Loose — the caller only knows a callback exists |
| Changing behavior | Requires rewriting the calling code | Just register a different object — no changes to the caller |
A regular function or task is called directly by name, and its behavior is fixed unless the code itself is rewritten. A callback is registered by one part of the code and invoked by another, and the invoking part never needs to know what the registered callback actually does.
Using Empty Placeholder Methods as Callbacks
Empty tasks can be positioned at key points in a class, allowing behavior to be added at those exact spots later, without touching the original code at all:
class MyTest;
...
virtual task body();
// some statements
pre_err_callback();
err_inj_seq();
post_err_callback();
// more statements
endtask
virtual task pre_err_callback();
// Empty callback
endtask
virtual task post_err_callback();
// Empty callback
endtask
endclass
class MyTestExt extends MyTest;
virtual task pre_err_callback();
// Implement things to be done before error injection
endtask
virtual task post_err_callback();
// Implement things to be done after error injection
endtask
endclass
This achieves the same decoupling as the 4-step pattern above, through a simpler mechanism: instead of a separate registration step, the "registration" is just choosing to instantiate MyTestExt instead of MyTest. pre_err_callback() and post_err_callback() are placeholders in the base class, and MyTestExt fills them in — body() itself never needs to change to support that new behavior.
Quick Check
Q: If pre_err_callback() and post_err_callback() were not declared virtual, would overriding them in MyTestExt actually change what body() executes?
Show Answer
No. Without virtual, a call through a base-class-typed reference always resolves to the base class's own version, regardless of what the actual object's type is — the override in MyTestExt would simply never run. virtual is what makes this whole pattern work.
Where Are Callbacks Used?
UVM builds an entire formal callback mechanism (uvm_callback and related base classes) directly on top of this same virtual-method-override idea, letting a verification environment hook into a driver, sequence, or scoreboard's behavior — injecting errors, modifying transactions, or adding checks — without ever needing to modify the original component's source code.
Common Beginner Mistakes
Mistake #1: Forgetting to check for null before invoking a callback
A registered callback handle defaults to null until something assigns it. Why? Calling a method through a null handle causes a run-time error — the if (cb != null) check in the 4-step example isn't optional boilerplate, it's what keeps an unregistered callback from crashing the simulation.
Mistake #2: Forgetting virtual on a placeholder method
As the second Quick Check shows, a non-virtual placeholder method silently defeats the entire pattern. Why? Without virtual, a call through a base-class handle or reference always resolves to the base class's own implementation, never a child class's override — the callback would simply never fire.
Mistake #3: Mixing up the two callback patterns
The explicit register/invoke pattern and the empty-placeholder-method pattern both achieve decoupling, but through different mechanisms — one uses a separate handle and registration call, the other uses inheritance directly. Why? Knowing which pattern a given codebase actually uses matters: adding a registration call to a codebase that expects you to simply extend a class (or vice versa) won't produce the intended hook.
Practice Exercise
Challenge: Add a mid_err_callback()
Extend the placeholder-method example with a third empty callback, mid_err_callback(), called partway through err_inj_seq() conceptually — placed in body() right after err_inj_seq() but before post_err_callback(). Override it in MyTestExt to print a message.
Hint
Follow the exact same pattern as pre_err_callback() and post_err_callback() — an empty virtual task in the base class, and an overriding implementation in the extended class.
Solution
class MyTest;
virtual task body();
pre_err_callback();
err_inj_seq();
mid_err_callback();
post_err_callback();
endtask
virtual task pre_err_callback(); endtask
virtual task mid_err_callback(); endtask
virtual task post_err_callback(); endtask
endclass
class MyTestExt extends MyTest;
virtual task mid_err_callback();
$display("Mid-injection callback fired");
endtask
endclass
Notice MyTestExt only needs to override mid_err_callback() here — pre_err_callback() and post_err_callback() are free to stay at their empty base-class defaults, since each placeholder is independent of the others.
Summary
In this tutorial, you learned:
- A callback lets one part of the code register behavior that another part invokes, without the invoking part needing to know what that behavior actually does
- Building one takes four steps: define a class with a virtual placeholder method, register a handle to it, implement the placeholder in a subclass, and invoke it through the registered handle
- Callbacks are looser-coupled than a regular method call, since behavior can change by registering a different object instead of rewriting the calling code
- An even simpler pattern uses empty
virtualplaceholder methods directly in a base class, "registered" just by extending it — no separate handle or registration call needed virtualis what makes either pattern work at all — without it, an override never actually takes effect through a base-class reference