C Part II · Module 19 · DPI-C Foundations
Imported Functions and Tasks
A SystemVerilog function cannot consume simulation time and a task can, and an import inherits whichever it was declared as. That single restriction decides what a C model is allowed to be: one that computes an answer can be a function, and one that must wait for something cannot. This chapter is that branch — the two C signatures it produces, the return-value protocol an imported task carries, why its outputs arrive as pointers, and the design question people reach for a task to avoid answering.
Advanced21 min readCPart IIVerificationDPI-C
Part II · Module 19 · Chapter 19.3 · Imported Functions and Tasks
1. One Restriction, Two Kinds of Import
SystemVerilog distinguishes functions from tasks by one rule: a function cannot consume simulation time; a task can. A function executes and returns within a single instant; a task may wait for an edge, a delay, or an event.
An import inherits whichever it was declared as. That produces two kinds, and the difference is not stylistic — it decides what the C is permitted to do and where it may be called from.
An imported function computes and returns. It cannot wait, and neither can anything it calls.
An imported task may consume time, which in practice means it may call back into SystemVerilog and have that wait. It may only be called from somewhere time is allowed to pass.
Everything else in this chapter follows from that.
2. The C Side of an Imported Function
Chapter 19.2 §3 established that the C is ordinary C, and for a function it is as ordinary as it gets:
int c_add(int a, int b) { return a + b; }Measured: c_add(40, 2) = 42, compiled with -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion, clean under AddressSanitizer and UndefinedBehaviorSanitizer, with no DPI header. The return value is the function's result and arrives in SystemVerilog as the call's value.
From the LRM: the SystemVerilog side declares it, and the declared return type and argument types determine the mapping. For the scalar cases that mapping is direct — Chapter 19.1 §3 measured sizeof(int) as 4, which is what SystemVerilog's int requires.
That is the whole story for a function, and it is why most DPI material stops here.
3. The C Side of an Imported Task
A task is different in two ways, and both show up in the C signature.
From the LRM: an imported task's C function returns int, and that value is not the task's result. It is a completion status — zero means the task ran to completion, non-zero means it was disabled. Any actual results come back through output arguments, which the C sees as pointers.
/* the C side of an imported TASK */
int c_fetch(int addr, int *data_out) {
if (addr < 0) return 1; /* non-zero: disabled / did not complete */
*data_out = addr * 2; /* results come back through a pointer */
return 0; /* zero: completed */
}Measured with a stub caller standing in for the simulator, which is Chapter 19.2 §5's practice:
ok path : rc=0 data=84 (addr 42)
error path : rc=1 data untouched (addr -1)Clean under the full flag set and both sanitizers.
Two consequences follow immediately and both are ordinary C problems.
The return value is a protocol, not a result. A caller that ignores it has ignored "the task did not complete". In C terms this is the same shape as a function returning a status that Part I's Chapter 12.8 insisted on checking.
The outputs are out-parameters, with every hazard out-parameters have. Chapter 13.11 §6 made carrying a pointer with its length one of the two habits that remove a whole bug class, and Chapter 14.7 built the discipline. A DPI task handed a pointer to a buffer gets a bare pointer, and if the SystemVerilog side's array is shorter than the C assumes, Chapter 14.7 §5 measured what that produces: twelve bytes into eight, clean exit, no diagnostic.
So the rule from Chapter 14.7 applies unchanged: if a pointer crosses, a length should cross with it. Nothing in DPI supplies one for you.
4. Which One a Model Needs
The question is not "function or task?" but "does this have to wait?"
It does not wait — compute an expected value, encode a packet, check a checksum, decode a field. A function. This is most reference models and all of Chapter 18.2's examples.
It waits — the C must drive a transaction and see the response, or must block until something happens. A task, and the waiting is done by SystemVerilog code the task calls back into, which is Chapter 19.2 §2's export direction and requires Chapter 19.5's context.
From the LRM: an imported task can only be called from a SystemVerilog context where time may pass — inside an initial or always block, or another task. It cannot be called from a function, which is the restriction propagating back up.
That restriction is worth noticing, because it has a design consequence: a task is harder to call. Making something a task narrows where in the testbench it can be used, and that narrowing is permanent.
5. The Third Option People Miss
There is a shape that avoids the question, and it is frequently better than either answer.
Let the testbench own the timing and call a function per step.
Instead of a C task that drives a transaction, waits, and returns a result:
int c_do_transaction(int addr, int *data_out); /* a task: it waits */write a C function that advances a model by one step and returns what it wants done:
/* a FUNCTION: no waiting. The testbench does the waiting. */
int c_next_request(struct request *out); /* what should happen next? */
void c_observe_response(const struct response *in); /* here is what happened */The testbench loops: ask the model what to do, do it (consuming time), tell the model what happened, repeat.
Four things that buys, and the last is the one that matters most:
The C stays a function, callable from anywhere, with no disable protocol.
The C stays testable standalone. Chapter 19.2 §5's stub practice works directly, and Chapter 18.2's argument applies — a model you can run over its own vectors is one you can interrogate.
The timing is in the testbench, where it can be seen, waveformed, and changed without touching the C.
The model does not own simulation time. A C task that waits has taken control of the simulation's progress into code the testbench cannot see into. When the simulation hangs, the thing that is waiting is in a shared object.
This is not always possible — some interactions genuinely need the C to block — but it is possible more often than it is attempted, and it is worth considering before reaching for a task.
6. The Misconception: Make It a Task, Tasks Are More Capable
"A task can do everything a function can and more, so declare imports as tasks and keep your options open."
A task is more capable and strictly harder to use, and the costs are concrete.
It is callable from fewer places. Only where time may pass. A function can be called from a function, from a continuous assignment context, from anywhere.
Every call site carries the protocol. The int return is a completion status that each caller should check, and §3's callout showed what happens when one does not.
It is harder to test standalone. A function is a function; a task that waits by calling back into SystemVerilog has a dependency your stub must emulate, and emulating waiting in a C stub is not the same as emulating a report.
It takes ownership of simulation time — §5's fourth point, and the one that costs most when something goes wrong.
The reasonable default is the opposite of the folklore:
Declare an import as a function unless it genuinely must wait. If it must wait, consider first whether the testbench can own the waiting and call a function per step.
That ordering — function, then restructure, then task — keeps the C in the shape that Chapter 19.2 §5 showed is developable and Chapter 18.2 showed is interrogable.
7. What You Should Be Able to Say Now
- One restriction produces the branch: a SystemVerilog function cannot consume simulation time; a task can. An import inherits whichever it was declared.
- An imported function's C side returns its result directly. Measured:
c_add(40,2) = 42, no DPI header, clean under the full flag set and both sanitizers. - An imported task's C side returns
intas a completion status — zero completed, non-zero disabled — and results come back through output pointers. Measured:rc=0 data=84on the ok path,rc=1with the output untouched on the error path. - The return value is a protocol, not a result. Ignoring it means ignoring "the task did not complete".
- Output arguments are ordinary out-parameters with ordinary hazards. If a pointer crosses, a length should cross with it (13.11 §6, 14.7) — DPI supplies no length.
- Decide what happens to outputs on failure, and write it down next to the declaration (16.8 §7). Not writing them is defensible; leaving it unstated is not.
- The question is "does this have to wait?", not "function or task?".
- A task is callable from fewer places — only where time may pass — and that narrowing is permanent.
- The third option: let the testbench own the timing and call a function per step. Keeps the C a function, standalone-testable, with the timing visible.
- Default to a function. Restructure before reaching for a task, because a task that waits has taken simulation time into a shared object.
8. Exercises
- Write
c_addandc_fetchfrom §2 and §3 and exercise both with a stub caller. Confirm the four measured values. - Write the contract comment from §3's callout for
c_fetch, then implement the other contract — a defined value on failure — and say which you would ship. - A task takes a pointer to an array of bytes and a count. Write the C signature, then say what Chapter 14.7 §5 would predict if the count were wrong.
- For three models you can think of, decide function or task by §4's question. How many needed a task?
- Take one of the ones that needed a task and restructure it into §5's shape. What did the testbench have to take on?
- §6 says a task is harder to test standalone. Write the stub you would need for a task that waits, and say what it cannot faithfully reproduce.
- A colleague declares all imports as tasks "to keep options open". Write the three-sentence reply, naming the concrete costs.
9. Summary
- One rule branches the interface: a function cannot consume time, a task can.
- Function: the C returns its result. Measured
c_add(40,2) = 42. - Task: the C returns
intas a completion status, results through output pointers. Measuredrc=0 data=84andrc=1with the output untouched. - The return value is a protocol, not a result.
- Out-parameters carry out-parameter hazards — a pointer that crosses needs a length with it; DPI supplies none.
- Decide and document the failure contract for outputs.
- Ask "does it have to wait?", not "function or task?".
- A task is callable from fewer places, permanently.
- Third option: the testbench owns the timing, the C is a function called per step — testable, interrogable, and the timing stays visible.
- Default to a function; restructure before reaching for a task.
Both kinds have now been described and neither has been written out in full. Putting one together end to end — the C, the declaration, the build, and a way to check it before a simulator is involved — turns out to need one more thing that none of the three chapters so far has had to name.
Standards & specifications
- Governing standard
- IEEE Std 1800 (SystemVerilog)(opens IEEE in a new tab)
Defines SystemVerilog language semantics — syntax, data types, scheduling and the behaviour a conforming simulator must produce. It does not define tool-specific synthesis support or vendor methodology.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
