C Part II · Module 23 · Calling SystemVerilog from C
Exported Functions and Tasks
Two kinds of subroutine can be exported and they are not interchangeable. The difference people notice is that one yields a value and the other does not; the difference that matters is that one may never consume simulation time and the other may. Measured here: both forms entered and left at the same instant, the ticker process never ran during either, and using one as the other builds cleanly and then refuses to run — exit 0 from the compiler, exit 1 from the simulator, with a message naming the line.
Expert23 min readCPart IIVerificationDPI-C
Part II · Module 23 · Chapter 23.2 · Exported Functions and Tasks
1. One Distinction, Not a Tour
Chapter 23.1 established when the reverse direction earns its place. This chapter is about the choice you make immediately afterwards, and the purpose names it precisely: what differs between exporting a function and exporting a task.
From the LRM, three differences, and only the third is load-bearing:
- A function is called inside an expression and yields a value. A task is called as a statement and yields none.
- A function's results come back as a return value; a task's come back through arguments — Chapters 22.2 and 22.3, unchanged.
- A function may never consume simulation time. A task may.
The third is the one this module is organised around, and Chapter 23.5 is where its consequences are measured in full. Here the job is to see both forms actually called, and to find out what happens when the wrong one is used.
2. Both Forms, Called
Measured, the function form invoked from an expression at time 5:
[SV] before, time = 5
[C] function body entered at sim time 5
[C] function body leaving at sim time 5
[SV] after, time = 5, it reported 5And the task form invoked as a statement at time 15:
[SV] before, time = 15
[C] task body entered at sim time 15
[C] task body leaving at sim time 15
[SV] after, time = 15Two things to read out of those blocks.
Neither advanced simulation time. Entered at 5, left at 5; entered at 15, left at 15.
And nothing else ran during either. A ticker process was running alongside, and measured, it ticked at 10, 20, 30 and 40 — never inside a call. That is Chapter 23.1 §3's finding from the other side: the call is one instant with the simulator suspended, and here you can see it in the timestamps.
3. The Honest Limit of This Measurement
The LRM says a task may consume simulation time, and the measurement above shows a task that did not. That is worth being exact about rather than glossing.
What was measured: both calling conventions work, both run to completion at one instant on this binding, and the misuse diagnostics in §4 are real.
What was not measured: a task actually waiting. The binding available here — VPI, since Chapter 19.1 §4 established DPI cannot run on this machine — does not provide the blocking form, so a claim that "a task can wait, see the output" would be a claim this chapter cannot support.
So the rule is cited, not demonstrated: from the LRM, an exported function may not consume simulation time and an exported task may.
It is worth saying that the half this module can measure is the half that causes trouble. Nobody is surprised that a task can wait. What people trip over is that a function cannot — and that the C calling it may be running at a moment when nothing can wait at all. Chapter 23.5 measures that directly, including what happens to 200 million iterations of host work.
4. Using One as the Other
A kind mismatch is the first thing that goes wrong, and it is worth knowing exactly how it presents.
Measured, a function used as a statement:
iverilog exit: 0
vvp exit: 1
tb_misuse.v:4: Error: $c_fn() is a system function, it cannot be called as a task.
tb_m1.vvp: Program not runnable, 1 errors.And a task used in an expression:
iverilog exit: 0
vvp exit: 1
tb_misuse2.v:4: Error: $c_tk() is a system task, it cannot be called as a function.Caught, loud, specific — and late. The compiler returned 0 in both cases; the failure arrives when the program is elaborated and refuses to run. Both messages name your line and the subroutine, which puts them in Chapter 22.1 §5's best category, except for the timing.
Practically, that means a kind mismatch is not something a build step will tell you about. It is something the first run tells you about, clearly, before any simulation happens — which is still early enough to be cheap, provided you actually run.
5. Choosing
Two questions, and the second is the one to ask first even though it feels less urgent.
Could the SystemVerilog being invoked ever need to wait? Not does it today — could it. A scoreboard compare that might one day need to sample a signal on the next clock edge, a reporting function that might acquire something, a hook whose implementation belongs to somebody else. If the honest answer is yes, it is a task, and it is a task from the beginning.
Does the caller need a value back? If yes and the first answer was no, a function is the natural fit. If yes and the first answer was yes, the result comes back through an output argument (Chapter 22.3) and the subroutine is still a task.
The asymmetry is deliberate. A function is the more constrained choice, and the constraint is not local: §6 is about why converting later is more expensive than it looks.
6. The Misconception: Start With a Function and Change It Later
"Export it as a function for now. If it ever needs to wait, we change it to a task — it is one keyword."
The keyword is one keyword and the change is not small, for four reasons that compound.
The kind is part of every call. A function is called in an expression; a task is called as a statement. Converting means editing every call site on both sides, and the C side's call changes shape too — a value-yielding call becomes a void one with an output argument.
The mismatch is caught at run time, not build time. Measured: iverilog exited 0 for both misuses, and vvp refused with exit 1. So a half-finished conversion builds clean and fails on the next run, which is fine if you run and expensive if the conversion lands in a branch nobody exercises until later.
The decision may not be yours. The exported subroutine is SystemVerilog, often somebody else's. "We will change it later" is a commitment on behalf of a file in another team's directory.
And the real constraint is about where it is called from. Chapter 23.5 measures what a call can and cannot do with simulation time, and the limit does not depend on the subroutine's kind alone — a task that may wait is useless if the only place it is called from is one where nothing can. Converting the subroutine does not move that constraint.
The accurate position:
Decide the kind from "could this ever need to wait", before writing either side. A function is the more constrained choice and the cheaper one to call; a task costs an output argument and keeps a door open that cannot be reopened cheaply later.
And the honest limit: for a pure computation that genuinely cannot wait — a lookup, a compare, a format — a function is right and a task would be ceremony. The question is whether the SystemVerilog implementation could need to wait, not whether today's version does.
7. What You Should Be Able to Say Now
- Two kinds, one load-bearing difference: from the LRM, a function may never consume simulation time; a task may.
- A function is called in an expression and yields a value; a task is a statement whose results come back through arguments (22.2, 22.3).
- Measured: the function form entered and left at 5; the task form at 15. Neither advanced time.
- Measured: the ticker ran at 10, 20, 30, 40 and never during a call — 23.1 §3's finding, visible in the timestamps.
- The honest limit: this binding's task form did not demonstrate waiting, so the "a task may wait" rule is cited from the LRM, not shown.
- The half that can be measured is the half that causes trouble — that a call cannot advance time (23.5).
- A kind mismatch is caught at run time: measured, compiler exit 0, simulator exit 1, with a message naming your line.
- Choose by "could it ever need to wait" first, and only then by whether a value is needed.
- Converting later is not one keyword: every call site changes, the failure is a run-time one, the file may be someone else's, and the real constraint is the caller's.
- Write the kind down beside the declaration — from C, a task and a function look like two ordinary functions, one returning
void.
8. Exercises
- Register the two calling conventions and call each correctly. Print the simulation time on entry and exit. What do you conclude, and what can you not conclude?
- Call the function form as a statement. Does the build succeed? Does the run? Record both exit codes without a pipeline in the way.
- Do the same for the task form in an expression. Compare the two messages.
- §3 separates what was measured from what is cited. Write the same separation for a claim in your own work.
- For three exported subroutines you can find, decide the kind using §5's two questions in order. Does any answer change if you ask them the other way round?
- Take a function-shaped export and write out every edit needed to convert it to a task, on both sides. How many files?
- Write the comment from §5's callout for an export you have seen. Which clause could a reader not have recovered from the code?
- Why is "could it ever need to wait" a better question than "does it wait today"? Answer in terms of who owns the SystemVerilog.
9. Summary
- One distinction matters: from the LRM, a function may never consume simulation time; a task may.
- Function in an expression, value returned; task as a statement, results through arguments.
- Measured: entered and left at 5 and at 15 — neither advanced time; the ticker never ran during a call.
- Honest limit: a waiting task was not demonstrated here, so that rule is cited, not shown. The constraint that bites is measured in 23.5.
- Kind mismatch: caught at run time — compiler exit 0, simulator exit 1, message names your line.
- Choose by "could it ever need to wait" first, then by whether a value is needed.
- Converting later touches every call site, fails at run time, and may be somebody else's file.
- Write the kind beside the declaration — from C the two look alike.
A subroutine has been named and its kind settled. What has not been settled is which one the call reaches, and that turns out to be a question with a surprising answer: the same name, called from two places, does not necessarily find the same object, and the scope a call reports is not the one a SystemVerilog reader would expect.
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.
